1. 字符分类函数
C语言中提供了一系列用于字符分类的函数,用于判断一个字符属于哪种类型。使用这些函数需要包含头文件 <ctype.h>。
函数功能总览
下表列出了常用的字符分类函数及其功能:
| 函数名 | 功能说明 | 判断条件 |
|---|---|---|
isalnum |
检查字符是否为字母或数字 | a-z, A-Z, 0-9 |
isalpha |
检查字符是否为字母 | a-z, A-Z |
isdigit |
检查字符是否为十进制数字 | 0~9 |
isxdigit |
检查字符是否为十六进制数字 | 0-9, a-f, A-F |
islower |
检查字符是否为小写字母 | a~z |
isupper |
检查字符是否为大写字母 | A~Z |
isspace |
检查字符是否为空白字符 | 空格' '、换页'\f'、换行'\n'、回车'\r'、制表符'\t'、垂直制表符'\v' |
ispunct |
检查字符是否为标点符号 | 任何不属于数字或字母的可打印图形字符 |
isgraph |
检查字符是否为任何图形字符(可打印,不包括空格) | 除空格外的所有可打印字符 |
isprint |
检查字符是否为任何可打印字符 | 包括图形字符和空白字符 |
iscntrl |
检查字符是否为任何控制字符 | ASCII码 0x00~0x1F 或 0x7F (DEL) |
代码示例
c
#include <stdio.h>
#include <ctype.h>
int main() {
char ch = 'A';
// 使用 isalpha 判断是否为字母
if (isalpha(ch)) {
printf("'%c' 是一个字母\n", ch);
}
// 使用 isdigit 判断是否为数字
if (isdigit(ch)) {
printf("'%c' 是一个数字\n", ch);
} else {
printf("'%c' 不是一个数字\n", ch);
}
// 使用 islower 和 isupper 判断大小写
if (islower(ch)) {
printf("'%c' 是小写字母\n", ch);
} else if (isupper(ch)) {
printf("'%c' 是大写字母\n", ch);
}
// 使用 isspace 判断空白字符
char space = ' ';
if (isspace(space)) {
printf("空格字符被识别为空白字符\n");
}
// 使用 ispunct 判断标点符号
char punct = '!';
if (ispunct(punct)) {
printf("'%c' 是一个标点符号\n", punct);
}
// 使用 isalnum 判断字母或数字
char num = '5';
if (isalnum(num)) {
printf("'%c' 是字母或数字\n", num);
}
// 使用 isxdigit 判断十六进制数字
char hex = 'F';
if (isxdigit(hex)) {
printf("'%c' 是十六进制数字\n", hex);
}
// 使用 isgraph 和 isprint 的区别
char printable = 'a';
printf("'%c' 是可打印字符: %d\n", printable, isprint(printable));
printf("'%c' 是图形字符: %d\n", printable, isgraph(printable));
// 控制字符(如换行符)不是可打印字符
char newline = '\n';
printf("换行符是可打印字符: %d\n", isprint(newline));
printf("换行符是控制字符: %d\n", iscntrl(newline));
return 0;
}
运行结果
'A' 是一个字母
'A' 不是一个数字
'A' 是大写字母
空格字符被识别为空白字符
'!' 是一个标点符号
'5' 是字母或数字
'F' 是十六进制数字
'a' 是可打印字符: 1
'a' 是图形字符: 1
换行符是可打印字符: 0
换行符是控制字符: 1
重点说明
- 返回值 :这些函数在判断条件成立时返回非零值(真) ,不成立时返回 0(假)。
- 头文件 :使用前必须包含
<ctype.h>。 - 参数类型 :函数接收
int类型的参数,实际传入char类型时会自动提升为int。 isgraphvsisprint:isprint包含空格字符,而isgraph不包含空格字符,这是两者唯一的区别。iscntrl:用于判断控制字符(如\n、\t、\0等),这些字符不可打印。
2. 字符转换函数
C语言中提供了两个字符转换函数,用于实现字母大小写的转换。使用这些函数同样需要包含头文件 <ctype.h>。
函数原型
c
int tolower(int c); // 将大写字母转换为小写字母
int toupper(int c); // 将小写字母转换为大写字母
函数说明
| 函数名 | 功能 | 参数 | 返回值 |
|---|---|---|---|
tolower |
将大写字母转换为小写字母 | 要转换的字符(int类型) | 如果参数是大写字母,返回对应的小写字母;否则返回原字符 |
toupper |
将小写字母转换为大写字母 | 要转换的字符(int类型) | 如果参数是小写字母,返回对应的大写字母;否则返回原字符 |
代码示例
c
#include <stdio.h>
#include <ctype.h>
int main() {
char upper = 'A';
char lower = 'b';
char digit = '3';
// 大写转小写
printf("tolower('%c') = '%c'\n", upper, tolower(upper));
// 小写转大写
printf("toupper('%c') = '%c'\n", lower, toupper(lower));
// 非字母字符转换(返回原字符)
printf("tolower('%c') = '%c'\n", digit, tolower(digit));
printf("toupper('%c') = '%c'\n", digit, toupper(digit));
// 实际应用:忽略大小写比较
char input = 'Y';
if (toupper(input) == 'Y') {
printf("用户确认操作\n");
}
return 0;
}
运行结果
tolower('A') = 'a'
toupper('b') = 'B'
tolower('3') = '3'
toupper('3') = '3'
用户确认操作
重点说明
- 非字母字符 :如果传入的参数不是字母,函数会原样返回该字符。
- 参数类型 :虽然参数是
int类型,但实际传入char类型时会自动提升。 - 头文件 :使用前必须包含
<ctype.h>。 - 常见用途:常用于忽略大小写的字符串比较、用户输入规范化等场景。
3. strlen的使用和模拟实现
3.1 函数介绍
strlen 是C语言中用于计算字符串长度的函数,定义在 <string.h> 头文件中。
c
size_t strlen(const char *str);
函数说明
| 项目 | 说明 |
|---|---|
| 功能 | 统计参数 str 指向的字符串的长度,统计的是字符串中 '\0' 之前的字符个数 |
| 参数 | str:指向要统计长度的字符串的指针 |
| 返回值 | 返回字符串的长度,类型为 size_t(无符号整数) |
| 头文件 | <string.h> |
使用注意事项
- 结束标志 :字符串以
'\0'作为结束标志,strlen返回的是'\0'前面出现的字符个数(不包含'\0')。 - 必须包含
'\0':参数指向的字符串必须 以'\0'结束,否则函数会继续向后访问内存,导致越界访问。 - 返回值类型 :返回类型为
size_t,是无符号整数,这在比较时容易出错(见下方易错点)。
3.2 strlen返回值详解
strlen 的返回值类型 size_t 在大多数编译器中是 unsigned int 或 unsigned long 的别名。由于是无符号类型,在比较时需特别注意:
c
#include <stdio.h>
#include <string.h>
int main() {
// 正确用法
const char *str1 = "Hello";
size_t len = strlen(str1);
printf("字符串 \"%s\" 的长度为: %zu\n", str1, len); // 输出 5
// 易错点:无符号类型比较
const char *str2 = "abc";
const char *str3 = "abcdef";
// 错误比较方式
if (strlen(str2) - strlen(str3) > 0) {
printf("str2 比 str3 长(错误判断)\n");
} else {
printf("str2 比 str3 短(正确结果)\n");
}
// 解释:strlen("abc") - strlen("abcdef") = 3 - 6 = -3
// 但由于 size_t 是无符号类型,-3 会被解释为一个很大的正数
// 正确比较方式
if (strlen(str2) > strlen(str3)) {
printf("str2 比 str3 长\n");
} else {
printf("str2 比 str3 短\n");
}
return 0;
}
运行结果
字符串 "Hello" 的长度为: 5
str2 比 str3 长(错误判断)
str2 比 str3 短
3.3 strlen的模拟实现
方式一:计数器方式
c
#include <stdio.h>
#include <assert.h>
size_t my_strlen_counter(const char *str) {
assert(str != NULL); // 断言,防止传入空指针
size_t count = 0;
while (*str != '\0') {
count++;
str++;
}
return count;
}
方式二:指针减指针方式
c
#include <stdio.h>
#include <assert.h>
size_t my_strlen_pointer(const char *str) {
assert(str != NULL);
const char *start = str;
while (*str != '\0') {
str++;
}
return (size_t)(str - start); // 两个指针相减得到元素个数
}
方式三:递归方式
c
#include <stdio.h>
#include <assert.h>
size_t my_strlen_recursive(const char *str) {
assert(str != NULL);
if (*str == '\0') {
return 0;
}
return 1 + my_strlen_recursive(str + 1);
}
完整测试代码
c
#include <stdio.h>
#include <string.h>
size_t my_strlen_counter(const char *str) {
size_t count = 0;
while (*str != '\0') {
count++;
str++;
}
return count;
}
size_t my_strlen_pointer(const char *str) {
const char *start = str;
while (*str != '\0') {
str++;
}
return (size_t)(str - start);
}
size_t my_strlen_recursive(const char *str) {
if (*str == '\0') {
return 0;
}
return 1 + my_strlen_recursive(str + 1);
}
int main() {
const char *test = "Hello World!";
printf("原始字符串: \"%s\"\n", test);
printf("库函数 strlen: %zu\n", strlen(test));
printf("计数器方式: %zu\n", my_strlen_counter(test));
printf("指针减指针方式: %zu\n", my_strlen_pointer(test));
printf("递归方式: %zu\n", my_strlen_recursive(test));
// 测试空字符串
const char *empty = "";
printf("\n空字符串测试:\n");
printf("库函数 strlen: %zu\n", strlen(empty));
printf("计数器方式: %zu\n", my_strlen_counter(empty));
printf("指针减指针方式: %zu\n", my_strlen_pointer(empty));
printf("递归方式: %zu\n", my_strlen_recursive(empty));
return 0;
}
运行结果
原始字符串: "Hello World!"
库函数 strlen: 12
计数器方式: 12
指针减指针方式: 12
递归方式: 12
空字符串测试:
库函数 strlen: 0
计数器方式: 0
指针减指针方式: 0
递归方式: 0
三种实现方式对比
| 实现方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 计数器方式 | 遍历字符串,每遇到一个非'\0'字符就计数+1 |
逻辑简单直观,易于理解 | 需要额外变量记录计数 |
| 指针减指针方式 | 用结束指针减去起始指针,得到元素个数 | 代码简洁,效率高 | 需要理解指针运算 |
| 递归方式 | 递归调用,每次递归计数+1 | 代码优雅,体现递归思想 | 效率低,字符串过长可能导致栈溢出 |
重点总结
strlen不包含'\0':返回的是'\0'之前的字符个数。- 字符串必须以
'\0'结尾:否则会导致越界访问。 - 返回值是
size_t:无符号类型,比较时注意不要直接相减。 - 模拟实现:计数器方式最常用,指针减指针方式最简洁,递归方式适合理解递归思想。
4. strcpy的使用和模拟实现
strcpy 是C语言中用于字符串拷贝的函数,定义在 <string.h> 头文件中。
c
char* strcpy(char *destination, const char *source);
函数说明
| 项目 | 说明 |
|---|---|
| 功能 | 将源字符串(包括 '\0')拷贝到目标空间 |
| 参数 | destination:指向目标空间的指针;source:指向源字符串的指针 |
| 返回值 | 返回目标空间的起始地址(即 destination 的值) |
| 头文件 | <string.h> |
4.1 代码演示
c
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello World";
char dest[20] = {0}; // 初始化为全0
printf("拷贝前: dest = \"%s\"\n", dest);
// 使用 strcpy 进行字符串拷贝
char *ret = strcpy(dest, src);
printf("拷贝后: dest = \"%s\"\n", dest);
printf("返回值: ret = \"%s\"\n", ret);
// 验证返回值就是目标空间的起始地址
if (ret == dest) {
printf("返回值与目标地址相同\n");
}
return 0;
}
运行结果
拷贝前: dest = ""
拷贝后: dest = "Hello World"
返回值: ret = "Hello World"
返回值与目标地址相同
使用注意事项
- 源字符串必须以
'\0'结束 :strcpy会一直拷贝直到遇到源字符串中的'\0',如果源字符串没有'\0',会导致越界访问。 - 会将源字符串中的
'\0'拷贝到目标空间 :拷贝完成后,目标字符串也会以'\0'结尾。 - 目标空间必须足够大 :目标空间的大小必须能够容纳源字符串的全部内容(包括
'\0'),否则会导致缓冲区溢出。 - 目标空间必须可修改:目标空间不能是字符串常量或只读内存区域。
4.2 模拟实现
下面展示三种逐步优化的 strcpy 模拟实现方式:
方式一:基础版本(使用临时变量)
c
#include <assert.h>
char* my_strcpy(char* dest, const char* src) {
char* ret = dest; // 保存目标起始地址用于返回
assert(dest && src); // 断言,保证指针有效性
while (*src != '\0') { // 遍历源字符串,直到遇到 '\0'
*dest = *src; // 将源字符拷贝到目标空间
dest++; // 目标指针后移
src++; // 源指针后移
}
*dest = *src; // 拷贝源字符串末尾的 '\0'
return ret; // 返回目标空间的起始地址
}
int main() {
char arr1[] = "abcdef";
char arr2[20] = "-------------";
char* p = my_strcpy(arr2, arr1);
printf("%s\n", arr2); // 输出: abcdef
printf("%s\n", p); // 输出: abcdef
return 0;
}
代码分析:
- 使用
assert确保dest和src都不是空指针。 - 用
while循环逐个字符拷贝,直到遇到'\0'。 - 循环结束后,单独拷贝
'\0'到目标空间。 - 返回保存的目标起始地址。
方式二:简化版本(指针自增合并)
c
#include <assert.h>
char* my_strcpy(char* dest, const char* src) {
char* ret = dest; // 保存目标起始地址用于返回
assert(dest && src); // 断言,保证指针有效性
while (*src != '\0') {
*dest++ = *src++; // 将拷贝和指针自增合并为一条语句
}
*dest = *src; // 拷贝源字符串末尾的 '\0'
return ret; // 返回目标空间的起始地址
}
int main() {
char arr1[] = "abcdef";
char arr2[20] = "-------------";
char* p = my_strcpy(arr2, arr1);
printf("%s\n", arr2); // 输出: abcdef
printf("%s\n", p); // 输出: abcdef
return 0;
}
代码分析:
*dest++ = *src++等价于*dest = *src; dest++; src++;,将三条语句合并为一条,代码更简洁。- 先执行
*dest = *src完成拷贝,然后dest++和src++分别自增。
方式三:极致精简版本(利用赋值表达式的值)
c
#include <assert.h>
char* my_strcpy(char* dest, const char* src) {
char* ret = dest; // 保存目标起始地址用于返回
assert(dest && src); // 断言,保证指针有效性
while (*dest++ = *src++) { // 拷贝字符,同时判断是否为 '\0'
; // 空循环体
}
return ret; // 返回目标空间的起始地址
}
int main() {
char arr1[] = "abcdef";
char arr2[20] = "-------------";
char* p = my_strcpy(arr2, arr1);
printf("%s\n", arr2); // 输出: abcdef
printf("%s\n", p); // 输出: abcdef
return 0;
}
代码分析:
- 这是最经典的
strcpy模拟实现方式,代码极其简洁。 *dest++ = *src++先执行赋值操作,然后返回赋值的结果(即拷贝的字符)。- 当拷贝到
'\0'时,赋值表达式的结果为0(假),循环结束。 - 关键点 :
'\0'已经被拷贝到目标空间,因此不需要再单独拷贝'\0'。 - 空循环体
;表示循环条件中已经完成了所有工作。
代码结束后 '\0' 的位置详解(以方式三为例)
假设 arr1(源字符串)的内存布局为:'a' 'b' 'c' 'd' 'e' 'f' '\0',arr2(目标空间)初始化为:'-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '\0'(共20个字符,最后一个为 '\0')。
逐步执行过程
c
while (*dest++ = *src++) {
;
}
逐次执行过程如下:
| 循环次数 | *src (赋值前) |
*dest = *src (赋值后) |
dest 自增后指向 |
src 自增后指向 |
赋值结果(判断条件) |
|---|---|---|---|---|---|
| 第1次 | 'a' |
dest[0] = 'a' |
指向 arr2 第2个元素 |
指向 'b' |
'a' (非0,继续) |
| 第2次 | 'b' |
dest[1] = 'b' |
指向 arr2 第3个元素 |
指向 'c' |
'b' (非0,继续) |
| 第3次 | 'c' |
dest[2] = 'c' |
指向 arr2 第4个元素 |
指向 'd' |
'c' (非0,继续) |
| 第4次 | 'd' |
dest[3] = 'd' |
指向 arr2 第5个元素 |
指向 'e' |
'd' (非0,继续) |
| 第5次 | 'e' |
dest[4] = 'e' |
指向 arr2 第6个元素 |
指向 'f' |
'e' (非0,继续) |
| 第6次 | 'f' |
dest[5] = 'f' |
指向 arr2 第7个元素 |
指向 '\0' |
'f' (非0,继续) |
| 第7次 | '\0' |
dest[6] = '\0' |
指向 arr2 第8个元素 |
指向 src 末尾之后 |
'\0' (0,循环结束) |
内存布局变化
拷贝前:
arr2: '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '\0'
↑dest (起始位置)
arr1: 'a' 'b' 'c' 'd' 'e' 'f' '\0'
↑src (起始位置)
拷贝后:
arr2: 'a' 'b' 'c' 'd' 'e' 'f' '\0' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '-' '\0'
↑ret (返回地址) ↑dest (循环结束后的位置)
arr1: 'a' 'b' 'c' 'd' 'e' 'f' '\0'
↑src (循环结束后的位置)
关键结论
-
源字符串的
'\0'被拷贝到了目标空间 :在第7次循环中,*dest++ = *src++将源字符串末尾的'\0'写入了arr2[6]位置。这是strcpy与strncpy的重要区别------strcpy一定会拷贝'\0'。 -
目标空间原有的
'\0'未被覆盖 :arr2初始化为"-------------"(13个'-'加一个'\0'),拷贝后arr2[0]~arr2[5]被'a'~'f'覆盖,arr2[6]被'\0'覆盖,arr2[7]~arr2[19]保持不变(其中arr2[19]仍然是初始的'\0')。但printf("%s", arr2)只打印到第一个'\0'(即arr2[6]),所以输出为"abcdef"。 -
dest指针的最终位置 :循环结束后,dest指向arr2[7](即新'\0'之后的下一个位置),而ret始终指向arr2[0](起始地址)。 -
方式一和方式二中的
'\0'处理 :在方式一和方式二中,循环结束后通过*dest = *src单独拷贝'\0',效果与方式三完全相同------源字符串的'\0'被写入目标空间。
三种实现方式对比
| 实现方式 | 代码行数 | 特点 | 适用场景 |
|---|---|---|---|
| 方式一 | 较多 | 逻辑清晰,每一步都明确分开 | 初学者理解,教学演示 |
| 方式二 | 中等 | 代码简洁,指针自增合并 | 日常开发,兼顾可读性和简洁性 |
| 方式三 | 最少 | 极致精简,利用赋值表达式返回值 | 高手写法,追求代码极致简洁 |
重点总结
strcpy会拷贝'\0':源字符串末尾的'\0'也会被拷贝到目标空间,这是确保目标字符串正确结束的关键。- 目标空间必须足够大 :这是使用
strcpy最容易出错的地方,可能导致缓冲区溢出。 - 返回值是目标地址 :返回
destination的原始值,支持链式调用。 - 模拟实现的核心 :逐个字符拷贝,直到遇到
'\0'为止,并且'\0'也会被拷贝。 - 最经典的实现 :
while (*dest++ = *src++);是C语言中广为流传的精简写法。
5. strcat的使用和模拟实现
strcat 是C语言中用于字符串追加(拼接)的函数,定义在 <string.h> 头文件中。
c
char *strcat(char *destination, const char *source);
函数说明
| 项目 | 说明 |
|---|---|
| 功能 | 将 source 指向的源字符串中的所有字符(包括 '\0')追加到 destination 指向的目标字符串的末尾 |
| 参数 | destination:指向目标空间的指针(必须已有字符串内容);source:指向源字符串的指针 |
| 返回值 | 返回目标空间的起始地址(即 destination 的值) |
| 头文件 | <string.h> |
5.1 代码演示
c
#include <stdio.h>
#include <string.h>
int main() {
char dest[20] = "hello "; // 目标空间,已有字符串 "hello "
char src[] = "world"; // 源字符串
printf("追加前: dest = \"%s\"\n", dest);
// 使用 strcat 进行字符串追加
char *ret = strcat(dest, src);
printf("追加后: dest = \"%s\"\n", dest);
printf("返回值: ret = \"%s\"\n", ret);
// 验证返回值就是目标空间的起始地址
if (ret == dest) {
printf("返回值与目标地址相同\n");
}
return 0;
}
运行结果
追加前: dest = "hello "
追加后: dest = "hello world"
返回值: ret = "hello world"
返回值与目标地址相同
使用注意事项
- 源字符串必须以
'\0'结束 :strcat会一直拷贝直到遇到源字符串中的'\0'。 - 目标字符串中也必须有
'\0':strcat需要找到目标字符串中的'\0'来确定追加的起始位置,否则无法知道从哪里开始追加。 - 目标空间必须足够大 :目标空间的大小必须能够容纳原有字符串 + 源字符串的全部内容(包括
'\0'),否则会导致缓冲区溢出。 - 目标空间必须可修改:目标空间不能是字符串常量或只读内存区域。
- 内存重叠问题:源字符串和目标空间不能有重叠,否则行为是未定义的。
5.2 模拟实现
c
#include <stdio.h>
#include <assert.h>
char* my_strcat(char* dest, const char* src) {
char* ret = dest; // 保存目标起始地址用于返回
assert(dest && src); // 断言,保证指针有效性
// 第一步:找到目标字符串的末尾(即 '\0' 的位置)
while (*dest) { // 等价于 while (*dest != '\0')
dest++;
}
// 此时 dest 指向目标字符串末尾的 '\0'
// 第二步:将源字符串拷贝到目标末尾(包括 '\0')
while (*dest++ = *src++) { // 拷贝字符,同时判断是否为 '\0'
; // 空循环体
}
// 循环结束时,源字符串的 '\0' 已经被拷贝到目标空间
return ret; // 返回目标空间的起始地址
}
int main() {
char arr1[20] = "hello"; // 目标空间,内容为 "hello\0"
char arr2[] = "world"; // 源字符串,内容为 "world\0"
char* p = my_strcat(arr1, arr2);
printf("%s\n", arr1); // 输出: helloworld
printf("%s\n", p); // 输出: helloworld
return 0;
}
代码逐步分析
假设 arr1 的内存布局为:'h' 'e' 'l' 'l' 'o' '\0' ...(剩余空间未初始化),arr2 的内存布局为:'w' 'o' 'r' 'l' 'd' '\0'。
第一步:找 '\0'
c
while (*dest) { // *dest 依次为 'h','e','l','l','o',都不为0,继续循环
dest++; // dest 指针依次后移
}
// 当 *dest 指向 '\0' 时,条件为假,循环结束
// 此时 dest 指向 arr1 中 'o' 后面的 '\0' 位置
第二步:数据拷贝
c
while (*dest++ = *src++) { // 先赋值,再自增,然后判断赋值结果
;
}
逐次执行过程如下:
| 循环次数 | *src (赋值前) |
*dest = *src (赋值后) |
dest 自增后指向 |
src 自增后指向 |
赋值结果(判断条件) |
|---|---|---|---|---|---|
| 第1次 | 'w' |
dest[0] = 'w' |
指向下一个位置 | 指向 'o' |
'w' (非0,继续) |
| 第2次 | 'o' |
dest[1] = 'o' |
指向下一个位置 | 指向 'r' |
'o' (非0,继续) |
| 第3次 | 'r' |
dest[2] = 'r' |
指向下一个位置 | 指向 'l' |
'r' (非0,继续) |
| 第4次 | 'l' |
dest[3] = 'l' |
指向下一个位置 | 指向 'd' |
'l' (非0,继续) |
| 第5次 | 'd' |
dest[4] = 'd' |
指向下一个位置 | 指向 '\0' |
'd' (非0,继续) |
| 第6次 | '\0' |
dest[5] = '\0' |
指向下一个位置 | 指向 src 末尾之后 |
'\0' (0,循环结束) |
代码结束后 '\0' 的位置详解
关键问题:代码结束后,'\0' 在哪里?是否被取代了?
-
目标字符串原有的
'\0'被覆盖了 :在第一步中,dest指针移动到了"hello"末尾的'\0'位置。在第二步的第1次循环中,*dest++ = *src++将'w'写入了这个位置,原有的'\0'被'w'覆盖。 -
新的
'\0'被追加到了末尾 :在第二步的第6次循环中,*dest++ = *src++将源字符串末尾的'\0'拷贝到了"helloworld"之后的位置。此时dest指向的是"helloworld"之后的下一个位置(即新'\0'之后的位置)。 -
最终内存布局:
追加前: arr1 = 'h' 'e' 'l' 'l' 'o' '\0' [未定义] [未定义] ... 追加后: arr1 = 'h' 'e' 'l' 'l' 'o' 'w' 'o' 'r' 'l' 'd' '\0' [未定义] ...- 原有的
'\0'(第6个元素)被'w'取代。 - 新的
'\0'被追加到了第11个元素的位置("helloworld"共10个字符,第11个位置是新的'\0')。
- 原有的
-
结论 :原有的
'\0'被源字符串的第一个字符取代 ,新的'\0'由源字符串末尾的'\0'提供 ,被追加到拼接后字符串的末尾。目标字符串始终以'\0'结尾,但'\0'的位置发生了变化。
重点总结
strcat会覆盖目标字符串的'\0':目标字符串末尾的'\0'会被源字符串的第一个字符覆盖。strcat会拷贝源字符串的'\0':源字符串末尾的'\0'会被拷贝到拼接后字符串的末尾,确保结果字符串以'\0'结尾。- 目标空间必须足够大 :这是使用
strcat最容易出错的地方,可能导致缓冲区溢出。 - 目标字符串必须有
'\0':否则strcat无法找到追加的起始位置。 - 模拟实现的核心 :先找到目标字符串的
'\0',然后从该位置开始拷贝源字符串(包括'\0')。
6. strcmp的使用和模拟实现
strcmp 是C语言中用于比较两个字符串的函数,定义在 <string.h> 头文件中。
c
int strcmp(const char *str1, const char *str2);
函数说明
| 项目 | 说明 |
|---|---|
| 功能 | 比较 str1 和 str2 指向的两个字符串,从第一个字符开始逐字符比较其ASCII码值,直到遇到不相等的字符或遇到 '\0' 为止 |
| 参数 | str1:指向要比较的第一个字符串的指针;str2:指向要比较的第二个字符串的指针 |
| 返回值 | 第一个字符串大于第二个字符串,返回大于0 的数字;相等则返回 0 ;第一个字符串小于第二个字符串,返回小于0的数字 |
| 头文件 | <string.h> |
返回值详解
strcmp 的返回值遵循以下规则:
str1 > str2:返回一个大于0的整数(不一定是1,不同编译器实现可能不同)。str1 == str2:返回 0。str1 < str2:返回一个小于0的整数(不一定是-1)。
注意 :标准只规定返回正数、0或负数,并没有规定具体数值。因此,不要 用
if (strcmp(s1, s2) == 1)来判断大于,而应该用if (strcmp(s1, s2) > 0)。
比较规则
- 逐字符比较:从两个字符串的第一个字符开始,比较它们的ASCII码值。
- 遇到不相等则停止:一旦找到ASCII码值不相等的两个字符,比较结束,根据这两个字符的ASCII码值大小决定返回值。
- 遇到
'\0'则停止 :如果一直比较到某个字符串的末尾(遇到'\0')都没有发现不相等的字符,则较短的字符串被视为"小于"较长的字符串。 - 完全相等:只有当两个字符串长度相等且每个对应位置的字符都相同时,才返回0。
代码演示
c
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "abcdef";
char str2[] = "abcdef"; // 与str1完全相同
char str3[] = "abcdeg"; // 第6个字符 'f' vs 'g'
char str4[] = "abc"; // 较短字符串
char str5[] = "abz"; // 第3个字符 'c' vs 'z'
// 情况1:完全相等
int result1 = strcmp(str1, str2);
printf("strcmp(\"%s\", \"%s\") = %d\n", str1, str2, result1);
// 输出: 0(相等)
// 情况2:第6个字符不同('f' < 'g')
int result2 = strcmp(str1, str3);
printf("strcmp(\"%s\", \"%s\") = %d\n", str1, str3, result2);
// 输出: 负数('f'的ASCII码102 < 'g'的ASCII码103)
// 情况3:str1比str4长(str4先遇到'\0')
int result3 = strcmp(str1, str4);
printf("strcmp(\"%s\", \"%s\") = %d\n", str1, str4, result3);
// 输出: 正数('d'的ASCII码100 > '\0'的ASCII码0)
// 情况4:第3个字符不同('c' < 'z')
int result4 = strcmp(str4, str5);
printf("strcmp(\"%s\", \"%s\") = %d\n", str4, str5, result4);
// 输出: 负数('c'的ASCII码99 < 'z'的ASCII码122)
// 正确判断方式:使用 >0、==0、<0
if (result1 == 0) {
printf("\"%s\" 等于 \"%s\"\n", str1, str2);
}
if (result2 < 0) {
printf("\"%s\" 小于 \"%s\"\n", str1, str3);
}
if (result3 > 0) {
printf("\"%s\" 大于 \"%s\"\n", str1, str4);
}
return 0;
}
运行结果
strcmp("abcdef", "abcdef") = 0
strcmp("abcdef", "abcdeg") = -1
strcmp("abcdef", "abc") = 100
strcmp("abc", "abz") = -23
"abcdef" 等于 "abcdef"
"abcdef" 小于 "abcdeg"
"abcdef" 大于 "abc"
使用注意事项
- 不要直接比较返回值是否为1或-1:标准只规定返回正数、0或负数,不同编译器实现可能返回不同的具体数值(如GCC可能返回ASCII码差值,而MSVC可能返回1或-1)。
- 字符串必须以
'\0'结尾 :否则strcmp会继续向后访问内存,导致越界访问。 - 区分大小写 :
strcmp是区分大小写的,'A'(ASCII 65)和'a'(ASCII 97)被视为不同的字符。 - 不要用
==直接比较字符串 :if (str1 == str2)比较的是指针地址,而不是字符串内容,必须使用strcmp。
6.1 模拟实现
下面展示两种 strcmp 的模拟实现方式:
方式一:使用 if-else 判断返回值
c
#include <stdio.h>
#include <assert.h>
int my_strcmp(const char* s1, const char* s2) {
assert(s1 && s2); // 断言,保证指针有效性
while (*s1 == *s2) { // 当两个字符相等时继续循环
if (*s1 == '\0') { // 如果遇到字符串末尾的 '\0'
return 0; // 说明两个字符串完全相等,返回0
}
s1++; // 指针后移,比较下一个字符
s2++;
}
// 循环结束说明找到了不相等的字符
if (*s1 > *s2) {
return 1; // s1的字符ASCII码更大,返回正数
} else {
return -1; // s1的字符ASCII码更小,返回负数
}
}
int main() {
char arr1[] = "abcdef";
char arr2[] = "acq";
int r = my_strcmp(arr1, arr2);
if (r > 0)
printf(">\n");
else if (r < 0)
printf("<\n");
else
printf("==\n");
return 0;
}
代码分析:
assert(s1 && s2):确保传入的指针都不是空指针,防止程序崩溃。while (*s1 == *s2):当两个字符相等时,继续循环比较下一个字符。if (*s1 == '\0') return 0;:如果在循环中遇到了'\0',说明两个字符串同时到达末尾且之前所有字符都相等,因此返回0表示相等。s1++; s2++;:指针后移,准备比较下一对字符。- 循环结束后的判断 :当
*s1 != *s2时退出循环,此时根据两个字符的ASCII码大小关系返回1或-1。
执行过程分析 (以 arr1 = "abcdef" 和 arr2 = "acq" 为例):
| 循环次数 | *s1 |
*s2 |
*s1 == *s2 |
操作 | 说明 |
|---|---|---|---|---|---|
| 第1次 | 'a' (97) |
'a' (97) |
相等 | s1++, s2++ |
继续比较 |
| 第2次 | 'b' (98) |
'c' (99) |
不相等 | 退出循环 | 找到第一个不相等的字符 |
退出循环后:
*s1 = 'b'(ASCII 98)*s2 = 'c'(ASCII 99)*s1 > *s2为假,执行else分支,返回-1- 因此
"abcdef" < "acq",输出<
方式二:使用指针相减(更接近库函数实现)
c
#include <stdio.h>
#include <assert.h>
int my_strcmp(const char* s1, const char* s2) {
assert(s1 && s2); // 断言,保证指针有效性
while (*s1 == *s2) { // 当两个字符相等时继续循环
if (*s1 == '\0') { // 如果遇到字符串末尾的 '\0'
return 0; // 说明两个字符串完全相等,返回0
}
s1++; // 指针后移,比较下一个字符
s2++;
}
// 返回两个字符的ASCII码差值
return *s1 - *s2; // 正数表示s1>s2,负数表示s1<s2,0表示相等
}
int main() {
char arr1[] = "abcdef";
char arr2[] = "acq";
int r = my_strcmp(arr1, arr2);
if (r > 0)
printf(">\n");
else if (r < 0)
printf("<\n");
else
printf("==\n");
return 0;
}
代码分析:
-
return *s1 - *s2;:这是方式一与方式二的核心区别。直接返回两个字符的ASCII码差值,而不是固定返回1或-1。- 如果
*s1 > *s2,差值为正数(如'b' - 'c' = 98 - 99 = -1,但这里'b' < 'c'所以是负数)。 - 如果
*s1 < *s2,差值为负数。 - 这种方式更接近标准库的实现,返回值能反映字符差异的大小。
- 如果
-
与方式一的区别:
- 方式一固定返回
1或-1,只表示大小关系,不反映差异程度。 - 方式二返回ASCII码差值,如
'b' - 'c' = -1,'d' - 'a' = 3,能反映字符差异的大小。
- 方式一固定返回
执行过程分析 (以 arr1 = "abcdef" 和 arr2 = "acq" 为例):
| 循环次数 | *s1 |
*s2 |
*s1 == *s2 |
操作 | 说明 |
|---|---|---|---|---|---|
| 第1次 | 'a' (97) |
'a' (97) |
相等 | s1++, s2++ |
继续比较 |
| 第2次 | 'b' (98) |
'c' (99) |
不相等 | 退出循环 | 找到第一个不相等的字符 |
退出循环后:
*s1 = 'b'(ASCII 98)*s2 = 'c'(ASCII 99)*s1 - *s2 = 98 - 99 = -1- 返回
-1,表示"abcdef" < "acq"
两种实现方式对比
| 实现方式 | 返回值特点 | 优点 | 缺点 |
|---|---|---|---|
| 方式一(if-else) | 固定返回1或-1 | 返回值固定,易于理解 | 丢失了字符差异信息 |
| 方式二(指针相减) | 返回ASCII码差值 | 更接近标准库实现,保留差异信息 | 返回值不固定,初学者可能困惑 |
重点总结
strcmp逐字符比较ASCII码值 :从第一个字符开始,直到遇到不相等的字符或'\0'。- 返回值规则 :大于返回正数,等于返回0,小于返回负数。不要 用
== 1或== -1来判断。 - 字符串必须以
'\0'结尾:否则会导致越界访问。 - 区分大小写:大写字母和小写字母的ASCII码值不同。
- 模拟实现的核心 :在
while循环中逐字符比较,遇到不相等或'\0'时返回结果。 - 最经典的实现 :
return *s1 - *s2;是C语言中广为流传的精简写法,直接利用ASCII码差值作为返回值。
7. strncpy函数详解
strncpy函数是C语言标准库中用于字符串拷贝的函数,它允许指定最大拷贝字符数,从而提供比strcpy函数更高的安全性。下面我将详细解释其功能、用法,并重点比较它与strcpy的区别,使用C语言代码示例演示不同场景下\0(字符串终止符)的位置变化。
7.1 strncpy函数介绍
函数原型:char *strncpy(char *destination, const char *source, size_t num);
- 功能 :将
source指向的字符串拷贝到destination指向的空间中,最多拷贝num个字符。 - 参数 :
destination:指向目标空间的指针。source:指向源字符串的指针。num:最大拷贝字符数(类型为size_t,通常是无符号整数)。
- 返回值 :返回
destination的起始地址。 - 行为 :
- 如果
source字符串的长度小于num,则拷贝整个字符串(包括\0),并用\0填充剩余空间。 - 如果
source字符串的长度大于或等于num,则只拷贝前num个字符,且不会自动添加\0(需要手动处理)。 - 源字符串不一定需要以
\0结尾,但拷贝后目标字符串可能不完整。
- 如果
7.2 比较strcpy和strncpy函数
-
strcpy函数:
- 功能:拷贝整个字符串直到遇到
\0。 - 风险:如果目标空间不足,会导致缓冲区溢出(越界),引发未定义行为(如程序崩溃或安全漏洞)。
- 示例:
char dest[5]; strcpy(dest, "HelloWorld");会溢出,因为源字符串长度10 > 目标空间5。 - 重点:不安全,因为它不检查目标空间大小。
- 功能:拷贝整个字符串直到遇到
-
strncpy函数:
- 功能:指定最大拷贝长度
num,限制拷贝字符数。 - 优势:更安全,因为开发者必须显式指定
num,强制考虑目标空间大小。 - 行为:
- 如果
num小于源字符串长度,拷贝num个字符,但不添加\0(目标字符串可能未终止)。 - 如果
num大于源字符串长度,拷贝整个字符串并填充\0到剩余空间。
- 如果
- 重点:避免了盲目溢出,但需要手动处理
\0以确保字符串正确终止。
- 功能:指定最大拷贝长度
核心区别 :strcpy依赖于源字符串的\0,易导致溢出;strncpy通过num参数控制拷贝长度,减少了溢出风险,但增加了对\0的管理需求。
7.3 C语言代码示例
下面通过代码演示strncpy的使用,并列举两种场景:当拷贝字符数超过源字符串长度时(num > 源长度),和没有超过时(num <= 源长度)。重点展示\0的位置。
c
#include <stdio.h>
#include <string.h>
int main() {
// 场景1: num 小于源字符串长度 (没有超过)
char source1[] = "HelloWorld"; // 长度10 (包括\0)
char dest1[10]; // 目标空间大小10
size_t num1 = 5; // 只拷贝5个字符
strncpy(dest1, source1, num1);
// 手动添加\0以确保字符串终止
dest1[num1] = '\0';
printf("场景1 (num <= 源长度):\n");
printf("源字符串: %s\n", source1);
printf("目标字符串: %s\n", dest1); // 输出 "Hello"
// \0位置: 在dest1[5]处 (索引从0开始)
// 场景2: num 大于源字符串长度 (超过)
char source2[] = "ABC"; // 长度4 (包括\0)
char dest2[10]; // 目标空间大小10
size_t num2 = 8; // 拷贝8个字符
strncpy(dest2, source2, num2);
// 不需要手动添加\0,因为strncpy会自动填充
printf("\n场景2 (num > 源长度):\n");
printf("源字符串: %s\n", source2);
printf("目标字符串: %s\n", dest2); // 输出 "ABC"
// \0位置: 在dest2[3]处(源字符串结束),剩余空间填充\0(dest2[4]到dest2[7]为\0)
return 0;
}
代码解释:
-
场景1 (num <= 源长度):
num1 = 5,源字符串长度10。- strncpy拷贝前5个字符('H','e','l','l','o')到
dest1。 - \0位置 :拷贝后
dest1没有自动添加\0(因为num <= 源长度),所以手动在dest1[5]添加\0来终止字符串。输出为"Hello"。 - 重点:如果不手动添加
\0,dest1可能不是有效字符串,导致后续操作出错。
-
场景2 (num > 源长度):
num2 = 8,源字符串长度4(包括\0)。- strncpy拷贝整个源字符串('A','B','C','\0')到
dest2。 - \0位置 :拷贝后
dest2[3]是源字符串的\0,strncpy自动用\0填充剩余空间(dest2[4]到dest2[7])。输出为"ABC"。 - 重点:strncpy确保目标字符串以
\0终止,并填充剩余空间。
7.4 安全考虑总结
- 为什么strncpy更安全 :通过指定
num,开发者必须评估目标空间大小(例如,num应小于或等于destination的大小)。这减少了缓冲区溢出的风险。 - 使用建议 :
- 总是确保
num不大于destination的空间大小。 - 在
num小于源长度时,手动添加\0(如destination[num] = '\0';)。 - 优先使用strncpy替代strcpy,尤其在处理用户输入或未知长度数据时。
- 总是确保
- 风险提示 :如果
num设置不当(如大于目标空间),strncpy仍可能溢出;因此,结合sizeof(destination)来计算num是最佳实践。
strncpy函数在字符串操作中提供了更可控的拷贝机制,通过理解其行为和正确处理\0,可以显著提升代码安全性。
8. strncat函数的使⽤
strncat 函数详解
功能: 将源字符串 (source) 的最多 num 个字符 (包括 \0 之前的字符)追加 到目标字符串 (destination) 的末尾(覆盖目标字符串原有的终止符 \0),并在追加后的新字符串末尾自动添加一个新的终止符 \0。
函数原型:
c
char * strncat ( char * destination, const char * source, size_t num );
参数:
destination: 指向目标字符串(必须足够大以容纳追加后的内容,包括新的\0)。source: 指向要追加的源字符串。num: 指定要从源字符串source中最多 追加的字符数(不包括\0)。
返回值: 返回指向目标字符串 destination 的指针(即追加后的新字符串的起始地址)。
strcat vs strncat 核心区别
-
参数差异:
strcat只有两个参数:destination和source。strncat有三个参数:destination、source和num。这个num参数是关键。
-
追加行为差异:
strcat: 它会一直追加 源字符串source的内容,直到遇到源字符串中的\0为止 。这意味着它会将源字符串的整个内容 (包括其\0)都复制过去(覆盖目标原有的\0),并在新的结尾处自动添加一个新的\0。它完全依赖源字符串以\0结尾。strncat: 它只追加源字符串source中最多num个字符 (不包括\0)。无论源字符串中是否有\0,或者\0在什么位置,它都只追加num个字符(或更少,如果源字符串长度小于num)。然后,它总是在最终结果的末尾添加一个新的\0。它不要求源字符串必须以\0结尾 (但源字符串通常应该以\0结尾)。
-
安全性与灵活性:
strcat是不安全的 。如果源字符串很长或者目标字符串空间不足,strcat会一直复制直到遇到\0,这很容易导致缓冲区溢出(Buffer Overflow),覆盖目标缓冲区之后的内存,引发程序崩溃或安全漏洞。strncat是更安全的 。通过num参数,开发者可以显式控制 最多追加多少字符,从而避免缓冲区溢出 的风险。开发者需要确保destination有足够的剩余空间容纳追加的num个字符加上 新添加的\0(即destination剩余空间至少为num + 1)。同时,strncat也更灵活,因为它可以处理不以\0结尾的字符数组(尽管这不是标准用法)。
\0 的位置处理详解
strncat 函数在追加完成后,总是在 新字符串的末尾添加一个新的 \0 。源字符串中可能存在的 \0 会被当作普通字符处理(如果它出现在前 num 个字符内)。
具体分两种情况讨论:
情况一:num 小于或等于 source 中 \0 之前的有效字符数
strncat会精确地复制源字符串source中的num个字符(不包括\0)到destination原有\0的位置(覆盖这个\0)。- 然后,在追加的
num个字符之后,立即添加一个新的\0。 \0的位置: 在目标字符串destination原有内容的末尾偏移num个字符处。
示例代码:
c
#include <stdio.h>
#include <string.h>
int main() {
char dest[20] = "Hello"; // dest: ['H','e','l','l','o','\0', ...]
char src[] = "World!"; // src: ['W','o','r','l','d','!','\0']
strncat(dest, src, 3); // 只追加 "Wor",共3个字符
// 结果:dest 变成 "HelloWor"
// 内存布局:['H','e','l','l','o','W','o','r','\0', ...]
// 新添加的 \0 在 'r' 后面
printf("Result: %s\n", dest); // 输出: HelloWor
return 0;
}
情况二:num 大于 source 中 \0 之前的有效字符数
strncat会复制源字符串source中的所有字符,直到遇到源字符串中的\0(即使num指定得更大,复制也会在遇到\0时停止)。- 然后,在源字符串的
\0之后(即目标字符串原有内容的末尾加上源字符串所有有效字符之后),立即添加一个新的\0。 \0的位置: 在目标字符串destination原有内容的末尾偏移strlen(source)个字符处(因为复制了源的所有有效字符)。
示例代码:
c
#include <stdio.h>
#include <string.h>
int main() {
char dest[20] = "Hello"; // dest: ['H','e','l','l','o','\0', ...]
char src[] = "World!"; // src: ['W','o','r','l','d','!','\0']
strncat(dest, src, 10); // 想追加10个字符,但 src 只有6个有效字符 + \0
// 实际行为:复制 src 的所有字符直到遇到 \0 ("World!"),共6个字符。
// 结果:dest 变成 "HelloWorld!"
// 内存布局:['H','e','l','l','o','W','o','r','l','d','!','\0', ...]
// 新添加的 \0 在 '!' 后面
printf("Result: %s\n", dest); // 输出: HelloWorld!
return 0;
}
关键点总结
num的含义: 它限制了从source中最多 复制的非\0字符数。\0的添加:strncat总是在 最终拼接后的字符串末尾添加一个新的\0。这个行为是固定的。- 源字符串的
\0:- 如果
num小于源字符串有效长度,源字符串中的\0不会被复制(它可能还在源字符串里,但不会被strncat处理)。 - 如果
num大于源字符串有效长度,复制会在遇到源字符串的\0时停止,并且这个源\0本身不会被复制到目标。
- 如果
- 安全性: 使用
strncat时,程序员必须 确保目标缓冲区destination有足够的剩余空间 。所需空间至少是:strlen(destination) + num + 1(原有长度 + 最多追加的字符数 + 1 给新的\0)。这是防止缓冲区溢出的关键。 - 灵活性:
strncat可以安全地用于处理固定长度的字符数组(即使它们内部没有\0),只要num不超过数组长度。而strcat必须要求源字符串以\0结尾。
安全使用 strncat 的示例
c
#include <stdio.h>
#include <string.h>
int main() {
// 目标缓冲区,初始大小为 10
char dest[10] = "Dest"; // 当前长度 4 (strlen("Dest")=4)
char src[] = "SourceString"; // 长度 12
// 计算 dest 剩余空间 (sizeof(dest) - strlen(dest) - 1)
// sizeof(dest)=10, strlen(dest)=4, 剩余空间为 10 - 4 - 1 = 5
// 这5个位置:1个给新 \0,剩下4个可以追加字符
size_t available = sizeof(dest) - strlen(dest) - 1;
// 安全追加:最多追加 available 个字符 (即4个)
strncat(dest, src, available);
// 结果:dest 变成 "DestSour" (4 + 4 = 8字符,加上末尾 \0)
// 内存布局: ['D','e','s','t','S','o','u','r','\0', 剩余位置]
printf("Result: %s\n", dest); // 输出: DestSour
return 0;
}
在这个例子中,通过计算 available,我们确保了 strncat 不会超出 dest 的边界,从而避免了缓冲区溢出。
9. strncmp函数的使⽤
strncmp函数的使用详解
strncmp函数是C语言标准库中的一个字符串比较函数,用于比较两个字符串的内容,但可以指定最多比较的字符数(num)。这使其比strcmp函数更灵活和安全,特别是在处理可能不完整的字符串或避免缓冲区溢出时。下面我将逐步解释其功能、参数、返回值,并与strcmp进行对比,最后通过代码示例和表格展示不同场景。
9.1 strncmp函数的功能和参数
- 函数原型 :
int strncmp(const char *str1, const char *str2, size_t num);str1和str2:指向要比较的两个字符串的指针。num:指定最多比较的字符个数(size_t类型,通常为无符号整数)。
- 功能:逐字符比较str1和str2指向的字符串,但最多只比较num个字符。如果遇到'\0'(字符串结束符)或达到num个字符,则停止比较。
- 返回值 :
- 如果str1大于str2(按字典顺序),则返回一个大于0的数字(例如,1)。
- 如果str1等于str2,则返回0。
- 如果str1小于str2,则返回一个小于0的数字(例如,-1)。
注意:返回值的具体数值由实现定义,但符号表示比较结果(>0>0>0、=0=0=0 或 <0<0<0)。
9.2 strncmp与strcmp的比较
- strcmp函数 :
int strcmp(const char *str1, const char *str2);- 比较整个字符串,直到遇到'\0'结束符。如果字符串较长,它会一直比较下去,可能导致不安全的行为(如访问无效内存)。
- 关键区别 :
- 参数不同:strncmp多了一个num参数,允许控制比较的最大长度。
- 灵活性:strncmp可以比较任意长度的子串,而不受字符串实际长度的限制。
- 安全性:strncmp通过限制比较长度,减少了缓冲区溢出的风险(例如,当字符串未正确以'\0'结尾时)。strcmp在遇到非终止字符串时可能引发未定义行为。
9.3 strncmp的优势
- 更安全:指定num参数可以防止比较超出字符串的有效范围,避免潜在的安全漏洞。
- 更灵活:适用于比较部分字符串,例如在解析固定长度数据或处理网络协议时。
9.4 代码示例
下面是一个简单的C程序,演示strncmp的使用。代码包括比较不同字符串的场景。
c
#include <stdio.h>
#include <string.h>
int main() {
const char *str1 = "hello";
const char *str2 = "world";
const char *str3 = "hello";
const char *str4 = "he";
// 场景1:比较整个字符串(num超过实际长度)
int result1 = strncmp(str1, str3, 10); // num=10,但字符串只有5字符
printf("比较 'hello' 和 'hello',num=10: 返回值 = %d\n", result1); // 输出: 0(相等)
// 场景2:比较部分字符串(num不超过实际长度)
int result2 = strncmp(str1, str2, 3); // 比较前3字符: "hel" vs "wor"
printf("比较 'hello' 和 'world',num=3: 返回值 = %d\n", result2); // 输出: 负数('e' < 'o'? 实际依赖ASCII)
// 场景3:比较到'\0'位置
int result3 = strncmp(str1, str4, 3); // 比较前3字符: "hel" vs "he"(str4较短)
printf("比较 'hello' 和 'he',num=3: 返回值 = %d\n", result3); // 输出: 正数('l' > '\0'?)
return 0;
}
代码解释:
- 场景1:num=10大于字符串长度(5),比较在'\0'处停止,返回0表示相等。
- 场景2:num=3小于字符串长度,比较前3个字符,由于"hel"和"wor"不同('h' vs 'w'),返回负数。
- 场景3 :num=3,但str4只有2个字符(以'\0'结尾),比较在str4的'\0'处停止,str1的第三个字符'l'大于'\0',返回正数。
注意:返回值的具体数值可能因编译器而异,但符号表示顺序。
9.5 不同场景下的行为表格
以下表格展示了strncmp在不同情况下的行为,重点关注当比较长度(num)是否超过或不超过字符串的'\0'位置。'\0'位置指字符串的结束符位置。
| 情况描述 | str1内容 | str2内容 | num值 | 返回值 | 解释 |
|---|---|---|---|---|---|
| num 超过 '\0'位置(即num > 字符串长度) | "abc"(长度3) | "abc"(长度3) | 5 | 0 | 比较最多5字符,但实际字符串以'\0'结束在位置3。比较在'\0'处停止,内容相同,返回0。 |
| num 不超过 '\0'位置(即num ≤ 字符串长度) | "apple"(长度5) | "app"(长度3) | 3 | 0 | 比较前3字符("app" vs "app"),内容相同,返回0。即使str2较短,比较在num=3处完成。 |
| num 不超过 '\0'位置,但内容不同 | "abc"(长度3) | "abd"(长度3) | 3 | 负数(例如-1) | 比较前3字符:'a'='a', 'b'='b', 'c'<'d'(ASCII中99<100),所以返回小于0的数字。 |
| num 超过 '\0'位置,且str2较短 | "hello"(长度5) | "he"(长度2) | 4 | 正数(例如1) | 比较最多4字符。str2在位置2有'\0',比较在str2的'\0'处停止。str1的第三个字符'l' > '\0'(因为'\0'的ASCII为0),所以返回大于0的数字。 |
表格说明:
- 在"num超过'\0'位置"时,比较提前在第一个'\0'处停止,忽略多余的num。
- 在"num不超过'\0'位置"时,比较严格按num个字符进行,即使字符串更长。
- 返回值基于字符的ASCII值比较:如果字符不同,较早的不等字符决定结果(c1−c2c1 - c2c1−c2,其中c1c1c1和c2c2c2是字符的整数值)。
总结
strncmp函数通过引入num参数,提供了比strcmp更高的灵活性和安全性。它允许精确控制比较长度,避免了潜在的内存访问错误。在实际编程中,推荐使用strncmp来处理不确定长度的字符串,特别是在安全敏感的场合。记住:总是检查num值以确保它不超过缓冲区大小,并结合字符串终止符'\0'来设计逻辑。
10. strstr 的使用和模拟实现
函数原型:
c
char *strstr(const char *str1, const char *str2);
功能 :
strstr 函数用于在字符串 str1 中查找子字符串 str2 的第一次出现位置。如果找到,返回指向该位置的指针;否则返回 NULL。使用前需包含头文件 <string.h>。
参数:
str1:指向被查找的字符串的指针。str2:指向要查找的子字符串的指针。
返回值:
- 成功:返回
str2在str1中首次出现的地址。 - 失败:返回
NULL。
10.1 代码演示
以下示例演示 strstr 的基本用法:
c
#include <stdio.h>
#include <string.h>
int main() {
const char *str1 = "Hello, world!";
const char *str2 = "world";
char *result = strstr(str1, str2);
if (result != NULL) {
printf("子字符串找到位置: %s\n", result); // 输出: world!
} else {
printf("子字符串未找到\n");
}
return 0;
}
输出:
子字符串找到位置: world!
重点:
strstr区分大小写(例如,"World" 不会被匹配)。- 如果
str2是空字符串(""),则返回str1的起始地址。 - 时间复杂度平均为 O(n)O(n)O(n),最坏情况 O(n×m)O(n \times m)O(n×m),其中 nnn 是
str1长度,mmm 是str2长度。
10.2 strstr 的模拟实现
以下是 strstr 的手动实现(模拟):
c
#include <stdio.h>
char *my_strstr(const char *str1, const char *str2) {
if (*str2 == '\0') { // 如果 str2 是空字符串
return (char *)str1;
}
const char *p1 = str1;
while (*p1 != '\0') {
const char *p1_start = p1;
const char *p2 = str2;
while (*p1 != '\0' && *p2 != '\0' && *p1 == *p2) {
p1++;
p2++;
}
if (*p2 == '\0') { // 子字符串匹配完成
return (char *)p1_start;
}
p1 = p1_start + 1; // 移动到下一个位置
}
return NULL; // 未找到
}
int main() {
const char *str1 = "C programming";
const char *str2 = "prog";
char *result = my_strstr(str1, str2);
if (result != NULL) {
printf("模拟实现找到位置: %s\n", result); // 输出: programming
} else {
printf("未找到\n");
}
return 0;
}
模拟实现解析:
- 检查
str2是否为空,是则返回str1。 - 遍历
str1,逐个字符与str2比较。 - 如果
str2的所有字符匹配,返回起始地址。 - 时间复杂度与标准实现类似。
11. strtok 函数的使用
函数原型:
c
char *strtok(char *str, const char *delim);
功能 :
strtok 用于根据分隔符 delim 将字符串 str 拆分为多个子字符串。它会修改原始字符串,将分隔符替换为 \0(字符串结束符)。使用前需包含 <string.h>。
参数:
str:首次调用时传入待分割的字符串;后续调用传入NULL,表示继续分割同一字符串。delim:包含所有分隔符的字符串(每个字符独立视为分隔符)。
返回值:
- 成功:返回当前子字符串的指针。
- 失败:返回
NULL(无更多子字符串)。
使用步骤:
- 首次调用:传入
str和delim。 - 后续调用:传入
NULL和相同delim。 - 结束:当返回
NULL时,分割完成。
11.1 代码演示
c
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "apple,banana;cherry";
const char *delim = ",;";
// 首次调用
char *token = strtok(str, delim);
while (token != NULL) {
printf("子字符串: %s\n", token);
token = strtok(NULL, delim); // 后续调用
}
return 0;
}
输出:
子字符串: apple
子字符串: banana
子字符串: cherry
11.2 注意事项
-
破坏性操作 :
strtok直接修改原始字符串(替换分隔符为\0)。如需保留原字符串,应先拷贝:cchar original[] = "data1;data2"; char copy[50]; strcpy(copy, original); // 先拷贝 char *token = strtok(copy, ";"); -
连续分隔符:多个连续分隔符被视为单个分隔符,不返回空字符串:
cchar str[] = "a,,b"; strtok(str, ","); // 返回 "a",跳过连续分隔符 -
空指针处理 :如果首次调用传入
NULL,行为未定义。确保首次调用传入有效字符串。 -
'\0' 位置分析 :
strtok在找到分隔符时将其替换为\0,下表展示不同情况:情况 原始字符串 分隔符 替换后效果 子字符串 备注 标准情况 "one,two" "," "one\0two" "one" 和 "two" 分隔符被替换为 \0连续分隔符 "a,b" "," "a\0\0b" "a" 和 "b" 跳过连续分隔符,不产生空字符串 字符串结尾 "end;" ";" "end\0" "end" 结尾分隔符被替换,子字符串以 \0结束无分隔符 "hello" "," 无变化 "hello" 返回整个字符串,不插入额外 \0解释:
- "存放数字超过" 可能指缓冲区溢出,但
strtok不分配内存;它操作现有字符串。确保字符串以\0结尾,否则行为未定义。 - "没有超过" 时,
\0在分隔符位置插入;"超过" 时(如字符串未正确终止),可能导致未定义行为。
- "存放数字超过" 可能指缓冲区溢出,但
12. strerror 函数的使用
函数原型:
c
char *strerror(int errnum);
功能 :
strerror 将错误码 errnum 转换为对应的错误信息字符串,并返回该字符串的首地址。主要用于标准库函数错误(如文件操作)。使用前需包含 <string.h> 和 <errno.h>。
参数:
errnum:错误码,通常传递全局变量errno的值(库函数失败时设置)。
返回值:
- 成功:返回错误信息字符串的指针。
- 失败:无显式失败(但无效
errnum可能返回未知错误字符串)。
12.1 代码演示
c
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
int main() {
FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
printf("错误码: %d\n", errno); // 输出错误码
printf("错误信息: %s\n", strerror(errno)); // 输出错误字符串
}
return 0;
}
输出示例(假设文件不存在):
错误码: 2
错误信息: No such file or directory
12.2 perror 函数
函数原型:
c
void perror(const char *str);
功能 :
perror 直接打印错误信息:先打印参数 str,后跟冒号、空格和 strerror(errno) 的内容。简化错误处理:
c
#include <stdio.h>
#include <errno.h>
int main() {
FILE *file = fopen("invalid.txt", "r");
if (file == NULL) {
perror("文件打开失败"); // 输出: 文件打开失败: No such file or directory
}
return 0;
}
重点:
strerror和perror依赖于errno,确保在库函数失败后立即使用(errno可能被后续调用覆盖)。- 错误码范围由系统定义(如 POSIX 标准),常见错误码如
EACCES(权限不足)。