Skip to content

指针是什么:一个变量,装着另一个变量的地址 ​

引言:指针这一卷的门槛 ​

C 语言基底那一卷咱们写了 12 章,刻意绕开了指针——只在讲数组那一章瞥过一眼“数组名退化成指针”,在字符串、IO 里也时不时要写 &a[i]、char*。可指针这东西,您越绕它、它越吓人。这一卷咱们正面攻它,而攻下它的钥匙只有一句话:指针不神秘,它就是一个“装着地址”的变量。这一章先不碰指针算术、不碰堆内存、不碰多级指针,就把“指针 = 地址”这一个事实立住,把 &(取地址)和 *(解引用)这两个运算符、指针的类型和大小、NULL 和野指针这几个根基概念真跑通。后面 11 章全建立在这个根基上。

从机器说起:变量有地址,指针装地址 ​

咱们要理解指针,得先在脑子里画一幅内存的图。计算机的内存(RAM)被分成一个个字节,每个字节能存 8 个比特,而且每个字节都有一个独一无二的编号、叫它的地址(从 0 开始往后数)。您声明一个变量,比如 int x = 42;,编译器就在内存里给它分配若干字节(int 通常 4 字节),这 4 个字节里第一个的地址,就叫**“x 的地址”**(§6.2.5 说变量的存储是有地址的)。

那“指针”是什么?ISO/IEC 9899:2011 §6.2.5p1 规定,指针类型是一种派生类型:“一个指针类型可以从一个对象类型派生,它指向那个类型的对象”。翻译成人话:指针变量就是一个专门用来“存地址”的变量。普通变量 int x 存的是一个整数 42;指针变量 int* p 存的是“某个 int 变量的地址”。如果 p 里存的是 x 的地址,咱们就说“p 指向 x”。仅此而已,指针就是地址,没有别的玄机。int* 里的那个 * 是“这是个指针”的标记,int 是“它指向的东西是 int 类型”。

& 取地址,* 解引用 ​

C 给指针配了一对运算符(§6.5.3.2):&(取地址,address-of)拿一个变量的地址、*(解引用,indirection)拿一个指针、顺着它存的地址去访问那个对象。咱们一对真跑就懂:

C
#include <stdio.h>

int main(void) {
    int x = 42;
    int* p = &x; /* p 存 x 的地址,「指向」x */

    printf("x = %d\n", x);
    printf("*p = %d\n", *p);          /* 通过地址读 x */
    printf("&x = %p\n", (void*)&x);   /* x 的地址 */
    printf("p  = %p\n", (void*)p);    /* p 里存的地址,和 &x 一样 */
    *p = 100;                         /* 通过地址改 x */
    printf("改 *p 后: x = %d\n", x);
    return 0;
}
终端
$ gcc -std=c11 -Wall basic.c -o b && ./b
x = 42
*p = 42
&x = 0x7ffe588a669c
p  = 0x7ffe588a669c
改 *p 后: x = 100

咱们看 int* p = &x; 把 x 的地址存进 p,于是 p 指向 x。读 *p 就是“顺着 p 里那个地址、把那块内存当 int 读出来”,得 42(和 x 一样)。&x 和 p 用 %p 打印出来是同一个地址 0x7ffe588a669c,这正是“p 存的就是 x 的地址”的直接证据(%p 是专门打印指针/地址的转换说明符,§7.21.6.1p8,注意它要求传 void*,所以要 (void*) 转一下;那个地址值每次运行都不一样,这是操作系统的 ASLR 地址随机化在做事,但 &x 和 p 一定相等)。

咱们看最关键的一行是 *p = 100;,它通过指针去改了 x,所以最后一行打印 x 已经是 100 了。一旦 p 指向 x,*p 就是 x 的别名:读 *p 等于读 x、写 *p 等于写 x。这就是指针最根本的能力——隔着地址去动另一个变量,第 3 章“用指针改调用者的变量”全靠这一招。

