c/c++ pointer

 

c/c++ 中, 每个variable/object(变量)在源码层面和运行时层面具有以下核心属性:

  • identifier / name(标识符/变量名)

源代码中的标识符, 是程序员为可读性而赋予内存块的逻辑符号

仅存在于编译期(源码和编译器的符号表中)

在编译和链接完成后, 最终生成的机器码中不存在变量名, 它会被编译器完全替换为具体内存地址或寄存器偏移量

变量名在运行时不占用任何内存空间

  • address(变量地址)

变量在进程虚拟内存空间中的唯一编号(首地址)

存在于运行期, 它是操作系统分配和 CPU 寻址时真正使用的物理/虚拟坐标

  • value(变量值)

存储在该变量地址所标识的内存空间中的具体数据(底层为比特流)

CPU 根据变量数据类型(如 int 读 4 字节, double 读 8 字节), 从首地址开始读取相应长度比特流, 并将其解释为具体数值或字符

graph TB;
    subgraph a[name: a]
        V2(value: 1)
        A2(address: 0x7ffa2ed1128)
    end
graph LR;
    %% 定义样式 (色彩分区)
    classDef compile fill:#f9f2f4,stroke:#c7254e,stroke-width:2px,color:#333;
    classDef runtime fill:#e8f4f8,stroke:#31708f,stroke-width:2px,color:#333;
    classDef memory fill:#dff0d8,stroke:#3c763d,stroke-width:2px,color:#333;
    classDef nodeStyle fill:#ffffff,stroke:#999,stroke-width:1px;

    subgraph CompileTime ["🛠️ 编译期 (compile-time)"]
        direction TB
        Name["标识符 / 变量名 (identifier)<br/><code>age</code><br/><i>仅存在于源码与符号表<br/>不占运行时内存</i>"]
    end

    subgraph RunTime ["💻 运行期 (run-time / 虚拟内存空间)"]
        direction TB
        subgraph MemoryBlock ["内存实体 (memory object: <code>int age = 25;</code>)"]
            direction TB
            Addr["📍 变量地址 (address)<br/><code>0x7FFA...1288</code><br/><i>内存的首坐标 / 门牌号</i>"]
            
            Type["📏 数据类型 (type)<br/><code>int</code> (4字节)<br/><i>决定读取长度与解释规则</i>"]
            
            Val["📦 变量值 (value)<br/><code>25</code> (底层: <code>0x00000019</code>)<br/><i>按 type 解释后的具体数据</i>"]
        end
    end

    %% 跨图连线与逻辑说明
    Name -- "1. 符号解析与重定位<br/>(编译后标识符消亡,<br/>替换为地址/偏移量)" --> Addr
    
    Addr -- "2. CPU 寻址定位<br/>(通过地址找到物理容器)" --> Type
    Type -- "3. 类型解释<br/>(按类型读取比特流并解析)" --> Val

    %% 源码层面的抽象访问 (虚线)
    Name -. "4. 源码直接访问 (逻辑抽象)<br/>(底层自动转换为:<br/>地址寻址 + 类型解析)" .-> Val

    %% 应用样式
    class CompileTime compile;
    class RunTime runtime;
    class MemoryBlock memory;
    class Name,Addr,Type,Val nodeStyle;

基于这些属性, 访问变量有两种方式:

  • 直接访问(direct access): 通过标识符读写数据

编译器在底层会将其解析为该标识符绑定的内存位置, 并生成直接寻址或基于基址/寄存器的机器指令

  • 间接访问(indirect access): 通过变量所在的内存地址读写数据

编译器会生成间接寻址指令, 先读取地址, 再访问该地址指向的内存位置

概念

在日常交流中, “指针”一词常被混用, 但在严谨的 c/c++ 语境中, 必须区分类型与变量

  • pointer type(指针类型, 简称为指针)

本质上是一种复合数据类型(compound type), 与int char等基础类型平级

它定义该类型实例的内存布局(固定大小, 如 4 或 8 字节)以及允许的操作(如指针算术、解引用)

  • pointer variable(指针变量)

是指针类型的实例化对象, 与int变量 char变量一样

与普通变量存储具体数据值(如整数、字符)不同, 指针变量存储的值(value)是另一个目标对象的内存地址(或空指针值 nullptr/NULL)

