适用对象:学过 strcpy / strcat / strcmp 的同学
本章目标:掌握安全版字符串函数 + strstr 子串查找 + strerror 错误处理
引入:strcpy 为什么会让人又爱又恨?
上一章我们学了 strcpy,它简单粗暴,复制速度也快------但它有一个致命问题:不检查目标空间是否够用。
如果目标空间只有 5 字节,但你拷贝了 20 字节的字符串,会发生什么?
内存越界写入,可能会把旁边的变量覆盖掉,或者触发段错误(Segmentation Fault)。更可怕的是,有时候程序不会马上崩,而是在某个完全不相关的地方突然出问题------这种 Bug 极难定位。
于是,C 语言标准委员会给几大函数都设计了带数字版本(strncpy / strncat / strncmp) ,把拷贝/追加/比较的长度交给程序员控制------更安全,但也更复杂。
第一部分:strncpy------安全拷贝 n 个字符
1.1 函数原型
c
char* strncpy(char *destination, const char *source, size_t num);
第三个参数 num:最多拷贝 num 个字符。
c
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[20] = {0};
char arr2[] = "abcdefghi";
strncpy(arr1, arr2, 5); // 只拷贝前 5 个字符
printf("%s\n", arr1); // 输出 abcde
return 0;
}
1.2 strncpy 的三个坑
坑一:源字符串不够 num 个字符时,用 \0 填充
c
char arr1[20] = {0};
char arr2[] = "hi";
strncpy(arr1, arr2, 10); // 源只有 2 个字符
printf("%s\n", arr1); // 输出 hi,后面的 8 个位置全部填 \0
坑二:源字符串够长时,不保证在第 num 位置写 \0
c
char arr1[20] = {0};
char arr2[] = "hello";
strncpy(arr1, arr2, 3); // 只拷贝 3 个字符 "hel"
printf("%s\n", arr1); // 输出 hel(没有 \0!arr1[3] 不是 \0)
⚠️ 提醒 :
strncpy在源字符串长度 ≥ num 时,不会自动补\0!这是最容易踩的坑。如果你在后面调用printf("%s", arr1),它会继续往后读内存,打印出一堆乱码。所以使用strncpy后,务必手动确保目标字符串以\0结尾:
cif (num < dest_size) arr1[num] = '\0';
1.3 strncpy vs strcpy:怎么选?
| 对比 | strcpy |
strncpy |
|---|---|---|
| 安全性 | ❌ 不检查边界 | ✅ 可控拷贝长度 |
| 何时用 | 源字符串短小确定 | 不确定源串长度时 |
| 长度足够时的 \0 | 一定会补 | 不保证 |
第二部分:strncat------安全追加 n 个字符
2.1 函数原型
c
char* strncat(char *destination, const char *source, size_t num);
追加最多 num 个字符,但无论源多长,都会追加 \0 (这是它比 strncpy 更安全的地方)。
c
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[20] = "hello ";
char arr2[] = "world";
strncat(arr1, arr2, 3); // 只追加前 3 个字符:"wor"
printf("%s\n", arr1); // 输出 hello wor
return 0;
}
2.2 strncat 的一个陷阱
strncat 追加到 destination 的 \0 位置开始,所以 destination 本身必须以 \0 结尾。如果 destination 忘了初始化:
c
char arr1[20]; // 未初始化,里面是垃圾值
strncat(arr1, "hi", 2); // 危险!从哪里开始追加?
💡 正确写法 :在使用
strncat之前,先确保destination有\0,或者用arr1[0] = '\0'清空。
第三部分:strncmp------安全比较 n 个字符
3.1 函数原型
c
int strncmp(const char *str1, const char *str2, size_t num);
最多比较前 num 个字符,返回规则同 strcmp。
c
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abcqw";
int ret1 = strncmp(arr1, arr2, 3); // 只比较前3个:abc = abc → 0
printf("%d\n", ret1); // 输出 0
int ret2 = strncmp(arr1, arr2, 4); // 比较前4个:d > q → 负数
printf("%d\n", ret2); // 输出负数
return 0;
}
3.2 典型应用:比较字符串前缀
c
// 判断字符串是否以 "http://" 开头
if (strncmp(url, "http://", 7) == 0)
printf("这是 HTTP 链接\n");
第四部分:strstr------在字符串里找子串
4.1 函数原型
c
char* strstr(const char *str1, const char *str2);
在 str1 里找 str2 第一次出现的位置。
c
#include <stdio.h>
#include <string.h>
int main()
{
char str[] = "This is a simple string";
char *pch = strstr(str, "simple");
if (pch != NULL)
printf("找到了:%s\n", pch); // 输出 simple string
else
printf("没找到\n");
return 0;
}
💡 返回值 :找到则返回指向该位置的指针 ,没找到返回
NULL。
4.2 手写 strstr:暴力匹配
核心思路:双层循环,外层遍历 str1,内层逐字符比较 str1 和 str2。
c
#include <assert.h>
char* my_strstr(const char *str1, const char *str2)
{
assert(str1 != NULL);
assert(str2 != NULL);
// 空字符串直接返回 str1(标准行为)
if (*str2 == '\0')
return (char*)str1;
const char *s1 = str1;
const char *s2 = str2;
while (*s1) {
// 记录当前位置
const char *cur = s1;
// 逐字符比较
while (*s1 && *s2 && *s1 == *s2) {
s1++;
s2++;
}
// s2 遍历完了 → 匹配成功
if (*s2 == '\0')
return (char*)cur;
// 没匹配上,重置 s2,回到 cur 的下一个位置继续
s1 = cur + 1;
s2 = str2;
}
return NULL;
}
第五部分:strerror------把错误码翻译成人话
5.1 什么是 errno?
C 语言程序启动时,系统会初始化一个全局变量 errno(定义在 <errno.h>)。当你调用标准库函数出错了,函数会把对应的错误码 写进 errno。
错误码是一个整数,比如 2、5、11------直接看数字完全不知道什么意思。
c
#include <errno.h> // 声明全局变量 errno
5.2 strerror:翻译错误码
c
char* strerror(int errnum);
把错误码翻译成人类可读的错误信息字符串。
c
#include <stdio.h>
#include <string.h>
int main()
{
for (int i = 0; i <= 10; i++)
printf("%d: %s\n", i, strerror(i));
return 0;
}
输出(Windows + VS2022):
0: No error
1: Operation not permitted
2: No such file or directory
3: No such process
4: Interrupted function call
5: Input/output error
6: No such device or address
7: Arg list too long
8: Exec format error
9: Bad file descriptor
10: No child processes
5.3 实战:fopen 出错了怎么办?
c
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
FILE *pFile = fopen("不存在的文件.txt", "r");
if (pFile == NULL) {
printf("文件打开失败:%s\n", strerror(errno));
// 输出:文件打开失败:No such file or directory
return 1;
}
// ... 正常读写文件 ...
fclose(pFile);
return 0;
}
5.4 perror:更简洁的写法
perror 是 strerror 的快捷版本,直接把错误信息打印出来:
c
perror("文件打开失败");
// 输出:文件打开失败:No such file or directory
相当于下面这行:
c
printf("文件打开失败:%s\n", strerror(errno));
💡 选哪个 :如果你需要打印 错误,用
perror更简洁;如果你需要存起来 或者自定义前缀 ,用strerror。
第六部分:经典面试题精讲
面试题一:strncpy 和 strcpy 的核心区别是什么?strncpy 有什么坑?
答案:
strcpy以\0为结束标志,拷贝整个源字符串strncpy以num为拷贝上限,但有两个坑:- 源不够长时用
\0填充剩余位置(有时候不是你想要的) - 源够长时不自动补
\0(比strcpy更危险)
- 源不够长时用
加分回答:
面试中如果能用上「源够 num 时不补
\0」这个坑点,面试官会觉得你理解得很深。建议使用strncpy后手动dest[num] = '\0'收尾。
面试题二:strstr 返回值是什么类型的指针?
答案 :char*,指向 str1 中首次匹配位置的指针。如果匹配失败,返回 NULL。
c
char str[] = "hello world";
char *p = strstr(str, "world");
printf("%s\n", p); // world
printf("%ld\n", p - str); // 6(world 在第6个位置起)
🔑 进阶用法:通过指针相减,可以算出子串在原串中的位置。
面试题三:下面的代码会输出什么?
c
#include <stdio.h>
#include <string.h>
int main()
{
char s1[] = "hello";
char s2[] = "world";
char buf[10];
strncpy(buf, s1, 10);
strncat(buf, s2, 3);
printf("%s\n", buf);
return 0;
}
推演:
strncpy(buf, s1, 10):buf大小 10 字节,s1只有 5 字符 +\0,所以全部拷贝,再填充 4 个\0。buf="hello\0\0\0\0\0"strncat(buf, s2, 3):从buf的\0位置开始,追加s2前 3 个字符"wor"。buf="hellowor\0"(第二个\0被覆盖,但后面还有 2 个\0)printf("%s\n", buf):从buf[0]打印到\0,输出hellowor
答案 :hellowor
本章总结
安全版字符串函数:
| 函数 | 额外参数 | 与基础版的关键区别 |
|---|---|---|
strncpy |
size_t num |
源够 num 时不补 \0(需手动处理) |
strncat |
size_t num |
追加 num 字符,但一定会补 \0 |
strncmp |
size_t num |
最多比较前 num 个字符 |
strstr 用法速记:
- 找得到 → 返回指针(指向匹配位置)
- 找不到 → 返回
NULL - 指针相减 → 子串在原串中的位置
错误处理速记:
| 函数 | 用法 | 特点 |
|---|---|---|
errno |
全局变量,记录最近一次错误码 | 调用标准库函数后检查 |
strerror |
strerror(errno) → 字符串 |
可存、可自定义 |
perror |
perror("提示") |
直接打印,等价于 printf + strerror |