🛡️

身份验证

答对题目即可进入,验证你是谁

剩余尝试次数:3

指针基础 — 地址与解引用

C语言学习教程

指针基础 — 地址与解引用

指针是 C 语言最强大的特性,也是最难的概念。这很正常——你的大脑需要时间适应”间接访问”这种思维方式。别着急,我们从零开始。

为什么要有指针?

指针不是 C 语言发明者故意为难你的,它解决了三个实际问题:

问题一:如何在函数里修改外部的变量? 普通传参只能传值(复印件),函数改不了原件。传地址就能改——函数拿到门牌号,直接找到变量。

问题二:如何高效传递大块数据? 10MB 的图像数据按值传递每次都要复制 10MB。传一个 8 字节的指针就够了。

问题三:如何在运行时动态分配内存? 程序运行前不知道要存多少数据。malloc 返回一个指针指向新分配的内存。

三个问题的答案一样:不直接拿数据,而是拿数据的地址。 这就是指针。

核心比喻:房子与门牌号

变量   = 一栋房子,里面住着数据
地址   = 房子的门牌号(如"解放路 128 号")
指针   = 一张纸条,纸条上写着门牌号

关键洞察:纸条不是房子,纸条只是写着房子在哪。 改纸条(换门牌号)不等于改房子里的东西。

把内存想象成一栋大公寓

计算机内存是一栋巨大的公寓楼:

┌───────┬───────┬───────┬───────┬───────┬───────┐
│ 1000  │ 1004  │ 1008  │ 100C  │ 1010  │ 1014  │ ... 每个房间4㎡(4字节)
├───────┼───────┼───────┼───────┼───────┼───────┤
│       │       │       │       │       │       │
└───────┴───────┴───────┴───────┴───────┴───────┘

- 每个房间有唯一编号(地址)
- int = 4 字节 = 需要连续 4 个房间
- 指针 = 8 字节 = 连续 8 个房间(64 位系统)

声明 int x = 42; :编译器找 4 个连续空房间,放 42,以后写 x 就自动替换成”那些房间里的内容”。

声明 int *p = &x; :编译器再找 8 个房间,写上 x 的门牌号,以后写 *p 就分两步:先读 p 里的门牌号,再按门牌号找过去读内容。

两个关键运算符:& 和 *

运算符名字含义读法
&取地址问”这个变量住哪?""……的地址”
*解引用按门牌号找过去”地址……里的内容”

注意:* 有两个身份!

int *p = &x;   // 声明中的 * :"p 是指向 int 的指针"
*p = 100;      // 执行中的 * :"p 指向的那个变量"

同一个符号,声明中和执行中含义完全不同。这是初学者第一个大坑。

示例一:最基础的指针操作

#include <stdio.h>

int main() {
    int x = 42;
    int *p = &x;       // p 里写下 x 的门牌号

    printf("x 的值:    %d\n", x);    // 42
    printf("x 的地址:  %p\n", &x);   // 0x7fff1a3b (举例)
    printf("纸条上的字: %p\n", p);    // 同上
    printf("按纸条找去: %d\n", *p);   // 42

    return 0;
}

内存图解:

假设 x 在 0x1000, p 在 0x1008

地址:     0x1000          0x1008
      ┌────────────┐ ┌────────────┐
      │    42      │ │  0x1000    │
      │   (x)      │ │   (p)      │
      └────────────┘ └────────────┘
            ↑              │
            └──────p 指向 x┘

指针值(p)              点向的值(*p)
┌──────────┐           ┌──────────┐
│ 0x1000   │  ───→     │   42     │
│(一张纸条)│           │(真正的房)│
└──────────┘           └──────────┘

画重点:p 的值是 0x1000(门牌号),*p42(房间里的东西),&x 也是 0x1000。注意 p 自己也有地址 &p = 0x1008

示例二:通过指针修改原变量——“遥控器”效果

#include <stdio.h>

int main() {
    int score = 60;
    printf("初始分数: %d\n", score);   // 60

    int *remote = &score;  // 拿到 score 的"遥控器"
    *remote = 90;          // 按遥控器改分数

    printf("修改后分数: %d\n", score); // 90
    // *remote 和 score 是同一个东西!改了一个另一个跟着变

    return 0;
}

