在C语言中,字符串并不是一种独立的数据类型,而是以空字符 \0 结尾的连续字符序列。要获取字符串长度,核心就是找到这个结束标志的位置。标准库提供的 strlen 函数完成这件事:从给定的首地址开始,一个字节一个字节向后扫描,遇到 \0 就停止,返回已经扫描过的字符个数。注意这个返回值不含结尾的 \0 本身。很多初学者把字符数组的占用空间和字符串长度混为一谈,导致缓冲区计算错误;也有人对未初始化的数组直接调用 strlen,结果读到越界内存。下面先看基础用法,再逐步拆解这些坑。

一、strlen 函数的基本用法与底层原理
strlen 声明在 <string.h> 头文件中,函数原型为 size_t strlen(const char *s);。它接收一个指向字符的指针,返回字符串中有效字符的数量。这里的关键点是参数使用了 const 修饰,说明函数在扫描过程中不会修改字符串内容,只做只读遍历。下面是一个最基础的使用示例:
#include <stdio.h>
#include <string.h>
int main(void) {
char str[] = "hello";
size_t len = strlen(str);
printf("length = %zu\n", len);
return 0;
}
这段代码输出 5,而数组 str 实际占用 6 个字节,因为字符串末尾自动补充了一个 \0。也就是说,strlen 统计的是 h、e、l、l、o 这 5 个字符,不会把结束符算进去。
从底层实现看,strlen 通常等价于这样一个循环:从指针 s 开始,判断当前字节是否为 \0,不是则计数器加一并移动指针,是则退出并返回计数。因此它的时间复杂度是 O(n),字符串越长,扫描耗时越长。这也意味着如果你已经知道字符串长度,在循环中反复调用 strlen 会造成不必要的性能损耗,更好的做法是把长度先缓存到一个变量中。
还有一个容易被忽视的现象:如果字符串中间就出现了 \0,strlen 会提前停止。比如下面的数组:
#include <stdio.h>
#include <string.h>
int main(void) {
char arr[] = {'a', 'b', '\0', 'c', 'd', '\0'};
printf("strlen = %zu\n", strlen(arr));
return 0;
}
输出结果是 2,因为 strlen 只扫描到第一个 \0 就返回了。这个特性在二进制数据处理、字符串截断等场景中非常重要,如果不清楚数据中是否嵌入了空字符,就不能简单依赖 strlen 来获取完整数据长度。
二、strlen 与 sizeof 的核心区别
strlen 是运行时函数,需要遍历字符串才能得到结果;而 sizeof 是编译期运算符,它在程序编译阶段就能确定操作数占用的内存大小。两者经常被混用,但行为差异极大。对于数组名,sizeof 返回整个数组占用的总字节数,包括末尾的 \0;对于指针,sizeof 返回的是指针变量本身的大小,通常是 4 字节或 8 字节,与指向的字符串长度完全无关。
下面这个例子可以清楚看出二者的差异:
#include <stdio.h>
#include <string.h>
int main(void) {
char arr[] = "hello";
char *p = arr;
printf("sizeof(arr) = %zu\n", sizeof(arr)); /* 6 */
printf("strlen(arr) = %zu\n", strlen(arr)); /* 5 */
printf("sizeof(p) = %zu\n", sizeof(p)); /* 8 或 4 */
printf("strlen(p) = %zu\n", strlen(p)); /* 5 */
return 0;
}
数组 arr 的长度在定义时已经确定,编译器知道它有 6 个字节,所以 sizeof(arr) 得到 6。而指针 p 只是一个保存地址的变量,本身不包含字符串长度信息,sizeof(p) 只能得到指针变量的大小。反过来,strlen 无论对数组名还是指针,都会顺着地址向下扫描,所以两者返回的字符串长度都是 5。
更大的坑出现在函数传参时。C语言中数组作为函数参数会退化为指针,函数内部拿到的不再是数组类型,而是 char *。因此函数内使用 sizeof 只会返回指针大小,而不是原始数组的字节数。看下面的代码:
#include <stdio.h>
#include <string.h>
void show_size(char str[]) {
printf("inside: sizeof=%zu, strlen=%zu\n", sizeof(str), strlen(str));
}
int main(void) {
char text[] = "hello world";
show_size(text);
printf("outside: sizeof=%zu, strlen=%zu\n", sizeof(text), strlen(text));
return 0;
}
在 64 位系统上,函数内部 sizeof(str) 通常输出 8,而外部 sizeof(text) 输出 12(11 个字符加上 1 个 \0)。如果开发者误以为函数内 sizeof(str) 能拿到数组长度,进而做内存复制、边界检查,就很容易造成缓冲区溢出或截断。正确做法是:在数组定义可见的作用域内,可以用 sizeof(arr) - 1 获取字符串长度;一旦传入函数,要么使用 strlen,要么显式传递长度参数。
三、手动实现字符串长度函数
自己实现一个类似 strlen 的函数,能帮助更深刻地理解字符串扫描过程。最简单的计数器版本如下:
#include <stddef.h>
size_t my_strlen(const char *s) {
size_t count = 0;
while (s[count] != '\0') {
++count;
}
return count;
}
这个实现逐个下标访问字符,直到遇到 \0。它的优点是逻辑直白,但每次循环都要计算一次 s[count] 的地址,效率略低。另一种常见写法使用指针移动,如下:
#include <stddef.h>
size_t my_strlen2(const char *s) {
const char *p = s;
while (*p != '\0') {
++p;
}
return (size_t)(p - s);
}
这种实现让指针 p 不断后移,最后用两个指针的差值得到字符数量。由于指针相减的结果本来就是有符号整数 ptrdiff_t,所以这里显式转换为 size_t 更符合无符号长度语义。两种实现功能一致,指针版本在多数平台上略快一些。
手动实现时还要注意空指针问题。标准 strlen 并没有要求传入非 NULL,如果调用 strlen(NULL) 通常会导致程序崩溃。你可以在自己的工具函数中加入防御性检查:
#include <stddef.h>
size_t safe_strlen(const char *s) {
if (s == NULL) {
return 0;
}
size_t count = 0;
while (s[count] != '\0') {
++count;
}
return count;
}
不过要清楚,加入 NULL 检查会改变语义,让函数不再严格等价于标准 strlen。在项目中是否采用这种防御策略,需要根据接口约定来决定。另一个更常见的问题是字符数组没有以 \0 结尾。例如 char buf[5] = {'a','b','c','d','e'}; 这个数组不包含空字符,调用 strlen(buf) 会一直向后读取,直到在相邻内存中碰巧遇到一个 0 字节,这是典型的未定义行为。为避免这种情况,分配缓冲区时最好预留一个字节给结束符,或者使用 strncpy 等带长度限制的函数。
四、处理中文和宽字符的字符串长度
C语言中的 char 类型通常是单字节,适合处理 ASCII 字符。当字符串包含中文、日文、韩文等多字节字符时,strlen 返回的是字节数,而不是用户感知的字符数。以 UTF-8 编码为例,一个常用汉字通常占 3 个字节,所以“你好”这个两个字的字符串在 UTF-8 环境下 strlen 会返回 6。
#include <stdio.h>
#include <string.h>
int main(void) {
char s[] = "你好";
printf("strlen = %zu\n", strlen(s)); /* 通常输出 6 */
printf("sizeof = %zu\n", sizeof(s)); /* 7,多一个 \0 */
return 0;
}
如果业务需要统计的是“字符个数”而不是“字节个数”,就不能直接用 strlen。一种方法是改用宽字符 wchar_t 以及对应的 wcslen 函数。宽字符比普通 char 能表示更多的字符,wcslen 返回宽字符的个数。示例如下:
#include <stdio.h>
#include <wchar.h>
#include <locale.h>
int main(void) {
setlocale(LC_ALL, "");
wchar_t ws[] = L"你好";
wprintf(L"wcslen = %lu\n", wcslen(ws));
return 0;
}
这段代码在大多数平台上会输出 2,表示两个宽字符。不过宽字符方案也并非完美:在 Windows 上 wchar_t 通常为 2 字节,某些生僻字需要两个 wchar_t 组成代理对,此时 wcslen 也会返回 2,而不是用户看到的 1 个字符。如果需要精确统计 Unicode 码点或用户可见字符,需要引入专门的 Unicode 处理库。
如果仍然使用 char 存储 UTF-8 字符串,也可以自己写一个简单的字符计数函数。UTF-8 的规则是:每个多字节序列的首字节高位不是 10,后续字节的高位是 10。因此只统计非 10 开头的字节,就能得到字符数量。
#include <stdio.h>
#include <stddef.h>
size_t utf8_char_count(const char *s) {
size_t count = 0;
unsigned char c;
while ((c = (unsigned char)*s) != '\0') {
if ((c & 0xC0) != 0x80) {
++count;
}
++s;
}
return count;
}
int main(void) {
char s[] = "你好abc";
printf("char count = %zu\n", utf8_char_count(s));
return 0;
}
这个函数输出 5,把“你”“好”各算作 1 个字符,加上三个 ASCII 字符。当然这只是简化版,没有严格校验 UTF-8 序列是否合法,但足以应对很多普通文本场景。实际开发中可以根据需求选择 strlen、wcslen 或自定义 UTF-8 计数函数,关键是明确你需要的到底是字节数、代码单元数还是用户可见字符数。
五、常见误用场景与排查思路
除了前面提到的 sizeof 混淆和未以 \0 结尾之外,还有几类典型错误值得注意。第一类是使用未初始化或部分初始化的字符数组。例如只声明 char s[100]; 而不赋值,数组内容是随机的,里面可能没有 0 字节,这时调用 strlen 会读取到数组边界之外。即使是部分初始化,如 char s[100] = {'a'};,虽然未赋值的元素会被自动置为 0,但前提是数组定义在静态区或编译器明确做了零初始化,如果依赖这一点不清楚,最好显式给字符串加上结束符。
第二类是把 strlen 用于二进制数据。二进制缓冲区中可能包含很多 0 字节,strlen 会在第一个 0 处停止,根本无法反映真实数据长度。处理二进制数据时应该使用显式的长度变量,或者使用 memcpy、fread 等函数返回的实际字节数。
第三类是在循环条件中反复调用 strlen。比如 for (int i = 0; i < strlen(s); ++i) 每次循环都会重新扫描整个字符串,把原本 O(n) 的操作变成 O(n²)。正确做法是提前把长度保存到变量:size_t len = strlen(s); for (int i = 0; i < len; ++i)。如果字符串在循环过程中会被修改,也要相应更新长度,否则可能越界。
排查字符串长度相关问题时,可以优先检查指针来源是否合法、缓冲区是否有结束符、传入函数时是否发生了数组退化、以及编码方式是否符合预期。理解了这些底层机制,使用 strlen 时就不容易掉进那些隐蔽的陷阱。