int *p_int;       // p_int 是一个指针, 专门存储 int 类型变量的地址
char *p_char;     // p_char 是一个指针, 专门存储 char 类型变量的地址
double *p_double; // p_double 是一个指针, 专门存储 double 类型变量的地址

日常使用中一般将pointer vaiable (指针变量)简称为 pointer(指针)

  • 指向关系(points-to)

当一个指针变量的值等于某个普通变量的内存地址时, 称该指针变量指向(points to)该普通变量

  • 双重属性

指针变量本身也是一个变量(对象), 因此它同样具备标识符和内存地址, 并占用独立的内存空间

指针变量的值: 存储的是目标变量的内存地址

指针变量的地址: 是指针变量自身在内存中的物理/虚拟位置

graph LR;
    subgraph a["普通变量 a(int a = 1)"]
        V2(value: 1)
        A2(address: 0x1000)
    end
    subgraph p["指针变量 p(int* p = &a)"]
        V1(value: 0x1000)
        A1(address: 0x2000)
    end
    V1 -->|p 的值是 a 的地址| A2

通过指针, 可间接访问或修改指针所指向变量的值(称为解引用), 而无需知道或依赖原变量名

基本操作

取地址(address of)

&运算符用于获取变量地址, 通常用于初始化指针变量

#include <stdio.h>

int main() {
    int a = 1;

    // 将 a 的地址赋值给指针变量 p
    int *p = &a;

    // pointer p value = 0x7ffa2ed1128
    // variable a address = 0x7ffa2ed1128
    printf("pointer p value = %p\n", p);
    printf("variable a address = %p\n", &a);

    return 0;
}
graph LR;
    subgraph a[int a]
        V2(value: 1)
        A2(address: 0x7ffa2ed1128)
    end
    subgraph p[int* p]
        V1(value: 0x7ffa2ed1128)
    end
    V1-->A2

解引用(dereference)

* 运算符可访问指针变量指向的内存位置(对象), 称为解引用

#include <stdio.h>

int main() {
    int a = 1;
    int *p = &a;

    // a value = 1
    // a address = 0x7ffc241ed608
    printf("a value: %d\na address: %p\n", a, &a);

    // p value = 0x7ffc241ed608
    // *p value = 1
    printf("p value: %p\n*p value: %d\n", p, *p);

    return 0;
}
graph LR;
    subgraph a[int a]
        V2(value: 1)
        A2(address: 0x7ffc241ed608)
    end

    subgraph p[int* p]
        V1(value: 0x7ffc241ed608)
    end

    V1=="*解引用"==>A2

属性

指针变量指向(指针值)

指针变量的值就是它所存储目标变量的地址, 也称指针变量指向目标变量

#include <stdio.h>

int main() {
    int a = 10;
    int *p = &a;

    // a = 10, &a = 0x7ffff423e928
    printf("a = %d, &a = %p\n", a, &a);

    // *p = 10, p = 0x7ffff423e928
    printf("*p = %d, p = %p\n", *p, p);

    return 0;
}
graph LR;
    subgraph a[int a]
        V2(value: 10)
        A2(address: 0x7ffff423e928)
    end
    subgraph p[int* p]
        V1(value: 0x7ffff423e928)
    end
    V1-->A2

改变指针变量指向

让指针变量存储一个新变量的地址, 称为改变指针变量的指向

#include <stdio.h>

int main() {
    int a = 255;
    int b = 170;
    int* p = &a;

    // p = 0x7fff62e598a8, *p = 255
    printf("p = %p, *p = %d\n", p, *p);

    // 改变指针指向
    p = &b;
    // p = 0x7fff62e5989c, *p = 170
    printf("p = %p, *p = %d\n", p, *p);

    return 0;
}
graph LR;
    subgraph S2[int b]
        direction LR
        V2(value: 170)
        A2(address: 0x7fff62e5989c)
    end

    subgraph S1[int a]
        direction LR
        V1(value: 255)
        A1(address: 0x7fff62e598a8)
    end

    subgraph S3[int* p]
        V3(value: 0x7fff62e598a8)
        V4(value: 0x7fff62e5989c)
    end

    V3-.①.->A1
    V4--②-->A2

间接修改变量值

通过指针变量存储的地址可以间接修改其指向变量的值

#include <stdio.h>

