一.strlen
strlen 是 C 语言标准库 <string.h> 中的核心函数,用于计算字符串的有效长度 (不包含字符串结束符 \0)。
一、基本语法
cs
size_t strlen(const char *str);
参数说明:
str:指向待计算长度的字符串的指针(字符串需以\0结尾,否则会导致未定义行为)。
返回值:
size_t:无符号整数类型(本质是unsigned int),返回字符串中有效字符的个数 (不包含\0)。
二、核心特性
- 只统计
\0之前的字符 :字符串的结束标志是\0,strlen从起始地址开始遍历,遇到\0立即停止,返回遍历过的字符数。 - 不修改原字符串 :参数用
const修饰,保证字符串只读。 - 无符号返回值 :
size_t是无符号类型,需注意与有符号数运算的坑(如strlen(a) - strlen(b)不会为负数)。
三、使用示例
cs
#include <stdio.h>
#include <string.h>
int main() {
// 示例1:正常字符串(含 \0)
char str1[] = "hello"; // 实际存储:h e l l o \0
printf("str1长度:%zu\n", strlen(str1)); // 输出 5(不含 \0)
printf("str1占用字节:%zu\n", sizeof(str1)); // 输出 6(包含 \0)
// 示例2:手动指定 \0 位置
char str2[] = "abc\0def";
printf("str2长度:%zu\n", strlen(str2)); // 输出 3(遇到 \0 停止)
// 示例3:空字符串
char str3[] = "";
printf("str3长度:%zu\n", strlen(str3)); // 输出 0
// 反例:无 \0 的字符数组(未定义行为,可能返回随机值)
char str4[3] = {'a', 'b', 'c'};
// printf("str4长度:%zu\n", strlen(str4)); // 危险!会越界遍历
return 0;
}
输出结果:
cs
str1长度:5
str1占用字节:6
str2长度:3
str3长度:0
四、常见坑点
-
与
sizeof的区别:特性 strlen sizeof 作用 计算字符串有效长度 计算变量 / 类型的字节大小 是否包含 \0不包含 包含(字符串数组) 类型 函数(运行时计算) 运算符(编译时计算) -
无符号返回值的陷阱:
cschar a[] = "123"; char b[] = "12345"; if (strlen(a) - strlen(b) > 0) { printf("a更长\n"); // 实际执行!因为无符号数相减结果仍为无符号(负数会转成大正数) }解决:先转为有符号数再运算:
csif ((int)strlen(a) - (int)strlen(b) > 0) { ... } -
空指针传入 :
strlen(NULL)会直接崩溃(访问非法内存),使用前需检查指针非空。
五、手动实现 strlen(模拟底层逻辑)
cs
#include <stdio.h>
// 自定义strlen:遍历字符串直到 \0
size_t my_strlen(const char *str) {
if (str == NULL) return 0; // 空指针保护
size_t len = 0;
while (str[len] != '\0') { // 遍历到 \0 停止
len++;
}
return len;
}
int main() {
char str[] = "hello world";
printf("自定义strlen:%zu\n", my_strlen(str)); // 输出 11
return 0;
}
总结
strlen核心是统计\0前的字符数 ,必须保证字符串以\0结尾;- 区分
strlen(运行时、有效长度)和sizeof(编译时、内存大小); - 注意
size_t无符号类型的运算陷阱,空指针需提前判断。
strlen 模拟
cs
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* p) {
assert(p);
size_t i = 0;
while ((*p++) != '\0') {
i++;
}
return i;
}
int main() {
char a[] = "abcde";
size_t s = my_strlen(a);
printf("%zu\n", s);
return 0;
}
使用size_t是为了匹配 "长度 / 大小" 的语义、保证范围足够、提升代码兼容性,具体原因如下:
1. 语义匹配:长度是 "非负数值"
字符串的长度、内存块的大小等属性天然是 "非负的" ,而size_t是 C 标准定义的无符号整数类型 (本质是unsigned int/unsigned long,随平台字长变化),完全契合 "长度不能为负" 的语义;若用int(有符号类型),则可能出现逻辑矛盾(比如长度被错误表示为负数)。
2. 范围足够:适配平台最大内存
size_t的取值范围与平台的最大可寻址内存对齐:
- 32 位系统中,
size_t通常是unsigned int(范围:0 ~ 2^32-1,约 4GB); - 64 位系统中,
size_t通常是unsigned long(范围:0 ~ 2^64-1,远超当前硬件内存上限)。
若用int(32 位下范围仅-2^31 ~ 2^31-1),当字符串 / 内存块的大小超过2^31-1时,会发生溢出 (结果变成负数),而size_t可覆盖平台支持的所有合法长度。
3. 类型一致性:与标准库接口统一
C 标准库中,所有表示 "大小 / 长度 / 索引" 的接口都使用size_t:
sizeof的返回值是size_t;- 内存分配函数
malloc(size_t size)的参数是size_t; - 数组索引、拷贝函数(如
memcpy)的长度参数也用size_t。
使用size_t能避免 "有符号 / 无符号类型混编" 的陷阱(比如strlen(a) - strlen(b)若用int会出现负数,用size_t则符合无符号运算规则)。
4. 可移植性:跨平台自动适配
size_t是 C 标准(<stddef.h>)定义的类型,不同编译器 / 平台会自动选择对应的无符号类型(无需手动修改);若用unsigned int等具体类型,在 64 位平台下会因字长不匹配导致兼容性问题。
综上,size_t是 "表示长度 / 大小" 的标准、安全、通用的类型 ,因此strlen等统计长度的函数会使用它。
二.strcpy函数
strcpy(string copy)是 C 标准库中用于字符串拷贝 的函数,定义在 <string.h> 头文件中,核心功能是将源字符串(包括终止符 '\0')完整复制到目标字符数组中。
一、函数原型
cs
char *strcpy(char *dest, const char *src);
参数说明
| 参数 | 说明 |
|---|---|
dest |
目标字符数组(接收拷贝的字符串),必须有足够空间且可修改(不能是只读内存) |
src |
源字符串(被拷贝的字符串),const 修饰表示不修改源字符串内容 |
返回值
返回 dest 的起始地址(方便链式调用,比如 strlen(strcpy(dest, src)))。
二、核心特性
- 自动拷贝终止符
'\0':会把src中从第一个字符到'\0'的所有内容(包括'\0')复制到dest。 - 无长度检查 :这是
strcpy最关键的安全隐患 ------ 如果dest空间小于src长度,会导致缓冲区溢出,触发未定义行为(程序崩溃、数据篡改等)。 - 内存不能重叠 :若
dest和src的内存区域重叠,结果未定义(需用memmove替代)。
三、正确使用示例
cs
#include <stdio.h>
#include <string.h>
int main() {
// 1. 目标数组足够大(需容纳源字符串 + '\0')
char dest[20]; // 长度20,远大于源字符串长度
const char *src = "Hello, strcpy!";
// 2. 执行拷贝
strcpy(dest, src);
// 3. 输出结果
printf("拷贝后的字符串:%s\n", dest); // 输出:Hello, strcpy!
printf("字符串长度:%zu\n", strlen(dest)); // 输出:12(不含'\0')
return 0;
}
四、常见错误示例
错误 1:目标缓冲区过小(缓冲区溢出)
cs
#include <stdio.h>
#include <string.h>
int main() {
char dest[5]; // 仅能容纳4个有效字符 + '\0'
const char *src = "Hello, strcpy!"; // 长度12 + '\0' = 13
strcpy(dest, src); // 溢出!未定义行为(程序可能崩溃)
printf("%s\n", dest);
return 0;
}
错误 2:目标是只读内存(字符串常量)
cs
#include <stdio.h>
#include <string.h>
int main() {
// char* dest 指向只读的字符串常量区,不可修改
char *dest = "固定字符串";
const char *src = "Hello";
strcpy(dest, src); // 运行时错误(段错误/Segmentation fault)
return 0;
}
五、安全替代方案
由于 strcpy 无长度检查,实际开发中优先使用更安全的替代函数:
1. strncpy(C 标准库,可控长度)
cs
char *strncpy(char *dest, const char *src, size_t n);
- 功能:最多拷贝
n个字符到dest。 - 注意:若
src长度 ≥n,不会自动添加'\0',需手动补全;若src长度 <n,剩余空间用'\0'填充。
示例:
cs
#include <stdio.h>
#include <string.h>
int main() {
char dest[20];
const char *src = "Hello, strncpy!";
size_t dest_size = sizeof(dest);
// 拷贝 dest_size-1 个字符(留1位给'\0')
strncpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0'; // 手动补终止符(关键!)
printf("拷贝结果:%s\n", dest); // 输出:Hello, strncpy!
return 0;
}
2. strlcpy(非标准 C,更安全)
- 定义:常见于 Linux/BSD 系统,需包含
<string.h>,自动补'\0'。 - 原型:
size_t strlcpy(char *dest, const char *src, size_t n); - 返回值:源字符串
src的长度(方便判断是否截断)。
示例:
cs
#include <stdio.h>
#include <string.h>
int main() {
char dest[10];
const char *src = "Hello, strlcpy!";
size_t src_len = strlcpy(dest, src, sizeof(dest));
printf("拷贝结果:%s\n", dest); // 输出:Hello, st(被截断)
printf("源字符串长度:%zu\n", src_len); // 输出:13(提示截断)
return 0;
}
六、总结
| 函数 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
strcpy |
简单、效率高 | 无长度检查,易缓冲区溢出 | 确定源字符串长度 ≤ 目标缓冲区大小 |
strncpy |
可控拷贝长度 | 需手动补 '\0',可能填充多余 '\0' |
需限制拷贝长度的场景 |
strlcpy |
自动补 '\0'、更安全 |
非标准 C,跨平台性差 | Linux/BSD 系统,追求安全优先 |
提示:C++ 中建议直接使用
std::string的赋值(std::string dest = src;),无需手动管理内存,完全规避缓冲区溢出问题。
七.函数复刻
cs
#include <stdio.h>
#include <assert.h> // 用于 assert 断言(调试阶段检查)
// 健壮版 my_strcpy:增加 NULL 检查
char *my_strcpy_safe(char *dest, const char *src) {
// 1. 空指针检查(调试阶段断言,Release 可替换为返回 NULL)
assert(dest != NULL && src != NULL); // 断言失败会终止程序并提示
// 生产环境可选写法:if (dest == NULL || src == NULL) return NULL;
// 2. 保存起始地址
char *dest_start = dest;
// 3. 逐字节拷贝(同基础版)
while (*dest++ = *src++) {}
return dest_start;
}
// 测试:传入 NULL 会触发断言
int main() {
char dest[20];
const char *src = NULL;
my_strcpy_safe(dest, src); // 运行时断言失败:Assertion failed: (dest != NULL && src != NULL)
return 0;
}
三.strcat函数
strcat(string concatenate,字符串拼接)是 C 标准库 <string.h> 中专门用于将源字符串拼接到目标字符串末尾 的函数,其核心逻辑是覆盖目标字符串原有末尾的 '\0',并在拼接完成后重新添加终止符 '\0'。
二、核心语法
1. 头文件(必须包含)
cs
#include <string.h>
2. 函数原型
cs
char *strcat(char *dest, const char *src);
3. 参数解析
| 参数 | 类型 / 权限 | 核心要求 |
|---|---|---|
dest |
可写 | 目标字符串(字符数组 / 可修改的字符指针),必须预留足够空间容纳拼接结果 |
src |
只读 | 源字符串(const 修饰),以 '\0' 结尾,内容不会被修改 |
4. 返回值
返回 dest 的首地址(支持链式调用,如 strcat(strcat(dest, "a"), "b"))。
三、基础使用示例(正确写法)
cs
#include <stdio.h>
#include <string.h>
int main() {
// 1. 目标字符串:预留足够空间("Hello" + " World!" + '\0' = 12 字节,这里给20字节)
char dest[20] = "Hello";
// 2. 源字符串:以'\0'结尾(字符串字面量默认带'\0')
const char src[] = " World!";
// 3. 拼接:src 拼到 dest 末尾
strcat(dest, src);
// 输出结果:Hello World!
printf("拼接后:%s\n", dest);
// 输出 dest 长度:11("Hello World!" 共11个可见字符,含'\0'则12)
printf("拼接后长度:%zu\n", strlen(dest));
return 0;
}
四、strcat 最容易踩的 3 个坑(必看)
坑 1:目标缓冲区空间不足(最常见)
strcat 不会检查 dest 的剩余空间,若空间不够会直接导致「缓冲区溢出」,破坏内存数据,程序可能崩溃 / 出现诡异行为。
❌ 错误示例(空间不足):
cs
// dest 仅6字节("Hello" + '\0'),拼接 " World!" 需额外7字节,总需13字节
char dest[6] = "Hello";
const char src[] = " World!";
strcat(dest, src); // 溢出!内存被破坏,程序大概率崩溃
坑 2:字符串未以 '\0' 结尾
strcat 依赖 '\0' 识别字符串末尾,若 dest/src 无 '\0',会一直遍历内存直到找到 '\0',引发未定义行为。
❌ 错误示例(无 '\0'):
cs
// 字符数组手动初始化,未加'\0',strcat 无法识别末尾
char dest[20] = {'H','e','l','l','o'};
const char src[] = " World!";
strcat(dest, src); // 未定义行为:可能拼接乱码,或程序崩溃
坑 3:源 / 目标字符串内存重叠
若 dest 和 src 指向同一块内存区域,strcat 行为不可预测(覆盖自身数据)。
❌ 错误示例(内存重叠):
cs
char str[20] = "abcdef";
// src 是 str+2(指向 "cdef"),与 dest(str)重叠
strcat(str, str+2); // 未定义行为:可能输出 "abcdefcdef",也可能崩溃
五、安全替代:strncat(推荐生产环境使用)
为解决 strcat 无长度检查的问题,C 提供了 strncat(指定最大拼接长度),核心是「限制拼接字符数,避免溢出」。
1. strncat 原型
cs
char *strncat(char *dest, const char *src, size_t n);
n:最多从src拼接n个字符到dest;- 自动在拼接后添加
'\0',无需手动加。
2. 安全用法示例(计算剩余空间)
cs
#include <stdio.h>
#include <string.h>
int main() {
char dest[20] = "Hello";
const char src[] = " World! 123456789";
// 关键:计算 dest 剩余可用空间 = 总长度 - 已用长度 - 1(留'\0'位置)
size_t dest_total = sizeof(dest); // dest 总空间:20
size_t dest_used = strlen(dest); // dest 已用长度:5("Hello")
size_t max_copy = dest_total - dest_used - 1; // 最大可拼接长度:14
// 仅拼接 max_copy 个字符,避免溢出
strncat(dest, src, max_copy);
// 输出:Hello World! 123456789(刚好填满,无溢出)
printf("安全拼接结果:%s\n", dest);
return 0;
}
六、strcat 进阶用法:链式调用
利用返回值是 dest 首地址的特性,可实现「连续拼接」:
cs
#include <stdio.h>
#include <string.h>
int main() {
char dest[30] = "I love ";
// 链式拼接:先拼 "C ",再拼 "programming!"
strcat(strcat(dest, "C "), "programming!");
printf("链式拼接:%s\n", dest); // 输出:I love C programming!
return 0;
}
七、总结:strcat 使用原则
- 能用
strncat就不用strcat:生产环境优先选strncat,避免缓冲区溢出; - 必须保证
dest空间足够 :若用strcat,需提前确认dest总长度 ≥ 原长度 + 源字符串长度 + 1('\0'); - 字符串必须以
'\0'结尾 :字符数组手动初始化时,务必加'\0'; - 禁止内存重叠 :
dest和src不能指向同一块内存的重叠区域。
八.函数复刻
cs
#include <stdio.h>
#include <stddef.h> // 用于 NULL 定义
// 复刻 strcat 函数
char *my_strcat(char *dest, const char *src) {
// 1. 边界检查:dest 或 src 为 NULL 直接返回 NULL
if (dest == NULL || src == NULL) {
return NULL;
}
// 2. 保存 dest 起始地址(最终要返回)
char *dest_start = dest;
// 3. 找到 dest 的末尾('\0' 位置)
while (*dest != '\0') {
dest++;
}
// 4. 逐字符复制 src 到 dest 末尾(包含 '\0')
while (*src != '\0') {
*dest = *src;
dest++;
src++;
}
// 确保最终字符串以 '\0' 结尾(上面循环结束后 src 是 '\0',这里显式赋值更稳妥)
*dest = '\0';
// 5. 返回 dest 的起始地址
return dest_start;
}
// 测试代码
int main() {
// 测试用例1:正常拼接
char dest1[20] = "Hello, "; // 预留足够空间
const char *src1 = "World!";
my_strcat(dest1, src1);
printf("测试用例1:%s\n", dest1); // 预期输出:Hello, World!
// 测试用例2:dest 为空字符串
char dest2[20] = "";
const char *src2 = "Test Empty Dest";
my_strcat(dest2, src2);
printf("测试用例2:%s\n", dest2); // 预期输出:Test Empty Dest
// 测试用例3:src 为空字符串
char dest3[20] = "Original";
const char *src3 = "";
my_strcat(dest3, src3);
printf("测试用例3:%s\n", dest3); // 预期输出:Original
// 测试用例4:NULL 指针(鲁棒性测试)
char *dest4 = NULL;
const char *src4 = "NULL Test";
char *result = my_strcat(dest4, src4);
if (result == NULL) {
printf("测试用例4:检测到 NULL 指针,返回 NULL\n"); // 预期输出
}
return 0;
}