C语言笔记(四):库函数、内存操作、字符串处理、缓冲区安全与高频手写题

张开发
2026/4/7 17:19:39 15 分钟阅读

分享文章

C语言笔记(四):库函数、内存操作、字符串处理、缓冲区安全与高频手写题
1. 本篇内容定位这一篇是专门整理 C 语言里另一块非常高频的内容常见库函数的真实语义内存操作函数和字符串函数的区别缓冲区越界与安全问题面试里常见的手写题工程里更稳妥的写法高频陷阱题和分析方式这一篇的重点不是“会背函数名”而是要真正分清这个函数处理的是字节块还是字符串它的结束条件是什么它会不会检查边界参数重叠时能不能用返回值有什么意义为什么有些写法看起来能跑实际上非常危险2. 先建立一张总图内存函数和字符串函数的区别C 语言里很多人一开始最容易混掉的就是把“内存块”和“字符串”当成一回事。实际上它们不是同一种东西。2.1 内存函数处理的是“原始字节块”例如memcpymemmovememsetmemcmp这些函数不关心你传进去的内容是不是字符串也不关心有没有\0。它们只认起始地址长度字节数2.2 字符串函数处理的是“以\0结尾的字符序列”例如strlenstrcpystrncpystrcmpstrncmpstrcatstrncatstrchrstrstr这些函数默认都建立在一个前提上传进去的是合法的 C 风格字符串也就是最终能遇到\0如果没有\0很多字符串函数都会一路往后读直到碰到偶然的 0 字节为止这就很容易出问题。内存函数 [起始地址] [字节数] 只按字节处理不认 \0 字符串函数 [起始地址] 从起点一直处理到遇见 \03.memcpy3.1 作用memcpy用于把一段内存中的n 个字节拷贝到另一段内存。函数原型void *memcpy(void *dest, const void *src, size_t n);含义dest目标地址src源地址n要拷贝的字节数3.2 最核心特点特点一按字节拷贝它不关心你拷贝的是intstruct数组字符串二进制数据它只认“多少字节”。特点二不检查边界它不会帮你判断目标空间够不够大源空间是否有效这些都要程序员自己保证。特点三源和目标不能重叠这一点特别重要。memcpy的要求是源内存区和目标内存区不能发生重叠否则行为未定义。3.3 正常示例#include stdio.h #include string.h int main(void) { int src[5] {1, 2, 3, 4, 5}; int dst[5] {0}; memcpy(dst, src, sizeof(src)); for (int i 0; i 5; i) { printf(%d , dst[i]); } return 0; }3.4 为什么不能处理重叠看这个例子char s[] abcdef; memcpy(s 2, s, 4);内存示意原始 a b c d e f 目标 从 s[0..3] 拷贝到 s[2..5]这里源区和目标区发生了重叠。如果从前往后复制过程可能变成第1步s[2] s[0] - a 第2步s[3] s[1] - b 第3步s[4] s[2] - 这里的 s[2] 已经被改了后面的数据就乱了。所以不重叠用memcpy可能重叠用memmove3.5 返回值有什么用memcpy返回的是dest。这让它可以方便连写char buf[100]; printf(%s\n, (char *)memcpy(buf, abc, 4));虽然能这么写但工程里一般不建议为了炫技写得太绕。4.memmove4.1 作用memmove和memcpy一样也是拷贝内存块。原型void *memmove(void *dest, const void *src, size_t n);4.2 和memcpy的最大区别memmove允许源区和目标区重叠。这是它最重要的特点。4.3 为什么memmove能处理重叠它的核心思路是如果目标区在源区前面从前往后拷贝如果目标区在源区后面从后往前拷贝这样就能避免源数据被提前覆盖。4.4 示例#include stdio.h #include string.h int main(void) { char s[] abcdef; memmove(s 2, s, 4); printf(%s\n, s); return 0; }结果通常为ababcd因为把前 4 个字符abcd平移到了后面。4.5 面试高频问法问memcpy和memmove的区别是什么memcpy用于普通内存拷贝效率通常更高但要求源区和目标区不能重叠memmove可以处理重叠内存它会根据地址关系选择合适的拷贝方向因此更安全但实现通常更复杂一些。5.memset5.1 作用把一段内存的前n个字节都设置成某个字节值。原型void *memset(void *s, int c, size_t n);5.2 本质是“按字节填充”这一点必须理解清楚。memset不是按int、按short、按结构体成员去写而是把目标内存的每一个字节都写成(unsigned char)c。5.3 常见用法用法一清零int arr[10]; memset(arr, 0, sizeof(arr));这很常见也很安全因为全 0 对几乎所有基础类型都能表示“零值状态”。用法二初始化字符缓冲区char buf[128]; memset(buf, 0, sizeof(buf));5.4 容易犯错的地方错误示例想把整型数组初始化成 1int arr[10]; memset(arr, 1, sizeof(arr));很多人以为这样会把每个int变成 1其实不是。假设int是 4 字节那么每个字节都会被填成0x01结果每个元素通常会变成0x01010101十进制一般是 16843009而不是 1。5.5 哪些值适合用memset最稳妥的情况设为0设为-1前提是按字节全0xFF的语义正好符合需求例如memset(arr, -1, sizeof(arr));会把每个字节都填成0xFF。对于常见补码机器int元素就会变成-1但写代码时要清楚这背后是“按字节填充”不是按整数语义赋值。6.memcmp6.1 作用比较两块内存的前n个字节。原型int memcmp(const void *s1, const void *s2, size_t n);6.2 比较规则它按字节逐个比较相等返回 0第一个不同字节如果s1 s2返回负数第一个不同字节如果s1 s2返回正数6.3 注意点memcmp比较的是原始字节序列不是“字符串逻辑内容”。例如两个结构体如果里面有填充字节直接memcmp不一定靠谱因为 padding 里的内容可能不同。这在面试里是一个很好的加分点。7.strlen7.1 作用计算字符串长度不包括结尾的\0。原型size_t strlen(const char *s);7.2 它的终止条件是什么从起始地址开始往后读直到遇到第一个\0为止。这意味着字符串必须合法内存中必须最终存在\0否则就是未定义行为。7.3 经典风险char buf[3] {a, b, c}; printf(%zu\n, strlen(buf));这是错误写法。因为buf里没有\0strlen会继续往后读读到哪里停完全不可控。8.strcpy8.1 作用把源字符串复制到目标字符串包括最后的\0。原型char *strcpy(char *dest, const char *src);8.2 关键点src必须是合法字符串dest必须有足够空间不会自动检查目标空间是否溢出会一直复制到\08.3 典型危险写法char buf[5]; strcpy(buf, hello);问题在于hello实际长度是 5但还要再加一个\0总共需要 6 字节。buf只有 5 字节必然越界。源字符串 h e l l o \0 - 共6字节 目标 buf[5] 只能放5字节 结果 最后一个 \0 写越界9.strncpy9.1 它的作用经常被误解很多人以为strncpy是“安全版strcpy”这句话并不准确。原型char *strncpy(char *dest, const char *src, size_t n);它的行为是最多拷贝n个字符如果src长度小于n剩余部分补\0如果src长度大于等于n不会自动保证目标串以\0结尾这一点非常关键。9.2 危险点示例char buf[5]; strncpy(buf, hello, sizeof(buf));这里buf最终可能是h e l l o没有\0。如果你后面再把它当字符串用比如printf(%s\n, buf);就有风险。9.3 更稳妥写法char buf[5]; strncpy(buf, hello, sizeof(buf) - 1); buf[sizeof(buf) - 1] \0;这才是很多工程里更稳的用法。10.strcmp和strncmp10.1strcmp按字典序比较两个字符串。原型int strcmp(const char *s1, const char *s2);规则相等0s1 s2负数s1 s2正数它比较的是字符的 ASCII/编码顺序不是字符串长度。10.2 示例strcmp(abc, abc) // 0 strcmp(abc, abd) // 负数 strcmp(abd, abc) // 正数 strcmp(abc, ab) // 正数因为比较到第三个字符时c大于\0。10.3strncmp只比较前n个字符。原型int strncmp(const char *s1, const char *s2, size_t n);常用于比较前缀协议头判断固定长度字段判断11.strcat和strncat11.1strcat把源字符串追加到目标字符串尾部。原型char *strcat(char *dest, const char *src);前提dest本身必须是一个合法字符串里面已经有\0目标空间必须足够大11.2 容易忽略的点strcat会先扫描dest找到它原本的\0然后从那里开始追加。所以它的时间开销并不只是“拷贝 src”还包括“先走一遍 dest”。11.3 示例char buf[20] abc; strcat(buf, def);结果abcdef11.4strncat限定最多追加n个字符但仍然会自动补\0。使用时同样要确保目标空间足够。12.snprintf比sprintf更值得记虽然很多基础面试资料只盯着strcpy但工程里真正更该养成的习惯是能限制长度就限制长度12.1sprintf把格式化内容写入字符串不检查目标缓冲区大小。sprintf(buf, value%d, x);如果buf不够大就可能溢出。12.2snprintf更稳妥。snprintf(buf, sizeof(buf), value%d, x);它最多写入指定大小通常更适合工程代码。12.3 面试加分点在真实项目里snprintf往往比sprintf更值得优先使用因为它能限制写入长度降低缓冲区溢出的风险。13.strchr、strstr、strtok这些函数在协议解析、命令解析中经常出现。13.1strchr在字符串中查找某个字符第一次出现的位置。char *p strchr(abc:def, :);如果找到返回指向该字符的指针找不到返回NULL。13.2strstr在字符串中查找子串。char *p strstr(hello world, world);找到则返回子串首次出现位置的地址。13.3strtok用于分割字符串。例如char s[] a,b,c; char *token strtok(s, ,);之后继续token strtok(NULL, ,);13.4strtok的问题它会修改原字符串把分隔符改成\0。而且它内部有静态状态不适合并发场景也不适合嵌套解析。所以工程里用它要比较谨慎。14. 缓冲区溢出到底是什么14.1 本质向一个固定大小的内存区域写入了超出边界的数据。例如char buf[8]; strcpy(buf, this is too long);这时写入的数据会冲到buf后面的内存区域。14.2 为什么危险因为越界覆盖的可能是其他变量函数返回地址栈帧数据控制信息轻则数据错误重则程序崩溃甚至构成安全漏洞。14.3 栈上缓冲区越界示意高地址 ------------------ | 返回地址 | ------------------ | 旧栈帧指针 | ------------------ | 其他局部变量 | ------------------ | buf[8] | ------------------ 低地址如果往buf里写太多就可能把上面的内容覆盖掉。15. 常见不安全写法总结15.1 不校验长度直接strcpychar buf[16]; strcpy(buf, input);风险input太长就越界。15.2 用gets这个函数本身就不安全现代标准里已经废弃。因为它完全不知道目标缓冲区大小。15.3sprintf写入固定数组如果拼接内容长度不可控也容易越界。15.4 把非字符串内存交给字符串函数例如没有\0却调用strlen、printf(%s)。15.5 忘记为\0留空间这是字符串处理最常见的错误之一。例如数组开 5 字节却想存hello实际应该开 6 字节。16. 工程里更稳妥的习惯16.1 明确区分“字节块”和“字符串”这是最重要的一条。如果处理二进制缓冲区用mem*如果处理文本字符串用str*不要混着用。16.2 能带长度就带长度例如传数组时同时传长度格式化输出优先snprintf拷贝时先判断目标空间是否足够16.3 对输入做边界控制所有外部输入都应当视为“不可信”串口数据网络数据命令行输入文件内容16.4 先清楚目标大小再写数据这比事后补救靠谱得多。17. 高频手写题一手写strlen17.1 基本版#include stddef.h size_t my_strlen(const char *s) { const char *p s; while (*p ! \0) { p; } return (size_t)(p - s); }17.2 思路不是用计数器而是直接用指针走到结尾然后做地址差。这样写更贴近底层也更容易体现对指针的理解。18. 高频手写题二手写strcpychar *my_strcpy(char *dest, const char *src) { char *ret dest; while ((*dest *src) ! \0) { } return ret; }18.1 为什么返回ret因为函数返回值要求是目标串起始地址而dest在循环过程中已经往后移动了所以要提前保存。18.2 这一句为什么能工作(*dest *src) ! \0执行顺序的本质是先取*src赋给*dest再判断赋过去的这个字符是不是\0然后两个指针都后移只要读懂这句字符串拷贝的核心就吃透了。19. 高频手写题三手写strcmpint my_strcmp(const char *s1, const char *s2) { while (*s1 ! \0 *s2 ! \0) { if (*s1 ! *s2) { return (unsigned char)*s1 - (unsigned char)*s2; } s1; s2; } return (unsigned char)*s1 - (unsigned char)*s2; }19.1 为什么常转成unsigned char因为字符比较时底层更稳妥的做法通常是按无符号字节值比较避免某些平台上char默认有符号带来的歧义。20. 高频手写题四手写memcpyvoid *my_memcpy(void *dest, const void *src, size_t n) { unsigned char *d (unsigned char *)dest; const unsigned char *s (const unsigned char *)src; while (n--) { *d *s; } return dest; }20.1 为什么用unsigned char *因为memcpy是按字节操作内存。把指针转成unsigned char *就能明确“一次处理一个字节”。20.2 这个版本有什么限制它没有处理重叠内存。所以它是memcpy风格不是memmove风格。21. 高频手写题五手写memmovevoid *my_memmove(void *dest, const void *src, size_t n) { unsigned char *d (unsigned char *)dest; const unsigned char *s (const unsigned char *)src; if (d s || n 0) { return dest; } if (d s || d s n) { while (n--) { *d *s; } } else { d n; s n; while (n--) { *--d *--s; } } return dest; }21.1 这里最关键的判断if (d s || d s n)含义没重叠或者目标在源前面正向拷贝否则反向拷贝这就是memmove的核心思想。22. 高频手写题六手写安全字符串拷贝函数工程里常见需求不是“无脑复制”而是“在给定缓冲区大小内安全复制”。#include stddef.h size_t my_strlcpy(char *dest, const char *src, size_t size) { const char *s src; size_t n size; if (n ! 0) { while (--n ! 0 *s ! \0) { *dest *s; } *dest \0; } while (*s ! \0) { s; } return (size_t)(s - src); }22.1 这个函数的思路它做了两件事最多只往dest写size - 1个字符保证结尾有\0这类设计思路在面试里很加分因为比单纯会写strcpy更贴近工程实践。23. 面试里常见追问点整理23.1memcpy为什么快因为它只按字节块做拷贝不做额外逻辑判断很多实现还会做字宽优化、对齐优化、批量拷贝优化。23.2 为什么strcpy危险因为它只看源串的\0不会检查目标缓冲区是否够大所以很容易造成溢出。23.3strlen的时间复杂度是 O(n)因为它必须从头扫描到\0。这也解释了为什么频繁对同一个长字符串调用strlen可能有性能浪费。23.4 为什么strncpy不一定安全因为它虽然限制了拷贝字符数但在源串过长时未必自动补\0后续如果把目标当成字符串使用仍然会出风险。23.5 为什么结构体不能简单用memcmp判等因为结构体内部可能有 padding两个逻辑上相同的结构体padding 字节值未必一样。这个回答在嵌入式面试里会显得比较细。24. 高频陷阱题24.1 题目一char a[10]; memset(a, A, sizeof(a)); printf(%s\n, a);问题这里未必能安全打印。因为数组里全是A没有\0所以%s打印会继续往后读直到偶然遇见 0 字节。24.2 题目二char a[5] abcd; strcat(a, e);风险越界。a[5] abcd实际内容是a b c d \0已经满了没有额外空间再追加e。24.3 题目三char s[] abcdef; memcpy(s 1, s, 3);行为未定义因为内存重叠。24.4 题目四char s[5]; strncpy(s, hello, sizeof(s)); printf(%s\n, s);风险s可能没有\0。24.5 题目五int arr[5]; memset(arr, 1, sizeof(arr));结果不是每个元素都等于 1而是每个字节都等于0x01。25. 面试回答模板整理25.1memcpy和strcpy的区别memcpy面向的是原始内存块按照给定字节数拷贝不依赖\0strcpy面向的是 C 风格字符串会一直复制到源串中的\0为止。memcpy更通用strcpy更适合明确的字符串场景但如果目标空间不足strcpy风险很大。25.2 为什么memmove比memcpy更安全因为memmove能处理源区和目标区重叠的情况它会根据地址关系选择合适的拷贝方向从而避免源数据在复制过程中被提前覆盖。25.3 为什么strncpy不能简单看作安全版strcpy因为strncpy只是限制最多拷贝多少字符但当源字符串长度大于等于这个上限时它不一定会在目标末尾补\0。如果后续把目标再当作普通字符串使用仍然可能出问题。25.4 为什么工程里更推荐snprintf因为它允许显式指定目标缓冲区大小可以限制写入长度降低格式化输出时发生缓冲区溢出的风险。26. 本篇真正要吃透的内容这一篇最核心的不是把所有函数都背下来而是建立下面这套判断方式第一层先判断你处理的是什么原始字节块还是 C 字符串第二层再判断终止条件是什么固定长度还是遇到\0第三层最后判断边界由谁负责函数本身会不会检查还是程序员自己必须保证27. 本篇收尾C 语言里很多 bug并不是因为算法多复杂而是因为把内存块当字符串把字符串当普通数组忘了长度忘了\0忘了边界忘了重叠忘了返回值语义

更多文章