int main() {
    int a = 255;
    int* p = &a;

    // p = 0x7ffc9fcdabf8, *p = 255
    // a = 255, &a = 0x7ffc9fcdabf8
    printf("p = %p, *p = %d\n", p, *p);
    printf("a = %d, &a = %p\n",a, &a);

    *p = 1;
    // p = 0x7ffc9fcdabf8, *p = 1
    // a = 1, &a = 0x7ffc9fcdabf8
    printf("p = %p, *p = %d\n", p, *p);
    printf("a = %d, &a = %p\n",a, &a);

    return 0;
}
graph LR;
    subgraph S1[int* p]
        V1(value: 0x7ffc9fcdabf8)
    end

    subgraph S2[int a]
        direction LR
        V2(value: 255)
        
        A2(address: 0x7ffc9fcdabf8)
    end

    V3(value: 1)
    
    V1-->A2
    V2==>V3

类型

指针变量去掉变量名后是指针类型

声明语句 指针类型 指针指向类型 说明
int *p1 int * int 决定解引用时读取 4 字节并按整数解释
char *p2 char * char 决定解引用时读取 1 字节并按字符解释
double **p3 double ** double * 指向类型本身也是一个指针

因为无论何类型指针都是存储地址值, 所以指针变量大小只与系统位数有关, 与类型无关

32位系统指针大小为4字节, 64位系统指针大小位8字节

#include <stdio.h>

int main() {
    int* p = NULL;
    double* p1 = NULL;
    printf("sizeof p = %d\nsizeof p1 = %d\n", sizeof(p), sizeof(p1));

    return 0;
}

指针操作的内存范围(步长与边界)

指针类型本身不包含连续内存的长度信息(数组指针除外)

指针变量只知道“指向类型的单个对象大小”, 并以此作为指针算术(如 p++)的步长

// 指针变量指向4字节大小区域
int a = 1;
int *p = &a;

当指针变量指向动态分配的连续内存时, 指向区域的实际大小是由程序员的业务逻辑保证的, 编译器和指针类型本身不进行边界检查

// 指针变量指向1024 字节 大小区域
const int SIZE = 1024;
char *p =(char *)malloc(sizeof(char) * SIZE);

特殊指针变量

pointer to const (指向常量的指针)

A pointer to a constant object.

指针指向的对象是常量

不能通过该指针间接修改目标变量的值(指向的值不可变), 但指针本身可以改变指向

const int *p

// 或
int const *p

指针类型 int *, 指针指向类型 const int

graph LR;
    subgraph S1[const int a]
    direction LR
        V(value: 不可变)
        A(address)
    end

    subgraph S2[const int* p]
        V2(value)
        direction LR
    end
    V2-->A===V
#include <stdio.h>

int main(void) {
    const int a = -1;
    const int b = 1;
    const int *p = &a;
    printf("%d\n", *p);

    // 尝试改变指针指向
    p = &b;
    printf("%d\n", *p);

    // 尝试改变指针指向值, 报错
    *p = -1;
    printf("%d\n", *p);

    return 0;
}

运行时报错

error: read-only variable is not assignable

const pointer (常量指针)

A constant pointer to an object.

指针本身是常量, 指针的指向在初始化后不可更改(指向不可变), 但可以通过该指针修改目标变量的值(指向的值可变)

int *const p;

指针类型 int *const, 指针指向类型 int

graph LR;
    subgraph S1[int a]
        V(value)
        A(address)
    end
    subgraph S2[int *const p]
        direction LR
        V2(value: 不可变)
    end
    V2==绑定===A
#include <stdio.h>

int main(void) {
    int a = -1;
    int *const p = &a;

    printf("%d\n", *p);
    // 尝试改变指针指向值
    *p = 1;
    printf("%d\n", *p);

    int b = 1;
    // 尝试改变指针指向, 报错
    p = &b;
    printf("%d\n", *p);

    return 0;
}

运行时报错

error: cannot assign to variable 'p' with const-quakufued type 'int *const'

note: variable 'p' declared const here

function pointer(函数指针)

指向函数的指针, 存储函数入口地址, 通过函数指针可间接调用函数

返回值(*)(参数...,)
#include <stdio.h>

int get_max(int x, int y) {
    return x > y ? x : y;
}

int main() {
    int(*p)(int, int) = NULL;
    p = get_max;

    // 2
    printf("%d\n", p(1, 2));

    return 0;
}

array of pointers(指针数组)

