五:指针——直接操作内存地址的能力

一、回顾与本篇目标

上一篇你学会了数组——一块连续的内存空间,存放相同类型的元素。你也知道了数组名其实是指向数组首元素的指针。

“指针”这个词你可能已经在之前的文章中见过好几次了。在 scanf 那篇,我们提到了 & 是“取地址”运算符。在数组那篇,我们提到了数组在内存中的地址。现在是时候正式面对 C 语言最核心、最强大、也最让人头疼的特性了。

如果说 JavaScript 和 Python 为你屏蔽了内存管理的细节,那 C 语言的指针就是把这块遮布掀开,让你直接看到和操作内存。学会指针,你就真正理解了计算机底层是怎么工作的。学不会指针,C 语言就等于没学。

本篇的目标:

  1. 理解内存地址的概念——变量到底存在哪里
  2. 掌握指针变量的声明和使用
  3. 彻底搞懂 &(取地址)和 *(解引用)两个运算符
  4. 理解指针和数组的关系
  5. 学会指针的算术运算
  6. 理解指针作为函数参数的用法

二、内存地址:变量的“门牌号”

在深入指针之前,先理解一个根本问题:变量到底存在哪里?

当你在 C 语言中声明一个变量:

int age = 28;

计算机在内存中找了一块空闲的空间(对于 int 来说,通常是 4 个字节),把值 28 存进去。这块空间有一个唯一的编号——就像每家每户都有一个门牌号。这个编号就是内存地址

内存地址通常用一个十六进制数字表示,比如 0x7ffeeb5c8a9c。你可以用 & 运算符获取任何变量的地址:

#include <stdio.h>

int main() {
    int age = 28;
    double price = 19.99;
    char grade = 'A';

    // %p 是打印地址的格式占位符
    printf("age 的值:%d,地址:%p\n", age, &age);
    printf("price 的值:%.2f,地址:%p\n", price, &price);
    printf("grade 的值:%c,地址:%p\n", grade, &grade);

    return 0;
}

输出类似:

age 的值:28,地址:0x7ffeeb5c8a9c
price 的值:19.99,地址:0x7ffeeb5c8a90
grade 的值:A,地址:0x7ffeeb5c8a8f

每次运行程序,地址可能会不同(因为操作系统每次分配的内存位置不同),但关键是:每个变量都有一个独一无二的地址,就像每家每户都有唯一的门牌号。

三、指针变量:存储地址的变量

既然地址也是一个数字,那能不能用一个变量把地址存起来?当然可以。用来存储地址的变量,就叫指针变量,简称指针

声明指针变量

类型 *指针变量名;

注意那个星号 *。它是声明指针的关键标志:

int *p;       // p 是一个指向 int 类型数据的指针
double *q;    // q 是一个指向 double 类型数据的指针
char *r;      // r 是一个指向 char 类型数据的指针

指针的类型表示这个指针指向什么类型的数据int *p 的意思是“p 里存的是一个 int 类型变量的地址”。这个类型信息非常重要——后面解引用时,编译器需要知道从那个地址开始读几个字节。

把地址赋给指针

int age = 28;
int *p = &age;    // p 现在存储了 age 的地址

printf("age 的值:%d\n", age);      // 28
printf("age 的地址:%p\n", &age);   // 0x7ffeeb5c8a9c
printf("p 的值(即 age 的地址):%p\n", p);  // 和上一行相同

可以把指针想象成一张写着门牌号的纸条。纸条本身不是房子,但通过纸条上的门牌号,你可以找到那栋房子。

四、解引用:通过指针访问原始数据

有了指针(门牌号),怎么通过指针去访问指针指向的那个变量的值?用解引用运算符 *

int age = 28;
int *p = &age;

printf("age 的值:%d\n", age);   // 28
printf("*p 的值:%d\n", *p);     // 28 —— 通过指针访问 age 的值

// 通过指针修改原始变量的值
*p = 30;
printf("age 的新值:%d\n", age);  // 30 —— age 被修改了!
printf("*p 的新值:%d\n", *p);    // 30

这里有一个非常重要的区分:星号 * 在 C 语言中有两种完全不同的含义,取决于它出现的位置:

出现位置 含义 示例
声明语句中 “这是一个指针变量” int *p; —— p 是指针
表达式(非声明)中 “解引用,访问指针指向的值” *p = 30; —— 修改 p 指向的变量的值

类比

  • int *p = &age; 相当于“这是一张纸条,上面写着 age 的门牌号”。
  • *p 相当于“按照纸条上的门牌号,找到那栋房子,进去看看里面有什么(或者放东西进去)”。

五、指针和数组的关系

上一篇我们留了一个悬念:数组名本质上就是一个指针。现在来揭开这个谜底。

数组名是指向首元素的指针

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

printf("arr 的值(地址):%p\n", arr);         // 输出一个地址
printf("&arr[0] 的值(地址):%p\n", &arr[0]); // 和上一行完全相同!

arr&arr[0] 指向同一个地址——数组第一个元素的位置。所以数组名就是一个指向首元素的指针

通过指针访问数组元素

