【C语言】字符串的本质是什么,为什么 strlen 会读到野内存
2026/7/13 23:50:58 网站建设 项目流程

很多人用了好一阵子printf("%s", str)之后才意识到,C 语言根本没有"字符串类型"。char数组加一个\0才是字符串,缺了那个\0,你调strlen会读到哪里去,没人知道。

这篇把字符串的底层机制说清楚,顺带讲几个能把程序搞崩的坑。


字符串的本质:字符数组 +\0

C 语言的字符串不是一个专门的类型,本质就是一块连续的内存,最后一个字节存着值为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 00

strlenprintf %sstrcpy这些函数都从起始地址开始往后读,碰到\0就停。这就是为什么\0是字符串的核心——没有它,这些函数不知道字符串在哪里结束,会一直往后读,进入它不该进的内存区域。


声明方式:三种写法的差异

写法一:字符数组(可修改)

char str[] = "hello"; /* 编译器自动算长度:6 个字节 */ str[0] = 'H'; /* 合法,数组内容可修改 */

str存的是局部变量,放在栈上,内容可以改。

写法二:指针指向字符串字面量(危险

char *p = "hello"; p[0] = 'H'; /* 运行时崩溃或静默失败 */

"hello"是字符串字面量,编译器把它放在只读数据段.rodata)。p指向那块只读内存,写入操作是未定义行为——在 Windows 上通常是访问违规崩溃,在某些平台上可能什么也不发生但结果是错的。

VS2013 编译这行不报错,但运行时会弹出一个访问违规的对话框。正确写法是加const

const 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)。把它放在循环条件里是典型的性能问题:

/* 错误写法:每次循环都调一次 strlen,O(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) { ... } /* 对!比较的是内容 */

ab是两个不同的数组,它们的地址不同,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 *

坑 5:strcpy目标缓冲区不够大

源字符串多长你得自己算,strcpy不帮你检查。在 VS2013 里优先用strcpy_s


最佳实践

  • 字符数组大小用#define定义常量,方便统一修改,也防止sizeof和硬编码不一致

  • 所有字符数组在声明后立即用memset清零,而不是依赖局部变量的初始值

  • 在 VS2013 环境下,用strcpy_sstrcat_ssprintf_s代替不安全的老版本

  • 循环遍历字符串时先把strlen结果存到变量,不要每次循环都调

  • 函数接收字符串参数时,同时传入缓冲区大小,永远不要假设调用方传进来的缓冲区够大


标签#C语言#字符串#内存安全#缓冲区溢出#VS2013

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询