C 语言指针的理解与使用第一节
指针基础
本文所有示例均遵循 C23 标准。相比旧版本,C23 最显著的变化是引入了 nullptr 关键字取代 C 风格的 NULL,与 C++ 的惯例保持一致。
编写示例时遵循以下约定:
- 使用
nullptr而非NULL表示空指针。 - 取地址时显式转换为
(void *)var或(void *)&var,确保类型明确。 - 优先采用显式代码风格,避免依赖编译器的隐式转换。
指针变量的定义与格式语法:
int vt = {9};
int *p = &vt;
printf("var -> vt : %d\n", vt);
printf("var -> *p : %d\n", *p); // 解引用操作
printf("ptr -> vt : %p\n", (void *)&vt);
printf("ptr -> *p : %p\n", (void *)&p);
printf("ptr -> *p\n");
printf(" |\n");
printf(" +> vt : %p\n", (void *)p);
/**
* vt 是普通变量,存储值(9),&vt 获取 vt 的内存地址。
* p 是指针变量,存储 vt 的地址值;指针本身大小在32位系统为4字节,64位系统为8字节。
* *p 是对 p 的解引用,通过 p 中存储的地址访问目标值(9)。
* &p 获取指针变量 p 自身的内存地址(与 p 中存储的 vt 地址不同)。
*
* 因此一个指针变量涉及两个地址:
* 1. 指针自身的地址(&p)
* 2. 指针存储的目标地址(p,即 &vt)
*
* 程序执行结果:
*
* var -> vt : 9
* var -> *p : 9
* ptr -> vt : 0x7ffc7b93e17c
* ptr -> *p : 0x7ffc7b93e180
* ptr -> *p
* |
* +> vt : 0x7ffc7b93e17c
*/
Stack Memory Layout
Address grows from high to low (direction: low addr)
-------------------------------------------------------
High Addr Low Addr
──────────────────────────────────────────────────────────────────────────►
0x7ffc7b93e180 0x7ffc7b93e17c
┌────────────────────────┐ ┌────────────────────────┐
│ int *p │ │ int vt │
│ ┌──────────────────┐ │ │ ┌──────────────────┐ │
│ │ 0x7ffc7b93e17c │ │───────────┼─►│ 9 │ │
│ │ (addr of vt) │ │ points │ │ (data) │ │
│ └──────────────────┘ │ to │ └──────────────────┘ │
│ 8 byte (64-bit) │ │ 4 byte │
└────────────────────────┘ └────────────────────────┘
▲ ▲
│ │
&p = 0x7ffc7b93e180 &vt = 0x7ffc7b93e17c
(addr of p itself) (addr of vt itself)
┌──────────────────────────────────────────────────────────────────────────
│ 注解:
│
│ int vt = {9}; // vt 是普通变量,存储值 9,&vt 获取 vt 的内存地址
│ int *p = &vt; // p 是指针变量,存储 vt 的地址值
│
│ 一个指针变量涉及两个地址:
│ 1. &p — 指针自身的地址 (0x7ffc7b93e180)
│ 2. p — 指针存储的目标地址 (= &vt, 0x7ffc7b93e17c)
│
│ *p 是解引用操作,通过 p 中存储的地址访问目标值 (9)
│ 指针本身大小: 32 位系统为 4 字节,64 位系统为 8 字节
└──────────────────────────────────────────────────────────────────────────
════════════════════════════════════════════════════════════════════════════
Expression Summary
════════════════════════════════════════════════════════════════════════════
Expression Meaning Value
────────── ─────── ─────
vt value of variable 9
&vt address of variable 0x7ffc7b93e17c
p value stored in pointer (= &vt) 0x7ffc7b93e17c
&p address of pointer itself 0x7ffc7b93e180
*p dereference (= vt) 9
════════════════════════════════════════════════════════════════════════════
Pointer Relationship
════════════════════════════════════════════════════════════════════════════
┌───────────┐ p (= &vt) ┌───────────┐
│ p │ ────────────────►│ vt │
│ stores │ │ = 9 │
│ addr of vt│ │ (data) │
└───────────┘ └───────────┘
▲ *p deref yields ──────┘
│ the value of vt (9)
│
&p (addr of p itself,
differs from &vt)
┌──────────────────────────────────────────────────────────────────────────
│ 注解:
│
│ 箭头是关键: p 里存的值恰好等于 &vt,所以通过 p 可以"跳"到 vt
│ *p 等价于 vt — 解引用 *p 就是沿着箭头找到 vt 的值 9
│
│ 注意: 实际栈增长方向因系统/编译器而异,上图为 x86-64 Linux 典型布局
│ vt 地址 < p 地址,说明 vt 先入栈(高地址),p 后入栈(低地址)
└──────────────────────────────────────────────────────────────────────────
const 指针
如果希望指针指向的数据不可被修改,需要用 const 修饰指针的基类型:
仅保护指向的数据(单 const)
注意:此处 const 限定的是 var 所指向的数据(即 *var 不可写),而指针变量自身仍可重新指向另一块内存。 若要同时禁止指针本身的重新赋值,则需再加一个 const
const char *var = {"00000000000000000000000000"};
printf("指针变量自身地址: %p\n", (void *)&var);
printf("指针变量值值地址: %p\n", (void *)var);
printf("指针变量指向的值: %s\n", var);
printf("\n");
var = "11111111111111111111111111"; // 更新值: 指针重新指向一块新的区域
printf("指针变量自身地址: %p\n", (void *)&var);
printf("指针变量值值地址: %p\n", (void *)var);
printf("指针变量指向的值: %s\n", var);
printf("\n");
第一次赋值: const char *var = "00000000000000000000000000";
栈区 (Stack) 只读数据区 (.rodata)
┌──────────────┐ ┌──────────────────────────────┐
│ var │ │ "00000000000000000000000000" │ 0x404020
│ ┌──────────┐ │ │ "11111111111111111111111111" │ 0x404039
│ │ 0x404020 │─┼────────► │ │
│ └──────────┘ │ └──────────────────────────────┘
│ &var=0x7ffd..│
└──────────────┘
第二次赋值: var = "11111111111111111111111111";
栈区 (Stack) 只读数据区 (.rodata)
┌──────────────┐ ┌──────────────────────────────┐
│ var │ │ "00000000000000000000000000" │ 0x404020
│ ┌──────────┐ │ │ "11111111111111111111111111" │ 0x404039
│ │ 0x404039 │─┼────────► │ ▲ │
│ └──────────┘ │ └──────────────┼───────────────┘
│ &var=0x7ffd..│ keep │
└──────────────┘ 指针重新指向了这里
同时保护指针与数据(双 const)
同时禁止指针本身的重新赋值:
const char *const let = {"66666666666666666666666666"};
printf("指针变量自身地址: %p\n", (void *)&let);
printf("指针变量值值地址: %p\n", (void *)let);
printf("指针变量指向的值: %s\n", let);
printf("\n");
// 如果强行修改:
// let = "11111111111111111111111111"; // 更新值: 指针重新指向一块新的区域
// 消息提示来自 IDE: CLion
// 编译前IDE提示:
// 无法分配给 只读类型 const char * const
// 编译时会看到一下错误,且无法通过编译:
//
// test00.c:62:9: error: 向只读变量‘let’赋值
// 62 | let = "11111111111111111111111111"; // 更新值: 指针重新指向一块新的区域
// | ^
// ninja: build stopped: subcommand failed.
/**
* 注释错误行: let = "11111111111111111111111111";
* 编译后结果:
*
* 指针变量自身地址: 0x7fff063ab580
* 指针变量值值地址: 0x55ea564ae0f4
* 指针变量指向的值: 66666666666666666666666666
*/
Memory Layout: const char *const let
──────────────────────────────────────────────────────────────────────
Stack .rodata (Read-Only Data)
┌────────────────────────┐ ┌───────────────────────────────┐
│ const char *const let │ │ "66666666666666666666666666" │
│ ┌──────────────────┐ │ │ ┌──────────────────────────┐ │
│ │ ptr value │ │──────────────────┼─►│ '6','6','6',...,'\0' │ │
│ └──────────────────┘ │ points to │ └──────────────────────────┘ │
│ 8 byte (64-bit) │ │ 27 byte (26 chars + \0) │
│ ┌──────────────────┐ │ │ ┌──────────────────────────┐ │
│ │ const: cannot │ │ │ │ const: read-only, │ │
│ │ reassign ptr │ │ │ │ cannot modify content │ │
│ └──────────────────┘ │ │ └──────────────────────────┘ │
└────────────────────────┘ └───────────────────────────────┘
▲
│
&let (指针变量自身的地址,在栈上)
┌────────────────────────────────────────────────────────────────────
│ 注解:
│
│ const char *const let 是双重 const 指针:
│ 1. 右侧 const — 指针本身不可重新指向 (let = xxx 非法)
│ 2. 左侧 const — 指向的内容不可修改 (*let = 'x' 非法)
│
│ 字符串字面量 "666..." 存储在 .rodata 段,属于只读内存
│ let 指针变量本身在栈上,存的值是 .rodata 中该字符串的首地址
│
│ 代码中:
│ &let — 取指针变量自身地址 (栈地址)
│ let — 取指针存的值,即字符串首地址 (.rodata 地址)
│ *let — 解引用,得到字符串首字符 '6'
└────────────────────────────────────────────────────────────────────
constexpr
constexpr 于 C23 标准(ISO/IEC 9899:2024)中正式引入,用于声明编译期求值的常量。
引入背景
C11/C17 中仅有 const 关键字,但 const 仅表示"只读",并不要求编译器在编译期求值——以下写法在 C11 中属于变长数组(VLA),而非真正的编译期常量:
const int n = 10;
int arr[n]; // C11: 可能被当作 VLA,非编译期常量
C23 引入 constexpr 后,编译器保证在编译期完成求值,可用于数组长度、case 标签等要求常量表达式的场景:
constexpr int n = 10;
int arr[n]; // 合法,n 是编译期常量
switch (1) {
case n: break; // 合法,case 要求常量表达式
}
适用范围
| 适用对象 | 说明 | 示例 |
|---|---|---|
| 文件/块作用域变量 | 必须用常量表达式初始化 | constexpr int x = 42; |
static 变量 | 同上 | static constexpr int x = 42; |
| 复合字面量 | 编译期可求值的聚合初始化 | constexpr int arr[] = {1,2,3}; |
不适用的场景:
- 函数参数、局部非
static变量 — C23 不支持constexpr函数(与 C++ 不同)。 - 运行时值赋给
constexpr变量 — 初始化器必须是常量表达式。
与 const 的区别
| 特性 | const | constexpr |
|---|---|---|
| 编译期求值 | 不保证 | 保证 |
可用于 case/数组长度 | 不保证合法 | 合法 |
| 初始化器 | 任意表达式 | 必须是常量表达式 |