很多初学者第一次学 C 指针,卡住的并不是语法,而是不清楚“变量值”和“变量地址”到底是什么关系。本文按基础概念、与数组的联系、与函数的配合以及常见风险几部分重组内容,把容易混淆的几组概念放在一起讲清,读完后你可以更快判断一段指针代码到底在访问值、传地址,还是在跨类型移动内存。
指针基础:先分清地址、取址和解引用
变量在 C 语言里至少有两种访问方式:
- 直接访问:通过变量名访问
- 间接访问:通过指针访问
什么是指针
用于专门存储地址的变量,就是指针变量,通常简称为指针。
系统位数不同,地址长度也不同:
- 64 位系统使用 64 位表示地址,所以指针通常占用 8 字节
- 32 位系统使用 32 位表示地址,所以指针通常占用 4 字节
指针怎么定义
一般格式如下:
数据类型 *标识符 = 地址;
int * num = &a;
下面几种写法都合法,本质没有区别:
int * num = &a;
int* num = &a;
int *num = &a;
`&` 和 `*` 分别表示什么
&是取地址运算符*是取值运算符,也叫解引用,用来取出指针所指向地址中的数据
int a = 1;
int * num = &a;
printf("%d", *num);
这里 num 保存的是 a 的地址,而 *num 取出的才是这个地址里存放的值,也就是 1。
指针运算:移动的不是 1 字节,而是一个数据单位
指针加减
指针和整数做加减,不是简单地按字节加 1、减 1,而是按“当前指针类型对应的数据单位”移动。
例如:
int *p = &a;
如果 int 占 4 字节,那么 p+1 表示地址加 4,p-1 表示地址减 4。
同类型指针相减
两个同类型指针相减,结果表示它们之间相差多少个同类型数据单位,而不是单纯差了多少字节。
- 高位地址减去低位地址,结果为正
- 低位地址减去高位地址,结果为负
这类结果的类型是 ptrdiff_t,它是有符号整数类型,格式化输出占位符是 %td。
指针比较
指针比较本质上是比较地址大小,返回的是 int 类型结果,通常为 1 或 0。
指针和数组:最容易混,但也最值得搞懂
数组名为什么常被当成指针用
数组名在大多数情况下会隐式转换为第一个元素的指针,只有在 sizeof 这种场景下不会发生这种转换。因此它看起来很像指针,也拥有指针的大部分行为。
例如:
arr[i] 其实就是 *(arr+i) 的语法糖。
但数组名和普通指针依然有两个关键区别:
- 数组名是标识符常量,不能改指向;指针变量可以重新赋值
sizeof 数组名得到的是整个数组大小;sizeof 指针得到的是指针本身大小
#include
int print(int arr[]) // 形参也为地址!
{
printf("%zun", sizeof arr); // 8
}
int main()
{
int arr[3] = {1, 2, 3};
int (*p)[3] = arr;
printf("%zun", sizeof arr); // 12
printf("%pn", arr); // 地址
printf("%pn", p); // 地址
return 0;
}
这个例子里,main 中的 sizeof arr 是整个数组大小;但作为函数参数传入后,arr[] 实际已经退化成地址,所以 sizeof arr 看到的是指针大小。
指针数组和数组指针别混淆
指针数组,是“数组”,里面的每个元素都是指针:
数据类型 *指针数组名[长度];
int *arr[3] = {&a, &b, &c};
数组指针,是“指针”,它指向的是整个数组:
数据类型 (*数组指针名)[长度];
int (*p)[3] = &arr;
这里要特别注意:数组指针指向的是整个数组地址,不是首元素地址。所以上面这个 p 的一个单位长度是整个数组大小;若数组共占 12 字节,那么 p+1 就表示地址加 12。
字符指针的特点
字符指针常用于处理字符和字符串:
char * pStr = "hello tom";
指向字符串字面量常量的字符指针,有几个常见特点:
- 相对字符数组名,字符指针本身可以改为指向新的字符串
- 不能通过下标去修改某个字符
- 字符指针不能直接接收
scanf输入的字符串
一张表看懂 arr、ptr 和 arr_ptr
int arr[5] = {10,20,30,40,50}; // arr 数组名
int * ptr = arr; // ptr 显式定义的首元素指针
int (*arr_ptr)[5] = &arr; // arr_ptr 是数组指针
| 标识符 | 性质 | 数据类型 | sizeof 计算结果 | +1 的值 | * 取值(解引用) | 能否重新赋值 |
|---|---|---|---|---|---|---|
| arr | 数组名 | int [5] | 整个数组的大小 | 第二个元素地址 | 首元素值 | 否 |
| ptr | 首元素指针 | int * | 地址大小 | 第二个元素地址 | 首元素值 | 是 |
| arr_ptr | 数组指针 | int (*)[5] | 地址大小 | 数组后面地址(越界) | 数组名(首元素地址) | 是 |
指针和函数:参数、返回值与回调分别是什么关系
指针传递
如果函数参数传的是普通非地址类型,那么实参会把值拷贝给形参,函数内外是两份独立数据。
如果传的是地址类型,那么函数内部修改该地址对应的数据,函数外部也会受到影响。
void fn(int arr[]); // arr 为地址
这也是为什么数组作为函数参数时,看起来写的是数组,实际上传进来的仍然是地址。
指针函数
如果一个函数的返回值是指针,这种函数就叫指针函数。
char * strlong(char *str1, char *str2)
{
if (strlen(str1) >= strlen(str2))
{
return str1;
}
else
{
return str2;
}
}
这里返回的是传入字符串地址中的一个。需要注意的是,指针函数不能返回局部变量的地址,因为函数结束后局部变量就销毁了;如果确实需要保留,可以返回静态局部变量的地址。
函数指针
函数指针本身是一个指针,只不过它指向的是函数。
函数名、&函数名、*函数名 都可以获取函数代码地址,也都可以用于调用。
返回类型 (*函数指针名)(参数列表);
int max()
{
...
}
int (*p)(int, int) = &max;
...
max(); *max(); &max();
p(); *p();
回调函数
函数指针还能作为另一个函数的参数使用,这就是回调函数最典型的形式。
void initArray(int *array, int arraySize, int (*f)())
{
for (int i = 0; i < arraySize; i++)
{
array[i] = f();
}
}
int main()
{
int myarray[10];
// rand 是系统函数,会返回一个随机整数,位于 标准库中
initArray(myarray, 10, rand);
}
这个例子中,initArray 不关心数据具体怎么产生,它只负责逐个调用传入的函数 f,把结果写入数组。
指针常量和常量指针
这两个名字很像,但限制对象不同。
指针常量:指针本身不能改。
int * const num = &a;
常量指针:通过该指针不能修改它指向的数据。
const int * num = &b; // b 可以不是常量,但通过 num 不能修改
int const * num = &c;
多级指针、空指针和野指针:最后要避开的坑
多级指针
多级指针就是“指向指针的指针”。数组指针、二维数组相关写法里,经常会遇到这种思路。
int *p = #
int **p2 = &p;
空指针
- 被赋值为
NULL的指针称为空指针 NULL是标准库中定义的值为 0 的宏常量- 声明指针变量时,如果暂时没有明确地址,先赋值为
NULL是较好的编程习惯 - 空指针不能被解引用
int *p = NULL;
野指针
野指针是指指针指向的位置不可知,继续使用会有很高风险。
常见原因主要有三类:
- 指针没有初始化
- 指针越界
- 指向已经失效的内存空间,比如函数调用结束后局部变量被销毁
指针声明速查
| 变量定义 | 类型表示 | 含义 |
|---|---|---|
| int i | int | i 是一个整型变量 |
| int *p | int * | p 是一个指向整型数据的指针 |
| int a[5] | int[5] | a 是一个 5 个元素的整型数组 |
| int *p[4] | int *[4] | p 是一个指针数组,每个元素都是指向整型数据的指针 |
| int (*p)[4] | int (*)[4] | p 是一个数组指针,指向一个 4 个元素的整型数组 |
| int f() | int () | f 是一个返回整型数据的函数 |
| int *f() | int *() | f 是一个指针函数,返回指向整数数据的指针 |
| int (*p)() | int (*)() | p 是一个函数指针,指向一个返回整型数据的函数 |
| int **p | int ** | p 是一个指向指针的指针 |