上面是书里的真跑。您想自己改指针、看 x 怎么变?试试这个(点“运行”就行;把 *p = 100 改成别的值、或让 p 指向另一个变量 y,看 x 和 *p 的关系,这就是“*p 是 x 的别名”):

Compiler Explorer

亲手玩:*p 是 x 的别名

int* p = &n 之后,*p 就是 n 的别名:读 *p 得 n 的值、改 *p 就是改 n。咱们改改 *p 的值、或让 p 指向另一个变量,看 n 怎么跟着变。

/demos/pointer_alias.c

咱们接着看 * 和 & 这对互逆运算符:& 把变量变成指向它的指针、* 把指针变回它指的变量。所以 *&x 这种怪写法,就是“取 x 的地址、再顺着地址取回那个对象”,等于 x 本身:

C
#include <stdio.h>

int main(void) {
    int x = 7;
    int y = *&x; /* * 是 & 的逆:*&x 就是 x */
    printf("x = %d, *&x = %d\n", x, y);
    return 0;
}
终端
$ gcc -std=c11 -Wall inverse.c -o inv && ./inv
x = 7, *&x = 7

运行结果是 *&x = 7,因为 *&x 就是 x 本身,求出来的值自然和 x 一样、也是 7。工程里没人真写 *&x(多此一举),这里只是用它印证“* 是 & 的逆”这件事。理解了这一点,您看到 *p 就能立刻反应过来:它是“p 指向的那个对象”、是个左值(能被赋值),和直接写那个对象是一回事。

指针的类型与大小 ​

咱们声明指针时必须写明“指向什么类型”:int* p(指向 int)、char* s(指向 char)、double* q(指向 double)。这个“指向类型”有意义吗?有,而且很关键——它决定解引用时读几个字节:*p 当 p 是 int* 时读 4 字节、当 char* 时只读 1 字节(指针算术的步长也由它定,下一章细讲)。但有趣的是,指针自己的大小和指向什么类型无关:

C
#include <stdio.h>

int main(void) {
    printf("sizeof(int*)    = %zu\n", sizeof(int*));
    printf("sizeof(char*)   = %zu\n", sizeof(char*));
    printf("sizeof(double*) = %zu\n", sizeof(double*));
    printf("sizeof(void*)   = %zu\n", sizeof(void*));
    /* 64 位机上,指针大小和它指向什么类型无关——存的都是地址 */
    return 0;
}
终端
$ gcc -std=c11 -Wall size.c -o sz && ./sz
sizeof(int*)    = 8
sizeof(char*)   = 8
sizeof(double*) = 8
sizeof(void*)   = 8

咱们看四个全 8。因为不管 int* 还是 char*,指针变量里装的都只是一份地址(一个编号),地址的宽度由机器的地址空间决定、和“指向的东西多大”无关。这台机器是 64 位的(地址总线 64 位),所以任何指针都是 8 字节(64 位);在 32 位机上所有指针都是 4 字节。这一点常被新手搞混:sizeof(int) 是 4、sizeof(int*) 是 8,因为前者是“一个 int 多大”、后者是“一个地址多大”,两码事。“指向类型”影响的是“解引用读几字节”“算术走几字节”,不影响指针本身的占地大小。

指针自己也是个变量,有自己的地址 ​

这一步是很多人卡住的地方:指针变量自己也在内存里、它自己也有地址。p 存着 x 的地址,但 p 这个变量本身存在内存某处,那块内存的地址是 &p,它和 p 里存的值(x 的地址)是两回事。真跑给咱们看:

C
#include <stdio.h>

int main(void) {
    int x = 42;
    int* p = &x;    /* p 指向 x */
    int** pp = &p;  /* pp 指向 p(指针的指针,多级指针的引子) */

    printf("&x  = %p\n", (void*)&x);
    printf("p   = %p  (p 的值 = x 的地址)\n", (void*)p);
    printf("&p  = %p  (p 自己的地址,和 p 的值不一样)\n", (void*)&p);
    printf("**pp = %d  (顺着 pp -> p -> x)\n", **pp);
    return 0;
}
终端
$ gcc -std=c11 -Wall ptr_of_ptr.c -o pp && ./pp
&x  = 0x7ffe9f86df44
p   = 0x7ffe9f86df44  (p 的值 = x 的地址)
&p  = 0x7ffe9f86df48  (p 自己的地址,和 p 的值不一样)
**pp = 42  (顺着 pp -> p -> x)

