一、回顾与本篇目标
上一篇你学会了数组——一块连续的内存空间,存放相同类型的元素。你也知道了数组名其实是指向数组首元素的指针。
“指针”这个词你可能已经在之前的文章中见过好几次了。在 scanf 那篇,我们提到了 & 是“取地址”运算符。在数组那篇,我们提到了数组在内存中的地址。现在是时候正式面对 C 语言最核心、最强大、也最让人头疼的特性了。
如果说 JavaScript 和 Python 为你屏蔽了内存管理的细节,那 C 语言的指针就是把这块遮布掀开,让你直接看到和操作内存。学会指针,你就真正理解了计算机底层是怎么工作的。学不会指针,C 语言就等于没学。
本篇的目标:
- 理解内存地址的概念——变量到底存在哪里
- 掌握指针变量的声明和使用
- 彻底搞懂
&(取地址)和*(解引用)两个运算符 - 理解指针和数组的关系
- 学会指针的算术运算
- 理解指针作为函数参数的用法
二、内存地址:变量的“门牌号”
在深入指针之前,先理解一个根本问题:变量到底存在哪里?
当你在 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?因为 p 是 int* 类型,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 循环用指针遍历数组——
p从scores开始,每次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 数组)、字符串的声明和初始化、常用字符串处理函数(strlen、strcpy、strcmp、strcat)、用指针遍历字符串。同时对比 JavaScript 和 Python 中的字符串,理解 C 字符串的“简陋”和高效。
C/C++ 零基础入门,每周更新。












暂无评论内容