C语言 字符串
- 引言
- 一、字符串的创建方式
- 二、字符串函数
- 1. strlen 函数
- 使用示例1
- 使用示例2
- 模拟 strlen 函数
- 2. strcpy 函数
- 使用示例
- 模拟 strcpy 函数
- 3. strcat 函数
- 使用示例
- 模拟 strcat 函数
- 4. strcmp 函数
- 使用示例
- 模拟 strcmp 函数
- 5. strncpy、strncat、strncmp
- 6. strstr 函数
- 使用示例
- 模拟 strstr 函数
- 7. strtok 函数
- 使用示例1
- 使用示例2
- 三、字符函数
- 使用示例
- 四、内存操作函数
- 1. memset 函数
- 使用示例
- 2. memcmp
- 使用示例
- 3. memcpy 函数
- 使用示例
- 4. memmove 函数
- 使用示例
引言
C语言中对字符和字符串的处理很频繁,但 C语言本身是没有字符串类型的,所以字符串通常放在常量字符串中或者字符数组中。
一、字符串的创建方式
程序清单:
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abc\0def";
char arr3[] = { 'a', 'b', 'c', 'd', 'e', 'f' };
char arr4[] = { 'a', 'b', 'c', 'd', 'e', 'f', '\0'};
printf("%s\n", arr1);
printf("%s\n", arr2);
printf("%s\n", arr3);
printf("%s\n\n", arr4);
printf("%d\n", strlen(arr1));
printf("%d\n", strlen(arr2));
printf("%d\n", strlen(arr3)); // 随机值
printf("%d\n", strlen(arr4));
return 0;
}
输出结果:
调试窗口:
注意事项:
① 在 C语言中,被双引号括起来是字符串字面值,简称为字符串。同样地,字符串也只能被双引号括起来。
② 字符串的结束标志是一个 ’ \0 ’ 的转义字符。在使用格式化输出时,’ \0 ’ 的作用相当于告诉了编译器,它是一个停止的标志。在使用 strlen 这个库函数计算字符串的长度时,也是一样的道理,它只计算 ’ \0 ’ 之前的长度。
③ 对比 arr1 和 arr3 这两个创建字符数组的方式,可以发现,由于在 C语言 中,内存具有连续性,所以如果没有 ’ \0 ’ 作为结束标志,就会导致我们使用 printf / strlen 的时候,一直向后找 ’ \0 ’ 这个结束标志。
二、字符串函数
1. strlen 函数
strlen - string length - 计算字符串长度
size_t strlen ( const char* str );
// 返回值:字符串中出现的字符个数(不包含 '\0')
// 参数:需要计算的起始位置的指针
这里应该注意 strlen 返回的类型为无符号数。
使用示例1
#include <stdio.h>
#include <string.h>
int main() {
char arr[] = "abcdef";
char arr2[] = "abc\0def";
printf("%d\n", strlen(arr)); // arr 表示数组名,即数组的起始地址
printf("%d\n", strlen(arr2));
return 0;
}
// 输出结果:
// 6
// 3
使用示例2
#include <stdio.h>
#include <string.h>
int main(){
const char* str1 = "abcdef"; // 6
const char* str2 = "bbb"; // 3
if (strlen(str2) - strlen(str1) < 0){
printf("正确\n");
}else{
printf("错误\n");
}
return 0;
}
// 输出结果:错误
注意: 由于 strlen 函数的返回值是无符号类型的,所以我们所理解的 " -3 ",其实输出的是一个很大的正整数。
模拟 strlen 函数
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* start) {
assert(start != NULL);
int count = 0;
while (*start != '\0') {
start++;
count++;
}
return count;
}
int main() {
char arr[] = "abcdef";
char arr2[] = "abc\0def";
printf("%d\n", my_strlen(arr));
printf("%d\n", my_strlen(arr2));
return 0;
}
// 输出结果:
// 6
// 3
2. strcpy 函数
strcpy - string copy - 字符串拷贝
char* strcpy(char* destination, const char* source );
// 返回值:返回 destination 的副本
// 参数:destination 为粘贴的起始指针,source 为字符串复制的起始指针
注意事项:
① 源字符串必须以 ‘\0’ 结束。
② 拷贝操作会将源字符串中的 ‘\0’ 拷贝到目标空间。
③ 目标空间必须足够大,以确保能存放源字符串。
④ 目标空间必须为一个可修改的字符串,而不是字符串常量值。
使用示例
#include <stdio.h>
#include <string.h>
int main() {
char arr1[] = "hello world";
char arr2[] = "xxxxxxxxxxxxxx";
printf("%s\n", arr2);
strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
// 输出结果:
// xxxxxxxxxxxxxx
// hello world
调试过程:
注意事项:
在调试窗口中,我们可以看到 arr2 数组中也被存入了 ‘\0’ 字符。由于 printf 打印的时候,是将 ‘\0’ 之前的字符作为输出内容的,所以后面的 XX 便不会被打印出来。
模拟 strcpy 函数
模拟思想:将目标空间的字符,进行逐个替换。
#include <stdio.h>
#include <assert.h>
char* my_strcpy(char* end, char* start) {
assert(start != NULL); // 断言
assert(end != NULL);
char* ret = end; // 返回粘贴数组的起始地址
while (*start != '\0') {
*end = *start; // 解引用替换值
start++;
end++;
}
*end = *start; // 把 '\0' 也进行替换
return ret;
}
int main() {
char arr1[] = "hello world";
char arr2[] = "xxxxxxxxxxxxxx";
printf("%s\n", arr2);
printf("%s\n", my_strcpy(arr2, arr1)); // 函数的链式访问
return 0;
}
// 输出结果:
// xxxxxxxxxxxxxx
// hello world
3. strcat 函数
strcat - 字符串追踪
char* strcat ( char* destination, const char* source );
// 返回值:返回 destination 的副本
// 参数:destination 追踪目标的指针,source 为字符串的源头指针
注意事项:
① 源字符串必须以 ‘\0’ 结束。
② 追踪操作会将源字符串中的 ‘\0’ 放到目标空间。
② 目标空间必须有足够的大,能容纳下源字符串的内容。
③ 目标空间必须为一个可修改的字符串,而不是字符串常量值。
使用示例
#include <stdio.h>
#include <string.h>
int main() {
char arr1[20] = "hello";
char arr2[] = "world";
printf("%s\n", arr1);
strcat(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
// 输出结果:
// hello
// helloworld
模拟 strcat 函数
模拟思想:
1. 找到目标空间的 ‘\0’.
2. 对目标空间 ‘\0’ 之后的字符,进行字符串替换。
#include <stdio.h>
#include <string.h>
#include <assert.h>
char* my_strcat(char* dest, const char* source) {
assert(dest != NULL);
assert(source != NULL);
char* ret = dest;
// 1. 找到目标空间的 '\0'
while (*dest != '\0') {
dest++;
}
// 2. 进行字符拷贝
while (*source != '\0') {
*dest = *source;
dest++;
source++;
}
*dest = *source;
return ret;
}
int main() {
char arr1[20] = "hello";
char arr2[] = "world";
printf("%s\n", arr1);
printf("%s\n", my_strcat(arr1, arr2));
return 0;
}
// 输出结果:
// hello
// helloworld
4. strcmp 函数
strcmp - string compare - 字符串比较
int strcmp ( const char * str1, const char * str2 );
// 参数:两个待比较字符串的起始指针
// 返回值:
// 第一个字符串大于第二个字符串,则返回大于 0 的数字
// 第一个字符串等于第二个字符串,则返回 0
// 第一个字符串小于第二个字符串,则返回小于 0 的数字
注意事项:
strcmp 函数比较不是字符串的长度,而是比较字符串中对应位置上的字符大小,如果相同就比较下一对,直到两个比较的字符不同或者都遇到了 ‘\0’ 才会停止。实际上,在 C语言 中,比较的也就是字符对应的 ASCII 码值。
使用示例
#include <stdio.h>
#include <string.h>
int main() {
char arr1[] = "abcd";
char arr2[] = "abc";
char arr3[] = "abcz";
char arr4[] = "abcd";
char arr5[] = "abz";
printf("%d\n", strcmp(arr1, arr2));
printf("%d\n", strcmp(arr1, arr3));
printf("%d\n", strcmp(arr1, arr4));
printf("%d\n", strcmp(arr1, arr5));
return 0;
}
// 输出结果:
// 1
// -1
// 0
// -1
注意事项: 虽然我们看到在 VS 编译器底下输出的 1,-1,0. 但在官方的标准文档中,输出规定的是大于0,小于0,或者为0.
模拟 strcmp 函数
模拟思想:挨个比较字符之间的 ASCII 码值,相同的字符就直接往后跳,如果遇到两个不同的字符,则开始比较。直至遇到 ‘\0’ 结束。
#include <stdio.h>
#include <assert.h>
int my_strcmp(const char* str1, const char* str2) {
assert(str1 != NULL);
assert(str2 != NULL);
int ret = 0;
while (*str1 == *str2) {
if (*str1 == '\0' || *str2 == '\0') {
break;
}
str1++;
str2++;
}
if (*str1 < *str2) {
ret = -1;
}else if (*str1 > *str2) {
ret = 1;
}
return ret;
}
int main() {
char arr1[] = "abcd";
char arr2[] = "abc";
char arr3[] = "abcz";
char arr4[] = "abcd";
char arr5[] = "abz";
printf("%d\n", my_strcmp(arr1, arr2));
printf("%d\n", my_strcmp(arr1, arr3));
printf("%d\n", my_strcmp(arr1, arr4));
printf("%d\n", my_strcmp(arr1, arr5));
return 0;
}
// 输出结果:
// 1
// -1
// 0
// -1
5. strncpy、strncat、strncmp
strcpy、strcat、strcmp 是一组长度不受限制的字符串函数。
而 strncpy、strncat、strncmp 是一组长度受限制的字符串函数。
所谓长度受限制,就是相比于前者,多了一个 size_t 类型的参数。例如:strncpy 就可以指定拷贝字符的数量、strncat 就可以指定追踪字符的个数、strncmp 就可以指定比较的字符个数。
6. strstr 函数
strstr - string string - 找子串
char* strstr ( const char* str1, const char* str2);
// 返回值:返回 str1 中的子串起始指针
// 参数:str1 为主串,str2 为子串
使用示例
#include <stdio.h>
#include <string.h>
int main() {
char arr1[] = "abcdefxyxyzwe";
char arr2[] = "bcd";
char arr3[] = "xyz";
char arr4[] = "xyw";
printf("%s\n", strstr(arr1, arr2));
printf("%s\n", strstr(arr1, arr3));
printf("%s\n", strstr(arr1, arr4));
return 0;
}
// 输出结果:
// bcdefxyxyzwe
// xyzwe
// (null)
模拟 strstr 函数
模拟思路:额外创建三个指针,s1, s2, flag.
flag 指针作为标志主串中子串的起始位置,s2 指针用来重置子串的开头,s1 指针用来和 s2 进行比较是否为相同字符。
模拟的三个场景:
#include <stdio.h>
#include <assert.h>
char* my_strstr(const char* str1, const char* str2) {
assert(str1 != NULL);
assert(str2 != NULL);
const char* s1 = str1;
const char* s2 = str2; // str2 用来重置 s2
char* flag = str1; // flag 指针用来记录需要返回的位置
while (*flag != '\0') {
s1 = flag;
s2 = str2;
while ( (*s1 == *s2) && (*s1 != '\0') && (*s2 != '\0') ) {
s1++;
s2++;
}
if (*s2 == '\0') {
// s2 走到字符串末尾,说明子串已经被找到
return flag;
}
flag++;
}
return NULL;
}
int main() {
char arr1[] = "abcdefxyxyzwe";
char arr2[] = "bcd";
char arr3[] = "xyz";
char arr4[] = "xyw";
printf("%s\n", my_strstr(arr1, arr2));
printf("%s\n", my_strstr(arr1, arr3));
printf("%s\n", my_strstr(arr1, arr4));
return 0;
}
// 输出结果:
// bcdefxyxyzwe
// xyzwe
// (null)
7. strtok 函数
strtok - 字符串分割函数
char* strtok ( char* str, const char* sep );
// 返回值:返回被分割的标记指针,若字符串被分割没有更多的标记,则返回 NULL
// 参数:str 为目标字符串,sep 为标记分隔符
使用示例1
经过上面的程序,可以得出结论:
① 字符串的内容经 strtok 函数分割后,会被改变,标记分隔符处会被改变为 ‘\0’.
② 当 strtok 函数的第一个参数不为 NULL 时,则会找到字符串的第一个起始指针;当 strtok 函数的第一个参数为 NULL 时,则表示从分隔符后继续寻找。
分割过程如下:
hello | world , 你好 | 世界
hello \0 world , 你好 | 世界 -> return 'h' 的地址
hello \0 world \0 你好 | 世界 -> return 'w' 的地址
hello \0 world \0 你好 \0 世界 -> return '你' 的地址
-> return NULL
使用示例2
#include <stdio.h>
#include <string.h>
int main() {
char arr[] = "hello|world,你好|世界";
const char* sep = ",|";
char* str = NULL;
for (str = strtok(arr, sep); str != NULL; str = strtok(NULL, sep)) {
printf("%s\n", str);
}
printf("\n%s\n", arr);
return 0;
}
// 输出结果:
// hello
// world
// 你好
// 世界
三、字符函数
使用示例
#include <stdio.h>
#include <ctype.h>
int main() {
char ch = 'A';
int ret = islower(ch);
printf("%d\n", ret); // 0
printf("%c\n", tolower(ch)); // a
return 0;
}
四、内存操作函数
注意事项:
内存操作函数,顾名思义,它是对内存操作的,换言之,它就是以字节为单位来操作数据的,明白这一点很重要。单位既不是字符、也不是整型…
1. memset 函数
memset - memory set - 内存设置函数 - 以字节为单位设置数据
void* memset( void* dest, int ch, size_t count );
// 返回值:dest 的副本
// 参数:dest 表示需要操作的起始指针位, ch 表示需要填充的数据, count 表示需要设置字节的数量
使用示例
将 arr 数组的前 8 个字节分别设置成 1;千万不要理解成将前两个元素设置成 1.
2. memcmp
memcmp - memory compare - 内存比较函数 - 以字节为单位比较两个数据
int memcmp( const void* str1, const void* str2, size_t count );
// 返回值:
// 第一个数据大于第二个数据,则返回大于 0 的数字
// 第一个数据等于第二个数据,则返回 0
// 第一个数据小于第二个数据,则返回小于 0 的数字
// 参数:str1 为第一个数据的起始指针,str2 为第二个数据的起始指针, count 表示需要比较的字节数
使用示例
#include <stdio.h>
#include <string.h>
int main() {
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 1,2,3,4,0x11223305 };
int ret1 = memcmp(arr1, arr2, 16);
int ret2 = memcmp(arr1, arr2, 18);
printf("%d, %d\n", ret1, ret2);
return 0;
}
// 输出结果: 0, -1
// 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00 (十六进制 arr1)
// 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 33 22 11 (十六进制 arr2)
3. memcpy 函数
memcpy - memory copy - 内存拷贝函数 - 以字节为单位拷贝数据
void* memcpy ( void* destination, const void* source, size_t count );
// 返回值:destination 的副本
// 参数:destination 为目标空间、source 为源数据、count 表示需要拷贝的字节数
memcpy 函数的使用思想和 strncpy 基本一致,但 memcpy 更加强大,它能够针对各种数据类型进行拷贝,只要涉及内存的,都可以。
使用示例
4. memmove 函数
memmove 函数和 memcpy 使用的思想基本相同,但早期的 memcpy 在使用时,若源空间和目标空间重叠,就可能出现数据覆盖的情况,所以才有 memmove 的诞生。然而,现在两者几乎没有区别,因为主流的 VS 编译器已经将两者实现差不多了。