写在前面

许多 C 语言初学者觉得指针难, 谈"指"色变😐
指针是 C/C++ 里面非常重要的知识点,你对它了解多少呢?一起来了解一下吧~

1. 内存和地址

要想深入理解指针,就得先认识内存和地址

1.1 内存

内存(Memory), 是计算机中最重要的部件之一, 它是程序与 CPU 进行沟通的桥梁. 内存又称主存, 其作用是存放CPU中的运算数据, 计算机中所有的程序的运行都是在内存中进行的.
简单来说, 你可以把内存想象成一栋宿舍楼. 在这个宿舍楼里面, 住着很多同学, 可以把电脑内存里的数据想象成这些同学.

1.2 地址

计算机内存里的数据有很多很多, CPU 要如何找到想要的数据的? 计算机把内存划分一个个内存单元, 每个内存单元的大小是 1 字节. 这些内存单元的编号就是地址.
在上面的例子中, 假如 8 个同学住一间宿舍, 这个宿舍就可以理解成内存单元, 宿舍号就可以理解成这个内存单元的地址.
C 语言给地址起了一个新名字: 指针.
最后小结一下:

内存单元的编号 == 地址 == 指针

2. 指针变量和地址

2.1 取地址操作符

C 语言中, 创建一个变量就是向内存申请了一块地址空间. 下面的代码就是向内存申请一块大小为4个字节的地址空间.

#include <stdio.h>
int main() {
int num = 10;
return 0;
}

如何得到 num 的地址呢? 取地址操作符 &, 只需要加上printf("%p\n", &num);这一段代码, 就可以打印出 num 的地址. 这里打印的是第一个较小字节的地址. & 就是取地址操作符.
打印地址

2.2 指针变量及解引用操作符

通常我们需要把取出来地址保存起来, 方便以后再使用, 那这样的地址值要存放到哪里去呢? 答案: 指针变量.
指针变量就是用来存放地址的, 存放在指针变量中的值被理解为地址.

#include <stdio.h>
int main() {
int num = 10;
int* p = &num; // 取出 num 的地址存放到指针 p 中
printf("%p\n", p);
return 0;
}

效果和上面的代码是一样的, 只不过每次运行程序, 系统分配给 num 的地址不同.

p 是指针变量, 它的类型是int*, 如何理解指针类型呢?
* 指明 p 是指针变量, int 指明指针变量 p 指向的是 int 类型的对象.

到此, 我们已经知道了如何保存地址, 那还有一个问题, 如何使用它呢? 答案: 解引用操作符 *.

#include <stdio.h>
int main() {
int num = 10;
int* p = &num;
*p = 20;
printf("num = %d\n", num);
printf("*p = %d\n", *p);
return 0;
}

*p就是通过 p 中存放的地址, 找到这个地址指向的内存单元,
*p = 20是修改 p 指向的内存单元的内容为 20, 由于 p 指向 num, 相当于num = 20.

这里你可能会问: 那为啥不直接写 num = 20呢?
哈哈, 用不同的代码实现同样的效果, 不是增加了代码的灵活性吗? 而且 C 语言是偏底层的高级语言, 在资源有限的状态下使用指针可以让时间空间效率成十倍的提高。

2.3 指针变量的大小

前面我们已经知道了指针变量是用来存放地址的, 32 位计算机的地址有 32 个 bit 位, 需要 4 个字节才能放下, 64 位计算机的地址有 64 个 bit 位, 需要 8 个字节才能放下.
小结一下:

指针变量的⼤⼩取决于地址的⼤⼩
32 位平台下指针 变量的大小是 4 字节
64 位平台下指针 变量的大小是 8 字节
需要注意: 指针变量的大小和类型是无关的

3. 指针变量的类型

前面我们已经知道指针变量的大小和类型无关, 那为什么还要那么多的指针变量类型?

3.1 指针解引用

指针的的类型决定了系统对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
⽐如:
char*的指针解引⽤就只能访问⼀个字节
int*的指针的解引⽤就能访问四个字节
double*的指针的解引⽤就能访问八个字节

3.2 指针加减整数

指针 + 1 或者 - 1, 就是向后或向前跳过一个指针指向的元素
指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)

3.3 void* 指针

void* 是一种特殊的指针类型, 可以理解为无具体类型的指针, 也称泛指型指针, 可以用来接收任意类型的指针.

局限性: void* 不能进行指针加减整数和解引用操作, 因为它没有指向具体的类型.
一般 void* 类型的指针使用在函数参数的部分, 用来接收不同数据类型的地址, 这样可以实现泛型编程的效果.

4. 指针运算

指针的基本运算:

  • 赋值, 取地址, 解引用
  • 指针 + - 整数
  • 指针 - 指针
  • 指针的关系运算

