C 语言指针部分的理解与使用 (STD: C23) 第一节

10 阅读8分钟

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 的区别

特性constconstexpr
编译期求值不保证保证
可用于 case/数组长度不保证合法合法
初始化器任意表达式必须是常量表达式