C语言之字符串操作
C 语言提供了丰富的字符串处理相关的库函数,这些函数基本上,都声明在头文件string.h当中,所以使用它们需要包含这个头文件。这里只介绍几种最基本的和最常用的,以及手动实现它们的方式。
字符串长度strlen
strlen
函数全名:string_length
函数声明:
size_t strlen (const char *s);
函数作用:返回当前字符串的长度,也就是字符数组中空字符'\0'前面的字符数量。不包括空字符'\0'!
int len;
char str[] = "abcd";
char str2[10] = "12345";
char str3[5] = { 'a','\0','c' };
len = strlen("abc"); /* len is now 3 */
len = strlen(""); /* len is now 0 */
len = strlen(str); /* len is now 4 */
len = strlen(str2); /* len is now 5 */
len = strlen(str3); /* len is now 1 */
char str4[4] = "1234";
len = strlen(str4); // str4并不能表示一个字符串,该函数调用会引发未定义行为
手动实现my_strlen
size_t my_strlen1(const char* str) {
size_t len = 0;
while (*str != '\0') { // 遍历直到遇到空字符
len++;
str++;
}
return len;
}
// 数组下标形式
size_t my_strlen2(const char* str) {
size_t len = 0;
while (str[len] != '\0') {
len++;
}
return len;
}
size_t my_strlen3(const char* str) {
const char* p = str; // 用一个指针记录数组首元素
while (*str) { // 惯用法: 遍历直到字符串的末尾,
str++;
} // 循环结束时,str指针指向空字符,p指针指向数组首元素
// 相当于空字符的下标减去首元素下标,结果就是字符串的长度
return str - p;
}
字符串复制strcpy,strncpy
strcpy
函数全名:string_copy
函数声明:
char *strcpy(char *dest, const char *src);
函数作用: 将 src 中存储的以空字符 '\0' 结束的字符串复制到 dest所指向的数组中。
也就是说,会从首字符开始逐个字符地从 src 复制字符,包括空字符在内,都会从dest首位置开始,复制到dest当中。
这个过程中,src数组是不会被修改的,所以它被const修饰。总之,该函数调用后,dest 将包含 src 的完整副本。
char src[] = "hello";
char dest[10];
// 调用函数后,src不变
// dest数组就会变成{'h','e','l','l','o','\0', ....}
// 其中前6个字符是字符串"hello"
strcpy(dest, src);
函数返回值:
该函数会返回指向目标数组 dest 的指针,一般来说,该函数的返回值没有什么意义,推荐忽略它。
但某些场景下,这个返回值允许strcpy 函数的调用,可以被嵌套使用或者用于更复杂的表达式中。比如:
// strcpy返回复制完成后指向 dest 数组的指针
char src[] = "hello";
char dest[10];
char dest2[10];
// 可以直接利用printf、puts函数打印复制后的目标数组
puts(strcpy(dest, src));
// 更加复杂的函数调用
// 将src复制到dest,再将dest复制给dest2
strcpy(dest2, strcpy(dest, src));
注意:
- strcpy函数是不安全的,它并不会检查dest是否真的能够包含src数组。如果dest不够大,就会因数组越界而产生未定义行为。
- 为了安全起见,可以考虑使用strncpy函数解决这一问题,虽然它可能效率更低一些。
strncpy
函数声明:
char *strncpy(char *dest, const char *src, size_t n);
函数作用:
该函数会将最多n个字符从src中复制到dest数组中:
- 如果
n < strlen(src) + 1
,也就是 n 小于源字符串的长度 + 1时,此时strncpy函数就无法将整个源字符串数据都复制到 dest 中,并且此时复制完成的 dest 数组也不会以空字符结尾,无法表示一个字符串。不过好在,它不会引起越界问题,是安全的操作。 - 如果
n = strlen(src) + 1
,即 n 恰好是源字符串长度 + 1时,strncpy函数会将src完整复制到dest数组中,包括空字符。 - 如果
n > strlen(src) + 1
,即 n 完全大于源字符串长度 + 1时,strncpy函数不仅会完整复制src字符串到dest中,还会将剩余 (n - strlen(src) - 1) 个字符设置为空字符。
strncpy函数一定会处理n个字符数据,如果n比较大,在复制完源数组后,它会顺道将 dest 数组中的元素置为空字符。
实际开发中,建议将n的值设定为 dest长度-1,并主动将dest的最后一个元素设置为空字符,这样总能安全地得到一个字符串。
也就是以下操作代码:
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';
char src[] = "hello";
char dest[10];
// 这个复制是没有问题的
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';
char dest2[5];
// 这个复制虽然没有真的将"hello"都复制到dest2,但是一个安全的操作。最终dest2存放字符串"hell"
strncpy(dest2, src, sizeof(dest2) - 1);
dest2[sizeof(dest2) - 1] = '\0';
这样的复制行为,若dest足够装下src,会导致dest剩余部分全都被设置为空字符。
手动实现my_strcpy、my_strncpy
my_strcpy
char* my_strcpy1(char *dest, const char *src) {
char *tmp = dest; // dest数组的首元素指针要作为返回值,所以要临时保存一下
while (*src) {
*dest++ = *src++;
} // 这个while循环没有复制空字符,所以需要手动来复制空字符
// while循环结束时,src指针指向空字符, dest指针也指向对应的位置
*dest = 0;
return tmp;
}
// 简化: 复制字符串的惯用法
char *my_strcpy2(char *dest, const char *src) {
char *tmp = dest; // dest数组的首元素指针要作为返回值,所以要临时保存一下
/*
(*dest++ = *src++)
主要作用: 返回*src的值
副作用: src和dest都往后挪动指针
当主要作用返回src指针指向的元素, 且返回空字符结束这个while循环时
=的副作用赋值并不会消失
所以所以这种写法, 空字符也会连带被复制
不需要考虑空字符的手动复制
*/
while (*dest++ = *src++)
;
return tmp;
}
my_strncpy
char *my_strncpy1(char *dest, const char *src, size_t n) {
char *tmp = dest;
/*
复制src到dest,直到达到n个字符或遇到src的终结符
while循环结束条件是:
1.n足够长时(n >= strlen(src)+1时),会将src中直到空字符的数据复制到dest中,然后结束循环
2.n不够长时(n < strlen(src)+1时),会将src中的前n个字符复制到dest中,然后结束循环
此时dest数组不会存储空字符,它不能表示字符串,但不会有越界风险
*/
while (n > 0 && (*dest++ = *src++)) {
n--;
}
/*
当n >= strlen(src) + 1时
上面的while循环依靠(*dest++ = *src++)结束循环
此时空字符被复制到dest中了
但循环体没有执行循环就终止了, n--没有执行, 即复制了一个字符但n没有减少1
所以下面要判断n > 1 而不是n > 0
*/
while (n > 1) {
*dest++ = '\0';
n--;
}
return tmp;
}
// 如果不将n设置为无符号类型,而设置为int类型
char *my_strncpy2(char *dest, const char *src, int n) {
char *tmp = dest;
// 这个循环不用担心无符号0减1变为最大值的情况发生,可以放心使用
while (n-- && (*dest++ = *src++))
;
// 由于n--正常执行,所以下面仍然可以判断n>0
while (n > 0) {
*dest++ = 0;
n--;
}
return tmp;
}
// 当然为了追求原汁原味, 还是应该保留n的size_t类型
// 同时为了保证n>0的正常使用,我们就需要保证n--正常执行
char *my_strncpy3(char *dest, const char *src, size_t n) {
char *tmp = dest;
while (n > 0 && (*dest = *src)) {
dest++;
src++;
n--;
}// while循环结束时,src指针指向指向空字符,dest指针也指向空字符
// 虽然空字符已经复制结束后,但由于指针仍然指向空字符,下面的while循环就可以n>0执行
while (n > 0) {
*dest++ = '\0';
n--;
}
return tmp;
}
字符串拼接strcat,strncat
strcat
函数全名: string_concat,顾名思义,它用于将一个字符串拼接到另一个字符串的末尾。
函数声明:
char *strcat(char *dest, const char *src);
函数作用: strcat 函数会将 src(源字符串)中存储的以空字符 '\0' 结束的字符串拼接到 dest(目标字符串)的末尾。
具体来说:
会从 dest 字符串的空字符 '\0' 开始,替换这个空字符,然后将src中表示字符串的字符数据从开头到空字符,全部拼接到dest末尾。
这个过程中,src 字符串不会被修改,所以它被 const 修饰。
总之,该函数调用后,dest 将包含 dest 和 src 的字符串拼接后的副本。
简单的代码示例:
char dest[20] = "Hello, ";
char src[] = "World!";
// 调用函数后,dest数组变为 "Hello, World!\0",其余部分填充为空字符
strcat(dest, src);
// 输出拼接后的字符串
printf("%s", dest); // 输出 "Hello, World!"
函数返回值:
该函数会返回指向目标数组 dest 的指针。一般来说这个返回值可以忽略,但有些场景中可以利用该返回值,对函数调用进行嵌套处理。
例如:
char dest[20] = "Hello, ";
char src[] = "Everyone!";
char src2[] = " Have a nice day!";
// 嵌套,将src和src2都拼接到dest上
strcat(strcat(dest, src), src2);
// 输出最终的拼接字符串
printf("%s", dest); // 输出 "Hello, Everyone! Have a nice day!"
注意:
- src 和 dest两个参数都必须是字符串,即以空字符串结尾的字符数组,否则将引发未定义行为。
- strcat 函数与 strcpy 一样,不会检查 dest 数组是否能够容纳拼接后的字符串。如果 dest 不够大,就会产生未定义行为,这是潜在的安全隐患。
- 可以考虑使用更安全的strncat函数来实现字符串拼接,它允许指定最大拼接的字符数。
strncat
函数声明:
char *strncat(char *dest, const char *src, size_t n);
函数作用:
仍旧是找到dest字符串末尾的空字符,然后从该字符开始,将 src 的首个元素复制到 dest末尾,直到:
-
已经复制了 n 个字符。
-
或者复制到达了
src
字符串的结尾,即遇到了src
的空字符串。所以该函数不会把src中的空字符复制到dest中。
最后,strncat
函数一定会在dest的末尾添加一个空字符,以确保dest能够表示一个字符串。由于这一步操作的存在,该函数仍然具有越界访问的风险,需要程序员自己设定合理的n取值,以规避这种情况。
为了安全的使用此函数,基于函数的特点,我们就可以得出n的计算公式:
int n = sizeof(dest) - strlen(dest) - 1; // dest数组的长度 - 已存储字符串的长度 - 1(留给存空字符)
表达式中-1
是必要的,否则会导致数组越界情况发生。
给strncat函数传入这样的一个n
参数,就能够保证一定安全的得到一个拼接后的结果字符串。
当然若dest容量不够大,拼接只会截取src的一部分字符数据。但安全是更重要的事情,这样的结果是我们可以接受的。
实际开发中,建议采取以下方式来调用此函数,避免dest空间不足导致溢出:
char src[] = "world";
char dest[10] = "hello, ";
// 确保dest是一个数组类型,而不是一个指针类型才能这样操作
int n = sizeof(dest) - strlen(dest) - 1;
strncat(dest, src, n);
dest最多再拼接(自身数组长度 - 字符串长度 - 1)个字符,最后留一个字节,strncat函数会自动拼接一个空字符表示字符串结束。
手动实现my_strcat、my_strncat
my_strcat
char* my_strcat(char* dest, const char* src) {
char* temp = dest;
// 找到dest的末尾,也就是让dest指针指向空字符
while (*dest) {
dest++;
}
// 将src的字符串数据从开头到空字符都拷贝到dest的末尾
while ((*dest++ = *src++))
;
return temp; // 返回dest的起始地址
}
my_strncat
char* my_strncat1(char* dest, const char* src, int n) {
char* temp = dest;
// 找到dest的末尾,也就是让dest指针指向空字符
while (*dest) {
dest++;
}
// 拷贝最多n个字符从src到dest的末尾
while (n-- && (*src)) {
*dest++ = *src++;
}
// 确保dest以空字符结尾,所以一定会将末尾置为空字符。这里没有判断越界,所以strncpy函数实际仍存在越界风险
// 这就要求我们传入一个正确合理的n以确保安全
*dest = '\0';
return temp; // 返回dest的起始地址
}
字符串比较大小strcmp
strcmp
函数全名:string_compare,用于比较两个字符串的大小。
函数声明:
int strcmp(const char *str1, const char *str2);
函数作用: strcmp函数按照字典顺序比较两个字符串的大小。当调用 strcmp函数时,它会逐个字符地比较两个字符串 str1 和 str2:
注意:
- 这个函数不会修改任何一个字符串,所以两个参数都被 const 修饰。
- 不同编译器和环境可能对strcmp函数返回值的表现有所不同。在某些环境中(比如VS),出于简化的目的,可能会将返回值归一化为 -1、0 或 1。但是标准的 C 语言库中 strcmp 的返回值是根据实际字符的ASCII值差异来确定的,而不是简单的 -1、0 或 1。
- 要确保传入的两个参数数组都表示字符串,都以空字符结尾,否则可能会导致比较越界。
- strcmp 是区分大小写的比较,如果需要进行不区分大小写的比较,可以使用 strcasecmp 函数。
- 由于strcmp是基于ASCII值进行比较的,它在处理非ASCII或多字节字符(如UTF-8编码的文本)时可能不会表现出预期的行为。
手动实现strcmp
int my_strcmp1(const char* str1, const char* str2) {
while (*str1 && *str2) {
if (*str1 != *str2)
{
return *str1 - *str2;
}
str1++;
str2++;
}
return *str1 - *str2;
}
// 这个实现可读性更差,但简洁很多
int my_strcmp2(const char* s1, const char* s2) {
while (*s1 && (*s1 == *s2)) {
// 移动两个指针,比较下一对字符
s1++;
s2++;
}
/*
* while循环结束的条件是:
* 1.s1指针指向了空字符
* 2.或者s2指针指向了空字符
* 3.或者s1和s2指针指向的字符不一致
* 不管是哪一种情况,返回它们的编码值差就是结果
*/
return *s1 - *s2;
}
字符相关的库函数
在操作字符串的过程中,经常需要对单个字符做出判断,比如:
- 判断字符是大写字母还是小写
- 判断字符是不是数字
- ...
C 标准函数库在头文件 <ctype.h> 中定义了大量此类功能函数。比如:
islower 和 isupper 函数是 C 标准库中声明在 <ctype.h> 头文件中的函数,它们用于检查给定的字符是否为小写字母或大写字母。
除此之外,<ctype.h> 头文件中还包括了以下类似的库函数:
isalpha(int c)
:检查传入的字符是否为字母(包括大写和小写)。isdigit(int c)
:检查传入的字符是否为十进制数字(0到9)。isalnum(int c)
:检查传入的字符是否为字母或数字。isspace(int c)
:检查传入的字符是否为空白字符,比如空格、制表符、换行符等。isblank(int c)
:检查传入的字符是否为空格或制表符。- ...