系列文章目录
文章目录
1. 概念
在C语言中,字符函数和字符串函数是用来处理单个字符和字符数组(通常作为字符串处理)的标准库函数。这些函数有各种功能,如字符检查(例如是否为字母、数字等)、转换(大写、小写转换)以及字符串的操作(比较、复制、连接等)。这些函数大多定义在 <ctype.h> 和 <string.h> 头文件中。
2. 函数
2.1 strlen
strlen 是一个标准库函数,定义在 <string.h> 头文件中。该函数用于计算给定的以 null 结尾的字符串的长度,不包括结尾的 null 字符('\0')。这意味着 strlen 返回的是字符串中字符的数量,直到第一个 null 字符为止。
size_t strlen(const char *s);
- 参数:
s
是指向字符数组(字符串)的指针,该数组应以 null 字符终止。 - 返回值:返回类型为
size_t
,表示字符串的长度。size_t
是一个无符号整数类型,足以表示任何数组的大小。
功能描述
strlen
函数通过遍历字符串,从首地址开始计数,直到遇到第一个 null 字符,然后返回计数值。该函数不计入结尾的 null 字符。
为什么需要以 null 字符结束?
在 C 语言中,字符串是以 null 字符结束的,这句话描述了 C 语言中字符串的一个基本约定:字符串总是以一个特殊的字符 \0
(null 字符,ASCII 码为 0)结尾。这个约定允许程序确定字符串的结束位置,即使不知道字符串的实际长度。
char str[] = "Hello";
这个数组在内存中的存储实际上是这样的:
'H' 'e' 'l' 'l' 'o' '\0'
当你手动创建一个字符数组用作字符串时,你必须确保自己在字符串的末尾添加了 null 字符。例如:
char str[6];
str[0] = 'H';
str[1] = 'e';
str[2] = 'l';
str[3] = 'l';
str[4] = 'o';
str[5] = '\0'; // 明确地添加 null 字符
代码示例
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "Hello, world!";
char str2[] = "";
// 获取并打印字符串str1的长度
size_t len1 = strlen(str1);
printf("The length of '%s' is %zu.\n", str1, len1);
// 获取并打印字符串str2的长度
size_t len2 = strlen(str2);
printf("The length of an empty string is %zu.\n", len2);
return 0;
}
输出结果:
The length of 'Hello, world!' is 13.
The length of an empty string is 0.
2.2 strcpy
char *strcpy(char *dest, const char *src);
- dest:目标字符串数组的指针,用于接收复制的内容。
- src:源字符串的常量指针,即要复制的字符串。
- 返回指向目标字符串 dest 的指针。
功能描述
strcpy
函数将 src
字符串(包括终止的 null 字符 \0
)复制到 dest
指向的位置。复制会一直进行,直到包括 src
中的终止 null 字符为止,因此 dest
必须有足够的空间来存储整个 src
字符串。
使用注意
使用 strcpy
时需要格外注意,因为它不会检查目标缓冲区的大小。如果 dest
缓冲区不够大,无法容纳 src
的全部内容,就会发生缓冲区溢出,可能导致数据损坏和程序崩溃。为了避免这种风险,建议使用更安全的字符串操作函数,如 strncpy
,或在较新的代码中使用边界检查的函数版本,如 strcpy_s
(在支持它的编译器中)。
示例代码:
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello, world!";
char dest[50]; // 确保有足够的空间
strcpy(dest, src);
printf("Copied string: %s\n", dest);
return 0;
}
这段代码首先定义了一个源字符串 src,然后定义了一个足够大的目标字符串数组 dest。使用 strcpy 将 src 复制到 dest 中,然后打印出 dest。
2.3 strncpy
char *strncpy(char *dest, const char *src, size_t n);
- dest:目标字符串数组的指针。这个数组应有足够的空间至少包含 n 个字符。
- src:源字符串的常量指针。
- n:要从源字符串复制的最大字符数。
- 返回指向目标字符串 dest 的指针。
功能描述
strncpy 函数从 src 字符串复制最多 n 个字符到 dest 字符串。如果 src 的长度小于 n,函数将复制 src 中的字符,然后添加足够的 null 字符(\0)到 dest 中,直到总共复制了 n 个字符。如果 src 的长度大于或等于 n,则复制前 n 个字符而不自动添加 null 字符到 dest 的末尾。
代码示例:
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello, world!";
char dest[20];
// 使用 strncpy 复制 5 个字符
strncpy(dest, src, 5);
dest[5] = '\0'; // 显式添加 null 字符
printf("Copied string: %s\n", dest); // 输出 "Hello"
return 0;
}
2.4 strcmp
int strcmp(const char *s1, const char *s2);
s1
:指向第一个要比较的 null 终止 C 字符串的指针。s2
:指向第二个要比较的 null 终止 C 字符串的指针。- 如果返回值 < 0,则表示
s1
小于s2
。 - 如果返回值 > 0,则表示
s1
大于s2
。 - 如果返回值 = 0,则表示
s1
和s2
相等。
功能描述
strcmp
函数比较两个字符串 s1
和 s2
。比较是基于每个字符的无符号字符值(通常是 ASCII 值)逐个进行的。一旦发现两个字符串中的字符不同,或者检测到字符串的结束符(null 字符 '\0'
),比较就会停止。
这种比较方式意味着比较结果是字典顺序的,即基于字符的数值。例如,字母 'a' 的 ASCII 值是 97,而 'b' 是 98,因此 "a" 小于 "b"。
示例代码
#include <stdio.h>
#include <string.h>
int main() {
char *str1 = "Hello, World!";
char *str2 = "Hello, world!";
int result = strcmp(str1, str2);
if (result < 0) {
printf("'%s' is less than '%s'\n", str1, str2);
} else if (result > 0) {
printf("'%s' is greater than '%s'\n", str1, str2);
} else {
printf("'%s' is equal to '%s'\n", str1, str2);
}
return 0;
}
在这个例子中,因为字符串 str1 和 str2 在第 7 个字符处不同(大写的 'W' 与小写的 'w'),并且 'W' 的 ASCII 值小于 'w' 的 ASCII 值,所以 strcmp 将返回一个小于 0 的值,表示 str1 小于 str2。
2.5 strcat
char *strcat(char *dest, const char *src);
dest
:目标字符串的指针,用于接收追加的字符串。必须有足够的空间来容纳追加后的结果,包括追加的字符串src
和终止的 null 字符。src
:源字符串的指针,这个字符串会被追加到dest
的末尾。- 返回指向目标字符串
dest
的指针。
功能描述
strcat 函数将 src 字符串追加到 dest 字符串的末尾。操作的方式是,首先在 dest 中找到终止的 null 字符(\0),然后从这个位置开始,将 src 中的字符逐个复制到 dest 中,包括 src 的终止 null 字符。这样操作后,dest 和 src 原来的内容将被合并,dest 成为新的、更长的字符串。
使用注意
使用 strcat 时需要确保 dest 数组有足够的空间来存储追加后的结果。如果 dest 的空间不足以容纳原来的 dest 加上 src 的内容,这将导致缓冲区溢出,可能引起程序崩溃或数据损坏。
示例代码
#include <stdio.h>
#include <string.h>
int main() {
char dest[50] = "Hello";
char src[] = ", world!";
// 使用 strcat 追加字符串
strcat(dest, src);
printf("Concatenated string: %s\n", dest); // 输出 "Hello, world!"
return 0;
}
2.6 strncat
char *strncat(char *dest, const char *src, size_t n);
dest
:目标字符串的指针,用于接收追加的字符串。它必须指向一个足够大的缓冲区,这个缓冲区能够存储追加后的字符串和终止的 null 字符。src
:源字符串的指针,将被追加到dest
的末尾。n
:最大复制字符数,指从src
复制到dest
的最大字符数,不包括自动追加的 null 字符。- 返回指向目标字符串
dest
的指针。
功能描述
strncat 函数从 src 开始复制字符到 dest 字符串的结尾(从 dest 的首个 null 字符开始)。它会复制字符直到复制了 n 个字符或者遇到 src 的结束 null 字符。不论是哪种情况发生,strncat 总是在最后添加一个 null 字符来终止 dest 字符串。
这意味着如果 src 的长度小于或等于 n,则 src 的全部内容都将被追加到 dest;如果 src 的长度大于 n,则只有其前 n 个字符被追加。
使用注意
dest
必须有足够的空间来存储追加的字符和终止的 null 字符。- 不要忽视
n
的设置,适当的n
值能防止缓冲区溢出。
示例代码
//strncat 追加了最多 7 个字符从 src 到 dest。
//即使 src 的实际长度超过了 7,函数仍然确保了不会超出指定的字符数,保护了 dest 的缓冲区不被溢出。
#include <stdio.h>
#include <string.h>
int main() {
char dest[20] = "Hello";
char src[] = ", world!";
// 使用 strncat 追加部分字符串,防止溢出
strncat(dest, src, 7); // 只追加 ", world",注意包括空格和逗号
printf("Concatenated string: %s\n", dest); // 输出 "Hello, world"
return 0;
}
2.7 strncmp
int strncmp(const char *s1, const char *s2, size_t n);
s1
:指向第一个要比较的字符串的指针。s2
:指向第二个要比较的字符串的指针。n
:要比较的最大字符数。- 如果两个字符串的前
n
个字符完全相同,则返回 0。 - 如果根据字典顺序
s1
小于s2
,返回一个小于 0 的值。 - 如果根据字典顺序
s1
大于s2
,返回一个大于 0 的值。
功能描述
strncmp 函数比较来自 s1 和 s2 的最多 n 个字符。比较是基于每个字符的无符号数值(通常是 ASCII 值)进行的。如果在比较达到 n 个字符之前,任一字符串的结束 null 字符被遇到,比较停止。这意味着,如果字符串中包含 null 字符或其长度小于 n,则比较可能提前结束。
使用注意
- 确保安全:虽然
strncmp
通过限制比较的字符数增加了安全性,使用它时仍应确保字符串至少有n
个字符长,或者你接受比较可能因为提前遇到 null 字符而结束。 - 处理结果:和
strcmp
一样,strncmp
的返回结果需要正确理解和使用,特别是在条件判断中。
示例代码
#include <stdio.h>
#include <string.h>
int main() {
const char *str1 = "Hello, world!";
const char *str2 = "Hello, there!";
// 比较前 7 个字符
int result = strncmp(str1, str2, 7);
if (result == 0) {
printf("The first 7 characters of both strings are the same.\n");
} else if (result < 0) {
printf("The first string is less than the second in the first 7 characters.\n");
} else {
printf("The first string is greater than the second in the first 7 characters.\n");
}
return 0;
}
输出结果:
The first 7 characters of both strings are the same.
尽管两个字符串在整体上不同,strncmp 只比较前 7 个字符,在这个范围内,两个字符串相同。
2.8 strstr
char *strstr(const char *haystack, const char *needle);
- haystack:要搜索的主字符串。
- needle:要查找的子字符串。
- 如果找到 needle,则返回一个指向 haystack 中第一次出现 needle 的位置的指针。
- 如果没有找到 needle,则返回 NULL。
功能描述
strstr 函数搜索 needle 子串的第一次出现在 haystack 字符串中的位置。搜索不区分大小写,是从左向右进行。如果 needle 是一个空字符串,strstr 会返回 haystack 的地址,因为空字符串被视为存在于任何字符串的开头。
使用注意
- 空字符串的处理:如果 needle 是空字符串,函数返回 haystack 的起始地址。这是逻辑上的处理,因为空字符串在任何位置都能匹配。
- 返回值:如果找到 needle,可以通过返回的指针对 haystack 进行操作,例如提取子串后面的部分。如果 needle 不存在于 haystack 中,返回 NULL,任何尝试访问此返回值的操作都应进行空指针检查。
示例代码
#include <stdio.h>
#include <string.h>
int main() {
const char *text = "Here is a simple example.";
const char *substr = "simple";
// 查找子串
char *result = strstr(text, substr);
if (result != NULL) {
printf("Found: '%s'\n", substr);
printf("Rest of the string after '%s': '%s'\n", substr, result + strlen(substr));
} else {
printf("Substring not found.\n");
}
return 0;
}
输出结果:
Found: 'simple'
Rest of the string after 'simple': ' example.'
2.9 strtok
char *strtok(char *str, const char *delim);
- str:要被解析的字符串,第一次调用时传入字符串的指针,后续调用传入 NULL。
- delim:作为分隔符的字符集,任何包含在此字符串中的字符都被视为一个分隔符。
- 返回下一个标记的指针。
- 如果没有更多的标记,则返回 NULL。
功能描述
strtok 在 str 指定的字符串中查找由 delim 指定的分隔符。当它找到一个分隔符时,它会将其替换为 \0(null 字符)并返回前一个标记的开始位置。在连续的调用中,应将 str 参数设置为 NULL,以指示函数继续处理上次调用时剩余的字符串。
由于 strtok 会修改输入字符串(通过替换分隔符为 \0),它并不是线程安全的。在多线程程序中,应考虑使用 strtok_r(在 POSIX 系统中可用),它是 strtok 的线程安全版本。
使用注意
- 非线程安全:
strtok
修改原始字符串,并使用一个静态缓冲区来存储函数的状态,因此它不是线程安全的。 - 修改输入字符串:
strtok
通过在分隔符位置插入 null 字符来“破坏”原始字符串。 - 多次调用:对于同一字符串的多次调用,除了第一次外,
str
应为NULL
。
示例代码
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello, world! Welcome to C programming.";
const char *delim = " ,.!"; // 包含了空格和几个标点符号作为分隔符
// 获取第一个标记
char *token = strtok(str, delim);
// 继续获取其他的标记
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, delim);
}
return 0;
}
输出结果:
Hello
world
Welcome
to
C
programming
2.10 strerror
char *strerror(int errnum);
- errnum:错误编号,通常是 errno 变量的值。
- 返回一个指向错误消息字符串的指针,该字符串描述了参数 errnum 指定的错误。
功能描述
strerror 函数根据给定的错误编号返回一个指向描述该错误的字符串的指针。这些错误消息是静态分配的,不应由程序修改或释放。每个错误编号对应一个特定的错误情况,例如文件访问问题、内存分配失败等。
使用注意
-
线程安全:标准的 strerror 函数不是线程安全的,因为它返回一个指向静态数据的指针,可能在多线程环境下由另一个线程覆盖。为了解决这个问题,POSIX 提供了 strerror_r 函数,这是一个线程安全版本的 strerror。
-
局限性:由于 strerror 返回的是静态数据的指针,其内容可能在下一次调用 strerror 或 strerror_r 时被覆盖。
函数
|
如果他的参数符合下列条件就返回真
|
iscntrl
|
任何控制字符
|
isspace
|
空白字符:空格‘ ’,换页‘\f’,换行'\n',回车‘\r’,制表符'\t'或者垂直制表符'\v'
|
isdigit
|
十进制数字 0~9
|
isxdigit
|
十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F
|
islower
|
小写字母a~z
|
isupper
|
大写字母A~Z
|
isalpha
|
字母a~z或A~Z
|
isalnum
|
字母或者数字,a~z,A~Z,0~9
|
ispunct
|
标点符号,任何不属于数字或者字母的图形字符(可打印)
|
isgraph
|
任何图形字符
|
isprint
|
任何可打印字符,包括图形字符和空白字符
|
示例代码
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main() {
// 故意造成一个错误,比如尝试打开一个不存在的文件
FILE *fp = fopen("nonexistentfile.txt", "r");
if (fp == NULL) {
int err = errno; // 获取错误编号
printf("Error opening file: %s\n", strerror(err));
} else {
fclose(fp);
}
return 0;
}
在这个例子中,尝试打开一个不存在的文件将导致 fopen 返回 NULL 并设置 errno。然后使用 strerror 函数来获取并打印错误消息。
输出:
Error opening file: No such file or directory
2.11 memcpy
void *memcpy(void *dest, const void *src, size_t n);
- dest:目标内存地址的指针,到哪里去复制数据。
- src:源内存地址的指针,从哪里来复制数据。
- n:要复制的字节数。
- 返回指向目标内存地址 dest 的指针。
功能描述
memcpy 函数将 src 指向的内存区域的前 n 个字节复制到 dest 指向的内存区域。这个函数不检查任何终止 null 字符,它只是简单地复制一定数量的字节,因此它非常适合用于任何类型的数据,包括字符数组、整数、结构体、类对象等。
使用注意
重叠问题:如果 src 和 dest 所指向的内存区域重叠,memcpy 的行为是未定义的。在内存区域可能重叠的情况下,应该使用 memmove 函数,memmove 是为处理重叠区域设计的,可以正确地处理源和目标内存区域重叠的情况。
示例代码
//如何使用 memcpy 函数来复制一个结构体数组。
#include <stdio.h>
#include <string.h>
typedef struct {
char name[40];
int age;
} Person;
int main() {
Person people[] = {{"Alice", 30}, {"Bob", 25}};
Person copy[2];
// 复制 people 数组到 copy 数组
memcpy(copy, people, sizeof(people));
for (int i = 0; i < 2; i++) {
printf("Name: %s, Age: %d\n", copy[i].name, copy[i].age);
}
return 0;
}
输出:
Name: Alice, Age: 30
Name: Bob, Age: 25
2.12 memmove
void *memmove(void *dest, const void *src, size_t n);
- dest:目标内存地址的指针,到哪里去复制数据。
- src:源内存地址的指针,从哪里来复制数据。
- n:要复制的字节数。
- 返回指向目标内存地址 dest 的指针。
功能描述
memmove 函数复制 src 指向的内存区域的前 n 个字节到 dest 指向的内存区域。与 memcpy 不同,memmove 考虑到内存区域可能重叠的情况,它首先判断 src 和 dest 的相对位置,如果有重叠并且 src 在 dest 的前面,则从后往前复制,否则从前往后复制。这样的处理确保了复制过程中源数据不会被覆盖,从而保证数据的正确性。
使用注意
内存重叠:memmove 是专为处理内存重叠设计的。当不确定内存是否重叠或明确知道内存重叠时,应选择 memmove 而不是 memcpy。
示例代码
//如何使用 memmove 在内存重叠时安全地复制数据。
#include <stdio.h>
#include <string.h>
int main() {
char data[100] = "Hello, world!";
printf("Original data: %s\n", data);
// 将数据向右移动5个字节
memmove(data + 5, data, strlen(data) + 1);
printf("After memmove: %s\n", data);
return 0;
}
输出:
Original data: Hello, world!
After memmove: HelloHello, world!
data 数组的内容被安全地向右移动了5个字节。
使用 strlen(data) + 1 来确保包括字符串终止符 \0 在内的所有字符都被复制。
2.13 memcmp
int memcmp(const void *s1, const void *s2, size_t n);
- s1:指向第一个内存块的指针。
- s2:指向第二个内存块的指针。
- n:要比较的字节数。
- 如果两个内存块在前 n 字节内相等,返回 0。
- 如果不相等,返回值将是两个不相匹配的字节的差值(取决于不相匹配的第一个字节对),具体表现为:
- 如果 s1 所指内存块的字节值大于 s2 所指的,返回一个大于零的值。
- 如果 s1 所指的字节值小于 s2 所指的,返回一个小于零的值。
功能描述
memcmp 函数按字节比较两个内存块。它从两块内存的起始位置开始,逐字节进行比较,直到第一个不匹配的字节被发现或比较了 n 字节。这使得 memcmp 非常适合比较任意类型的数据块,不论它们是否包含文本数据。
使用注意
- 二进制安全:
memcmp
是二进制安全的,可以用于任何类型的数据,包括包含空字节('\0'
)的数据。 - 用途限制:尽管
memcmp
在比较数据方面非常有效,但不用于需要考虑特定编码或文化区别的字符串比较,例如文本字符串的本地化比较。
示例代码
//如何使用 memcmp 来比较两个简单的结构体数组是否相同
#include <stdio.h>
#include <string.h>
typedef struct {
int id;
double balance;
} Account;
int main() {
Account accounts1[2] = {{1, 100.50}, {2, 150.75}};
Account accounts2[2] = {{1, 100.50}, {2, 150.75}};
Account accounts3[2] = {{1, 100.50}, {3, 150.75}};
// 比较 accounts1 和 accounts2
if (memcmp(accounts1, accounts2, sizeof(accounts1)) == 0) {
printf("accounts1 and accounts2 are the same.\n");
} else {
printf("accounts1 and accounts2 are different.\n");
}
// 比较 accounts1 和 accounts3
if (memcmp(accounts1, accounts3, sizeof(accounts1)) == 0) {
printf("accounts1 and accounts3 are the same.\n");
} else {
printf("accounts1 and accounts3 are different.\n");
}
return 0;
}
输出:
accounts1 and accounts2 are the same.
accounts1 and accounts3 are different.
3. 函数总结
- strlen
- strcpy
- strcat
- strcmp
- strncpy
- strncat
- strncmp
- strstr
- strtok
- strerror
- memcpy
- memmove
- memset
- memcmp
4. 模拟实现库函数
模拟实现strlen,可以更好的理解函数的结构和逻辑
方法一:计数器方式
//使用一个循环遍历字符串,每遍历一个非 null 字符就将计数器增加1。
int my_strlen(const char *str) {
int count = 0; // 初始化计数器
while (*str) { // 继续循环,直到遇到 null 字符
count++; // 对每个非 null 字符增加计数
str++; // 移动指针到下一个字符
}
return count; // 返回计数,即字符串的长度
}
方法二:递归方式(不使用临时变量计数器)
int my_strlen(const char *str) {
if (*str == '\0') // 基本情况:如果当前字符是 null 字符
return 0; // 返回 0,递归结束
else
return 1 + my_strlen(str + 1); // 递归调用,移动到下一个字符,并加一计算长度
}
//递归方法通过每次调用自身来处理字符串的下一个字符,直到遇到字符串的结束符。
//这种方法不使用循环或外部变量,而是利用调用栈来计数。
方法三:指针- 指针的方式
int my_strlen(char *s) {
char *p = s; // p 指针用来遍历字符串
while (*p != '\0') { // 循环直到 p 指向的字符是 null 字符
p++; // 移动 p 到下一个字符
}
return p - s; // 返回两个指针之间的差值,即字符串的长度
}
//利用两个指针来确定字符串的长度。一个指针指向字符串的开始,另一个遍历到字符串的末尾。