ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C语言完美演绎4-6:字符串、循环、数组与指针实战精讲

C语言完美演绎4-6:字符串、循环、数组与指针实战精讲 我拿到“C语言完美演绎4-6”这个题目的时候第一反应是这串数字到底指什么是教材的第4到第6章是某个OJ平台上的4到6号题还是课程视频的第4到第6讲说实话我盯着这几个字符想了挺久。后来我换了个思路——与其纠结编号的准确含义不如把“4-6”当成C语言学习路上最容易被卡住的那段“腰部区间”。带过不少新人之后我发现90%的人并不是死在入门语法上而是死在“语法看得懂、题目写不出”的过渡阶段。而这个阶段对应的内容恰恰就是很多经典教材第4到第6章常讲的字符串与格式化输入输出、运算符与表达式、循环控制、数组与指针。所以这篇文章我打算用“46”的方式来演绎4个核心知识板块6道能直接跑通的经典实战题。把理论拆开揉碎把代码一行行讲明白让“看得懂”真正变成“写得出来”。无论你是刚学完C语言基础、正在为PTA或NOJ作业发愁的同学还是工作几年想回头补底子的开发者这篇都值得从头到尾过一遍。1. 为什么把“4-6”理解为C语言的黄金拐点1.1 教材第4到6章恰好是“看得懂”与“写得出来”的分水岭很多人学C语言会有一种错觉printf会了、变量会了、if会了就以为自己会编程了。结果一到OJ上做题第一道题就卡半小时。原因很简单前面几章教你的是“单词”从第4到第6章开始你才真正学“造句”。字符串怎么存、输入输出怎么处理、循环怎么控制流程、数组怎么组织数据这些内容一旦拼到一起才是程序的基本骨架。我带过的一位朋友就是这样教材前三章背得滚瓜烂熟变量类型、printf格式张口就来但让他写一个“输入年份判断闰年”的小程序他愣是不知道从哪下手。问题就出在缺少“组合逻辑”的训练。第4到第6章这个区间核心目的就是把你从“认单词”推向“组句子”。这也是为什么我把4-6当成拐点而不是某个具体编号。1.2 我的“46”演绎方案怎么选材怎么排列既然4-6是个区间那我就用“4个核心板块6道实战题”把它装填满。一开始我也考虑过只讲语法、只列知识点但那样就成了说明书没有“演绎感”。真正的演绎必须落到代码上。先看4个核心板块怎么选板块常考/常踩的知识点主要解决的问题字符串与格式化IOscanf缓冲区、字符串函数、文件缓冲区让输入输出不再像黑盒运算符与表达式ab这类自增陷阱、优先级让表达式不再靠猜分支与循环while/do-while/for、嵌套循环让控制流程真正可控数组与指针数组表示、指针运算、指针数组/数组指针完成数据组织的思维切换再看6道实战题怎么挑题目核心考点完数检测循环、取模、因子、函数返回值九九乘法表嵌套循环、输出格式控制字符串逆序strlen、双指针、原地交换日期求第几天数组查表、闰年判断、分支逻辑复数四则运算结构体、函数传参、数学建模冒泡排序双层循环、指针传参、算法优化每一道题都不是孤立的基本都能覆盖2到3个知识板块。比如日期计算既考循环又考数组还考分支复数四则运算既考结构体又考函数设计。这样你练一道题等于复习好几个点效率比散刷高得多。2. 四个知识板块把最容易卡住的地方逐个讲透2.1 字符串与格式化IOscanf的脾气你得摸清后台搜“c语言学习之scanf”的人一直不少这个函数确实容易翻车。最常见的坑是缓冲区残留。比如先scanf(%d, n)再scanf(%c, c)第二个读到的往往不是你想要的字符而是上一次回车留下的\n。#include stdio.h int main() { int n; char c; scanf(%d, n); scanf( %c, c); // 注意 %c 前面的那个空格 printf(%d %c\n, n, c); return 0; }关键在于scanf家族对空白字符的处理。读数字时会自动跳过前面的空白但%c不会。解决办法我常用三种第一种是格式串里加空格就像上面这样第二种是前面用getchar()消费掉多余字符第三种更彻底直接用fgets读一行再sscanf解析尤其适合一行输入多个数据的情况。字符串函数也是这个板块的重头戏。strlen拿到的是长度不含末尾的\0strcpy不会检查目标空间够不够strcmp比较的是ASCII码而不是长度strcat拼接时同样不管空间。下面这几个是刷题最高频的函数作用重点提醒strlen(s)返回字符串长度不含\0strcpy(d, s)字符串复制目标数组要够大strcat(d, s)字符串拼接目标数组要预留空间strcmp(a, b)字符串比较返回0表示相等strchr(s, c)查找字符找不到返回NULLisalpha/isdigit字符分类返回非0就是真不一定是1这里特别说一下字符分类函数很多人以为isalpha返回1其实只要非0就代表真。代码里如果写if (isalpha(ch) 1)在某些环境下没问题换一个平台可能就翻车。正确写法是if (isalpha(ch))。文件缓冲区也值得在学字符串IO的时候一起理解。stdout在终端下通常是行缓冲遇到\n才真正输出但重定向到文件时往往变成全缓冲没关文件就退程序数据可能没落盘。程序崩溃时缓冲数据丢失这也是很多新手写文件操作“明明写了却没内容”的根源。fclose不仅能关文件还会把缓冲区内容刷新出去。如果你用fscanf、fprintf道理和scanf、printf一样只是操作对象从标准输入输出变成了文件指针。2.2 运算符与表达式b前面的那个坑有人直接搜“c语言 a b解释”可见这个知识点折磨了不少人。给你个最直观的例子int b 2; int a b; // b先自增为3再把3赋给a结果a3,b3 int a2 b; // 先把b3赋给a2再让b自增为4结果a23,b4b是前缀自增先改值再返回b是后缀自增先返回原值再改值。单独写b和b效果一样但一旦卷进表达式里参与运算的值就完全不同。我的个人建议是别在复杂表达式里堆自增自减比如i i这种写法在C标准里属于未定义行为不同编译器结果可能都不一样。代码是给人看的清晰比炫技重要。顺便把其他容易踩的点也说了。整数除法直接截断5/2等于2而不是2.5想要小数至少一边是浮点型逻辑或和逻辑与都存在短路求值比如if (a ! 0 b/a 1)a等于0时后面不会执行这反而是一种保护位运算里移位比乘除快但别滥用新手阶段先保证正确再说性能。2.3 分支与循环while、do-while和for到底怎么选有人搜“c语言while和do-while区别”核心就一句话while先判断再执行可能一次都不进do-while先执行一次再判断至少执行一次。看这个对比更清楚循环方式执行时机典型使用场景for先判断次数明确遍历数组、固定次数循环while先判断次数不确定条件控制、读文件到结尾do-while先执行一次再判断菜单选择、至少执行一次的业务语法细节上do-while结束时要加英文分号很多新手漏掉这个分号直接编译报错。至于while(1)和for(;;)两者等价选哪个看个人习惯我习惯用for(;;)表示死循环语义上更明确。选择循环类型的经验法则是明确知道循环几次就选for不知道具体次数只知道退出条件的选while不管条件真假都得先做一次的选do-while。在3.2节的九九乘法表里你会看到for嵌套就是最自然的写法。2.4 数组与指针从“第几个”到“哪个地址”的思维转换“c语言 数组 指针 移动 指定位输出 字符”这类搜索词背后其实是同一个困惑数组名到底是不是指针严格说数组名在表达式里会退化为指向首元素的指针但它是“常量指针”不能做自增自减。int a[10]a的类型是int*值是a[0]而a是整个数组的地址类型是int(*)[10]。这俩写起来很像含义完全不同。指针运算的逻辑是p1不是地址加1个字节而是加一个“指向类型”的大小。int加1跳过4个字节char加1跳过1个字节。所以*(pi)完全等价于p[i]这也是为什么数组下标本质是语法糖。数组与指针声明里的优先级问题更要命。很多人问“c语言 四组指针指针怎么表示”我们先分清这三个长相接近的声明声明名称含义int *p[4]指针数组p是一个数组数组有4个元素每个元素是int*int (*p)[4]数组指针p是一个指针指向“有4个int的数组”int **p指针的指针p指向一个int*通常用于二维数组的模拟记忆诀窍是看p先跟谁结合中括号优先级高于星号所以int *p[4]里p先和[4]结合是数组int (*p)[4]里括号强制p先和星号结合是指针。别小看这个区别它直接影响你写二维数组传参、动态分配内存时的类型匹配。数组作为函数参数时int arr[]等价于int *arr所以在函数里对参数arr做sizeof拿到的是指针大小而不是数组大小这也是一个高频坑。3. 六道经典实战题C语言完美演绎4-6的核心现场3.1 完数检测完美数字的C语言打开方式“完数”也叫完美数指一个数恰好等于它的所有真因子之和。1236所以6是第一个完数。“完美演绎4-6”里藏着“完美”两个字我用完数打头阵再合适不过。下面这个程序找出1到1000范围内所有完数#include stdio.h int isPerfect(int n) { int sum 0; for (int i 1; i n / 2; i) { if (n % i 0) { sum i; } } return sum n; } int main() { for (int n 2; n 1000; n) { if (isPerfect(n)) { printf(%d\n, n); } } return 0; }关键点有两个。第一个是因子循环上界一个数除自身外的最大真因子不可能超过n/2所以循环到n/2就够了比循环到n少一半时间。第二个是函数设计把“判断是否完数”从main里拆出去返回值为1表示是完数为0表示不是逻辑清晰后续想扩大范围也方便。如果你还想优化可以用sqrt(n)配合配对因子比如28的因子1和28、2和14、4和7遍历到sqrt(28)附近就够了但要额外处理完全平方数的重复计数。新手先把n/2版本跑通再去琢磨这种优化别上来就把自己难住。3.2 九九乘法表循环嵌套的基本功九九乘法表是嵌套循环的教科书题。外循环控制行内循环控制列核心是内层循环的终止条件跟着行号走#include stdio.h int main() { for (int i 1; i 9; i) { for (int j 1; j i; j) { printf(%d*%d%-2d , j, i, i * j); } printf(\n); } return 0; }这里有个输出格式的小细节%-2d表示左对齐占2个字符宽度乘法结果后面跟一个空格整个表看起来整齐。很多人只记得%-2d却忘了在结尾加空格结果数字全部挤在一起。还有换行的位置一定要放在内层循环外面放在里面的话每一列都会换行直接变成竖版九九表。再看while版本的写法就是为了让你理解for其实是while的紧凑形式#include stdio.h int main() { int i 1; while (i 9) { int j 1; while (j i) { printf(%d*%d%-2d , j, i, i * j); j; } printf(\n); i; } return 0; }对比之后你会发现for把初始化、条件、步进都集中在一行不容易忘写循环变量的更新while则要把i、j手动放到循环体末尾一旦漏写就是死循环。这个案例我建议你两个版本都敲一遍敲完自然理解为什么大多数有明确次数的循环都优先选for。3.3 字符串逆序双指针操作字符串的高频题PTA里字符串逆序出现频率很高这类题表面考逆序实际考你对字符串结束符的理解。先看不修改原串、直接倒序输出的简单版#include stdio.h #include string.h int main() { char s[100] hello; int len strlen(s); for (int i len - 1; i 0; i--) { putchar(s[i]); } putchar(\n); return 0; }如果要原地逆序就得用双指针一个指向开头一个指向结尾交换字符后两边向中间移动直到相遇。注意循环条件是left right不是left right因为中间那个字符跟自己交换没有意义。#include stdio.h #include string.h void reverse(char *s) { int left 0; int right strlen(s) - 1; while (left right) { char tmp s[left]; s[left] s[right]; s[right] tmp; left; right--; } } int main() { char s[100] hello; reverse(s); printf(%s\n, s); return 0; }最容易被忽视的是right的初始值。strlen返回长度但最后一个有效字符的下标是长度减1因为\0只占位置不算内容。新手经常把right写成strlen(s)结果把\0换到了开头输出直接变成空串。这种错误不编译报错但运行结果完全不对排查起来很费神。另外如果你的字符串数组长度不够存结尾符比如char s[5] hello这本身就是数组越界属于未定义行为别问为什么有时候能跑有时候崩。3.4 日期计算两种方法把“查表”思路练熟题目很常见输入一个日期的年、月、日输出它是这一年的第几天。这个题有两个主流解法正好对应“c语言两种方法优化”这个搜索词。第一种是switch累加把每个月的天数直接写进分支#include stdio.h int main() { int y, m, d; scanf(%d %d %d, y, m, d); int days 0; switch (m - 1) { case 11: days 30; case 10: days 31; case 9: days 30; case 8: days 31; case 7: days 31; case 6: days 30; case 5: days 31; case 4: days 30; case 3: days 31; case 2: days 28; case 1: days 31; } if ((m 2) ((y % 4 0 y % 100 ! 0) || y % 400 0)) { days 1; } days d; printf(%d\n, days); return 0; }switch的case穿透在这里反而成了优点从目标月份往前累加代码短但不好读。第二种查表法更适合日常工作和后续扩展#include stdio.h int isLeap(int y) { return (y % 4 0 y % 100 ! 0) || y % 400 0; } int main() { int y, m, d; scanf(%d %d %d, y, m, d); int monthDays[12] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (isLeap(y)) { monthDays[1] 29; } int total 0; for (int i 0; i m - 1; i) { total monthDays[i]; } total d; printf(%d\n, total); return 0; }查表法的结构清晰一张表存每月天数一个函数判断闰年一个循环累加前m-1个月的天数。闰年判断要注意条件优先级能被4整除且不能被100整除或者能被400整除。写成return那行就很好不需要加多余的括号。如果你还想进一步优化可以预计算一个前缀和数组sum[13]sum[m]直接表示前m个月的总天数这样就不用循环累加了时间从O(m)降到O(1)但代码稍微绕一点。先用循环版把逻辑搞通再考虑前缀和。3.5 复数四则运算用结构体把数学公式搬进代码PTA的“复数四则运算”题很经典它把结构体、函数、数学公式三样东西揉在一起考。复数abi在C语言里可以用结构体表示实部和虚部分别存成两个double#include stdio.h typedef struct { double real; double imag; } Complex; Complex add(Complex a, Complex b) { Complex c; c.real a.real b.real; c.imag a.imag b.imag; return c; } Complex sub(Complex a, Complex b) { Complex c; c.real a.real - b.real; c.imag a.imag - b.imag; return c; } Complex mul(Complex a, Complex b) { Complex c; c.real a.real * b.real - a.imag * b.imag; c.imag a.real * b.imag a.imag * b.real; return c; } Complex div(Complex a, Complex b) { double d b.real * b.real b.imag * b.imag; Complex c; c.real (a.real * b.real a.imag * b.imag) / d; c.imag (a.imag * b.real - a.real * b.imag) / d; return c; } void printComplex(Complex c) { printf(%.1f%.1fi\n, c.real, c.imag); } int main() { Complex a {1.0, 2.0}; Complex b {3.0, 4.0}; printComplex(add(a, b)); printComplex(sub(a, b)); printComplex(mul(a, b)); printComplex(div(a, b)); return 0; }加法减法就是实部虚部分别运算乘法用分配律展开(abi)(cdi) (ac-bd) (adbc)i注意中间是减号。除法最容易被公式绕晕核心是给分子分母同时乘以分母的共轭c-di把分母变成c方加d方的实数再分别除。我在代码里直接用d表示分母的模平方实部虚部分别算比背公式稳。输出格式是这类题的另一个考点%.1f保留一位小数%.1f会在正数前显示加号这样1.03.0i能直接输出成1.03.0i。如果你写成普通%f正数前的加号就丢了PTA很可能判你格式错误。结构体作为函数参数时是整个拷贝进去的性能上有开销但数据量小的时候不必纠结清晰优先。3.6 冒泡排序排序是算法入门也是指针应用的试金石冒泡排序人人都会写但能把边界条件写对、把swap函数设计到位的人不多。先看完整代码#include stdio.h void swap(int *a, int *b) { int tmp *a; *a *b; *b tmp; } void bubbleSort(int *arr, int n) { for (int i 0; i n - 1; i) { int swapped 0; for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { swap(arr[j], arr[j 1]); swapped 1; } } if (!swapped) { break; } } } int main() { int arr[] {5, 2, 8, 1, 9}; int n 5; bubbleSort(arr, n); for (int i 0; i n; i) { printf(%d , arr[i]); } return 0; }外层循环为什么是n-1趟因为每一趟都会把一个最大值放到正确位置n个数最多n-1趟就能排完。内层循环为什么结束条件是n-1-i因为每一趟之后数组末尾的i个元素已经有序不需要再参与比较。swap必须传指针这正好复习2.4节的内容。如果写成void swap(int a, int b)参数是副本函数里交换得再热闹main函数里的arr[j]和arr[j1]一点不受影响。数组传参bubbleSort(arr, n)则是把arr退化成指针传进去所以函数内修改arr元素会真实生效。很多人学指针时觉得抽象写一遍冒泡排序再回头看就通了。优化标志位swapped是个加分项如果某一趟一个交换都没有发生说明数组已经有序直接break最好情况下能省不少比较。这种小优化写进代码里面试官或老师看了会印象分拉满。4. 我第一次写这段代码时踩过的坑4.1 scanf缓冲区残留输入“吃”掉了你的字符我现在带人刷题时几乎每周都会有人问“为什么我输入完数字程序不让我输字符就结束了”十有八九是缓冲区残留问题。刚学的时候我也在这上面栽过写完scanf(%d, n)紧接着想读一个字符结果读到的是回车。以前的老办法是getchar()吃一次回车但要注意如果输入里有两个换行还得吃两次很脆。我更推荐scanf( %c, c)这种格式串加空格的方式或者干脆用fgets读整行再sscanf解析。不是所有情况都要处理缓冲区但一旦遇到“输入顺序和预期不符”第一个怀疑对象就是缓冲区残留。4.2 数组越界不报错的错误最致命数组越界的可怕之处在于它经常不报错。我见过有人写char s[5] hello编译器可能没意见运行时也可能没崩溃但那个存不下的\0已经写到相邻内存去了。哪天程序突然出现莫名其妙的现象比如循环次数不对、变量值被改你查一小时都不一定想到是越界写坏了邻居。C语言不检查数组边界这是它高效的原因也是它坑人的地方。写代码时养成习惯数组长度为n有效下标是0到n-1字符串数组一定多留一个位置给\0循环边界老老实实写清楚。调试时如果遇到诡异问题优先怀疑越界用gdb去看变量的地址和值就能定位。4.3 swap不传地址值传递的经典翻车现场“我写了swap(a, b)为什么没交换”这是百度贴吧时代的月经贴。C语言函数参数默认是值传递swap(int a, int b)拿到的只是a和b的拷贝换的是拷贝原函数里的变量纹丝不动。必须传地址swap(a, b)形参写成指针函数里通过*a *b来操作原变量。数组的情况比较特殊arr传给函数后形参退化为指针所以函数内可以直接改数组元素不需要再加。很多人因为数组“自动传址”就以为所有类型都这样直到在结构体上翻车才回头补课。记住一句话除非传的是指针或数组名否则函数里改不了外面的变量。4.4 文件缓冲区不刷新数据就没了搜“文件缓冲区 c语言程序”的人多半是在写文件读写时遇到“程序结束没报错但文件里什么都没有”的问题。正常情况下向文件写入内容会先到缓冲区等缓冲区满了、显式fflush或fclose时才真正写盘。如果你写了fprintf之后程序崩溃缓冲区的数据说丢就丢。所以写文件代码时fclose不是可选项是必须项。实在不想关文件至少调一次fflush(fp)把缓冲强制刷新。往屏幕printf的时候同理stdout在终端里遇到\n通常会刷新可一旦重定向到文件\n可能就不那么灵了。调试输出“少了一截”的时候往这个方向想想。5. 工具与调试把4-6这一段代码写稳的技巧5.1 VS Code里跑C语言一次配置少走半学期弯路很多新人卡在环境上题没写几道先被编译器搞崩溃。VS Code并不是开箱即用的C语言IDE你需要先装好编译器。Windows上我建议装MinGW-w64安装时把bin目录加进系统PATH然后在终端执行gcc --version确认成功。接着是tasks.json和launch.json。tasks.json负责编译下面这个配置能让你按CtrlShiftB直接编译当前文件{ tasks: [ { type: cppbuild, label: gcc-build, command: gcc, args: [ ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe ], group: { kind: build, isDefault: true } } ] }launch.json负责调试比tasks稍微复杂核心是program字段指向编译生成的exe路径miDebuggerPath填gdb路径。我见过最多的两个问题一是终端提示找不到gcc多半是PATH没配好二是输出中文乱码Windows下终端默认GBKVS Code默认UTF-8最简单的办法是代码里别写中文提示或者统一把终端编码切到UTF-8。环境配置这种东西一次配明白后面所有题都能跑别嫌麻烦。5.2 gdb调试别只会printf用printf调试不是不行但遇到段错误、循环边界错乱这种问题printf效率太低。gdb是C语言调试的老牌工具记几个基础命令就能上手命令作用gcc -g 文件.c -o 程序编译并生成调试信息gdb ./程序启动调试break main 或 break 行号设置断点run运行到断点next / step单步执行next不进入函数step会进入print 变量名查看变量值list显示当前源码遇到段错误最头疼直接run程序崩溃后输入bt查看函数调用栈它能告诉你程序卡在哪个函数哪一行。再配合print打印关键变量的值基本能定位是空指针访问还是越界。我调数组越界就是这样抓出来的崩溃在某个函数往上查栈里数组名和下标一眼看到下标超出了范围。gdb用熟之后你会觉得printf调试太原始了。6. 关于“完美演绎4-6”的个人体会把4个板块和6道题全部跑通之后我对C语言学习的理解又清晰了一层。这段“腰部区间”真正重要的不是记住某个语法点而是把“输入-处理-输出”这条链路在脑子里搭起来。完数题让你明白函数怎么拆九九乘法表让你明白循环怎么管字符串逆序让你明白指针怎么用日期计算让你明白数据怎么组织。六道题连起来几乎就是把C语言最常见的使用姿势演了一遍。我个人的建议是学完这6道题后不要急着往下冲链表和高级算法而是自己再找几道类似题练手。比如把冒泡排序改成对字符数组排序把日期计算改成输出两个日期之间差多少天把完数检测从1到1000扩展到1到1000000顺便想想性能怎么办。这样的组合练习比盲目刷几十道题更扎实。最后分享一个我写代码的小习惯拿到题目先在纸上写伪代码哪怕是三五行比如“读入数据-循环判断-输出结果”。这能让你的思路先成型再落成C语言比直接敲键盘少改很多错误。4-6这个区间是C语言学习的黄金期跨过去之后后面学函数指针、结构体、文件操作都会顺很多。
返回列表