文章目录
前言
字符串就是一串零个或多个字符,以一个ASCIIC码为0的‘\0’字符结尾,\0是字符串的终止符,它并不是字符串的一部分。语言中对字符和字符串的处理很是频繁,但是C语言本身是没有显式的字符串数据类型的,因为字符串通常以字符串常量的形式出现或者存储于字符数组中。字符串常量适用于那些对它不做修改的字符串函数。
头文件string.h包含了字符串函数所需的原型和声明。
库函数介绍和模拟实现
求字符串长度的字符串函数
strlen
-
函数功能:求一个字符串的长度,就是它所包含的字符个数(不包括\0)。
-
函数原型
size_t strlen(const char* string);
- 模拟实现
#include<stdio.h>
#include<assert.h>//断言所需头文件
size_t my_strlen(const char* str)
{
assert(str);//断言
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char str[20] = "hello world";
int len = my_strlen(str);
printf("%d\n", len);
return 0;
}
- 注意事项
-
参数指向的字符串必须要以 ‘\0’ 结尾。
-
注意函数的返回值为size_t,是无符号的( 易错 )
#include <stdio.h>
#include <string.h> //如果不包含头文件可能会得到相反的结果
int main()
{
const char*str1 = "abcdef";
const char*str2 = "bbb";
if(strlen(str2)-strlen(str1)>0)//结果为无符号数,永远大于等于0
{
printf("str2>str1\n");
}
else
{
printf("srt1>str2\n");
}
return 0;
}
长度不受限制的字符串函数
最常用的字符串函数都是“不受限制”的,就是说它们只是通过寻找字符串参数结尾的\0来判断字符串的长度。
strcpy
- 函数功能:
把参数src字符串复制到参数dest,如果这两个参数在内存中出现重叠,其结果是未定义的。
- 函数原型
char* strcpy(char* dest, const char* src);
返回目标字符数组的首地址
- 模拟实现
#include<stdio.h>
#include<assert.h>//断言所需头文件
char* my_strcpy(char* dest, const char* sorc)
{
char* ret = dest;
assert(dest);
assert(sorc);
while (*dest++ = *sorc++)
{
;
}
return ret;
}
int main()
{
char str1[20] = "xxxxxxxxxxxx";
char str2[10] = "hello";
printf("%s\n", my_strcpy(str1, str2));//链式访问
return 0;
}
- 注意事项
- 必须保证目标字符数组的空间能够容纳需要复制的字符串,否则将覆盖数组后面的内存空间。
- 源字符串必须以 ‘\0’ 结束,以便能够结束拷贝。
- 会将源字符串中的 ‘\0’ 拷贝到目标空间
strcat
-
函数功能:把一个字符串连接到另一个字符串的后面。strcat函数找到dest参数字符串的末尾,并将src字符串的一份拷贝添加到这个位置,如果src和dest的位置发生重叠,其结果是未定义的。
-
函数原型
char* strcat(char* dest, const char* src);
返回目标字符数组的首地址
- 模拟实现
#include<stdio.h>
#include<assert.h>//断言所需头文件
char* my_strcat(char* dest, const char* sorc)
{
assert(dest);
assert(sorc);
char* ret = dest;
while (*dest) //找到目标字符串末尾
{
dest++;
}
while (*dest++ = *sorc++)//开始字符串拷贝
{
;
}
return ret;
}
int main()
{
char str1[20] = "xxxxx";
char str2[10] = "hello";
printf("%s\n", my_strcat(str1, str2));
return 0;
}
- 注意事项
-
源字符串必须以 ‘\0’ 结束。
-
目标空间必须有足够的大,能容纳下源字符串的内容。
strcmp
- 函数功能:对两个字符串对应的字符逐个进行比较(比较ASCII码值),直到发现不匹配为止。
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字
- 函数原型
int strcmp(const char* str1, const char* str2);
- 模拟实现
#include<stdio.h>
#include<assert.h>//断言所需头文件
int my_strcmp(const char* str1, const char* str2)
{
assert(str1);
assert(str2);
while (*str1 == *str2)
{
if (*str1 == '\0')
return 0;
str1++;
str2++;
}
return *str1 - *str2;
}
int main()
{
char str1[10] = "abcd";
char str2[10] = "abcd";
printf("%d\n", my_strcmp(str1, str2));
return 0;
}
- 注意事项
- 不要写出下面的表达式
if(strcmp(a, b))
误认为两个字符串相等,结果会是真,事实恰恰相反
- 不要误以为返回值只有1,0,-1
长度受限制的字符串函数
这些函数接受一个显式的长度参数,用于限定进行复制或比较的字符个数。
strncpy
-
函数功能:拷贝num个字符从源字符串到目标空间(结果不会包含\0)。
如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加\0,直到num个。
-
函数原型
char* strncpy(char* dest, const char* src, size_t num);
- 模拟实现
char* my_strncpy(char* dest, const char* src, size_t num)
{
char* ret = dest;
assert(dest && src);
while (num--)
{
if (*dest = *src)
{
dest++;
src++;
}
else
{
dest++;
}
}
return ret;
}
int main()
{
char dest[20] = "xxxxxxxxxxxxxx";
char src[10] = "hello";
printf("%s\n", my_strncpy(dest, src, 7));
return 0;
}
strncat
- 函数功能:将源字符串的前num个字符附加到目标字符串,加上一个终止空字符\0。
如果源字符串中字符串的长度小于num,则只复制直到终止符空字符的内容。
- 函数原型
char* strncat(char* dest, const char* src, size_t num);
- 模拟实现
char* my_strncat(char* dest, const char* src, size_t num)
{
assert(dest && src);
char* ret = dest;
while (*dest != '\0')
{
dest++;
}
while (num--)
{
*dest = *src;
if (*src == '\0')
return ret;
dest++;
src++;
}
*dest = '\0';
return ret;
}
int main()
{
char dest[20] = "hello\0xxxxxxxxx";
char src[10] = "world";
printf("%s\n", my_strncat(dest, src, 7));
return 0;
}
strncmp
-
函数功能:最多比较两个字符串num个字符
-
函数原型
int strncmp(const char* str1, const char* str2, size_t num);
字符串查找函数
strstr
-
函数功能:在str1中查找整个str2第一次出现的位置,并返回指向该位置的指针,如果没找到就返回空指针。
-
函数原型
char* strstr(const char* str1, const char* str2);
- 模拟实现
#include<stdio.h>
#include<assert.h>//断言所需头文件
char* my_strstr(const char*str1, const char*str2)
{
assert(str1);
if (str2 == NULL)
{
return str1;
}
char* ret = str1;
while (ret)
{
char* s1 = ret;
char* s2 = str2;
while (*s1 && *s2 && *s1 == *s2)
{
s1++;
s2++;
}
if (*s2 == '\0')
return ret;
else if (*s1 == '\0')
return NULL;
ret++;
}
return NULL;
}
int main()
{
char str1[10] = "abcdbbab";
char str2[10] = "abb";
printf("%s\n", my_strstr(str1, str2));
return 0;
}
strtok
-
函数功能:字符串分割,把一个字符串按照分割标志分割为几个字符串。
-
函数原型
char* strtok(char* string, const char* sep);
-
sep参数是个字符串,定义了用作分隔符的字符集合
-
第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标
记。
-
strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。
-
strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串
中的位置。
- strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标
记。
- 如果字符串中不存在更多的标记,则返回 NULL 指针。
- 函数使用
#include <stdio.h>
#include <string.h>
int main()
{
char email[] = "1234567890@qq.com";
char* sep = "@.";
char tmp[20] = "";
//由于strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都会临时拷贝一份,操作拷贝的数据
strcpy(tmp, email);
printf("%s\n", strtok(tmp, sep)); //第一次第一个参数传递被切割字符串的首地址
printf("%s\n", strtok(NULL, sep)); //第二次及以后第一个参数传递空指针(strtok会记住上一次切割的位置)
printf("%s\n", strtok(NULL, sep));
return 0;
}
当我们不知道目标字符串的内容时,我们就不知道调用几次strtok,但我们可以利用for循环的判断条件来限制调用strtok函数的次数,同时我们知道,strtok函数第一次调用时需要传递目标字符串的地址,其余调用都只需要传递NULL即可,那我们可以把第一次调用当作for循环的初始化条件,一箭双雕。
#include<stdio.h>
#include<string.h>
int main()
{
char str[20] = "you.know@who,I am";
char sep[10] = ".@, ";
char temp[20] = { 0 };//临时拷贝
strcpy(temp, str);
for (char* ret = strtok(temp, sep); ret != NULL; ret = strtok(NULL, sep))
{
printf("%s\n", ret);
}
return 0;
}
错误信息报告
strerror
-
函数功能:C语言有一系列的库函数,当这些库函数调用失败时,会返回相应的错误码,操作系统把错误码存在一个外部的整型变量errno中的,而strerror函数的作用就是获取错误码对应的错误信息的首地址,使用者可以通过这个地址把错误信息打印出来。
-
函数原型
char* strerror(int error_number);
- 函数使用
#include <stdio.h>
#include <string.h>
#include <errno.h>//必须包含的头文件
int main ()
{
FILE * pFile;
pFile = fopen ("unexist.ent","r");
if (pFile == NULL)
printf("Error opening file unexist.ent: %s\n",strerror(errno));
return 0;
}
printf("Error opening file unexist.ent: %s\n",strerror(errno));
等价于
perror("Error opening file unexist.ent: ") //自动获取错误码,自动打印错误信息,省时又省心
字符函数
使用字符函数的程序比那些自己执行字符测试和转换的程序更具有移植性。
字符分类函数
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 |
字符转换函数
int tolower(int c) | 返回对应小写字母的ASCII值 |
---|---|
int toupper(int c) | 返回对应大写字母的ASCII值 |
内存操作函数
根据定义,字符串由一个\0字符结尾,所有字符串内部不能包含任何\0字符。但是非字符串类型的数据内部包含零值的情况并不少见,你无法用字符串函数来处理这类数据,不过我们可以使用另外一组相关的函数,它们的操作和字符串函数类似,但是这些函数能够处理任意类型的数据。
memcpy
- 函数功能:函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
- 函数原型
void* memcpy(void* dest, const void* src, size_t num);
- 模拟实现
#include<stdio.h>
#include<string.h>
#include<assert.h>
void my_memcpy(void* dest, const void* src, size_t num)
{
assert(dest && src);
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
int main()
{
int num1[10] = { 0 };
int num2[10] = { 1,2,3,4,5,6,7,8,9,0 };
my_memcpy(num1, num2, 21);
//memcpy(num1, num2, 20);
for (int i = 0; i < 10; i++)
{
printf("%d ", num1[i]);
}
return 0;
}
- 注意事项
-
这个函数在遇到 ‘\0’ 的时候并不会停下来。
-
如果source和destination有任何的重叠,复制的结果都是未定义的。
-
从上面我们memcpy的模拟实现中也可以看出,memcpy是从前向后拷贝的,这就导致在拷贝重叠内存数据时会发生数据覆盖,但是在VS下的memcpy函数是具备拷贝重叠数据的能力的,也就是说,VS下的memcpy函数同时实现了memmove函数的功能,但是其他编译器下的memcpy函数是否也具备memmove函数功能是未知的,所以我们在处理重叠内存数据拷贝的时候尽量还是使用memmove函数,以免发生错误。
memmove
-
函数功能:内存移动,将一块内存数据中的内容移动覆盖至另一块内存数据,常用来处理重叠内存数据的拷贝。
-
函数原型
void* memmove(void* dest, const void* src, size_t num);
- 模拟实现
#include<stdio.h>
#include<assert.h>//断言所需头文件
#include<string.h>
void my_memmove(void* dest, const void* src, size_t num)
{
assert(dest && src);
if (dest < src)
{
//从前向后拷贝
for (int i = 0; i < num; i++)
{
*((char*)dest + i) = *((char*)src + i);
}
}
else if(dest > src)
{
//从后向前拷贝
while (num--)
{
*((char*)dest + num) = *((char*)src + num);
}
}
}
int main()
{
int num[10] = { 1,2,3,4,5,6,7,8,9,0 };
my_memmove(num + 2, num, 20);
//my_memmove(num, num + 3, 20);
//memmove(num + 2, num, 20);
for (int i = 0; i < 10; i++)
{
printf("%d ", num[i]);
}
return 0;
}
- 注意事项
memmove函数的行为和memcpy差不多,只是它的源和目标操作数可以重叠,按理说memmove的功能完全覆盖了memcpy,memcpy为什么没有被完全取代呢?因为memmove通常无法使用某些机器提供的特殊的字节-字符串处理指令来实现,所以它可能比memcpy慢一些。
memset
-
函数功能:内存设置,把一块内存中num个字节的内容设置为指定的数据。
-
函数原型
void* memset(void* dest, int n, size_t num);
// void* 函数返回值,返回目标空间的地址;
// int n 函数参数,指定你想要初始化的数据;
// size_t num 函数参数,指定初始化的字节数
- 模拟实现
#include <stdio.h>
#include <assert.h>
void* my_memset(void* dest, int n, size_t num)
{
assert(dest != NULL);
void* ret = dest; //记录目标空间的地址
while (num--) //循环将num个字节的内容初始化为指定值
{
*(char*)dest = n;
dest = (char*)dest + 1;
}
return ret;
}
int main()
{
char str[] = "hello world";
my_memset(str, 'x', 5);
printf("%s\n", str);
return 0;
}
memcmp
-
函数功能:内存比较,比较两块内存中前num个字节的大小。
-
函数原型
int memcmp(const void* a, const void* b, size_t num);
- 模拟实现
#include <stdio.h>
#include <assert.h>
int my_memcmp(const void* a, const void* b, size_t num)
{
assert(a && b);
size_t count = 0; //用来标记相等的字节数
while (*(char*)a == *(char*)b && count < num) //一直循环,直到找到不相等的字节数据
{
count++; //进入一次循环表示有一对字节相等,count++
a = (char*)a + 1;
b = (char*)b + 1;
}
if (count < num) //如果count小于num,说明两块内存的前num个字节中有不相等的数据
return *(char*)a - *(char*)b;
else
return 0;
}
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 1,2,3 };
int ret = my_memcmp(arr1, arr2, 3 * sizeof(int));
printf("%d\n", ret);
return 0;
}
提醒
- 尽管你能够自己模拟实现库函数,但不要自己编写相同功能的函数试图取代库函数。
- 使用字符分类和转换函数可以提高函数的移植性。
- strtok函数是会修改它所处理的字符串
- strtok函数在连续的几次调用中,即使它们的参数相同,其结果也可能不同
- 在表达式中不要混用有符号数和无符号数