既然数组名是指针,那就可以用解引用运算符来访问数组元素:

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

// 以下三种写法完全等价
printf("%d\n", arr[0]);    // 10 —— 传统的下标写法
printf("%d\n", *arr);      // 10 —— 解引用数组名(即首元素)
printf("%d\n", *(arr + 0));// 10 —— 首元素地址加 0,然后解引用

这引出了 C 语言中最重要的一条规则:arr[i]*(arr + i) 是完全等价的。 方括号 [] 本质上就是对指针运算的语法糖。

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

printf("%d\n", arr[2]);      // 30
printf("%d\n", *(arr + 2));  // 30 —— 完全等价

arr + 2 不是把地址值加 2 个字节,而是加 2 × sizeof(int) 个字节,即跳过了 2 个 int 元素,指向第三个 int。这个行为叫指针算术,下一节详细讲。

六、指针的算术运算

指针可以进行加减运算,但规则和普通数字不同:指针加 1,不是地址值加 1,而是加上“指针指向的类型的大小”。

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

printf("%p → %d\n", p, *p);       // 地址1 → 10
p++;                              // 指针加 1
printf("%p → %d\n", p, *p);       // 地址1+4 → 20(跳过了 4 字节)
p += 2;                           // 指针加 2
printf("%p → %d\n", p, *p);       // 地址1+12 → 40(跳过了 2 个 int)

为什么 p++ 会让地址增加 4?因为 pint* 类型,int 占 4 个字节。指针加 1 意味着“指向下一个同类型元素”。

同理,如果是指向 char 的指针(char 占 1 字节),加 1 就只增加 1 个地址值。如果是指向 double 的指针(double 占 8 字节),加 1 就增加 8 个地址值。

指针的算术运算只有三种有意义的形式:

  • 指针 + 整数:向后移动若干个元素。
  • 指针 – 整数:向前移动若干个元素。
  • 指针 – 指针:计算两个指针之间相隔几个元素(只有在指向同一个数组时才有意义)。

遍历数组的两种等价写法

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

// 方式一:下标遍历(你熟悉的写法)
for (int i = 0; i < 5; i++) {
    printf("%d ", arr[i]);
}
printf("\n");

// 方式二:指针遍历(C 语言的特色写法)
for (int *p = arr; p < arr + 5; p++) {
    printf("%d ", *p);
}
printf("\n");

两种写法输出的结果完全相同。指针遍历在某些场景下比下标遍历更高效(因为省去了每次计算 首地址 + i × sizeof(int) 的过程),但现代编译器通常会自动优化,两者性能差异可以忽略。选择你更习惯的写法即可。

七、指针作为函数参数

这是指针最强大的用途之一。先看一个问题:

// 这个函数试图交换两个变量的值,但失败了
void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    swap(x, y);
    printf("x = %d, y = %d\n", x, y);  // x = 10, y = 20 —— 没变!
    return 0;
}

为什么没变? 因为 C 语言中,函数参数的传递是传值——swap(x, y) 把 x 和 y 的(10 和 20)复制了一份,传给 a 和 b。函数内部交换的是 a 和 b 的副本,外部的 x 和 y 完全不受影响。

解决方案:传指针。 把 x 和 y 的地址传给函数,函数通过地址直接修改外部的变量:

// 正确的 swap:接收两个指针
void swap(int *a, int *b) {
    int temp = *a;   // 通过 a 指针访问 x 的值
    *a = *b;         // 通过 b 指针访问 y 的值,赋给 x
    *b = temp;       // 把暂存的值赋给 y
}

int main() {
    int x = 10, y = 20;
    swap(&x, &y);          // 传 x 和 y 的地址
    printf("x = %d, y = %d\n", x, y);  // x = 20, y = 10 —— 正确!
    return 0;
}

传值和传指针的区别:

  传值 传指针
传给函数的是什么 变量的副本 变量的地址
函数能修改原始变量吗 不能
适用场景 函数只需要读取数据 函数需要修改外部变量

现在你理解 scanf 为什么需要 & 了吗?

int age;
scanf("%d", &age);  // 把 age 的地址传给 scanf,scanf 通过地址把读取的值写进去

如果 scanf 不接收地址,它就没办法修改 age 的值。所有需要“修改外部变量”的场景,都必须传指针。

八、空指针和野指针

指针是一把双刃剑。用好了性能飞升,用不好程序崩溃。

空指针(NULL)

NULL 是一个特殊的指针值,表示“这个指针不指向任何有效地址”。它相当于 JavaScript 的 null、Python 的 None

int *p = NULL;  // p 不指向任何东西

// 解引用 NULL 指针会导致程序崩溃(段错误)
// printf("%d\n", *p);  // 千万不要这样做!

使用指针前,先检查是否为 NULL:

if (p != NULL) {
    printf("%d\n", *p);  // 安全
}

野指针

野指针是指指向未知地址或已释放内存的指针。常见的野指针来源:

// 1. 未初始化的指针
int *p;       // p 里是垃圾值,指向一个随机地址
// *p = 10;   // 危险!可能破坏其他数据

