一、指针基础:内存地址的概念、指针的定义与初始化、取地址符&与解引用符*
各位同学,咱们今天聊指针。说实话,指针是C语言里最让新手头疼的东西,但也是C语言的灵魂。我做了十几年嵌入式开发,可以负责任地告诉你——搞不懂指针,你写不出高质量的C代码。
先别急着害怕。指针没那么玄乎。说白了,它就是地址。你想想看,你家有个门牌号,内存里的每个字节也有个编号,这个编号就是地址。指针就是用来存这个编号的变量。
1.1 内存地址——每个字节都有门牌号
计算机的内存,你可以想象成一排连续的小格子。每个格子能存1个字节(8个比特)。每个格子都有一个唯一的编号,这个编号就是内存地址。
举个例子,你定义一个变量:
int a = 100;
编译器会在内存里找一块地方放这个100。假设它从地址0x1000开始,占了4个字节(int通常是4字节),那么地址0x1000~0x1003这4个字节里存的就是100这个数。
我在项目中遇到过一件事:有个同事调试程序,发现某个全局变量莫名其妙被改了。查了两天,最后发现是数组越界写,把隔壁变量的内存给覆盖了。你看,理解地址有多重要。
核心概念:内存地址是内存字节的编号,从0开始,以字节为单位。32位系统地址范围0x00000000~0xFFFFFFFF,64位系统更大。
1.2 指针的定义——存地址的变量
指针变量,就是用来存放内存地址的变量。定义指针的语法很简单:
类型名 *指针变量名;
比如:
int *p; // p是一个指针,指向int类型的数据
char *cp; // cp是一个指针,指向char类型的数据
double *dp; // dp是一个指针,指向double类型的数据
这里的星号*是类型的一部分,告诉编译器:我要定义一个指针变量。类型名决定了这个指针能指向什么类型的数据。
我的习惯:写指针定义时,我喜欢把星号紧挨着变量名,比如int *p。这样读起来是「p是一个指针,指向int」。当然也有人写成int* p,语法上没错,但容易让人误解int*是一个整体类型——其实不是,int* p, q里只有p是指针,q是普通int。
1.3 取地址符&——拿到变量的门牌号
有了变量,怎么知道它的地址?用取地址符&。
int a = 100;
int *p = &a; // p保存了a的地址
这里&a就是变量a的地址。我们把a的地址存到了指针p里。现在p指向了a。
你可以打印出来看看:
printf("a的地址: %p\n", &a);
printf("p的值: %p\n", p);
// 这两个打印结果是一样的
嗯,这里要注意:%p是专门打印地址的格式符,输出十六进制形式。
1.4 解引用符*——通过门牌号找到人
有了地址,怎么访问地址里存的数据?用解引用符*。
int a = 100;
int *p = &a;
printf("%d\n", *p); // 输出100,*p就是p指向的那个变量
*p = 200; // 等价于 a = 200
printf("%d\n", a); // 输出200
*p的意思是:取出p里存的地址,然后访问那个地址里的数据。说白了就是「顺着地址找过去,拿到里面的值」。
我曾经踩过的坑:刚学指针时,我总把定义时的*和使用时的*搞混。定义时的int *p是说「p是一个指针」,使用时的*p是说「p指向的那个变量」。这是两码事!
1.5 指针的初始化——别让指针乱指
指针定义后一定要初始化。不初始化的指针叫「野指针」,它里面存的是垃圾值,指向哪里你根本不知道。用野指针写数据,轻则程序崩溃,重则破坏系统数据。
我见过最惨的一次:一个同事的野指针恰好指向了操作系统的关键数据结构,一赋值系统直接死机。从那以后,我养成了两个习惯:
- 定义指针时立即初始化——要么赋有效地址,要么赋NULL
- 使用前检查是否为NULL
int a = 100;
int *p1 = &a; // 正确:指向已有变量
int *p2 = NULL; // 正确:指向空,表示不指向任何地方
int *p3; // 危险!野指针
// 使用前检查
if (p2 != NULL) {
*p2 = 200; // 不会执行,因为p2是NULL
}
if (p1 != NULL) {
*p1 = 200; // 安全,a被改为200
}
黄金法则:指针要么指向有效的内存,要么指向NULL。永远不要留一个未初始化的指针在那里。
1.6 指针的大小——跟类型无关,跟系统有关
很多人以为int指针占4字节,char指针占1字节。错!指针的大小只跟系统位数有关,跟指向的类型无关。
| 系统位数 | 指针大小 | 地址范围 |
|---|---|---|
| 32位 | 4字节(32位) | 0x00000000 ~ 0xFFFFFFFF |
| 64位 | 8字节(64位) | 0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF |
printf("int* 大小: %zu\n", sizeof(int*)); // 32位系统输出4,64位输出8
printf("char* 大小: %zu\n", sizeof(char*)); // 同上
printf("double* 大小: %zu\n", sizeof(double*)); // 同上
为什么会这样?因为指针存的是地址,地址的位数由系统地址总线决定,跟指向的数据类型没关系。你想想看,门牌号的长度跟房子里住的是人还是狗有关系吗?没有。
1.7 知识体系总览
下面这张图把指针基础的核心概念串起来了,你看一遍就能理清关系:
1.8 避坑指南——我踩过的那些坑
最后,分享几个我实际项目中遇到的指针问题,你记住了能少走弯路:
- 野指针:定义后不初始化,直接使用。后果是随机崩溃。解决方案:定义时赋NULL。
- 空指针解引用:对NULL指针用
*操作。程序直接段错误。解决方案:解引用前检查是否为NULL。 - 类型不匹配:比如把
int*赋给char*。编译器会警告,但很多人忽略。结果解引用时读的数据完全不对。 - 混淆定义和解引用:
int *p;里的*是定义的一部分,*p = 10;里的*是解引用。初学者最容易搞混。
一个小技巧:读指针声明时,从右往左读。比如int *p,从右往左:p是一个指针,指向int。再比如int **pp,从右往左:pp是一个指针,指向一个指针,那个指针指向int。这样读,再复杂的指针声明也能看懂。
好了,指针基础就讲到这里。记住三个核心:地址是什么、怎么取地址、怎么通过地址访问数据。把这三点吃透了,后面的指针运算、指针和数组、指针和函数,学起来就轻松多了。
公众号:蓝海资料掘金营,微信deep3321