指针详解¶
Abstract
指针是保存地址的变量. 类型决定解引用时读多少字节, 以及指针算术按几个元素步进.
声明不初始化会得到野指针; 用 NULL 表示"暂时无有效对象". 函数想改调用方的对象就传对象地址; 想改调用方的指针就传指针的地址 (T **).
基础语法, 数组衰减, 字符串与 NULL 哨兵见 C Pointers, Arrays, and Strings. 栈上局部变量何时不能把地址传出去见 C Memory Management.
约定¶
-
定义指针应立刻给初值, 或写成
NULL. 未初始化就*p是undefined behavior. -
同一语句里每个指针都要有自己的
*: 写int *p, *q;, 不要写成int* p, q;. -
const写在*左边约束所指对象, 写在*右边约束指针本身. 字符串字面量用const char *声明. -
形参里的
T a[]和T *a对编译器等价, 都是指针. 需要长度时另传一个参数. -
int *a[10]是指针数组;int (*a)[10]是指向数组的指针.[]比*更紧, 靠括号改结合顺序. -
不要返回指向本函数局部变量的指针. 尝试访问栈帧弹出后那块栈内存会触发未定义行为.
基本概念¶
int i;
int *pi = &i;
char c;
char *pc = &c;
& 是取地址运算符, 操作数必须是左值. * 是间接寻址 / 解引用, 操作数必须是指针. 若 E 可做左值, *&E 与 E 等价; 若 E 是指针, &*E 与 E 等价.
指针本身也占存储. 32 位平台常见 4 字节, 64 位常见 8 字节, 所占空间与编译平台的数据模型相关, 与所指类型无关.
同类型指针可以互相赋值, 本质是复制地址. int * 与 char * 不能直接互赋; 强转后数值相同, 但解引用宽度不同, 容易越界读写.
NULL 与 void *¶
Garbage Addresses and NULL Pointers | C Pointers, Arrays, and Strings
野指针指向不确定地址, 出错时机随机, 难查. 空指针把地址写成 0:
#define NULL ((void *)0) /* <stddef.h> */
int *p = NULL;
常见实现里地址 0 附近不会映射有效数据, 解引用立刻段错误, 比野指针更容易暴露.
void * 是通用对象指针: 与其他对象指针之间可隐式转换. 不能定义 void 变量, 也不能直接 *pv, 必须先转成具体类型再解引用. 接口里常用它表示"任意对象的地址", 例如 malloc 的返回类型.
指针参数与返回值¶
C 传参是值传递. 形参拿到的是实参的副本. 若要把调用方的 int 换掉, 传的是它的地址:
int *swap(int *px, int *py)
{
int temp = *px;
*px = *py;
*py = temp;
return px; /* 返回的也是地址值的副本 */
}
int i = 10, j = 20;
int *p = swap(&i, &j); /* *p 即 i */
swap(&i, &j) 相当于用 &i, &j 初始化形参 px, py. 函数里写 *px 就是在改 main 里的 i.
若函数要改的是调用方手里的那个指针本身, 形参类型则升一级成 T **.
返回指针时, 所指对象的生命周期必须覆盖调用方后续的使用. 返回局部数组地址会变成悬空指针:
char *bad(void)
{
char buf[32];
return buf; /* 返回后栈帧已释放 */
}
指针与数组¶
int a[10];
int *pa = a; /* 等价于 &a[0], 数组名做右值时衰减为首元素指针 */
pa++; /* 前进 sizeof(int) 字节, 指向 a[1] */
下标是语法糖: E1[E2] 等价于 *((E1)+(E2)). 因此 a[2], pa[2], *(a+2), *(pa+2) 同一套机制. 指针算术按元素个数步进, 不是按字节.
同一数组内的指针可以比较和相减 (结果是元素个数差). 两个指针相加无意义, 且ISO/IEC标准不允许.
数组名做左值时仍表示整块数组, 不能 a++ 或 a = pa. 但可以取 &a, 类型是指向整个数组的指针, 见下文.
形参里下面的三种写法在编译器看来是等价的, 都是指针:
void f(int a[10]);
void f(int a[]);
void f(int *a);
写成 [] 只是提示读者"这里通常是一段元素的首地址". 长度不会跟着传进去, 需要就另加参数, 比如在排序算法中常见的声明写法中形参都是数组指针+长度的形式 sort(int arr[], int len).
指针与 const¶
从右往左读, 或看 const 相对 * 的位置:
| 写法 | 可改指针 a | 可改 *a |
|---|---|---|
const int *a / int const *a | 是 | 否 |
int * const a | 否 | 是 |
int const * const a | 否 | 否 |
非 const 对象的地址可以隐式赋给 const T * (只读视图加严). 反过来不行, 否则可能透过非 const 指针改掉只读对象.
字符串字面量通常在只读段. 应用 const char * 承接:
const char *p = "abcd"; /* 推荐 */
下面的写法在编译时可能可以通过:
char *q = "abcd";
但是在后续的开发中有可能会碰到如下几个问题:
-
尝试写入
*q会触发未定义行为 若执行*q = 'A'(或q[0] = 'A')等于改只读数据. C 标准不规定此时必须怎样, 属于undefined behavior: 程序可能立刻崩溃, 也可能"看起来没事". -
可能段错误 典型实现里字面量在
.rodata, 尝试写入它会触发操作系统的保护机制(如segmentation fault, 段错误).
可变副本应使用字符数组进行初始化: char buf[] = "abcd";.
字面量与字符数组的区别参考笔记C Strings | C Pointers, Arrays, and Strings.
指针与结构体¶
struct unit {
char c;
int num;
};
struct unit u;
struct unit *p = &u;
(*p).c = 'x';
p->num = 1; /* 等价于 (*p).num */
. 作用于结构体值, -> 是"先解引用再取成员". 结构体往往较大, 接口里多传指针避免整份拷贝.
二级指针与指针数组¶
int i;
int *pi = &i;
int **ppi = π /* *ppi 是 pi, **ppi 是 i */
指针数组: 每个元素都是指针.
int *a[10];
int **pa = a; /* pa[i] 与 a[i] 同一元素, 类型是 int * */
main 的 argv 在原型里写成 char *argv[], 对编译器就是 char **argv: 指向"一串 char *"的首元素, 每个 char * 指向一个参数字符串, 末尾常有 NULL 哨兵.
int main(int argc, char *argv[])
{
int i;
for (i = 0; argv[i] != NULL; i++)
printf("argv[%d]=%s\n", i, argv[i]);
return 0;
}
argv[0] 是程序名 (或符号链接名). 习题里三种 const 与二级指针的组合:
-
const char **p:**p只读 -
char * const *p: 中间那一层指针只读 (*p不能改指向) -
char ** const p: 最外层指针本身只读
指向数组的指针与多维数组¶
int *a[10]; /* 10 个 int *, 指针数组 */
int (*pa)[10]; /* 一个指针, 指向"10 个 int 的数组" */
用 typedef 拆开记:
typedef int *t;
t a[10]; /* 同 int *a[10] */
typedef int t[10];
t *pa; /* 同 int (*pa)[10] */
&a 与 &a[0] 数值常相同, 类型不同: 前者是 int (*)[N], 后者是 int *. 步进时 pa++ 跳过整行 N * sizeof(int) 字节.
二维数组 int a[5][10] 的元素类型是 int [10], 所以指向一行的指针类型是 int (*)[10]:
int a[5][10];
int (*pa)[10] = &a[0]; /* 也可写成 a, 衰减成指向首行的指针 */
/* pa[1][2] 与 a[1][2] 同一元素 */
函数指针¶
函数名做右值时衰减成函数指针. 函数指针保存的是入口地址 (汇编码中通常在 .text 段).
void say_hello(const char *str)
{
printf("Hello %s\n", str);
}
void (*f)(const char *) = say_hello; /* 也可写 &say_hello */
f("Guys"); /* 直接用指针调用 */
(*f)("Guys"); /* 先解引用再调用, 效果相同 */
typedef 能把声明拆开:
typedef int F(void); /* 函数类型: 无参, 返回 int */
F *fp; /* 等价于 int (*fp)(void); */
函数不能返回函数类型或数组类型, 但可以返回函数指针.
按"类型标签"选实现时, 可用函数指针表代替一长串 if / switch, 让每种表示各自一组小函数, 通过表项分派. 这是低耦合的一种写法.
不完全类型与复杂声明¶
C 的类型大致分成函数类型, 对象类型, 不完全类型. 指针属于标量, 可当布尔值使用: NULL 为假, 非空为真.
不完全类型指暂时不知道空间占用大小的类型, 例如 struct s;, char str[];.
同一对象可经多次声明合成完全类型. 结构体里可以放指向尚未定义完整的结构体的指针, 因为指针大小已知; 但不能嵌套一个尚未完整的结构体对象本身 (因为可能会导致无穷大).
struct node {
char data[6];
struct node *next; /* 处理到这里时 struct node 仍不完全, 指针却已完整 */
};
熟悉数据结构的话, 这种写法应该不陌生. 这就是链表, 树等结构的基本单元, next == NULL 表示末端.
复杂声明按"标识符起, 先看右边再看左边, 括号优先"拆, 或用 typedef 分层:
/* signal(2) 风格 */
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
/* 再拆一例: fp 是指针 → 指向"参数 void *, 返回指向 int[10] 的指针"的函数 */
int (*(*fp)(void *))[10];
三条骨架:
-
T *p— 指针 -
T a[]— 数组 (形参例外, 变成指针) -
T1 f(T2, ...)— 函数
与 C++ 的衔接¶
C 的原始指针规则在 C++ 里仍然成立. 迭代器接口模仿指针的 *, ++, !=; 所有权则更多交给引用与智能指针.