资讯动态

C语言指针编程练习题解析:从数组操作到函数参数避坑指南

发布时间:2026/10/21 10:19:06 来源:尧图企业网站定制
简介c语言指针编程练习题.pdf是一份面向C语言初学者与应试者的指针专项练习资料精选11道典型题目覆盖指针基本操作、数组遍历与交换、字符串处理、矩阵转置、数字进制转换及函数参数传递等关键场景。每道题均提供参考答案及代码示例帮助读者在实战中理解指针与数组、字符串、函数之间的内在联系逐步建立底层内存操作的直觉。资源为单个PDF文档容量约97KB轻量易用适合随时下载打印或在线对照练习。目前已有3100人学习使用题目难度由浅入深既可用于期末复习也可作为自学查漏补缺的备查手册。通过完成这些练习读者可切实掌握用指针访问数组元素、通过指针交换变量、模拟循环报数、求字符串长度、实现奇数偶数求和等方法与技巧并能独立完成八进制转十进制、字符串复制及三字符串排序等综合任务对提升C语言编程能力与代码调试水平有明显帮助。1. C 语言指针编程练习题这份 PDF 到底能让你练到什么程度看到「C 语言指针练习题.pdf」这个文件名不少人第一反应是「又是一堆 scanf、printf 的入门题」。实际上这份文档里藏着一组非常典型的指针训练序列从最基本的交换两个数的值到数组移位、字符串排序、矩阵转置再到用指针数组做参数传回多个结果基本覆盖了 C 语言指针从「会声明」到「敢用在函数参数里」的全过程。很多人在学校学到指针就卡在「数组名是不是指针」「为什么交换函数传了指针还是没换回来」这类问题上这份练习题恰好就是针对这些卡点设计的。它适合两类人一类是刚学完指针语法、想靠题目把概念落地的初学者另一类是准备机考或面试、需要快速过一遍指针常见用法的从业者。文档里 11 道题目全部配有参考答案虽然有些答案的排版和注释比较老旧但核心思路是对的照着敲一遍比看十遍教程都管用。接下来我会按「数组操作 → 字符串处理 → 函数参数 → 常见坑」的顺序拆开讲每一道题都会给出可运行的代码和参数说明。2. 指针与数组交换、移位与最值查找的三种必练写法数组和指针的关系是 C 语言里最微妙的部分数组名在大多数表达式里会退化成首元素地址但sizeof和操作符除外。这导致很多人在写int a[10]; int *p a;之后分不清p[i]、*(pi)、a[i]到底谁是谁。这一章的三道练习题就是用来彻底打通这个关系的。2.1 用指针交换两个变量swap 函数的标准形态题目 2 要求输入三个整数按从小到大输出参考答案用了三次 swap。交换是后续所有数组操作的基础先把这一步写对#include stdio.h void swap(int *pa, int *pb) { int temp; temp *pa; *pa *pb; *pb temp; } int main() { int a, b, c; scanf(%d %d %d, a, b, c); if (a b) swap(a, b); if (a c) swap(a, c); if (b c) swap(b, c); printf(%d %d %d\n, a, b, c); return 0; }这段代码的核心在于swap(a, b)传的是地址swap内部通过*pa解引用来修改调用者的变量。如果写成void swap(int pa, int pb)那交换的只是函数内部的副本主调函数里 a、b 的值不会变化——这是初学者最容易翻车的地方。参数说明int *pa和int *pb接收的是两个 int 类型变量的地址交换的过程必须借助第三个变量temp不能直接*pa *pb; *pb *pa;因为第二步时*pa已经被覆盖判断流程上三次比较的顺序是「先用 a 和剩下两个比再用 b 和 c 比」这个顺序不能乱否则会出现漏比较的情况。你可以试着把三个 if 的顺序调换一下结果很可能就不对了。2.2 数组最大值与最小值的位置交换两种查找策略题目 3 要求在数组中找到最大值并与第一个元素交换找到最小值并与最后一个元素交换。很多人的第一版代码会写成「先找出最大值交换再找出最小值交换」但这样会引入一个隐蔽的 bug如果最小值原本就在第一个位置第一次交换值最大值之后最小值的位置可能已经被换走了。#include stdio.h void swap(int *pa, int *pb) { int temp *pa; *pa *pb; *pb temp; } int main() { int arr[10] {3, 7, 1, 9, 4, 6, 2, 8, 5, 0}; int n 10; int max_idx 0, min_idx 0; int i; for (i 1; i n; i) { if (arr[i] arr[max_idx]) max_idx i; if (arr[i] arr[min_idx]) min_idx i; } swap(arr[0], arr[max_idx]); // 如果最小值原本在 0 号位交换最大值时它被移走了 if (min_idx 0) min_idx max_idx; swap(arr[n-1], arr[min_idx]); for (i 0; i n; i) { printf(%d , arr[i]); } printf(\n); return 0; }这里的if (min_idx 0) min_idx max_idx;是关键补丁。逻辑说明先分别记录最大值和最小值的下标第一次交换把最大值放到数组头部如果最小值原本就在头部那么它已经被换到了max_idx这个位置所以要把min_idx更新成max_idx第二次交换才能找到真正的最小值。另一种策略是「同时记录两个极值的下标然后分两种情况处理」但上面的补丁写法更直观也更容易记住。从指针的角度看arr[i]和*(arr i)完全等价你可以在循环里换成指针遍历加深理解。2.3 数组循环右移 m 位原地算法的三种实现题目 5 要求把 n 个整数前面的各数顺序向后移 m 个位置最后 m 个数变成最前面的 m 个数。这道题有三个常见的解法开新数组拷贝、每次移动一位循环 m 次、三次逆置法。参考答案采用的是第一种思路但我更推荐你写一遍三次逆置法因为它在面试里经常被追问。#include stdio.h void reverse(int arr[], int left, int right) { while (left right) { int temp arr[left]; arr[left] arr[right]; arr[right] temp; left; right--; } } int main() { int arr[] {1, 2, 3, 4, 5, 6, 7, 8}; int n 8; int m 3; m m % n; // 关键m 可能大于 n reverse(arr, 0, n - 1); // 整体逆置 reverse(arr, 0, m - 1); // 前 m 个逆置 reverse(arr, m, n - 1); // 后面 n-m 个逆置 for (int i 0; i n; i) { printf(%d , arr[i]); } printf(\n); return 0; }运行逻辑原数组{1,2,3,4,5,6,7,8}整体逆置后变成{8,7,6,5,4,3,2,1}前 m3 个逆置得到{6,7,8,5,4,3,2,1}后 5 个逆置得到{6,7,8,1,2,3,4,5}正好是右移 3 位的结果。参数说明reverse函数接受数组首地址和左右边界下标左闭右闭区间m m % n这一步很容易漏如果不取模当 m 大于 n 时会多做一整轮无效移动逆置法的时间复杂度是 O(n)空间复杂度是 O(1)比「每次移动一位循环 m 次」的 O(n×m) 要高效得多如果把问题改成「左移」只需要调整后面两次 reverse 的分界即可。这一题练完你对「数组下标 元素位置」的理解会上一个台阶。3. 指针与字符串长度、复制与排序的三个经典动作字符串在 C 语言里就是字符数组但操作字符串时的指针细节比 int 数组多不少字符串常量存在只读区、字符数组和字符指针的初始化方式不同、字符串比较不能直接用。这一章的练习题集中覆盖了这些点。3.1 自定义 strlen为什么参考答案不直接用库函数题目 7 要求写一个函数求字符串长度。有人觉得直接调strlen就行但题目考察的是你知不知道字符串以\0结尾、以指针遍历时怎么判断结束。#include stdio.h int str_len(const char *s) { const char *p s; while (*p ! \0) { p; } return p - s; } int main() { char str[100]; printf(请输入一个字符串); gets(str); printf(字符串长度为%d\n, str_len(str)); return 0; }逻辑说明指针p从字符串首地址开始逐字节向后移动每移动一次计数加一通过指针差p - s实现直到遇到\0结束。这里刻意用了const char *s表明函数不应该修改传入的字符串内容。参数说明返回值类型是int但极端情况下字符串长度可能超过 int 上限工程上一般用size_tgets在较新的编译器中会报警告建议换成fgets(str, sizeof(str), stdin)它会保留换行符指针差p - s得到的是两个指针之间的元素个数在同一个数组内才定义良好。这个写法比用计数器变量count更简洁也更贴近指针运算的本质。3.2 字符串复制strcpy 的实现原理题目 10 要求复制字符串。参考答案直接用了strcpy但更值得写一遍的是手写版实现它能帮你理解\0拷贝的重要性#include stdio.h char *my_strcpy(char *dest, const char *src) { char *ret dest; while ((*dest *src) ! \0) { // 先赋值再判断同时把 dest 和 src 后移 } return ret; } int main() { char src[] hello pointer; char dest[50]; my_strcpy(dest, src); printf(%s\n, dest); return 0; }逻辑说明*dest *src这条语句做了三件事——把*src赋值给*dest、两个指针各自后移、判断赋值结果是否为\0。当\0被拷贝到 dest 时循环结束所以不需要在循环外额外补\0。参数说明返回dest的原始值是为了支持链式调用比如strlen(strcpy(dest, src))调用方必须保证 dest 的空间足够否则会发生缓冲区溢出这是 C 语言字符串操作最常见的隐患3.3 三个字符串排序字符指针交换的边界题目 11 要求输入三个字符串按从小到大输出参考答案用字符指针p1, p2, p3直接做交换。这里容易踩坑如果用char ch1[20]定义数组然后p1 ch1; p2 ch2; p3 ch3;交换指针变量的指向是允许的但数组本身的内容没有变。#include stdio.h #include string.h int main() { char *t; char *p1 NULL, *p2 NULL, *p3 NULL; char ch1[20] {0}, ch2[20] {0}, ch3[20] {0}; p1 ch1; p2 ch2; p3 ch3; printf(请输入三个字符串\n); scanf(%s, p1); scanf(%s, p2); scanf(%s, p3); if (strcmp(p1, p2) 0) { t p1; p1 p2; p2 t; } if (strcmp(p1, p3) 0) { t p1; p1 p3; p3 t; } if (strcmp(p2, p3) 0) { t p2; p2 p3; p3 t; } printf(%s\n%s\n%s\n, p1, p2, p3); return 0; }逻辑说明strcmp返回正数表示第一个字符串大于第二个这里用指针交换的方式调整 p1、p2、p3 的指向最终让 p1 指向最小的字符串。这种做法的好处是字符串本身不用移动只交换地址成本很低。参数说明char ch1[20] {0}初始化保证数组全部清零避免未初始化的字符数组里残留垃圾数据strcmp比较的是字典序不是字符串长度。如果想按长度排序得改成strlen(p1) strlen(p2)注意交换指针变量和交换数组内容有本质区别。上面代码中t p1; p1 p2; p2 t;交换的是指针的指向ch1、ch2、ch3 数组里的内容从头到尾没有变过。如果写成strcpy(ch1, ch2)才是真正修改数组内存。4. 指针做函数参数一维数组指针、指针数组与多返回值这一章的练习题难度开始上升不再满足于「用指针访问数组元素」而是要求你理解「指针作为函数参数传递时到底传了什么」。核心是三种场景指向一维数组的指针、指针数组、通过指针参数返回多个值。4.1 指向一维数组的指针做参数sort 函数的完整实现题目 11 的进阶版出现在文档里用指向一维数组的指针做函数参数对 10 个字符串排序。这种写法在二维字符数组传参时特别常见但很多人会写错类型。#include stdio.h #include string.h void sort(char (*s)[6], int n) { int i, j; char temp[6]; for (i 0; i n - 1; i) { for (j 0; j n - 1 - i; j) { if (strcmp(s[j], s[j 1]) 0) { strcpy(temp, s[j]); strcpy(s[j], s[j 1]); strcpy(s[j 1], temp); } } } } int main() { char str[10][6]; int i; printf(请输入 10 个字符串\n); for (i 0; i 10; i) { scanf(%s, str[i]); } sort(str, 10); printf(排序结果\n); for (i 0; i 10; i) { printf(%s\n, str[i]); } return 0; }char (*s)[6]读法s是一个指针指向一个长度为 6 的 char 数组。也就是说s的类型是「指向一维数组的指针」它和char *s完全不同。参数说明函数形参char (*s)[6]可以完美匹配实参char str[10][6]因为str在传参时退化成指向第一行的指针类型正好是char (*)[6]冒泡排序的外层循环i n - 1内层j n - 1 - i每轮确定一个最大值放到末尾交换时必须用strcpy逐字节拷贝不能直接s[j] s[j1]因为数组名不是可修改的左值这里有几个容易写错的点把char (*s)[6]写成char *s[6]。前者是「指向数组的指针」后者是「元素为字符指针的数组」含义完全相反。你可以这样记*和s之间有没有括号决定了*先和谁结合。4.2 指针数组排序交换指针比移动字符串高效同样是对 10 个字符串排序但如果字符串本身长度很大用strcpy交换整个数组就不划算了。更高效的做法是定义指针数组让每个指针指向对应字符串的首地址排序时只交换指针。#include stdio.h #include string.h void sort(char *p[], int n) { int i, j; char *temp; for (i 0; i n - 1; i) { for (j 0; j n - 1 - i; j) { if (strcmp(p[j], p[j 1]) 0) { temp p[j]; p[j] p[j 1]; p[j 1] temp; } } } } int main() { char str[10][20]; char *p[10]; int i; for (i 0; i 10; i) { p[i] str[i]; scanf(%s, p[i]); } sort(p, 10); for (i 0; i 10; i) { printf(%s\n, p[i]); } return 0; }逻辑说明char *p[10]定义了一个指针数组每个元素存放一个字符串的首地址。p[i] str[i]让第 i 个指针指向二维数组的第 i 行。排序时temp p[j]只交换地址值不搬动任何字符串字节在字符串较长时性能优势明显。参数说明形参char *p[]和char **p在函数参数列表里等价因为数组参数都会退化成指针strcmp(p[j], p[j 1])比较的是 p[j] 和 p[j1] 各自指向的字符串内容而不是指针值本身4.3 用指针返回多个结果odd 和 even 的出参模式题目 10 要求写一个函数分别求出数组中所有奇数之和以及所有偶数之和用odd指针返回奇数和用even指针返回偶数和。这种「靠指针出参带回多个返回值」的模式在工程中非常常见。#include stdio.h void fun(int arr[], int n, int *odd, int *even) { int i; *odd 0; *even 0; for (i 0; i n; i) { if (arr[i] % 2 0) { *even arr[i]; } else { *odd arr[i]; } } } int main() { int a[] {1, 8, 2, 3, 11, 6}; int odd_sum 0, even_sum 0; fun(a, 6, odd_sum, even_sum); printf(奇数之和%d偶数之和%d\n, odd_sum, even_sum); return 0; }逻辑说明odd和even在函数内部通过解引用操作直接修改主调函数中的变量。这里的本质是「把变量的地址传进去在函数内部通过地址找到变量的存储位置再写入」。参数说明出参必须传地址odd_sum和even_sum否则函数内部对形参的赋值不影响外部变量函数进入时先清零*odd和*even防止残留上一轮数据4.4 矩阵转置二维数组的指针下标访问文档里还附了一道 3×3 矩阵转置的题目参考答案用的是*(*(ai)j)这种运算符组合。对于二维数组a[3][3]a[i][j]、*(*(ai)j)、*(a[i]j)三种写法完全等价。#include stdio.h void transpose(int a[][3], int row) { int i, j, temp; for (i 0; i row; i) { for (j i 1; j 3; j) { temp *(*(a i) j); *(*(a i) j) *(*(a j) i); *(*(a j) i) temp; } } } int main() { int a[3][3] {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; transpose(a, 3); for (int i 0; i 3; i) { for (int j 0; j 3; j) { printf(%d , a[i][j]); } printf(\n); } return 0; }逻辑说明转置就是把下三角元素和上三角对应元素交换所以内层循环从j i 1开始避免交换两次又换回去。参数说明形参int a[][3]编译时会退化成「指向长度为 3 的 int 数组的指针」即int (*a)[3]不能省略列数因为编译器需要知道每一行的长度才能计算a[i][j]的地址二维数组传参时的「数组指针」概念和 4.1 节一脉相承如果你能把这部分完全吃透后面遇到char **argv这种命令行参数也不会再怕。5. 指针避坑指南五个最容易出错的写法与排查思路指针的错误通常是两种类型编译能过但运行崩溃段错误或者结果诡异但程序不死。下面这五条是我从类似练习题评论区、考试错题和实际调试里反复见到的典型问题按「现象 → 原因 → 解决」逐条拆。5.1 scanf 传参写成scanf(%d, *p)导致程序崩溃现象程序一运行到 scanf 就崩或者编译能过但输入后异常退出。原因scanf需要的是存储地址*p是变量的值。如果把值传进去scanf 把用户输入写到这个「值」所代表的地址上这个地址大概率非法直接触发段错误。解决int 类型用scanf(%d, p)如果 p 已经指向目标变量如果要给char *指向的缓冲区输入字符串直接scanf(%s, p)而不是scanf(%s, p)。记住一个原则scanf 的参数列表里加不加取决于变量本身是不是地址。5.2 数组指针的 p 步长被误认为是 1 个字节现象int a[5] {1,2,3,4,5}; int *p a; p;之后*p打印出来是 0 或者奇怪的值。原因指针自增的步长是「所指向类型的 sizeof」。int *每次加 1 移动 4 个字节在 32 位或 64 位系统上通常如此不是 1 个字节。很多人以为 p 会移动到数组的下一个字节然后输出错乱。解决先明确 p 指向什么类型。int *p 移动到下一个 int 元素char *p 才移动 1 个字节。调试时可以用printf(%p\n, p)观察地址变化看到地址差 4 或 8 就说明走对了。5.3 对字符串常量赋值导致写入只读区现象char *s hello; s[0] H;运行时报段错误但char s[] hello; s[0] H;就正常。原因hello是一个字符串字面量存储在只读数据段。char *s只是把首地址存到了指针里通过指针修改只读区域会触发保护错误。而char s[] hello是在栈上拷贝了一份可以自由修改。解决如果字符串内容可能被修改一定要用字符数组初始化只有在确定只读的情况下才用char *指向字符串字面量。这是一个非常容易在笔试选择题里出现的话题。5.4 字符串比较直接写成if (p1 p2)现象两个字符串内容明明符合条件但排序结果不对。原因p1 p2比较的是两个指针的地址值不是字符串内容。地址大小取决于内存布局和字典序没有任何关系。解决一律用strcmp(p1, p2) 0来判断 p1 指向的字符串是否大于 p2 指向的字符串。做题时看到指针比较优先级很高十有八九是在考这一点。5.5 把字符指针数组和指针数组混淆现象char *p[10]被读成「字符串指针 p」实际它是「10 个字符指针组成的数组」。原因[]的优先级高于*所以char *p[10]先读作「p 是一个数组元素类型是 char *」。而char (*p)[10]先读作「p 是一个指针指向 char[10] 数组」。解决带括号的优先级更高没括号的优先结合成数组。这个知识点基本是 C 语言指针考试的送分题但也是很多人丢分的地方。遇到这类声明用「右左法则」读一遍先看变量名右边再看左边。提示调试指针问题时建议开编译器的-Wall警告选项。很多问题比如类型不匹配、未初始化指针编译器其实会提醒只是默认不打开。5.6 函数返回了局部变量的地址现象函数返回后调用方拿到的指针指向的数据变成乱码。原因函数内声明的局部变量存储在栈上函数返回后栈帧被回收那块内存的内容随时会被后续调用覆盖。返回局部变量的地址是悬垂指针行为未定义。解决要么用 static 变量要么把结果通过出参写回要么在堆上 malloc。练习题里经常出现「写一个函数返回字符串」的场景最容易犯这个错。6. 报数问题与级数求和两个容易被忽略的边界技巧文档里的第 5 题「n 个人围成一圈报到 3 的人退出」本质上是一个约瑟夫环问题。很多人直接用数组和计数变量硬模拟但如果你试着用指针数组来写会对「指针就是地址、地址就是另一个变量」有更深的理解。第 7 题「奇数偶数分别求级数和」看起来简单但它考验的是对 n 的奇偶性分支处理的细粒度而且隐藏着一个运算精度的问题。6.1 约瑟夫环的指针数组模拟常见做法是用一个数组记录每个人是否出圈然后循环数数。指针数组版本把「人」抽象成地址删除时直接把指针移出数组思维上更贴近链表操作#include stdio.h int main() { int n 10; int m 3; int people[10]; int *p[10]; int i, count, idx; for (i 0; i n; i) { people[i] i 1; p[i] people[i]; } count n; idx 0; while (count 1) { idx (idx m - 1) % count; printf(%d 号退出\n, *p[idx]); for (i idx; i count - 1; i) { p[i] p[i 1]; } count--; if (idx count) idx 0; } printf(最后留下的是 %d 号\n, *p[0]); return 0; }逻辑说明p数组的每个元素指向一个原始编号。每轮报数用(idx m - 1) % count定位出圈者然后通过移动指针数组的方式把后面的指针往前覆盖等效于删掉这个元素。if (idx count) idx 0处理的是删掉最后一个元素后下标越界的问题。参数说明m 3表示报到 3 的人退出idx m - 1里减 1 是因为本轮从当前位置开始数当前已经算一个取模% count保证循环绕圈用指针数组而非直接删除 people 数组元素的原因是people 数组在内存中连续删除中间元素需要搬移整个数组效率低而移动指针数组只搬运地址代价小很多6.2 奇数偶数的级数求和n 是偶数和奇数的双分支题目 8 要求 n 为偶数时求1/2 1/4 ... 1/n为奇数时求1/1 1/3 ... 1/n。这道题有两个考点一个是「偶数和奇数走不同分支」的逻辑正确性另一个是「整数除法和浮点数除法的区别」。#include stdio.h double sum_series(int n) { double sum 0.0; int start (n % 2 0) ? 2 : 1; int i; for (i start; i n; i 2) { sum 1.0 / i; } return sum; } int main() { int n; printf(请输入 n); scanf(%d, n); printf(级数和为%lf\n, sum_series(n)); return 0; }逻辑说明start根据 n 的奇偶性决定首项是 2 还是 1循环步长为 2 就能覆盖所有偶数或奇数分母。1.0 / i中的 1.0 是 double 类型保证整个除法以浮点数方式运算。如果写成1 / i整数除法会把 1/2 算成 0结果全错。参数说明n 为负数或 0 时函数返回 0但题目一般不会考察这种边界也可以把1.0 / i改成(double)1 / i效果一样6.3 八进制转十进制的指针遍历题目 9 要求把八进制字符串转成十进制数。用指针从字符串末尾向开头遍历每位的权值依次乘 8#include stdio.h #include string.h #include math.h int oct_to_dec(const char *oct) { int len strlen(oct); const char *p oct len - 1; // 指向最后一位 int weight 0; int result 0; while (p oct) { int digit *p - 0; if (digit 0 || digit 7) { return -1; // 非法八进制字符 } result digit * (int)pow(8, weight); weight; p--; } return result; } int main() { char input[20]; scanf(%s, input); int val oct_to_dec(input); if (val -1) { printf(非法八进制数\n); } else { printf(十进制%d\n, val); } return 0; }逻辑说明从最低位开始*p - 0把字符转成数字pow(8, weight)计算当前位的权值。指针p从后往前移动循环终止条件是p oct。参数说明oct_to_dec返回 -1 表示输入非法但 -1 本身也可能是一个合法计算结果所以更稳妥的做法是额外加一个出参标记合法性pow返回 double这里强转成 int 是安全的因为权值都是整数从做这些练习题到今天我养成的一个习惯是不管题目看起来多简单写完代码后强制自己用边界值跑一遍。交换题就试两个数相等的情况移位题就试 m 等于 n 和 m 大于 n 的情况约瑟夫环就试 n 等于 1 的情况。指针这玩意越是看似理所当然的地方越容易藏着未定义行为。这份 PDF 里的题目未必是最新的但每一道都值得反复写、反复改直到你能不看答案直接敲出来。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