指针数组是数组, 数组中元素为指针

int *p[3];

指针类型 int *, 指针指向类型 int

#include <stdio.h>

int main(void) {
    int *p[3];
    int a = 1;
    int b = 2;
    int c = 3;

    p[0] = &a;
    p[1] = &b;
    p[2] = &c;
    for(int i = 0; i < 3; i++) {
        // p[0] = 1, &p[0] = 0x7fffa48dbfd0
        // p[1] = 2, &p[1] = 0x7fffa48dbfd8
        // p[2] = 3, &p[2] = 0x7fffa48dbfe0
        printf("p[%d] = %d, &p[%d] = %p\n", i, *p[i], i, &p[i]);
    }

    return 0;
}

pointer to pointer(二级指针)

指向指针的指针, 其值是另一个指针变量的地址。常用于在函数内部修改外部指针的指向, 或表示二维数组

int **a;
graph LR;
    subgraph S1[变量]
        direction LR
        V1(value: ...)
        A1(address: ...)
    end

    subgraph S2[一级指针]
        direction LR
        V2(value: ...)
        A2(address: ...)
    end

    subgraph S3[二级指针]
        direction LR
        V3(value: ...)
    end

    V3-->A2
    V2-->A1
#include <stdio.h>

int main() {
    int a = 1;
    int *p = &a;
    int **sp = &p;

    // a: 1
    // &a: 0x7ffd79d476d8
    printf("a: %d\n, &a: %p\n\n", a, &a);
    
    // *p: 1
    // p: 0x7ffd79d476d8
    // &p: 0x7ffd79d476d0
    printf("*p: %d\n, p: %p\n, &p: %p\n\n", *p, p, &p);

    // **sp: 1
    // *sp: 0x7ffd79d476d8
    // sp: 0x7ffd79d476d0
    printf("**sp: %d\n, *sp: %p\n, sp: %p\n", **sp, *sp, sp);

    return 0;
}
graph LR;
    subgraph S1[int a]
        direction LR
        V1(value: 1)
        A1(address: 0x7ffd79d476d8)
    end

    subgraph S2[int* p]
        direction LR
        V2(value: 0x7ffd79d476d8)
        A2(address: 0x7ffd79d476d0)
    end

    subgraph S3[int** sp]
        direction LR
        V3(value: 0x7ffd79d476d0)
    end

    V3-->A2
    V2-->A1

pointer to array (数组指针)

是一个指针, 指向一个完整的数组(而非数组的首元素)

常用于多维数组的传参

int(*p)[3];

指针类型 int *, 指针指向类型 int [3]

#include <stdio.h>

int main(void) {
    int a[3] = {1, 2, 3};
    int(*p)[3] = &a;

    for(int i = 0; i < 3; i++) {
        // &a[0] = 0x7ffdeaf17b0, a[0] = 1
        // &a[1] = 0x7ffdeaf17b4, a[1] = 2
        // &a[2] = 0x7ffdeaf17b8, a[2] = 3
        printf("&a[%d] = %p, a[%d] = %d\n", i, &a[i], i, a[i]);
    }
    for(int i = 0; i < 3; i++) {
        //(*p + 0) = 0x7ffdeaf17b0, *(*p + 0) = 1
        //(*p + 1) = 0x7ffdeaf17b4, *(*p + 1) = 2
        //(*p + 2) = 0x7ffdeaf17b8, *(*p + 2) = 3
        printf("(*p + %d) = %p, *(*p + %d) = %d\n", i,(*p + i), i,*(*p + i));
    }

    return 0;
}
graph LR;
    subgraph S1["int a[]"]
        subgraph A1[a0]
            V0("value")
            A0("address")
        end
        subgraph A2[a1]
            V12("value")
            A12("address")
        end
        
    end
    subgraph S2["int(*p)[]"]
        V2(value)
        direction LR
    end

    A0-->A12
    V2==>A0

pointer to object (对象指针)

c++c 结构体中, 指向类或结构体实例的指针

除了使用 * 解引用外, 还可使用 -> 运算符来简化成员访问

#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main() {
    struct Point pt = {10, 20};
    struct Point *p_pt = &pt;

    // 使用 -> 运算符访问对象成员, 等价于(*p_pt).x
    printf("x = %d, y = %d\n", p_pt->x, p_pt->y); 
    
    return 0;
}