指针变量的操作与解引用
学习目标
学完本篇,你会掌握:
- 指针变量的声明与初始化方式。
*解引用运算符的含义与用法。- 如何通过指针修改外部变量。
- 空指针
NULL和野指针的危险。 - 指针
+1、-1到底跨越多少字节。
指针变量的声明与初始化
上一篇我们知道,指针变量保存的是地址。声明方式是在类型后面加一个 *:
int *p; // p 可以保存 int 型变量的地址
char *pc; // pc 可以保存 char 型变量的地址
double *pd; // pd 可以保存 double 型变量的地址
* 是类型的一部分,写在类型和变量名之间都行。下面两行是等价的:
int* p;
int *p;
我推荐 int *p;,因为它更清楚地表达“*p 是一个 int”。
初始化时,必须给它一个地址:
int a = 10;
int *p = &a; // p 指向 a
解引用:顺着地址找到值
* 放在指针前面,表示“取出这个地址里的内容”,也叫解引用。
#include <stdio.h>
int main(void) {
int a = 10;
int *p = &a;
printf("a = %d\n", a);
printf("*p = %d\n", *p); // 通过 p 访问 a 的值
*p = 20; // 通过 p 修改 a 的值
printf("修改后 a = %d\n", a);
return 0;
}
输出:
a = 10
*p = 10
修改后 a = 20
关键理解:*p 和 a 在这里是同一个内存位置的两种叫法。你改 *p,就是改 a。
用指针修改函数外部的变量
还记得函数值传递的痛点吗?函数内部修改参数,外面不变。现在用指针就能解决:
#include <stdio.h>
void swap(int *x, int *y) {
int tmp = *x;
*x = *y;
*y = tmp;
}
int main(void) {
int a = 3, b = 5;
printf("交换前:a=%d, b=%d\n", a, b);
swap(&a, &b);
printf("交换后:a=%d, b=%d\n", a, b);
return 0;
}
输出:
交换前:a=3, b=5
交换后:a=5, b=3
为什么不用普通参数传值
如果 swap 写成 void swap(int x, int y),函数里交换的是副本,main 里的 a、b 不会变。传地址进去,才能操作原变量。
用 const 保护只读参数
用指针传参虽然能修改外部变量,但有些时候你只是希望避免复制大块数据(比如后面会学的数组或结构体),并不需要修改它。这时可以用 const 告诉调用者:这个指针指向的数据是只读的,我不会改它。
#include <stdio.h>
void print_value(const int *p) {
// *p = 10; // 编译错误!const 指针指向的内容不能修改
printf("%d\n", *p);
}
int main(void) {
int x = 42;
print_value(&x);
return 0;
}
加上 const 的好处是:
- 接口自文档化——看到
const int *,调用者就知道这个函数不会修改传入的数据。 - 编译器帮你检查——如果你在函数里不小心写了尝试修改的代码,编译器会直接报错,而不是让你在运行时排查。
- 更安全——防止无意中改掉不该改的数据。
const 的位置
const int *p 表示“指向 const int 的指针”,指针本身可以变(可以指向别处),但不能通过它修改指向的内容。如果写 int *const p,则表示“指针本身是常量”,不能改变指向,但可以通过它修改内容。初学阶段先掌握 const int *p 这种最常见的用法即可。
空指针 NULL
如果一个指针暂时不指向任何有效地址,最好让它指向 NULL:
int *p = NULL;
NULL 通常等价于 (void *)0,表示“这个指针不指向任何地方”。
访问 NULL 指针会导致程序崩溃,但崩溃好过“悄悄改到不该改的内存”。所以判断指针是否为空是良好习惯:
if (p != NULL) {
printf("%d\n", *p);
}
野指针与未初始化指针
比 NULL 更危险的是野指针:它指向一个未知、无效、甚至已经被释放的地址。
int *p; // 未初始化,里面是个随机地址
*p = 10; // 灾难:往一个随机地址写数据
这种错误往往不会立刻报错,但可能破坏其他数据,导致程序莫名其妙地崩溃。养成良好的初始化习惯:
int *p = NULL; // 暂时没有目标,就指向 NULL
释放内存后,也应该把指针置空,避免它变成“悬空指针”:
free(p);
p = NULL;
指针的赋值
指针之间可以互相赋值,前提是类型匹配:
int a = 10;
int *p = &a;
int *q = p; // q 也指向 a
*q = 99;
printf("a = %d\n", a); // 输出 a = 99
不同类型的指针不要直接互相赋值,否则会触发警告或导致未定义行为。
指针的基本算术
指针可以做加减运算,但这里的 +1 不是地址加 1 个字节,而是加“一个元素的大小”。
#include <stdio.h>
int main(void) {
int arr[3] = {10, 20, 30};
int *p = arr;
printf("p 指向:%d\n", *p);
printf("p 的地址:%p\n", (void *)p);
p = p + 1;
printf("p+1 指向:%d\n", *p);
printf("p+1 的地址:%p\n", (void *)p);
return 0;
}
输出中你会发现,p+1 的地址比 p 的地址大了 4(在 32/64 位系统上 int 通常是 4 字节)。这就是“加 1 跳过一个 int”。
同理,对于 char *,+1 跳 1 个字节;对于 double *,+1 跳 8 个字节。
指针算术依赖类型
指针加减的单位是“元素大小”,而不是字节。C 语言之所以这样设计,是为了让数组遍历更自然。下篇会深入讲指针和数组的关系。
常见错误
- 未初始化就解引用。
int *p; *p = 10;是典型的野指针错误。 - 解引用 NULL 指针。
int *p = NULL; printf("%d", *p);会崩溃。 - 混淆
*在声明和表达式中的含义。声明里int *p表示 p 是指针;表达式里*p表示取内容。 - 把整数直接赋给指针。
int *p = 100;只在极少数底层场景有意义,初学不要这么写。
const 与指针的三种常见组合
const int *p1; // 不能通过 p1 修改目标,p1 可以改指向
int *const p2 = &x; // p2 不能改指向,可以修改目标
const int *const p3 = &x; // 两者都不能通过该名字修改
阅读时从变量名向外看,并确认 const 修饰的是目标类型还是指针本身。
指针算术只在同一数组内有意义
C 允许指针在同一数组对象内移动,并允许形成“尾后指针”:
int data[4];
int *begin = data;
int *end = data + 4; // 可以比较或作为循环终点,但不能解引用
for (int *p = begin; p != end; p++) {
printf("%d\n", *p);
}
越过尾后位置、对互不相关对象的指针做减法或顺序比较,都不属于通用合法操作。
void * 失去元素类型
void * 可以保存对象地址,C 中可与对象指针互转,但不能直接解引用,也不能按标准 C 做指针加法。使用前必须恢复为正确类型。它适合通用接口,不应成为绕过类型系统的借口。
空指针只表示“不指向对象”
NULL 不等于悬空指针,也不等于未初始化指针。空指针可比较、可传递、可 free,但不能解引用。接口允许空值时要写清语义;不允许时应在入口检查或用文档建立前置条件。
小结
*在表达式中表示解引用,用来读写指针指向的内容。- 传指针给函数,可以让函数修改调用者的变量。
- 不确定指向时把指针设为
NULL,不要让它悬空。 - 指针
+1跳过“一个元素大小”,而不是一个字节。
下一篇预告
下一篇《指针与数组的映射关系》会揭示数组名和指针之间的紧密联系,以及为什么 arr[i] 和 *(arr + i) 是等价的。