// 2. 指针指向的内存已经被释放(后面动态内存分配篇会讲)
// 3. 指针超出了数组的有效范围
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr + 10;  // 越界了!p 指向数组之外的未知内存

防范野指针的方法:

  • 声明指针时立即初始化为 NULL 或一个有效的地址。
  • 指针不再使用时,设置为 NULL
  • 确保数组索引不越界。

九、综合演示:用指针操作数组

下面这段代码综合运用了指针声明、取地址、解引用、指针算术、指针作为函数参数:

#include <stdio.h>

// 用指针遍历数组并求和
int sum_array(int *arr, int length) {
    int total = 0;
    for (int i = 0; i < length; i++) {
        total += arr[i];   // arr[i] 等价于 *(arr + i)
    }
    return total;
}

// 用指针找数组中的最大值
int find_max(int *arr, int length, int *max_index) {
    // max_index 是指针,用来“返回”最大值的位置
    int max = arr[0];
    *max_index = 0;

    for (int i = 1; i < length; i++) {
        if (arr[i] > max) {
            max = arr[i];
            *max_index = i;
        }
    }
    return max;
}

int main() {
    int scores[] = {85, 92, 78, 95, 88, 73, 90};
    int len = sizeof(scores) / sizeof(scores[0]);

    // 测试求和
    int total = sum_array(scores, len);
    printf("总分:%d\n", total);

    // 测试找最大值
    int max_pos;
    int max_val = find_max(scores, len, &max_pos);
    printf("最高分:%d,位置:第 %d 个学生\n", max_val, max_pos + 1);

    // 用指针遍历并输出每个成绩的等级
    printf("\n成绩等级:\n");
    for (int *p = scores; p < scores + len; p++) {
        if (*p >= 90) printf("%d → 优秀\n", *p);
        else if (*p >= 80) printf("%d → 良好\n", *p);
        else if (*p >= 70) printf("%d → 中等\n", *p);
        else printf("%d → 需努力\n", *p);
    }

    return 0;
}

代码解析:

  • sum_array 接收一个指针和长度。你传数组名 scores 进去,函数内部可以用下标 arr[i] 来访问——因为 scores 就是指针。
  • find_max 展示了指针作为输出参数的用法。函数需要返回最大值的位置,但 return 只能返回一个值。于是通过第三个参数 max_index(一个指针)把位置“写出去”。调用时传 &max_pos,函数内部用 *max_index = i 修改外部的 max_pos 变量。
  • 最后的 for 循环用指针遍历数组——pscores 开始,每次 p++ 跳到下一个元素,直到 p < scores + len 为止。

十、本篇动手练习

练习 1:交换两个变量的值

新建 practice5-1.c,写一个 swap 函数,用指针交换两个 double 类型变量的值。在 main 中测试。

练习 2:用指针反转数组

新建 practice5-2.c,用指针(不用下标)实现数组反转。声明两个指针,一个指向数组首部,一个指向数组尾部,交换它们指向的值,然后向中间移动,直到相遇。

练习 3:计算字符串长度

新建 practice5-3.c,写一个 my_strlen 函数,用指针计算字符串的长度(不调用 strlen)。提示:字符串以 \0 结尾,用指针遍历直到遇到 \0,统计移动了多少次。

练习 4:返回多个值

新建 practice5-4.c,写一个 get_stats 函数,接收一个整数数组和长度,通过指针参数返回最小值、最大值和平均值三个值。在 main 中调用并打印结果。

十一、本篇小结

这一篇你直面了 C 语言最核心的概念——指针:

  • 内存地址:每个变量都有一个唯一的内存地址,用 & 获取。地址就像门牌号。
  • 指针变量:存储地址的变量。声明时加 *(如 int *p;)。指针有类型,表示它指向什么类型的数据。
  • 解引用:用 * 运算符通过指针访问原始数据。*p 就是“去 p 存的地址那里,把里面的值拿出来(或放进去)”。
  • 数组和指针数组名就是指向首元素的指针arr[i]*(arr + i) 完全等价。方括号本质是指针运算的语法糖。
  • 指针算术p + 1 不是地址加 1,而是加 sizeof(类型) 个字节,即跳到下一个元素。
  • 指针作为函数参数:传值不能修改外部变量,传指针才能修改。这就是 scanf 需要 & 的原因。
  • 空指针和野指针NULL 表示不指向任何有效地址。野指针指向未知地址,是程序崩溃的常见原因。使用指针前要确保它指向有效内存。

指针是 C 语言的灵魂。理解了指针,你就能看懂很多底层的代码,也能理解 JavaScript 和 Python 中“引用类型”的底层原理。下一篇,我们学习 C 语言中另一个指针的“近亲”——字符串。在 C 中,字符串就是一个以 \0 结尾的字符数组,而操作字符串就是操作指针。

下一篇预告

下一篇——《字符串——字符数组与字符串处理》:C 语言中的字符串是什么(以 \0 结尾的 char 数组)、字符串的声明和初始化、常用字符串处理函数(strlenstrcpystrcmpstrcat)、用指针遍历字符串。同时对比 JavaScript 和 Python 中的字符串,理解 C 字符串的“简陋”和高效。

C/C++ 零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容