内存变化:

步骤1: int score = 60;
      ┌──────────┐
      │    60    │  ← score
      └──────────┘

步骤2: int *remote = &score;
      ┌──────────┐     ┌──────────┐
      │    60    │ ←┐  │0x??score │  ← remote
      └──────────┘  └──└──────────┘

步骤3: *remote = 90;   (按门牌号找过去改)
      ┌──────────┐     ┌──────────┐
      │    90    │ ←┐  │0x??score │  remote 没变
      └──────────┘  └──└──────────┘

示例三:swap 函数——指针最经典的用途

为什么普通传参不能交换两个变量?

#include <stdio.h>

// ✘ 错误:改的是复印件
void swap_wrong(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

// ✔ 正确:传入地址,间接改原件
void swap_right(int *a, int *b) {
    int temp = *a;   // 按 a 纸条找过去,暂存
    *a = *b;         // 把 b 指向的值复制到 a 指向的房间
    *b = temp;       // 暂存的值放进 b 指向的房间
}

int main() {
    int x = 10, y = 20;

    printf("交换前: x=%d, y=%d\n", x, y);     // 10, 20

    swap_wrong(x, y);
    printf("swap_wrong 后: x=%d, y=%d\n", x, y);  // 10, 20(没变!)

    swap_right(&x, &y);
    printf("swap_right 后: x=%d, y=%d\n", x, y);  // 20, 10(成功!)

    return 0;
}

swap_right 内存图解:

调用前:
      ┌──────┐  ┌──────┐
      │  10  │  │  20  │
      │  x   │  │  y   │
      └──────┘  └──────┘

进入 swap_right(&x, &y):
      a 纸条写 x 的门牌号,b 纸条写 y 的门牌号

temp = *a:  读 a 纸条→x→得到10,暂存 temp=10
*a = *b:    读 b 纸条→y→得到20,写入 a 纸条→x→x 变成 20
*b = temp:  把 10 写入 b 纸条→y→y 变成 10

函数结束,a 和 b 消失,但 x 和 y 已经被改!

传值 vs 传地址——一表区分:

swap_wrong(x, y)swap_right(&x, &y)
传了什么10 和 20(值)x 的地址 和 y 的地址
函数拿到a=10, b=20a=纸条(x), b=纸条(y)
修改什么局部变量 a, b*a 即 x, *b 即 y
结果x,y 不变(复印件改了)x,y 交换(直接改原件)

示例四:指针与数组——一个秘密

数组名就是指向第一个元素的指针(常量指针):

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};

    // arr 就是 &arr[0]
    printf("arr    = %p\n", arr);       // 0x1000
    printf("&arr[0] = %p\n", &arr[0]);  // 0x1000(一样!)

    int *p = arr;

    // 五种等价方式访问 arr[2]
    printf("%d\n", arr[2]);     // 30
    printf("%d\n", *(arr+2));   // 30
    printf("%d\n", p[2]);       // 30(指针也能用下标)
    printf("%d\n", *(p+2));     // 30
    printf("%d\n", 2[arr]);     // 30(a[b] == *(a+b))

    return 0;
}

数组在内存中连续存放:

地址:    0x1000  0x1004  0x1008  0x100C  0x1010
      ┌───────┬───────┬───────┬───────┬───────┐
      │  10   │  20   │  30   │  40   │  50   │
      │arr[0] │arr[1] │arr[2] │arr[3] │arr[4] │
      └───────┴───────┴───────┴───────┴───────┘
        ↑ p                              ↑ p+4

p+1 不是"地址加 1 字节",而是"跳到下一个 int"
p+1 = 0x1000 + sizeof(int) = 0x1004

指针遍历数组(两种写法):

int arr[] = {10, 20, 30, 40, 50};

// 写法一:下标
int *p = arr;
for (int i = 0; i < 5; i++) printf("%d ", p[i]);

// 写法二:指针滑动
for (int *p = arr; p < arr + 5; p++) printf("%d ", *p);

数组名 vs 指针——关键区别:

