把内存想成一排编了号的房间:每个字节一个门牌号(地址)。变量占据一个或多个房间,房间号就是它的地址。
值和类型是两回事:同一块 4 字节内存,按 int 读是 65,按 float 读是另一个数,按 char* 读是 4 个房间中的第 1 个门牌。类型是编译器给地址附加的「解读说明书」——所以 int *p 声明的是「p 存地址,且按 int 格式解读目标」。
int a = 10;
int *p = &a; /* & 取地址:p 存下 a 的门牌号 */
printf("%d\n", *p); /* * 间接访问:按门牌上门取货,打印 10 */
*p = 20; /* 上门「改货」:a 变成 20 */
printf("%d\n", a); /* 20 */& 和 * 互为逆操作:*&a 就是 a 本身。结合左值概念理解第二处赋值——*p 标识了「a 的存储位置」,所以它是左值,能被赋值。这正是函数内改外部变量、数组遍历、链表操作的全部底层原理:
void swap(int *x, int *y) /* C 只能传值:传地址的值才能改外面 */
{
int t = *x;
*x = *y;
*y = t;
}
swap(&a, &b); /* 交出两块房间的门牌号 */指针变量自己也有地址,于是可以存进另一个「更高级」的指针:
int a = 5;
int *p = &a;
int **pp = &p; /* pp 指向「指向 int 的指针」 */
printf("%d\n", **pp); /* 两次解引用:pp -> p -> a */
*pp = NULL; /* 能改 p 本身!这是二级指针的杀手锏 */二级指针的经典用途:在函数里修改指针本身。比如「往链表头部插入节点」要把头指针换成新节点,调用方必须交出头指针的地址(第 12 章实战)。读复杂指针声明用右左法则:从标识符出发,先右后左交替结合:
int **p; /* p 是指针,指向「int*」:二级指针 */ int *v[5]; /* v 先结合[]:数组,元素是 int* —— 指针数组 */ int (*w)[5]; /* 括号先结合 *:w 是指针,指向「int[5]」—— 数组指针 */
指针 ± 整数,不是门牌号加减,而是按所指向类型的大小跳跃:
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; /* 数组名退化为首元素地址 */
printf("%d\n", *p); /* 10 */
printf("%d\n", *(p + 2));/* 30:跨过 2 个 int = 8 字节 */
int *q = &arr[4];
printf("%td\n", q - p); /* 4:同数组内相减得元素个数(类型 ptrdiff_t) */
if (p < q) { /* 同数组内可比较大小:指向靠前者更小 */ }| 运算 | 合法性 | 说明 |
|---|---|---|
| p + n / p − n | 限定于数组内(含末尾后一格) | 越出范围是未定义行为 |
| q − p | 仅同一数组 | 结果为元素个数差,不是字节差 |
| p < q / p == q | 仅同一数组(NULL 除外) | 跨数组比较无意义且未定义 |
| p + q | 非法 | 两个指针相加毫无意义 |