一、指针基础:内存地址的概念、指针的定义与初始化、取地址符&与解引用符*

各位同学,咱们今天聊指针。说实话,指针是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 指针的初始化——别让指针乱指

指针定义后一定要初始化。不初始化的指针叫「野指针」,它里面存的是垃圾值,指向哪里你根本不知道。用野指针写数据,轻则程序崩溃,重则破坏系统数据。

我见过最惨的一次:一个同事的野指针恰好指向了操作系统的关键数据结构,一赋值系统直接死机。从那以后,我养成了两个习惯:

  1. 定义指针时立即初始化——要么赋有效地址,要么赋NULL
  2. 使用前检查是否为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 知识体系总览

下面这张图把指针基础的核心概念串起来了,你看一遍就能理清关系:

指针基础核心概念 内存 0x1000 地址 0x1004 地址 ... a = 100 指针变量 p 0x1000 变量名 存的是地址 指向 两个核心操作符 &a → 取变量a的地址 *p → 访问p指向的数据 总结 指针 = 存地址的变量 | & = 取地址 | * = 取数据 指针大小由系统位数决定,与指向类型无关

1.8 避坑指南——我踩过的那些坑

最后,分享几个我实际项目中遇到的指针问题,你记住了能少走弯路:

  • 野指针:定义后不初始化,直接使用。后果是随机崩溃。解决方案:定义时赋NULL。
  • 空指针解引用:对NULL指针用*操作。程序直接段错误。解决方案:解引用前检查是否为NULL。
  • 类型不匹配:比如把int*赋给char*。编译器会警告,但很多人忽略。结果解引用时读的数据完全不对。
  • 混淆定义和解引用:int *p;里的*是定义的一部分,*p = 10;里的*是解引用。初学者最容易搞混。

一个小技巧:读指针声明时,从右往左读。比如int *p,从右往左:p是一个指针,指向int。再比如int **pp,从右往左:pp是一个指针,指向一个指针,那个指针指向int。这样读,再复杂的指针声明也能看懂。

好了,指针基础就讲到这里。记住三个核心:地址是什么、怎么取地址、怎么通过地址访问数据。把这三点吃透了,后面的指针运算、指针和数组、指针和函数,学起来就轻松多了。


公众号:蓝海资料掘金营,微信deep3321