数组名 arr指针 p
能改值吗arr = ??? 报错p = &x 可以
sizeof整个数组 20 字节指针本身 8 字节
arr++;编译错误可以

示例五:通过指针遍历字符串

字符串就是字符数组,指针遍历是经典 C 风格:

#include <stdio.h>

int main() {
    char str[] = "Hello";
    // 内存: 'H' 'e' 'l' 'l' 'o' '\0'

    // 经典指针遍历
    char *p = str;
    while (*p != '\0') {
        printf("%c ", *p);
        p++;
    }
    printf("\n");

    // 更简洁
    for (char *q = str; *q != '\0'; q++) {
        printf("%c ", *q);
    }
    printf("\n");

    return 0;
}
字符数组内存布局:
地址:   0x1000 0x1001 0x1002 0x1003 0x1004 0x1005
      ┌──────┬──────┬──────┬──────┬──────┬──────┐
      │ 'H'  │ 'e'  │ 'l'  │ 'l'  │ 'o'  │ '\0' │
      └──────┴──────┴──────┴──────┴──────┴──────┘
        ↑ p                                       '\0' 停止

示例六:NULL 指针与安全范式

NULL 是特殊值,表示”空白纸条,没有写任何有效门牌号”。解引用前必须检查:

#include <stdio.h>

int main() {
    int *p = NULL;  // 明确:暂不指向任何东西

    int x = 100;

    // 安全范式:先检查再用
    if (p != NULL) {
        *p = 42;   // 不会执行,p 是 NULL
    } else {
        printf("p 是空指针,不能解引用\n");
    }

    p = &x;  // 赋有效地址后才安全
    if (p != NULL) printf("*p = %d\n", *p);  // 100

    return 0;
}

五大常见错误

错误一:野指针——没初始化就解引用

✘ int *p;                    ✔ int x;
  *p = 100;  // p 指向哪?      int *p = &x;
  // 不知道!崩溃/乱写         *p = 100;  // 安全
                              // 或至少 int *p = NULL;

比喻:拿着一张开白纸条就说”按纸条找过去放 100”——纸条上什么都没写,你要去哪?

错误二:把数值当地址

✘ int *p = 1000;  // 1000 号房间里有啥?不知道!
✔ int x = 1000; int *p = &x;

比喻:编了一个不存在的门牌号写在纸条上,然后按这个门牌号去找房子——撞上墙或别人家。

错误三:混淆”换指向”和”改值”

int x = 10, y = 20;
int *p = &x;

p = &y;    // 改了纸条!p 现在指向 y(*p 变成 20)
*p = y;    // 改了房间!x 变成 20(p 还指向 x)

一句话:p = 新地址 是改纸条内容,*p = 新值 是按纸条找到房间改内容。

错误四:返回局部变量的地址(致命!)

✘ int* bad() {               ✔ int* good() {
      int local = 42;               int *p = malloc(sizeof(int));
      return &local;                *p = 42;
  }   // 函数结束 local 就没了!      return p;  // 调用者用完要 free
  }                               }

比喻:函数里临时搭了一个帐篷,你记下位置。函数结束帐篷拆了,你把位置告诉别人——什么都没有。

错误五:声明时 int *p, q; 陷阱

int *p, q;   // 只有 p 是指针!q 是普通 int
int *p, *q;  // 两个都是指针——每人要有自己的 *

指针运算速查

int arr[] = {10, 20, 30, 40, 50};
int *p = arr;     // p 指向 arr[0]

p       // arr[0] 的地址
*p      // arr[0] 的值 = 10
*(p+1)  // arr[1] 的值 = 20
p[2]    // *(p+2) = 30
p++     // p 移到下一个元素

重点回顾

概念读法例子
&xx 的地址问”x 住哪?“
*pp 指向的值按纸条找过去
int *p = &xp 是指向 int 的指针纸条写 x 的门牌号
*p = 100改 p 指向的房间按纸条找过去换内容
p = &y改纸条内容不再指向 x,改指 y
NULL空指针空白纸条

三条黄金法则:

  1. 永远初始化指针——= &某变量= NULL
  2. 解引用前检查是否为 NULL
  3. 想清楚改的是指针本身(p = ...)还是指向的东西(*p = ...