1. 指针的本质:内存地址与变量、指针的声明与初始化、取地址符&与解引用符*
指针这东西,说白了就是地址。我刚开始学C的时候,总觉得指针很玄乎,好像是什么高深莫测的东西。后来踩的坑多了,才明白——指针就是一个装着内存门牌号的变量。你想想看,我们平时用变量名访问数据,编译器帮我们查地址;而指针呢,是我们自己拿着地址去敲门。
1.1 内存地址与变量
每个变量在内存里都有一块地盘。这块地盘的起始位置,就是它的地址。我习惯把内存想象成一排带编号的柜子,每个柜子能放一个字节的数据。变量名只是贴在上面的标签,真正定位数据的是柜子编号——也就是地址。
核心概念:变量名是给程序员看的,地址是给CPU看的。指针就是连接这两者的桥梁。
举个例子,你声明了一个 int a = 10;。编译器会在内存里找一块4字节的空间(假设是32位系统),把10放进去。这块空间的起始地址,比如 0x7ffd1234,就是变量a的地址。你写 a 的时候,编译器自动帮你翻译成对这个地址的操作。
我在项目中遇到过一个问题:一个同事用全局变量传参,结果多线程环境下数据老是被篡改。查了半天,发现是不同线程通过变量名访问了同一块内存。后来改成用指针传递局部变量的地址,反而更安全——因为你能清楚地知道数据在哪、谁在用。
1.2 指针的声明与初始化
指针的声明,语法上就是 类型 * 变量名;。这个星号*的位置其实挺灵活的,可以靠类型、靠变量名、或者放中间。我个人习惯把星号靠变量名放,比如 int *p;,这样读起来就是「p是一个指针,指向int类型的数据」。
int *p; // 声明一个指向int的指针
char *cp; // 声明一个指向char的指针
void *vp; // 泛型指针,可以指向任何类型
注意:声明指针后,它里面存的是垃圾值。这时候解引用它,等于拿着一个随机地址去访问内存——轻则段错误,重则把别人的数据改得乱七八糟。我曾经在调试一个嵌入式程序时,就是因为忘了初始化指针,结果指针指向了中断向量表,一写数据系统直接崩溃。
初始化指针,就是把一个合法的地址赋给它。常见的方式有:
- 取已有变量的地址:
int a = 10; int *p = &a; - 动态分配内存:
int *p = (int *)malloc(sizeof(int)); - 指向数组首元素:
int arr[5]; int *p = arr; - 初始化为NULL:
int *p = NULL;(表示指针不指向任何有效内存)
嗯,这里要注意:NULL 和未初始化是两码事。NULL 是明确的「空」,你可以在代码里检查它;未初始化是「未知」,你根本不知道它指向哪。
1.3 取地址符&与解引用符*
这两个符号是指针操作的核心。取地址符&,作用是获取变量的内存地址。解引用符*,作用是通过地址访问变量本身。
int a = 10;
int *p = &a; // &a 取出a的地址,赋给p
printf("a的值: %d\n", a); // 直接访问
printf("a的地址: %p\n", &a); // 取地址
printf("p的值: %p\n", p); // p里存的就是a的地址
printf("通过p访问a: %d\n", *p); // 解引用,得到a的值
*p = 20; // 通过指针修改a的值
printf("修改后a的值: %d\n", a); // 输出20
你想想看,& 和 * 其实是互逆的操作。& 从变量到地址,* 从地址到变量。就像门牌号和房子之间的关系——你拿着门牌号能找到房子,你站在房子前也能知道门牌号。
小技巧:我调试指针相关bug时,经常在关键位置打印指针的值和它指向的值。比如 printf("p=%p, *p=%d\n", p, *p);。这样一眼就能看出指针是不是NULL、指向的数据对不对。很多越界问题,其实在打印时就能发现端倪。
1.4 指针的常见误区
我见过不少新手(甚至老手)在指针上翻车。这里列几个典型的坑:
| 误区 | 错误示例 | 正确做法 |
|---|---|---|
| 声明多个指针时漏写星号 | int* p, q;(q是int,不是指针) |
int *p, *q; |
| 解引用未初始化的指针 | int *p; *p = 10; |
先让p指向合法内存 |
| 混淆指针和指针指向的值 | int *p = &a; p = 10;(把10赋给了地址) |
*p = 10; |
| 对NULL指针解引用 | int *p = NULL; *p = 10; |
先判断p != NULL |
我曾经在维护一个遗留系统时,发现一个函数里声明了十几个指针,一半都没初始化。调用方传进来的参数也是各种野指针。那段时间我每天的工作就是加 if (ptr != NULL) 判断,加得我快吐了。后来我养成了一个习惯:声明指针的同时就初始化,要么赋有效地址,要么赋NULL。
1.5 本章知识体系
下面这张图,是我梳理的指针基础核心逻辑。你看一遍,应该能对指针有个整体的认识。
这张图从左到右展示了指针工作的完整链路:变量 → 取地址 → 指针 → 解引用 → 值。下面那部分内存模型,是我特意画的——你看,变量a占了4个字节的柜子,指针p里存的就是a的起始柜子编号0x1000。用*p解引用,就是去0x1000这个柜子里拿数据。
我的习惯:每次写指针代码前,我都会在脑子里过一遍这个图。变量在哪?指针指向哪?解引用后得到什么?想清楚了再动手。这个习惯帮我避免了很多低级错误。
好了,指针的基础就这些。说白了就是三件事:声明一个存地址的变量、用&拿到地址、用*通过地址访问数据。别把它想复杂了,它就是内存世界的导航系统。