ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C语言试卷考点解析:指针、宏定义与字符串陷阱

C语言试卷考点解析:指针、宏定义与字符串陷阱 简介长安大学2006-2007学年第二学期C语言程序设计试题A卷正式试卷适合高校本专科学生、备考期末及考研复试的考生也可作为教师出题或课堂练习的参考。试卷以选择题和程序分析题为主覆盖程序结构、常量合法性、运算符优先级与自增自减、scanf格式匹配、流程控制、宏定义展开、联合体内存占用、数组与字符串处理、字符数组操作等核心考点题目设计贴近常见易错点有助于系统检验C语言基础。资源为1份doc文档单文件、容量约86KB方便直接打开打印或对照复习。当前已有98人学习浏览适合在考前集中刷题、查漏补缺也可作为平时章节练习的补充材料。通过逐题分析选项与运行结果可加深对C语言语法规则、类型转换和内存管理的理解提升应试能力。1. 长安大学0607C语言A卷指针、宏与字符串陷阱的一次集中展示一份2006-2007学年的C语言试卷放到今天依然是很好的自测素材。选择题60分里常量合法性、表达式求值、自增运算符、scanf格式匹配占了近半数阅读题则指向宏展开、union内存布局、指针偏移这类只靠背语法根本答不对的题目。适合两类人看正在准备计算机二级C语言的在校生以及多年不写C、想快速检查自己是否还踩得住指针坑的从业者。下面按照试卷的题目顺序把值得反复推敲的考点逐一拆开给出可复现的代码和参数说明。2. 选择题考点拆解常量合法性、scanf格式串与自增运算的边界选择题覆盖面广但真正的分水岭在第1、4、5、6、7题它们分别测试字面量合法性、自增副作用、格式串与参数类型匹配、逻辑短路。这几个点不是考前背一下就能稳拿的因为C语言把“错误”留给了运行时。先从第一题入手。2.1 常量合法性一眼识破非法的字面量题目列出四组常量要求选出“均不合法”的一组。要快速判断需要记住三条规则单引号内只能放一个字符或一个转义序列以0开头的整数是八进制不能出现8或9以外的非八进制字符转义序列不能产生歧义。/* 合法示例 */ int a 0xabc; /* 十六进制对应10进制的2748 */ int b 01177; /* 八进制对应10进制的639 */ char c \\; /* 转义反斜杠是合法字符常量 */ /* 非法示例 */ char d as; /* 单引号内出现两个字符标准未定义 */ int e -0ffff; /* 0开头按八进制解析但f不在0-7范围 */ char f \0xa; /* 转义序列截断歧义属于多字符常量 */参数说明\0是八进制转义值为0它一旦被编译器识别后面的xa就变成了多余字符整个\0xa不再是合法字符常量。-0ffff里的负号属于单目运算符不是字面量的一部分因此判断合法性只看0ffff前导0表示八进制而f不是八进制数字所以非法。表格补充常见字面量的判断字面量类型是否合法原因\01char合法八进制转义值1-0x18int合法负号作用于合法十六进制数\0char合法值为0的字符a字符串合法双引号内是字符串字面量\0xachar非法转义序列歧义0xabcint合法十六进制整数这组判断在嵌入式C里尤其重要因为芯片寄存器地址常以十六进制字面量出现稍不注意就写成0x加非十六进制字符编译报错还不好排查。2.2 scanf格式串与指针类型匹配第5题表面考scanf实质考格式说明符和参数指针的类型匹配。%d要求int*%x和%o要求unsigned int*%f要求float*%lf才要求double*。把%f和double*配在一起在多数x86平台不会立刻崩溃但属于未定义行为数据可能完全错乱。#include stdio.h int main(void) { int x 0, y 0; double z 0.0; /* 常见写法%f 应匹配 float*z 是 double*类型不匹配 */ scanf(%d%x,%f, x, y, z); /* 正确写法 */ scanf(%d%x,%lf, x, y, z); printf(%d %d %lf\n, x, y, z); return 0; }说明第一行scanf之所以危险是因为z占8字节而%f解析4字节浮点数之后写内存时长度不一致第二行%lf告诉scanf按8字节double解析。格式串里的普通字符,要求输入中对应位置出现逗号否则读取提前终止这也是容易被忽视的点。格式说明符参数指针类型常见错误%dint*传入double*%xunsigned int*与%d混用%ounsigned int*传入int*%ffloat*传入double*%lfdouble*老编译器不支持%lf在C99之前某些编译器只认%lf用于输出、不用于输入写跨平台代码时建议统一按C99编译并打开编译告警。2.3 自增运算与逻辑短路一条语句里的多个副作用第4题的i和i是入门必考。i先把i加1再把值交给printfi先把当前值交给printf再执行加1。所以语句int i 3; printf(%d,, i); /* i先变4再输出4 */ printf(%d, i); /* i输出4随后i变为5 */结果是4,4。第6题在此基础上叠加了逻辑短路难度上一个台阶int x -1, y 4, k; k (x 0) || (!(y-- 0)); /* 展开 */先计算x 0x当时是-1-1 0成立整个||左边为1已经能决定表达式为真因此||右侧的!(y-- 0)根本不会执行y保持4。注意x是后自增表达式结束后x才变成0。最终k1, x0, y4选A。这个结论可以推广任何逻辑表达式里一旦左侧确定了结果右侧的赋值、自增、函数调用都不会发生设计短路的初衷本来就是为了少算。2.4 strlen与转义字符字符串长度不是看“样子”而是看内存第11题的strlen也是常考陷阱。char c[] \t\b\\\0will\n;中\t一个字符、\b一个字符、\\一个字符随后是\0strlen遇到\0返回当前计数值3后面的will\n根本不会被统计。这里要区分strlen和sizeofsizeof会算上数组实际分配的字节而strlen只数到结束符前。char c[] \t\b\\\0will\n; printf(%zu\n, strlen(c)); /* 3 */ printf(%zu\n, sizeof(c)); /* 10含字符串结尾的\0 */数组初始化时用字符串字面量sizeof包含末尾的\0所以是10strlen只统计到第一个\0所以是3。写日志解析类代码时这种差别会直接影响缓冲区长度计算。3. 阅读题里的硬骨头宏展开、union内存布局与字符串指针偏移程序阅读题共3道分值10分。它们比选择题更接近工程场景宏参数没加括号导致的计算错误、union按最大成员对齐、指针偏移后再拷贝字符串。这三类问题在企业代码review中很常见。3.1 带参宏的文本替换陷阱宏定义M(x,y,z) x*yz在预处理阶段纯粹做字符替换不计算、不加括号。M(ab,bc,ca)替换后就变成了/* 宏替换后的表达式 */ ab*bcca代入a1, b2, c3运算顺序是1 2*2 3 3 1最终结果是12。很多人直接按函数调用误算成19就是因为把宏当成了函数。正确做法是每个参数都加括号#define M(x, y, z) ((x) * (y) (z))这个案例可以直接迁移到业务代码的宏定义规范凡是带参宏参数在替换后如果参与运算必须整体加括号否则优先级会改变。3.2 union大小与平台位数题8考察union类型占用字节数所有成员共享同一块内存大小为最大成员。long a[2]在32位平台是2*48字节int b[4]是16字节char c[8]是8字节取最大16字节。在64位Linux下long是8字节a[2]变成16字节最大值仍是16字节union { long a[2]; /* 32位:8字节64位:16字节 */ int b[4]; /* 16字节 */ char c[8]; /* 8字节 */ } our; sizeof(our); /* 两种平台上都是16 */可以写一个小程序验证。union的实用性在于节省内存和做类型转换但它不负责类型安全往一个成员写数据再从另一个成员读结果依赖字节序和平台笔试里考它更在意你是否理解“取最大成员”。3.3 字符串指针做偏移后调用strcpy第19题把指针p指向字符串字面量abcdefghp 3后p指向d接着strcpy(p, ABCD)从p位置覆盖写入ABCD。strcpy会复制A B C D \0共5个字节strlen对结果求长度只统计到结束符之前得到4。char *p abcdefgh; p 3; printf(%d\n, strlen(strcpy(p, ABCD))); /* 输出4 */这里有一个需要注意的地方把字符串字面量的地址赋给char*并写入在现代C标准里是未定义行为。很多教材基于旧编译器写成这样但实际工程中不要把字面量当成可写缓冲区应该改用字符数组char buf[] abcdefgh; char *p buf; p 3; strcpy(p, ABCD); /* buf变为abcABCD */如果对只读内存区域写入在部分嵌入式平台会触发硬件异常这也是面试官常追问的扩展点。3.4 数组名不是指针变量a为什么非法指针与数组边界是这份试卷的另一条暗线。第29题中int a[9], *pa;四个选项里p1、a1、p都能表示a[1]的地址唯独a不合法因为a是数组名在表达式中被当作数组首元素的常量指针常量不能自增。可以这样理解int a[9] {0}; int *p a; p 1; /* 合法结果是指向a[1]的指针 */ a 1; /* 合法a隐式转换成int*后加1 */ p; /* 合法p是变量 */ a; /* 非法a是常量地址 */这正是指针变量和数组名的区别数组名不是指针变量它没有自己的存储空间不能出现在自增运算符左侧。做底层驱动时如果你需要依赖地址自增遍历缓冲区一定要额外定义一个指针变量承接首地址。3.5 switch穿透、static局部变量与结构体成员引用阅读第1题的switch没有在case后写break读入字母d时会先执行case a: case d: s1;然后由于没有break继续落到default: s1 1; s2 1;s1总共加2s2加1。输入day三个字符后需要逐一结算最终输出s15,s23。结论就是测试你对“case到下一个case之间没有break就会穿透”的敏感度。第15题的static int x10;让x只在第一次进入fun时初始化第二次调用时保留上次的30再加20得到50所以输出30 50。static变量常用于计数器和缓存但在多线程环境下要考虑并发安全。第30题结构体成员引用中p-age、(*p).age、stul.age都合法age裸写不合法这提醒我们操作结构体时不要丢掉对象名或指针。4. 程序填空与编程题数组边界、文件EOF与阶乘溢出的落地实现程序填空20分集中在三类任务数组相邻元素求和、二进制文件字符计数、字符串拼接。编程题10分分别考阶乘累加和二维数组左下三角输出。这些题和日常开发很接近重点是边界条件。4.1 数组相邻元素求和下标从1开始更清晰题目数组a有10个元素要求把相邻两个元素的和存入数组b并按每行3个输出。填空处的循环条件是i10结合后面输出b[1]到b[9]可以发现b[0]被跳过所以前一个循环应该从i1开始for (i 1; i 10; i) b[i] a[i - 1] a[i]; /* a[0]a[1] 存入 b[1] */ for (i 1; i 10; i) { printf(%3d, b[i]); if (i % 3 0) printf(\n); /* 每3个元素换行 */ }逻辑说明第一个循环产生9个和值第二个循环输出时用i % 3 0控制换行i从1开始所以第3、6、9个值后换行。这种“从1开始而不是从0开始”的选择看似别扭却能让输出部分的下标和换行判断保持一致。实际工程中从1开始的下标也常用于报表列号关键是前后统一。4.2 二进制文件字符计数feof在这里是个坑原题给出while(!feof(fp))配合fgetc(fp)计数。这个写法错在feof标志只有在读取越过文件末尾后才会被设置。也就是说当fgetc读到EOF时它已经消耗了一次“不存在的字符”循环体里的count会多算一次。#include stdio.h int main(void) { FILE *fp; int ch; long count 0; fp fopen(letter.dat, rb); if (fp NULL) { perror(fopen); return 1; } while ((ch fgetc(fp)) ! EOF) /* 读到EOF立即停止 */ count; fclose(fp); printf(count%ld\n, count); return 0; }参数说明fgetc返回int而不是char因为EOF是负数如果用char接收某些平台会把EOF截断成正常字符造成死循环。rb模式在Windows下会关闭换行符转换在Linux下r和rb没有区别。用perror输出错误原因比直接打印“Cannot open file”更有排错价值。4.3 字符串连接指针移动与结束符补写conj函数要求把s2拼到s1之后。常见填空答案是void conj(char *p1, char *p2) { while (*p1) p1; /* 让p1指向s1的\0 */ while (*p2) { *p1 *p2; /* 依次复制s2字符 */ p1; p2; } *p1 \0; /* 补上结束符 */ }实现要点第一个while没有任何赋值只是移动指针到s1末尾第二个while把p2指向的字符逐个写入p1位置两者的指针同步向后移动最后必须补一个\0否则printf输出时会越界读取直到偶然遇到内存中的0。如果s1空间不足这个函数会直接越界写因此调用方要保证s1数组足够容纳连接后的长度例如用char s1[80]。4.4 阶乘累加与二维数组左下三角编程题第一问是1!2!...20!。20!约2.43e18int最多存21亿13!已经溢出所以必须用long long或者double#include stdio.h int main(void) { long long sum 0; long long term 1; for (int i 1; i 20; i) { term * i; /* term i! */ sum term; /* 累加 */ } printf(%lld\n, sum); return 0; }nn!能否用int12479001600刚好能装下136227020800溢出202432902008176640000需long long用long long时打印格式必须是%lld在Windows MSVC老版本要写%I64d跨平台容易踩坑。第二问是5×5数组左下三角输出先循环赋值再通过行号和列号的关系裁剪输出范围int a[5][5], n 1; for (int i 0; i 5; i) for (int j 0; j 5; j) a[i][j] n; /* 按行填1到25 */ for (int i 0; i 5; i) { for (int j 0; j i; j) /* 只有列号行号才输出 */ printf(%4d, a[i][j]); putchar(\n); }输出左下半包括对角线第0行1个数第1行2个数第4行5个数。将j i改成j i就得到严格左下三角这是题目最容易变通的地方。5. 从这份C语言试卷延伸出的边界检查与编译验证技巧这份试卷暴露的问题几乎都是同一个根源未定义行为或边界条件失控。把考点转换为一组可执行的检查动作比多刷十套题更有用。5.1 用-Wall -Werror把潜在错误拦在编译期考试环境里的Turbo C只报语法错误不提示隐患。现代GCC和Clang能帮你查出很多试卷里的坑gcc -stdc99 -Wall -Werror -o exam exam.c clang -stdc99 -Weverything -Werror -o exam exam.c-Wall打开常见告警-Werror把告警升级为错误迫使你处理而不是忽略。宏参数未加括号、printf格式串和参数类型不匹配、赋值语句当条件等都会在这一步暴露。唯一要注意的是-Weverything太激进有些与风格相关的告警需要单条关闭。5.2 把试卷考点落成检查清单检查项试卷对应题固定动作数组越界27题a[3]循环写循环前先列合法下标区间宏参数优先级7题M宏带参宏每个参数和整体都加括号EOF判断填空第3题用fgetc返回值判EOF不用feof自增副作用4、6题一条语句内只出现一次i字符串字面量可写性19题可写字符串用数组存放scanf类型匹配5题%f配float*%lf配double*static初始化15题确认是否真想保留上次值以数组越界为例int a[3]的合法下标是0、1、2循环里写for(i3;i1;i--)访问a[3]直接越界。更隐蔽的是二维数组实参传递int a[][3]可以int a[][]不行因为编译器必须知道第二维才能计算行偏移。函数参数退化为指针后sizeof计算也会出错建议对数组操作统一采用“传入首地址长度”的方式杜绝在函数内部推测数组大小。写C语言代码真正的稳定不是靠记忆而是靠每次编译都敲响的告警和一份常驻脑海的边界清单。把上面这几条固定成习惯再回头做这份试卷选择题和阅读题的丢分项基本都能找回来。本文还有配套的精品资源点击获取
返回列表