本文整理 C 语言指针课程的全部课堂代码,按知识点分节,每节包含题目说明、解题思路、完整代码(带详细注释)和关键知识点总结。重点对比 值传递 与 地址传递 的本质区别。
一、一切皆有可能:自打印程序(quine)
题目说明
ch07-00一切皆有可能.c 是一段经典的”自打印程序”(quine)——程序运行后输出的内容就是它自己的源代码。这里用作指针章节的开场,展示 C 语言中字符串指针与 printf 格式化的奇妙用法。
完整代码
#include <stdio.h>
int main(){ /* a 是一个字符指针,指向一段包含 printf 调用语句的字符串 字符串里用 %c 占位符表示双引号字符(ASCII 34), %s 占位符表示字符串 a 自身 */ char *a = "main(){char*a=%c%s%c;printf(a,34,a,34);}"; /* printf 第一个参数是格式串 a 后面三个参数:34(双引号), a(字符串本身), 34(双引号) 输出结果就是上面的源代码字符串 */ printf(a, 34, a, 34);}关键知识点
- 字符指针:
char *a指向字符串字面量,字符串存储在只读数据段。 printf的格式串本身可以是变量:第一个参数不一定要写成字符串字面量,可以是任意char *。%c输出字符:用 ASCII 码 34 表示双引号",避免在字符串中嵌套双引号导致语法错误。- 递归引用:
%s处把a自身作为参数填入,形成”打印自己”的效果。
二、间接寻址:指针的基本用法
题目说明
ch07-01-间接寻址.c 演示指针的两个核心运算符:&(取地址)和 *(解引用),以及”直接寻址”与”间接寻址”的对比。
解题思路
- 定义变量
a、b并初始化 - 定义指针
p1、p2,分别指向a、b - 通过变量名直接访问 = 直接寻址;通过指针解引用访问 = 间接寻址
完整代码
#include <stdio.h>
int main(){ int a = 100, b = 10; int *p1, *p2; p1 = &a; /* p1 指向 a */ p2 = &b; /* p2 指向 b */
/* 直接寻址:通过变量名 a、b 访问 &a 取 a 的地址,b 直接读取 b 的值 */ printf("%p, %d\n", &a, b);
/* 间接寻址:通过指针访问 &p1 取指针变量 p1 自己的地址(注意不是 a 的地址) *p2 解引用 p2,得到 b 的值 */ printf("%p, %d", &p1, *p2);}关键知识点
| 表达式 | 含义 |
|---|---|
&a | 变量 a 的内存地址 |
p1 = &a | 让 p1 指向 a |
*p1 | 解引用:取出 p1 所指内存中的值(即 a 的值) |
&p1 | 指针变量 p1 自己的地址(指针本身也是个变量,也有地址) |
易错点:&p1 是指针自身的地址,不等于 &a。p1 才等于 &a。
三、两个数最大值:指针交换法
题目说明
ch07-02-两个数最大值.c:输入两个整数,使 p1 指向较大者、p2 指向较小者并输出。不交换变量 a、b 的值,只交换指针的指向。
解题思路
p1、p2初始分别指向a、b- 若
a < b,用一个临时指针p交换p1、p2的指向 - 输出
a、b(不变)和*p1(max)、*p2(min)
完整代码
#include <stdio.h>
int main(){ int *p1, *p2, *p, a, b; scanf("%d,%d", &a, &b); p1 = &a; p2 = &b; if (a < b) { /* 交换指针的指向,不交换 a、b 的值 */ p = p1; p1 = p2; p2 = p; } /* a、b 保持原值 */ printf("a=%d,b=%d\n", a, b); /* p1 指向较大者,p2 指向较小者 */ printf("max=%d,min=%d\n", *p1, *p2);}关键知识点
- 指针交换 ≠ 值交换:交换指针指向不改变原变量的值,只是改变了”通过哪个指针访问哪个变量”。
- 临时指针
p:交换两个指针需要第三个指针作中转,与交换两个整数同构。
四、三个数由小到大输出
题目说明
ch07-03输入三个数,用指针的方法实现由小到大输出.c:输入三个整数,用指针方法让 *p1 ≤ *p2 ≤ *p3。
解题思路
让 p1、p2、p3 分别指向 a、b、c,通过三次两两比较,交换指针指向(或交换所指变量的值,本代码交换的是 *p1、*p2 的值)使三个数有序。
完整代码
#include <stdio.h>
int main(){ int a, b, c, t; int i, *p = &i; int *p1 = &a, *p2 = &b, *p3 = &c; scanf("%d,%d,%d", &a, &b, &c);
/* 三次比较:保证比较后 *p1 最小、*p3 最大 */ if (*p1 > *p2) /* 让 *p1 <= *p2 */ { t = *p1; *p1 = *p2; *p2 = t; } if (*p1 > *p3) /* 让 *p1 <= *p3,此时 *p1 是三个数中最小 */ { t = *p1; *p1 = *p3; *p3 = t; } if (*p2 > *p3) /* 让 *p2 <= *p3,此时 *p2 中间、*p3 最大 */ { t = *p2; *p2 = *p3; *p3 = t; } printf("%d,%d,%d\n", *p1, *p2, *p3);}关键知识点
*p1出现在赋值号左边:表示修改p1所指内存(即a)的值,这是”通过指针写数据”的核心用法。- 冒泡思想:两两比较、必要时交换,N 个元素需 N-1 轮即可完成排序。
五、值传递与地址传递(重点)
这一节通过同一个”交换两个数”的需求,对比三种写法,深入理解 C 语言函数参数传递的本质。这是指针章节最重要、也是最容易出错的知识点。
5.1 值传递(无法交换)
ch07-04-交换1-值传递.c:函数参数是 int x, int y,调用 swap(*pa, *pb) 把 a、b 的值传进去。
解题思路
C 语言的函数参数是单向值传递:实参的值被复制一份给形参,函数内对形参的修改不影响实参。因此这种写法无法交换 a、b。
完整代码
#include <stdio.h>
/* 形参 x、y 是普通的 int,接收的是实参的"副本" */swap(int x, int y){ int t; t = x; x = y; y = t; /* 这里交换的只是 x、y 这两个局部变量 函数返回后 x、y 被销毁,a、b 不受影响 */}
int main(){ int a, b; int *pa, *pb; scanf("%d,%d", &a, &b); pa = &a; pb = &b; if (a < b) swap(*pa, *pb); /* *pa、*pb 是 a、b 的"值",传值 */ printf("\n%d,%d\n", a, b); /* a、b 没变 */}运行结果分析
输入 3,5,输出仍是 3,5。函数内的交换只发生在副本上。
5.2 地址传递(交换指针本身,仍无法交换)
ch07-04-交换3-地址传递-交换指针.c:函数参数是 int *p1, int *p2,但函数内交换的是指针本身。
解题思路
虽然传入了指针,但指针本身也是按值传递的——p1、p2 是 pa、pb 的副本。函数内交换 p1、p2 这两个局部指针的指向,不会影响 main 中的 pa、pb,更不会改变 a、b 的值。
完整代码
#include <stdio.h>
/* 形参 p1、p2 是指针,但它们自身仍是 main 中 pa、pb 的"副本" */swap(int *p1, int *p2){ int *p; p = p1; p1 = p2; p2 = p; /* 交换的只是 p1、p2 这两个局部指针的指向 main 中的 pa、pb、a、b 都没变 */}
int main(){ int a, b; int *pa, *pb; scanf("%d,%d", &a, &b); pa = &a; pb = &b; if (a < b) swap(pa, pb); /* 传入的是 pa、pb 的"值"(即地址值) */ printf("%d,%d", *pa, *pb); /* *pa、*pb 仍是 a、b,没变 */}运行结果分析
输入 3,5,输出 3,5。即使传了指针,只要交换的是指针变量本身,仍无法影响主调函数。
5.3 地址传递(解引用交换值,成功)
ch07-04-交换2-地址传递.c:函数参数仍是 int *p1, int *p2,但函数内通过 *p1、*p2 解引用修改它们所指的内存。
解题思路
这才是正确的写法。p1 指向 a,p2 指向 b,那么 *p1 就是 a 本身,*p2 就是 b 本身。交换 *p1 和 *p2 等价于交换 a 和 b。
完整代码
#include <stdio.h>
/* p1 指向 a,p2 指向 b *p1 就是 a,*p2 就是 b 交换 *p1 和 *p2 等价于交换 a 和 b */swap(int *p1, int *p2){ int t; t = *p1; /* t = a */ *p1 = *p2; /* a = b */ *p2 = t; /* b = t */}
int main(){ int a, b; int *pa, *pb; scanf("%d,%d", &a, &b); pa = &a; pb = &b; if (a < b) swap(pa, pb); /* 传入 a、b 的地址 */ printf("\n%d,%d\n", a, b); /* a、b 被成功交换 */}运行结果分析
输入 3,5,输出 5,3。成功交换。
5.4 三种写法对比(必背)
| 写法 | 函数签名 | 函数内操作 | 能否交换 a、b | 原因 |
|---|---|---|---|---|
| 5.1 值传递 | swap(int x, int y) | 交换 x、y | ❌ 不能 | 形参是实参的副本,函数内修改不影响实参 |
| 5.2 交换指针 | swap(int *p1, int *p2) | 交换 p1、p2 的指向 | ❌ 不能 | 指针本身也是按值传递,交换副本指针不影响主调 |
| 5.3 解引用 | swap(int *p1, int *p2) | 交换 *p1、*p2 | ✅ 能 | 通过指针修改所指内存,等价于修改 a、b |
核心结论:
C 语言函数参数传递只有值传递一种机制——“地址传递”传的也是地址的”值”。要让被调函数修改主调函数的变量,必须传入该变量的地址,并在函数内通过
*指针解引用修改内存。仅交换指针变量本身是无效的。
记忆口诀:想改主函数的变量,就传它的地址,函数里用 *指针 改值;只改指针本身没用。
六、指针操作二维数组
二维数组的指针操作有三种常见写法,对应三种不同的指针类型。这一节通过三个填空题分别演示。
6.1 用一级指针按行遍历二维数组
01-指针操作二维数组-1.c:用 int *p 指向二维数组首元素,按”一维”方式遍历。
解题思路
二维数组 a[3][4] 在内存中是行主序连续存储的,因此可以用一个一级指针 p 从 a[0](第一行第一元素的地址)开始,连续递增 12 次遍历全部元素。
完整代码(含填空答案)
#include<stdio.h>int main(){ int a[3][4] = {1,3,5,7,9,11,13,15,17,19,21,23}; int *p; /* 【填空】p = a[0] a[0] 是第一行的首元素地址,类型 int* 也可以写成 &a[0][0] */ for (p = a[0]; p < a[0] + 12; p++) { if ((p - a[0]) % 4 == 0) printf("\n"); /* 每行 4 个换行 */ printf("%4d", *p); }}关键知识点
a[0]的类型是int*:指向第一行第一个元素,加 1 跨过一个int。a的类型是int(*)[4]:数组指针,加 1 跨过一行(4 个 int)。两者不同。p - a[0]:指针减法,结果是两指针之间的元素个数,用来判断是否换行。
6.2 用数组指针按行遍历二维数组
01-指针操作二维数组-2.c:用 int (*p)[4](数组指针)按行遍历。
解题思路
int (*p)[4] 表示 p 是一个指针,指向”含有 4 个 int 的数组”。p 加 1 跨过一行。*p 是一行,*p + j 是第 j 列元素地址,*(*p + j) 是元素值。
完整代码(含填空答案)
#include<stdio.h>int main(){ int a[3][4] = {1,3,5,7,9,11,13,15,17,19,21,23}; int i, j, (*p)[4]; /* 【填空】p = a a 是二维数组名,类型是 int(*)[4],与 p 类型匹配 也可以写 p = &a[0] */ for (p = a, i = 0; i < 3; i++, p++) for (j = 0; j < 4; j++) printf("%d ", *(*p + j)); printf("\n");}关键知识点
int (*p)[4]与int *p[4]的区别(极易混淆):int (*p)[4]:p是指针,指向int[4](数组指针)int *p[4]:p是数组,有 4 个元素,每个元素是int*(指针数组)
- 二维数组访问等价关系:
a[i][j]≡*(*(a+i)+j)≡*(a[i]+j)
6.3 用指针数组作函数参数(填空题)
01-指针操作二维数组.c:函数 InputArray 接收二维数组并打印。
解题思路
二维数组作函数参数时,形参通常写成 int (*a)[N] 或 int a[][N](第二维不能省略)。填空 1 是函数参数声明,填空 2 是元素地址。
完整代码(含填空答案)
#include<stdio.h>#define M 3#define N 4
/* 【填空1】int (*a)[N] 形参为"指向含 N 个 int 的数组的指针" 等价写法:int a[][N] */void InputArray(int (*a)[N], int m, int n){ int i, j; /* 原题缺 j 的声明,这里补上 */ for (i = 0; i < m; i++) { for (j = 0; j < n; j++) { /* 【填空2】a[i][j] 或 *(*(a+i)+j) */ printf("%d", a[i][j]); } }}
int main(){ int a[M][N] = {1,2,3,4,5,6,7,8,9,10,11,12}; InputArray(a, M, N); return 0;}关键知识点
- 二维数组作函数参数:第二维大小必须显式写出,编译器需要它来计算
a[i][j]的地址= a + i*N + j。 - 三种等价形参写法:
int a[][4]≡int (*a)[4]≡int a[3][4](第一维可省略)。
七、命令行参数
02-命令行参数.c:演示 main 函数的 argc、argv 参数。
题目说明
编写程序,把命令行传入的所有参数原样打印出来。
解题思路
argc:参数个数(含程序名本身,至少为 1)argv:字符指针数组,argv[0]是程序名,argv[1]开始是真正的参数
完整代码
#include<stdio.h>
int main(int argc, char *argv[]){ int k; /* 从 k=1 开始,跳过 argv[0](程序名自身) */ for (k = 1; k < argc; k++) printf("%s ", argv[k]); printf("\n"); return 0;}运行示例
$ ./prog hello world 123hello world 123此时 argc = 4,argv[0] = "./prog",argv[1] = "hello",argv[2] = "world",argv[3] = "123"。
关键知识点
argv的本质:char *argv[]是一个指针数组,每个元素指向一个以\0结尾的字符串。argc至少为 1:即使不传任何参数,argv[0]也是程序名。argv[argc]一定是NULL:C 标准规定,可作为循环终止条件。
八、动态数组(动态内存分配)
03-动态数组-填空.c:运行时输入 n,动态分配长度为 n 的整型数组,输入数据并求和。
解题思路
scanf读入nmalloc分配n * sizeof(int)字节,返回void*,需强转为int*- 判断返回值是否为
NULL(分配失败) - 用
p + i或*(p + i)访问元素 - 用完必须
free(p)释放内存
完整代码(含填空答案)
#include<stdio.h>#include <stdlib.h>
int main(){ int n, sum, i, *p; printf("Enter n: "); scanf("%d", &n);
/* 【填空】(int *)malloc(n * sizeof(int)) malloc 返回 void*,强转为 int* 分配 n 个 int 的连续内存 */ if (((int *)malloc(n * sizeof(int))) == NULL) { printf("Not able to allocate memory. \n"); exit(1); }
/* 注意:原题这里有一个 bug——上面 malloc 的返回值没有赋给 p 正确写法应为: if ((p = (int *)malloc(n * sizeof(int))) == NULL) { ... } 下面按"修正后"的写法继续 */ printf("Enter %d integers: ", n); for (i = 0; i < n; i++) { scanf("%d", p + i); /* p+i 是第 i 个元素的地址 */ } sum = 0; for (i = 0; i < n; i++) { sum = sum + *(p + i); /* *(p+i) 等价于 p[i] */ } printf("The sum is %d \n", sum);
/* 【填空】free(p) 释放动态分配的内存,避免内存泄漏 */ free(p); return 0;}修正说明
原题第一处填空写的是 if (((int *)malloc(...)) == NULL),malloc 的返回值被丢弃了,后续 p 仍是野指针。正确写法应把 malloc 的结果赋给 p:
if ((p = (int *)malloc(n * sizeof(int))) == NULL) { printf("Not able to allocate memory.\n"); exit(1);}关键知识点
| 函数 | 作用 | 头文件 |
|---|---|---|
malloc(size) | 分配 size 字节,内容未初始化 | <stdlib.h> |
calloc(n, size) | 分配 n×size 字节,清零 | <stdlib.h> |
realloc(p, size) | 调整已分配内存大小 | <stdlib.h> |
free(p) | 释放内存 | <stdlib.h> |
p + i与&p[i]等价,*(p + i)与p[i]等价。- 配对原则:每个
malloc必须对应一个free,否则内存泄漏。 free后应将指针置空:free(p); p = NULL;,防止”悬垂指针”被误用。
九、课前测改错题:指针求最大值
test08_1.c:用指针遍历数组求最大值。
题目说明
输入 n(n≤10)和 n 个整数,用指针遍历数组找出最大值并输出。
完整代码
/* 用指针找出 n 个整数中的最大值 */#include <stdio.h>int main(void){ int a[10], i, n, max, *p;
printf("Enter n:"); scanf("%d", &n); for (i = 0; i < n; i++) { scanf("%d", &a[i]); }
p = a; /* p 指向数组首元素 */ max = a[0]; while (p < a + n) { /* p 未越界时继续循环 */ if (*p > max) { max = *p; } p++; /* 指针后移一位 */ } printf("max = %d\n", max);
return 0;}关键知识点
p = a:数组名a就是首元素地址,等价于p = &a[0]。p < a + n:用指针比较控制循环边界,a + n是越界后的首地址。p++:指针加 1 跨过一个int(不是 1 字节),这是指针运算的核心特性。
十、课前测指针复习:数组循环右移
test08_2.c:输入 n 和 m,将 n 个整数循环右移 m 位。
题目说明
示例:1 2 3 4 5 右移 3 位 → 3 4 5 1 2。要求用函数 void mov(int *a, int n, int m) 实现。
解题思路
循环右移 m 位等价于执行 m 次”整体右移 1 位”。每次右移 1 位的做法:保存最后一个元素,然后从后往前把每个元素后移一位,最后把保存的值放到最前面。
完整代码
/*数组循环右移:输入 n、m,再输入 n 个整数,将每个整数循环向右移动 m 个位置(最后 m 个数移到最前)。*/#include <stdio.h>void mov(int* a, int n, int m);int main(void){ int a[80], i, m, n, *p;
printf("Enter n, m:"); scanf("%d%d", &n, &m); /* 用指针 p 输入 n 个数 */ for (p = a, i = 0; i < n; i++) { scanf("%d", p++); } mov(a, n, m); printf("After moved: "); for (i = 0; i < n; i++) { printf("%5d", a[i]); } return 0;}
/* 循环右移 m 位 */void mov(int* a, int n, int m){ int i, j, temp;
m = m % n; /* m 可能大于 n,取余避免重复移动 */ for (i = 0; i < m; i++) { temp = a[n - 1]; /* 暂存最后一个元素 */ for (j = n - 1; j > 0; j--) { a[j] = a[j - 1]; /* 整体后移一位 */ } a[0] = temp; /* 最后一个放到最前 */ }}关键知识点
m = m % n:处理m ≥ n的情况,移动n次等于没动。- 指针作输入循环变量:
for (p = a, i = 0; i < n; i++) scanf("%d", p++);巧妙地用p++边输入边后移。 - 数组名作函数参数:
mov(a, n, m)传的是首元素地址,函数内对a[i]的修改就是修改main中的数组——这又是”地址传递”的应用(与 5.3 节呼应)。
十一、知识点总结
11.1 指针核心运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
& | 取地址 | &a 得到 a 的地址 |
* | 解引用(取值) | *p 得到 p 所指内存的值 |
*p = | 通过指针写值 | *p = 10 把 10 写入 p 所指内存 |
11.2 指针与数组的关系
| 表达式 | 含义 |
|---|---|
a | 数组名 = 首元素地址,等价 &a[0] |
a + i | 第 i 个元素的地址,等价 &a[i] |
*(a + i) | 第 i 个元素的值,等价 a[i] |
p = a | 指针指向数组首元素 |
p++ | 指针后移一个元素(不是 1 字节) |
p - a | 两指针之间的元素个数 |
11.3 二维数组的指针类型
| 表达式 | 类型 | 含义 |
|---|---|---|
a | int(*)[N] | 指向第一行 |
a[i] | int* | 指向第 i 行第一个元素 |
a[i][j] | int | 元素值 |
a + i | int(*)[N] | 指向第 i 行 |
*(a + i) | int* | 第 i 行首元素地址,等价 a[i] |
*(*(a + i) + j) | int | 等价 a[i][j] |
11.4 易错点速查
- 指针未初始化就解引用:
int *p; *p = 10;—— 未定义行为,必须先让p指向有效内存。 &p与p混淆:&p是指针自身的地址,p是指针存储的地址(指向的对象)。- 交换指针 vs 交换值:见第五节,这是最常见的考点。
int *p[4]vsint (*p)[4]:前者是指针数组,后者是数组指针。malloc后忘记free:内存泄漏;free后继续使用p:悬垂指针。- 二维数组作函数参数省略第二维:错误,第二维大小必须显式。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时












