ARTICLE DETAIL

资讯详情

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

C语言内存视角核心知识点总结:指针、结构体对齐与字符串逆序实战

C语言内存视角核心知识点总结:指针、结构体对齐与字符串逆序实战 简介这是一份面向C语言初学者和备考者的知识点浓缩讲义全篇按章节梳理核心考点适合课程讲解、期末复习或准备计算机二级考试时快速查阅。资源包含1个PDF文件体积约425KB内容非常紧凑利用14页篇幅系统归纳了三大程序结构、main函数唯一入口、位与字节概念、合法标识符辨析、二进制/八进制/十六进制转换、整数与实数、算术与赋值表达式、自增自减与逗号表达式、字符及ASCII编码、转义字符、位运算等高频必考内容。文中穿插了大量易错提醒例如编译预处理不能加分号、/与%的正确使用、逗号运算符优先级最低、赋值左侧只能是变量、大小写字母ASCII相差32等可帮助读者快速避开常见失分点。目前已有56人学习适合正在备考或希望系统梳理C语言知识框架的师生直接参考使用。 先把话说明白这篇文章不是给你抄的是给你用的。这些年我带过不少刚入门C语言的新人也帮人改过无数段“编译能过但跑起来就崩”的代码。你问我C语言知识点总结到底该总结什么我的答案从来都不是把教材目录抄一遍。真正该记下来的是那些“当时想不通、后来吃了亏才明白”的底层机制。所以这篇文章我按“内存视角”把C语言的核心知识点串起来讲每一块都配上踩坑经验和可直接跑的最小示例。适合三类人看刚开始学C语言、被指针和内存搞得头皮发麻的初学者准备考研或找工作、需要系统过一遍C语言考点的人以及带新人、想给团队做内部培训的嵌入式或后端工程师。1. 数据类型的坑内存位宽、整数溢出与double取余为什么报错1.1 别把“变量”理解成数学里的未知数它是内存里的一块地皮很多教材告诉你变量是“用来存储数据的容器”。这句话不能算错但它没说到根子上。C语言的变量本质上是你在内存里申请的一块固定大小的区域变量名只是这块区域的别名。你写int a;编译器就帮你圈了4个字节在常见32位/64位平台上出来后续所有对a的操作都发生在这块地皮上。这里有个关键点C标准只规定了每种类型的最小取值范围并没有规定固定字节数。也就是说int在PC上是4字节在一些老式或嵌入式平台上可能是2字节。所以我建议你拿到一个新平台第一件事就是printf(%zu\n, sizeof(int));把所有基本类型的大小打一遍再写业务代码。跨平台项目里因为int位宽假设错误导致的bug我亲眼见过不止一次。1.2 整数溢出是“静默杀手”浮点数取余要用fmod无符号整数溢出是回绕的比如unsigned char c 255; c;c会变成0编译器不会给你任何提示。带符号整数溢出则是未定义行为编译器理论上可以“自由发挥”在你本地跑正常换个优化级别结果就变了。这里特别想提一下搜索热词里那个高频问题“C语言double类型数据取余”。C语言里%运算符只支持整数类型对两个double写a % b编译直接报错。正确做法是引入math.h调用fmod(a, b)。再提醒一句不要为了用%先把浮点数强转成整数精度会白白丢掉计算逻辑也可能彻底变味。1.3 隐式类型转换负数参与无符号比较时最阴险C语言在做算术运算或比较时会把“级别低”的类型自动转成“级别高”的类型也就是隐式类型转换。听起来很贴心实际是个大坑。比如int a -1; unsigned int b 1; if (a b) { printf(a b\n); }你直觉上觉得-1肯定小于1但这段代码会输出a b。原因是比较时a被转换成了unsigned int-1变成了4294967295当然比1大。这种bug在代码评审里极难发现因为它连警告都不太容易触发。我的建议很简单比较表达式里不要让有符号和无符号混用如果一定要混用先自己写清楚强转或者编译时加上-Wall -Wextra把告警全开。2. 指针不是“存地址”这么简单寻址步长、const读法与动态内存2.1 指针真正存的是“带类型的地址”它决定了步长初学者最喜欢把指针背成“指针就是存储地址的变量”。这句话对但不够。指针类型最关键的作用是告诉你从这个地址出发能读写多大内存、p 1到底往前进几个字节。int a[3] {1, 2, 3}; int *p a; char *q (char *)a; printf(%p %p\n, (void *)p, (void *)p 1); // 地址差4 printf(%p %p\n, (void *)q, (void *)q 1); // 地址差1int *p做p1时实际地址加的是sizeof(int)即4个字节char *q做q1只加1个字节。这也是为什么强制类型转换在某些场合是必须的——比如你需要把一个结构体指针转成unsigned char *按字节查看或发送内存数据。指针类型一旦搞错步长就算错数据读写位置自然全乱。2.2 const在指针里的读法从右往左读一秒拆清楚const int *p和int * const p无数人栽在这里。记忆方法其实很机械从右往左读遇到*就停下来。const int *p从右往左是“p is a pointer to const int”指针指向的内容是只读的但指针本身可以改。int * const p从右往左是“p is a const pointer to int”指针本身不能改但指向的内容可以改。写接口设计时我强烈建议优先用const int *p这类写法。它相当于告诉函数的调用者“这个函数只读你的数据不会改”能把很多低级错误挡在编译阶段。2.3 malloc和free背后是一份责任泄漏、野指针、重复释放动态内存是C语言里事故率最高的区域无非这三类忘记free导致内存泄漏free之后指针没置NULL后面又解引用成了野指针同一块内存被free两次。写动态内存的标准姿势应该是int *arr (int *)malloc(sizeof(int) * 10); if (arr NULL) { // 处理分配失败 return; } // 业务逻辑 free(arr); arr NULL;两点经验第一malloc的返回值一定要检查不可想当然认为分配一定成功。第二free之后顺手置NULL哪怕只是多打几个字符也能避免很多离奇的崩溃问题。嵌入式环境里更要注意堆空间本来就有限频繁小规模分配释放会产生碎片最后明明总内存够却malloc不出大块连续内存。3. 数组、字符串、结构体的内存布局差异从字符串逆序到二维数组传参3.1 数组名什么时候“退化”成指针是新手最容易懵的点int a[10]在定义它的作用域里sizeof(a)返回40整个数组的大小。但如果你把a传给一个函数在函数里sizeof(a)只会返回指针大小通常是8。这是因为数组名在绝大多数表达式中会“退化”为指向首元素的指针只有sizeof和是例外。这个特性直接导致一个规矩在C语言里通过参数传递数组必须同时把长度传过去。比如void print_array(int arr[], int n);函数签名里的int arr[]只是为了照顾阅读习惯本质上它和int *arr没有任何区别。不要在函数内部用sizeof(arr) / sizeof(arr[0])去推长度那算出来一定不是你想要的。3.2 字符串的“方向感”strlen不数\0sizeof连\0一起数字符串在C语言里就是char数组约定以\0结尾。两个函数要分清楚strlen(hello)返回5它只数\0之前的字符不含结尾符。sizeof(hello)返回6它算的是整个数组大小包括最后的\0。这也解释了为什么strcpy、strcat这些函数在使用时必须保证目标缓冲区的空间足够。strcpy不检查目标长度越界写入是缓冲区溢出攻击的主要来源之一。现代编译器会给你警告更保险的做法是用strncpy这类带长度限制的版本但同时要注意strncpy如果源字符串长度超过n它不会自动补\0目标字符串可能没有终结符又是一坑。字符串逆序是实现最经典的问题之一双指针写法void reverse_str(char *s) { if (s NULL) return; int left 0; int right strlen(s) - 1; while (left right) { char tmp s[left]; s[left] s[right]; s[right] tmp; left; right--; } }这里最关键的细节是右指针初始值必须是strlen(s)-1把结尾的\0空出来绝不能从数组末尾开始交换。另外一个容易被忽略的点如果你用字符串常量char *s hello;去调用这个函数程序崩溃的概率极高因为字符串常量通常存放在只读区不能修改。必须用可写的字符数组char s[] hello;。3.3 结构体对齐为什么struct的大小不等于各字段之和很多人第一次print结构体大小时都愣住一个char加一个int直觉是5个字节但sizeof经常返回8。这是因为编译器为了让CPU访问更高效会在字段之间插入填充字节让每个字段落在对齐边界上。struct A { char c; int i; };在32位平台上char c占了1字节后面有3字节的填充然后才是4字节的int i整个结构体8字节。如果调整字段顺序struct B { int i; char c; };依然是8字节因为结构体整体大小要对齐到最大成员对齐数的整数倍c后面的3字节填充省不掉。这块内存布局在写网络协议解析、二进制文件读写、共享内存通信时特别重要。两个进程如果编译时对齐方式不一致结构体数据交换过来就是错位的。解决手段有按成员大小从大到小排列字段用#pragma pack(1)压缩对齐或者干脆不用结构体直接序列化。顺便提醒#pragma pack(1)虽然能省空间但会降低访问效率在某些硬件平台上还可能引发总线错误项目没逼到那份上别乱开。4. 流程控制与函数的边界while与do-while、for循环顺序、递归栈4.1 while和do-while的真实区别一个可能一次都不跑一个至少要跑一次while (条件) { 循环体 }是典型的“先判断后执行”如果条件初始就不成立循环体一次都不会执行。do { 循环体 } while (条件);则是“先执行一次再判断”循环体至少执行一次注意do-while结尾那个分号不能省。这个差异最典型的使用场景是“先尝试一次再检查结果”的逻辑。比如串口读取不管缓冲区有没有数据先读一次再根据返回结果决定要不要继续循环读取。如果你用while从头判断可能一次都进不去直接跳过了必要的初始化读取。4.2 for循环的执行顺序初始化只做一次更新在循环体之后for (初始化; 条件; 更新)的执行顺序是初始化语句执行一次 - 判断条件 - 执行循环体 - 执行更新 - 再判断条件。很多人以为更新语句在循环体之前执行于是写出这种bugfor (int i 0; i 10; i) { // 循环体里又改了i }如果你在循环体里修改了i更新语句还会再执行一次循环次数会变得比你预期得更“诡异”。另外要记住break只能跳出最近一层循环continue是跳过本轮的剩余语句、进入更新和下一次判断。嵌套循环里想跳出外层得用标志位或者goto但用goto前先想清楚有没有更清晰的结构。4.3 递归、函数栈与作用域为什么深递归会崩函数调用是有成本且讲规矩的。每次调用系统会把返回地址、局部变量、参数依次压入内存中的“栈”区域函数返回时再弹出。所以递归本身不是问题问题是你有没有给递归写终止条件以及递归深度能不能被栈容纳。long long fib(int n) { if (n 1) return n; return fib(n - 1) fib(n - 2); }这段代码逻辑没错但fib(50)会慢到让你怀疑人生还会产生非常深的调用链。嵌入式环境里栈可能只有几KB递归几千层直接就栈溢出了。所以工程上能用循环解决的递归尽量用循环尤其在高可靠性场景下不是迫不得已别写深递归。int x 10; void f(void) { int x 20; // 这个x是局部变量遮蔽了外部x printf(%d, x); }作用域这块还有个常见误解局部变量可以“遮蔽”同名全局变量但本质上它们是两块不同的内存改局部变量不会影响全局变量。5. 文件读写与缓冲机制fscanf/fprintf这些函数背后发生了什么5.1 fopen的mode是第一个坑w会清空已有文件fopen(file.txt, w)如果文件已存在会先把它截断成空文件再写。这个行为害过很多人一个手滑或代码逻辑错误旧数据全没了。读文件用r追加用aw只用在你确定要覆盖的场合。还有一个高频错误是文件打开后不检查返回值。fopen失败时返回NULL你直接拿NULL去调fscanf或fprintf等价于空指针解引用程序大概率直接崩。5.2 fscanf格式化读取的边界空格就断EOF要靠返回值判断fscanf(fp, %s, buf)看着方便实际上它读到空白字符空格、换行、Tab就停。你要是用它读一行带空格的配置比如key value with spaces只能读到key。正确做法是fgets先按行读再配合sscanf做解析。文件末尾判断也是个重灾区。新手常用feof(fp)来判断文件是否结束但feof在“你尝试读取越过文件末尾之后”才会返回真边读边判断逻辑很容易多读一次或者漏读一次。更可靠的做法是检查读取函数的返回值fscanf返回成功匹配并赋值的输入项个数读到文件尾时返回EOFfgets读到文件尾返回NULL。5.3 缓冲区为什么程序明明写了文件断电后数据却丢了标准I/O为了提升性能数据会先写进内存缓冲区等缓冲满了或者你主动fclose、fflush才真正写进磁盘。所以fprintf成功返回不代表数据已经落盘。程序如果在这时崩溃或断电缓冲区里没冲刷的数据就丢了。日志系统里这个问题尤其致命。正确做法是写完关键日志后主动调fflush(fp)或者直接fclose。嵌入式设备做掉电保护也是同理重要参数写完必须立刻刷新否则下一次上电读到的还是旧值。6. 必背代码模板与高频易错点从排序到滤波从宏陷阱到编译告警6.1 冒泡排序和快速排序别背代码背思路冒泡排序是最容易理解的排序每一轮把相邻元素两两比较大数往后冒经过n-1轮排完。时间复杂度O(n^2)数据量小的时候无所谓量大就明显慢。快速排序的思路是“分治”从数组中选一个基准值把小于它的放左边大于它的放右边然后递归处理左右子区间。面试让手写快排时核心在partition函数搞懂了基准值怎么选、元素怎么交换剩下的递归结构很好写。平均时间复杂度O(n log n)但最坏情况比如已经有序的数组配上固定选最后一个元素做基准会退化成O(n^2)。工程中通常用随机选基准或三数取中来规避退化。6.2 字符串逆序的两个变体整串反转和按单词反转整串反转的双指针写法前面已经给过。另一个常考变体是“按单词反转”输入hello world输出world hello。思路是先反转整个字符串变成dlrow olleh再逐个单词反转回来变成world hello。这个题考察的是字符串边界处理能力写之前先在纸上把下标算清楚。6.3 ADC滤波、宏陷阱和那个“unreferenced label”告警搜索热词里有个“c语言adc值滤波函数”在MCU开发里很常见。最实用的是两种中值滤波和滑动平均。中值滤波是连续采集N个样本排序后取中间值能有效滤掉毛刺脉冲缺点是每次要排序、实时性差。滑动平均是维护一个长度为N的窗口每来一个新值窗口减掉最老的值、加进新值再除以N。它比每次重新累加所有窗口值要高效得多代价是窗口要用数组或环形队列维护好。宏这块最经典的坑#define SQUARE(x) ((x) * (x))写宏别偷懒省括号。SQUARE(1 2)如果不加括号展开成1 2 * 1 2结果是5不是9。多语句宏习惯用do { ... } while(0)包一层这样在if / else里调用也不会出现悬空else的语法问题。至于“unreferenced label”其实就是你写了一个标签名:但没有对应的goto来使用它编译器给你一个警告。如果你留着没用直接删掉如果是忘了写goto回去补上。别一边编译一边忽略告警告警多的代码留着就是定时炸弹。最后再说个实际建议我整理这类知识点总结从来不是抄教材目录。每个知识点后面跟一个“当时是怎么踩坑的”再用一小段最小代码验证把编译运行结果记在旁边。下一次复习或排查问题的时候翻到那一页就能看到结论和示例比重新翻书快得多。你要是也在整理自己的C语言学习笔记可以按这个思路试一试。本文还有配套的精品资源点击获取
返回列表