赋值, 取地址, 解引用, 均在上文有说明
指针 + - 整数, 就是指针向后或向前移动几个元素. 例如指针 p 指向一个数组的首地址, p + i就表示数组下标为 i 的这个元素的地址
指针 - 指针, 这两个指针一定是指向同一块区域. 指针 - 指针的值的绝对值, 就是两个指针指向的地址之间有几个元素, 也可以理解为两个元素之间的距离
指针的关系运算与其他数据类型相似

#include <stdio.h>
#include <stdlib.h>
int main(){
char* p = (char*)malloc(10);
if(p == NULL){
printf("faild to allocate memory.\n");
}else{
printf("allocate memory successfully.\n");
}
free(p);
return 0;
}

malloc 的返回值不为 NULL, 就代表内存分配成功, 这里p == NULL就是在做指针的关系运算.

5. const 修饰指针

首先, const 修饰一个变量, 使得这个变量有了常属性, 不可修改, 成为常变量, 但它的本质还是变量, 只是语法层面上限制了它不能被修改. 但是, 可以通过指针绕过const修改这个变量.

const int num = 10;
int* p = &num;
*p = 20;

这样就把 const 修饰的常变量的值给修改了.

c++中, const修饰一个变量, 这个变量就变成了常量而不是常变量

const既然可以修饰普通变量,那也可以修饰指针变量.

const 修饰指针变量, 可以放在 * 的左边或右边
放在 * 的左边表示指针指向的对象不能通过指针来修改了, 但指针变量本身可以被修改
放在 * 的右边表示指针变量本身不能被修改了, 但指针变量指向的对象可以通过指针变量修改

看到这里, 我们就可以把上文的代码修改一下:const int* p = &num, 这样就不能通过 p 来修改 num 的值了.

6. 野指针

首先给出野指针的概念: 指针指向的位置是不可知的(随机的, 不正确的, 没有明确限制的)

6.1 野指针的成因

  • 指针未初始化
#include <stdio.h>
int main(){
int* p;
*p = 10;
return 0;
}

p是局部变量, 未初始化, 默认是内存里的脏数据, 可以理解为随机值

  • 指针越界访问
#include <stdio.h>
int main(){
int arr[10] = { 0 };
int* p = arr;
for(int i = 0; i <= 10; i++){
*(p++) = i;
printf("%d ",arr[i]);
}
return 0;
}

当指针指向的范围超过数组 arr 的范围时, p 就成了野指针

  • 指针指向的空间释放
#include <stdio.h>
int* func() {
int n = 100;
return &n;
}
int main(){
int* p = func();
printf("%p\n", p);
return 0;
}

这段代码的执行过程是: 函数确实会返回里面创建的临时变量的地址, 指针 p 也可以接收到这个地址, 但是请注意: n 是局部变量, 在离开函数范围后就会被销毁, n 所占的这块空间也就不归当前程序了. 也就是说, 当 p 接收到这个地址的瞬间, 就已经变成野指针了.

6.2 如何规避野指针

  • 指针初始化
    如果明确知道指针要指向哪里就直接赋值地址, 如果不知道要指向哪里, 可以赋值 NULL.

NULL 是 c 语言的一个标识符常量, 值为 0, 0 也是它的地址, 这个地址无法读取.

#include <stdio.h>
int main(){
int num = 10;
int* p = &num;
int* q = NULL;
return 0;
}
  • 小心指针越界
    程序向内存申请了多少内存, 通过指针只能访问这些空间, 超出访问范围就是越界访问.
  • 指针变量不再使用时, 及时置为 NULL, 指针使用之前检查有效性
    当不再使用这个指针去访问一块区域时, 就把它置为 NULL.
    一般约定: 只要是 NULL 指针就不访问, 使用指针之前判断是否为 NULL.

assert断言, 在 assert.h 头文件中定义了宏 assert(), 用于在运行时确保程序符合指定条件,
如果不符合, 就会报错终止运行.
assert(p != NULL);, 当程序执行到这一行代码时, 会验证 p 是否等于 NULL,
如果确实不等于, 程序就会继续执行; 如果等于, 程序就会报错终止运行.

  • 避免返回局部变量的地址
    如在野指针的成因里面所示, 返回局部变量的地址, 可能会造成野指针, 所以不要返回局部变量的地址.

7. 指针的使用

学习指针的目的是使用指针解决问题, 那什么问题非指针不可?

当我们给函数传参的时候, 实参传递给形参, 形参会单独创建一份临时空间来接收实参, 可以理解为:
形参是实参的一份临时拷贝, 对形参的修改不会影响实参. 所谓传值调用.
而如果是将变量的地址传递给函数, 接可以实现在函数内部修改主调函数中变量的内容. 所谓传址调用.

写在最后

当你看完这篇博客, 相信你已经对指针有了新的认识和理解, 日后多加练习, 你肯快就能掌握它~