咱们看仔细这三行地址:&x 是 x 的地址(df44),p 的值也是 df44(因为 p 存的就是 x 的地址),可 &p 是 df48——这是 p 这个变量自己在内存里的位置,和 p 里存的值(df44)完全不是一回事。栈上 x 和 p 这两个变量被安排在了相邻的位置(df44 和 df48,差 4 字节,因为 x 是 int 占 4 字节),但谁挨着谁、隔多远是实现细节,重点是“p 有自己的地址 &p”。

既然 p 自己有地址,那就可以再声明一个指针来存它的地址,这就是 int** pp = &p;(一个“指向 int* 的指针”,即指针的指针)。顺着它走两步:*pp 得到 p(一个 int*)、**pp 再解一层得到 x(得 42)。多级指针听着吓人,但本质就是“指针装地址、装的是另一个指针的地址”这同一件事的套娃,第 8 章会专门展开。咱们先记住:指针是变量,变量就有地址,能想通这一句,多级指针、&p 这些就不晕了。

两个必崩的坑:NULL 与野指针 ​

指针的能力(隔着地址动内存)也是它的危险,指错了地方,轻则乱写别人内存、重则当场段错误。咱们最该警惕的两个坑,都源于“指针还没指向一个合法对象就被用了”。

先说 NULL 空指针。NULL(在 <stddef.h>/<stdio.h> 里定义,§7.19)是一个特殊的指针值,表示“明确不指向任何对象”。它常用来给指针一个“还没指向有效目标”的初始值、或表示“查无结果”(比如 strchr 找不到字符就返回 NULL,讲字符串那一章见过)。但解引用 NULL 是未定义行为(§6.3.2.3p3,空指针不指向任何对象,当然不能解引用),咱们真跑必崩:

C
#include <stdio.h>

int main(void) {
    int* p = NULL; /* 空指针:明确不指向任何对象 */
    *p = 42;       /* UB:解引用空指针 */
    return 0;
}
终端
$ gcc -std=c11 -Wall null.c -o nl && ./nl; echo $?
139

程序没来得及输出任何东西,咱们拿到的是退出码 139(128 + 11,SIGSEGV 段错误)。地址 0 那一页被操作系统刻意映射成不可访问,CPU 一执行写就触发段错误。开 ASan 看得更直白:

终端
$ gcc -std=c11 -Wall -fsanitize=address null.c -o nl_asan && ./nl_asan
==55412==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
SUMMARY: AddressSanitizer: SEGV (.../nl_asan+0x11a4) in main
==55412==ABORTING

SEGV on unknown address 0x000000000000——地址 0 就是 NULL,ASan 直接告诉咱们“您在解引用地址 0”。所以拿到一个可能是 NULL 的指针(比如函数返回值),先判空再用是基本素养:if (p != NULL) { *p = ...; }。

咱们看另一个坑更阴险:未初始化的野指针。声明 int* p; 却没给它赋值,p 里的内容是内存里残留的垃圾,是个随机地址。这时去 *p 就是“往一个随机地址写”,是 UB:

C
#include <stdio.h>

int main(void) {
    int* p;  /* 未初始化:野指针,p 的值是内存里的垃圾地址 */
    *p = 42; /* UB:解引用野指针 */
    return 0;
}
终端
$ gcc -std=c11 -Wall uninit.c -o un
uninit.c:5:8: warning: 'p' is used uninitialized [-Wuninitialized]
    5 |     *p = 42; /* UB:解引用野指针 */
      |       ~~~^~~~

