【C语言】字符串的本质是什么,为什么 strlen 会读到野内存

【C语言】字符串的本质是什么,为什么 strlen 会读到野内存
很多人用了好一阵子printf(%s, str)之后才意识到C 语言根本没有字符串类型。char数组加一个\0才是字符串缺了那个\0你调strlen会读到哪里去没人知道。这篇把字符串的底层机制说清楚顺带讲几个能把程序搞崩的坑。字符串的本质字符数组 \0C 语言的字符串不是一个专门的类型本质就是一块连续的内存最后一个字节存着值为0的\0空字符null terminator。char name[6] {h, e, l, l, o, \0};等价写法char name[6] hello;hello这个字符串字面量本身就含\0编译器会把它展开成 6 个字节不是 5 个。内存布局地址: 1000 1001 1002 1003 1004 1005 内容: h e l l o \0 十六进: 68 65 6C 6C 6F 00strlen、printf %s、strcpy这些函数都从起始地址开始往后读碰到\0就停。这就是为什么\0是字符串的核心——没有它这些函数不知道字符串在哪里结束会一直往后读进入它不该进的内存区域。声明方式三种写法的差异写法一字符数组可修改char str[] hello; /* 编译器自动算长度6 个字节 */ str[0] H; /* 合法数组内容可修改 */str存的是局部变量放在栈上内容可以改。写法二指针指向字符串字面量危险char *p hello; p[0] H; /* 运行时崩溃或静默失败 */hello是字符串字面量编译器把它放在只读数据段.rodata。p指向那块只读内存写入操作是未定义行为——在 Windows 上通常是访问违规崩溃在某些平台上可能什么也不发生但结果是错的。VS2013 编译这行不报错但运行时会弹出一个访问违规的对话框。正确写法是加constconst char *p hello; /* 明确告诉自己和编译器这块内存不能改 */写法三固定长度数组char buf[64]; /* 声明一个 64 字节的缓冲区 */buf里的内容是未初始化的垃圾值没有\0。直接printf(%s, buf)会打印出一堆乱码直到碰到内存里某个恰好是 0 的字节才停。初始化方式buf[0] \0; /* 方法一把第一个字节清零等价于空字符串 */ memset(buf, 0, 64); /* 方法二全部清零更安全 */常用字符串函数用法与陷阱strlen求长度不含\0#include string.h char s[] hello; size_t len strlen(s); /* 返回 5不是 6 */性能陷阱strlen每次调用都从头扫到\0时间复杂度 O(n)。把它放在循环条件里是典型的性能问题/* 错误写法每次循环都调一次 strlenO(n²) */ for (i 0; i strlen(s); i) { ... } /* 正确写法先算好长度 */ int len strlen(s); for (i 0; i len; i) { ... }字符串不长的时候看不出差别字符串一旦上了几千字节循环里的strlen会让程序明显变慢。strcpy复制字符串char dst[10]; strcpy(dst, hello); /* 把 hello\0 复制到 dst */strcpy不检查目标缓冲区够不够大是经典的缓冲区溢出来源char dst[4]; strcpy(dst, hello); /* 写了 6 个字节进 4 字节的空间溢出 */溢出会覆盖dst后面的内存可能改掉其他变量可能破坏返回地址可能什么都不发生——但这正是最危险的情况因为 bug 不在当场发作而是在你根本不想到的地方崩溃。VS2013 的安全版本/* strcpy_s 会检查目标缓冲区大小超出时触发错误处理 */ strcpy_s(dst, sizeof(dst), hello);strcmp比较字符串这里是最常见的初学者错误用比较两个字符串。char a[] hello; char b[] hello; if (a b) { ... } /* 错比较的是两个数组的地址永远不相等 */ if (strcmp(a, b) 0) { ... } /* 对比较的是内容 */a和b是两个不同的数组它们的地址不同a b永远是假。strcmp返回 0 表示两个字符串内容相同正数/负数表示大小关系按字典序。strcat拼接字符串char buf[20] hello; strcat(buf, world); /* buf 变成 hello world */同样不检查目标缓冲区大小同样有溢出风险。安全版本用strncat指定最多追加多少字节或 VS2013 的strcat_s。完整示例字符串处理的正确姿势#include stdio.h #include string.h #define BUF_SIZE 64 int main() { char src[] Hello, C language; char dst[BUF_SIZE]; int i; int len; /* 安全复制 */ strcpy_s(dst, BUF_SIZE, src); printf(复制结果%s\n, dst); /* 统计长度先存起来避免循环里重复调用 */ len (int)strlen(dst); printf(长度%d\n, len); /* 遍历字符把小写字母转大写 */ for (i 0; i len; i) { if (dst[i] a dst[i] z) { dst[i] dst[i] - a A; } } printf(转大写%s\n, dst); /* 字符串比较 */ if (strcmp(src, dst) ! 0) { printf(两个字符串内容不同\n); } return 0; }在 VS2013 里按 F5输出复制结果Hello, C language 长度17 转大写HELLO, C LANGUAGE 两个字符串内容不同避坑清单坑 1数组太小没给\0留位置char s[5] hello; /* 错hello 需要 6 个字节s 只有 5 个 */VS2013 会报警告或错误。规则数组大小至少是字符串长度 1。坑 2忘记初始化直接当字符串用char buf[64]; printf(%s\n, buf); /* buf 里是垃圾可能打出一堆乱码 */声明字符数组后先memset(buf, 0, sizeof(buf))或buf[0] \0。坑 3用比较字符串内容已在上面说过比的是地址不是内容。一律用strcmp。坑 4向只读字符串字面量写入char *p hello; p[0] H; /* 运行时崩溃 */需要修改的字符串用char []而不是char *。坑 5strcpy目标缓冲区不够大源字符串多长你得自己算strcpy不帮你检查。在 VS2013 里优先用strcpy_s。最佳实践字符数组大小用#define定义常量方便统一修改也防止sizeof和硬编码不一致所有字符数组在声明后立即用memset清零而不是依赖局部变量的初始值在 VS2013 环境下用strcpy_s、strcat_s、sprintf_s代替不安全的老版本循环遍历字符串时先把strlen结果存到变量不要每次循环都调函数接收字符串参数时同时传入缓冲区大小永远不要假设调用方传进来的缓冲区够大标签#C语言#字符串#内存安全#缓冲区溢出#VS2013