gcc 的 -Wall 能在编译期就警告 'p' is used uninitialized,又一个 -Wall 救命的例子。这次裸跑它恰好也段错误了(垃圾地址落在不可写页),但咱们要记住 UB 的本质:它可能崩、可能不崩、可能“成功”地把 42 写进了别人那块内存里、引发完全无关的诡异 bug,哪种结果都不保证,这才是 UB 最可怕的地方。野指针比 NULL 更难查,因为 NULL 至少会很快崩在地址 0、栈里能看到,野指针可能“静悄悄作恶”很久。所以咱们说指针的生命从“初始化”开始:要么 int* p = &x; 指向一个已知对象、要么 int* p = NULL; 明确置空、要么(第 6 章)int* p = malloc(...) 从堆要一块,绝不留一个未初始化的指针悬着。

小结 ​

指针不是神秘事物,它就是一个“装着地址”的变量(§6.2.5p1,从对象类型派生的派生类型):内存分字节、每字节有地址,变量占若干字节、其首字节地址就是“变量的地址”,指针变量专门存这种地址。&(取地址,§6.5.3.2p3)拿一个变量的地址、*(解引用,§6.5.3.2p4)顺着指针里存的地址去访问那个对象,两者互逆(*&x 就是 x)。一旦 int* p = &x,*p 就是 x 的别名(真跑读 *p 得 42、改 *p 让 x 变 100),这是“隔着地址动另一个变量”的全部魔力,也是第 3 章用指针改调用者变量的根基。指针必须写明指向类型(int*/char*),它决定解引用读几字节、算术走几字节,但指针自己的大小和指向类型无关:64 位机上 int*/char*/double*/void* 的 sizeof 全是 8(存的都是同样宽的地址)。要破除“多级指针”的迷雾,咱们记住一句:指针自己也是变量、也有自己的地址(真跑 &p 和 p 的值不是一回事),于是可以有“指向指针的指针”int** pp = &p、**pp 顺着走两层(第 8 章展开)。两个必崩的坑:解引用 NULL 是 UB(地址 0 不可访问,真跑段错误 139、ASan 报 SEGV on address 0x000000000000);未初始化的野指针更阴险(gcc -Wuninitialized 能抓,但 UB 可能静悄悄乱写别人内存而不崩),所以指针的生命从初始化开始:指向已知对象、或置 NULL、或从 malloc 要,绝不悬空。%p 是打印地址的转换说明符(要 (void*) 转一下,地址值随 ASLR 每次不同)。下一章咱们把指针和讲数组那一章接上,进入指针算术,看 p + n 怎么“跳 n 个元素”而不是 n 个字节。

参考资源 ​

  • ISO/IEC 9899:2011 §6.2.5p1(指针类型派生自对象类型)、§6.5.3.2(& 取地址 p3、* 解引用 p4)、§6.3.2.3(指针转换:p2 整型 0 转空指针、p3 空指针不可解引用为 UB)、§7.19(<stddef.h> 的 NULL 宏)、§7.21.6.1p8(%p 转换说明符、要求 void*)
  • K. N. King《C Programming: A Modern Approach》第 11 章 Pointers(11.1 指针变量的机器级含义、11.2 & 与 * 运算符、指针必须初始化)
  • Robert C. Seacord《Effective C》第 2 章·Pointer Types 小节(指针类型、对象与指针的关系)
  • C 语言基底·第 1 章:程序结构与编译四阶段(变量在内存里)、C 语言基底·第 2 章:整型与 sizeof、C 语言基底·第 10 章:数组(数组名退化为指针、a[i]≡*(a+i))、C 语言基底·第 12 章:基础 IO(%p、& 取地址给 scanf、类型不匹配 UB)
  • 开发环境·第 5 章:编译阶段看汇编(内存段布局);开发环境·第 11 章:Sanitizer 门禁(本章 NULL/野指针验证用 ASan);第 2 章:指针算术;第 8 章:多级指针

87bb5f2 · 87bb5f2 · 2026-09-21