字符串函数的秘密
文章目录
- 字符串函数的秘密
- 1.浅谈字符串(c)
- 2.常用字符串函数介绍及部分的模拟实现
- 2.1 [strlen](https://cplusplus.com/reference/cstring/strlen/?kw=strlen)
- 2.2 [strcpy](https://cplusplus.com/reference/cstring/strcpy/)
- 2.3 [strcat](https://cplusplus.com/reference/cstring/strcat/)
- 2.4 [strcmp]([strcmp - C++ 参考 (cplusplus.com)](https://cplusplus.com/reference/cstring/strcmp/))
- 2.4 [strncpy]([strncpy - C++ 参考 (cplusplus.com)](https://cplusplus.com/reference/cstring/strncpy/)),[strncat]([strncat - C++ 参考 (cplusplus.com)](https://cplusplus.com/reference/cstring/strncat/)),[strncmp]([strncmp - C++ 参考 (cplusplus.com)](https://cplusplus.com/reference/cstring/strncmp/?kw=strncmp))
- 2.5 [strstr](https://cplusplus.com/reference/cstring/strstr/)
- 2.6 [strtok](https://cplusplus.com/reference/cstring/strtok/)
- 2.7 [strerror]([斯特雷罗尔 - C++参考 (cplusplus.com)](https://cplusplus.com/reference/cstring/strerror/?kw=strerror))
- 2.8 字符分类函数
- 2.9 [memcpy](https://cplusplus.com/reference/cstring/memcpy/?kw=memcpy)
- 2.10 [memmove](https://cplusplus.com/reference/cstring/memmove/)
- 毒鸡汤
1.浅谈字符串(c)
C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在
常量字符串中或者字符数组 中。
字符串常量适用于那些对它不做修改的字符串函数.
2.常用字符串函数介绍及部分的模拟实现
2.1 strlen
返回 C 字符串 str 的长度。
C 字符串的长度由终止空字符确定:C 字符串的长度与字符串开头和终止空字符之间的字符数一样长(不包括终止空字符本身)。
这不应与保存字符串的数组的大小相混淆。例如:
char mystr[100]=“test string”;
定义了一个大小为 100
char
s 的字符数组,但用于初始化 mystr 的 C 字符串的长度只有 11 个字符。因此,当sizeof(mystr) 的
计算结果为100
时,strlen(mystr)
返回11
。
标准库的声明
size_t strlen ( const char * str );
-
字符串已经 ‘\0’ 作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包
含 ‘\0’ )。 -
参数指向的字符串必须要以 ‘\0’ 结束。
-
注意函数的返回值为size_t,是无符号的( 易错 )
模拟实现:strlen的三种模拟实现
2.2 strcpy
标准库的声明
char * strcpy ( char * destination, const char * source );
-
将源指向的 C 字符串复制到目标所指向的数组中,包括终止空字符(并在该点停止)。
为避免溢出,目标所指向的数组的大小应足够长,以包含与源相同的 C 字符串(包括终止空字符),并且内存中不应与源重叠。
-
源字符串必须以 ‘\0’ 结束。
-
会将源字符串中的 ‘\0’ 拷贝到目标空间。
-
目标空间必须足够大,以确保能存放源字符串
-
目标空间必须可变。
模拟实现
char* my_strcpy( char* dest, const char* src) {
assert(dest && src);
char* ret = dest;
while (*dest++ = *src++);
return ret;
}
2.3 strcat
将源字符串的副本追加到目标字符串。目标中的终止空字符被源的第一个字符覆盖,并且由目标中两者的串联形成的新字符串的末尾包含一个空字符。
目的地和来源不得重叠。
标准库声明
char * strcat ( char * destination, const char * source );
- 源字符串必须以 ‘\0’ 结束。
- 目标空间必须有足够的大,能容纳下源字符串的内容
- 目标空间必须可修改。
模拟实现
char* my_strcat(char* dest, const char* src) {
assert(dest && src);
char* ret = dest;
while (*dest) {
dest++;
}
while (*dest++ = *src++);
return ret;
}
2.4 [strcmp](strcmp - C++ 参考 (cplusplus.com))
将 C 字符串 str1 与 C 字符串 str2 进行比较。
此函数开始比较每个字符串的第一个字符。如果它们彼此相等,则继续使用以下对,直到字符不同或达到终止空字符。(比较的时字符的ASCII值得大小)
标准库说明
int strcmp ( const char * str1, const char * str2 );
返回值 表明 0 两个字符串的内容相等 ,<0 第一个不匹配的字符在 ptr1 中的值低于在 ptr2 中的值 >0 第一个不匹配的字符在 ptr1 中的值大于在 ptr2 中的值
模拟实现
int my_strcmp(const char* str1, const char* str2) { assert(str1 && str2); while (*str1 == *str2) { if (*str1 == '\0') { return 0; } str1++; str2++; } if (*str1 > *str2) { return 1; } else { return -1; } }
2.4 [strncpy](strncpy - C++ 参考 (cplusplus.com)),[strncat](strncat - C++ 参考 (cplusplus.com)),[strncmp](strncmp - C++ 参考 (cplusplus.com))
这三个函数与前面提到得strcpy,strcat,strcmp可以说是一一对应得,唯一的得区别时他们多了一个参数size_t num;这个参数可以说是约束这三个函数得,因为这三个函数在操作内存是是没有限制得,举个栗子,加入我们要把一个字符串复制到一个较小得数组空间中,就会发生越界得情况,因为这个函数只有遇到’\0’才会停下里,那么我们设置size_t得目的就是防止这种情况得发生,由我们程序员自己去设置需要操作得字节数!从而可有有效避免危险得发生!
strncmp
int strncmp ( const char * str1, const char * str2, size_t num );
比较两个字符串的字符
将 C 字符串 str1 的数目与 C 字符串 str2 的数目进行比较。
此函数开始比较每个字符串的第一个字符。如果它们彼此相等,则继续使用以下对,直到字符不同,直到达到终止空字符,或者直到两个字符串中的数字字符匹配,以先发生者为准。strncpy
char * strncpy ( char * destination, const char * source, size_t num );
从字符串中复制字符
将源**的第一个字符复制到目标。如果在复制 num 个字符之前找到源 C 字符串(由空字符表示)的末尾,则用零填充目标,直到向其写入了总共 num 个字符。
如果源长于 num,则不会在目标末尾隐式追加空字符。因此,在这种情况下,不应将目标视为空终止的C字符串(读取它会使溢出)。
目标和源不得重叠(请参阅 memmove 了解重叠时更安全的替代方法)。
strncat
char * strncat ( char * destination, const char * source, size_t num );
从字符串追加字符
将源的第一个字符追加到目标,以及终止空字符。
如果源中 C 字符串的长度小于 num,则仅复制终止空字符之前的内容。
模拟实现
这3函数得实现基本与相对应得无限制函数相似,需要注意一些特殊情况下得细节,思路有了,其它得就是自由发挥了。这里我就不展示代码了,有兴趣得小伙伴可以去写一写,不算太难。
2.5 strstr
const char * strstr ( const char * str1, const char * str2 );//母串不可改变(值) char * strstr ( char * str1, const char * str2 );//母串可改变(值)
查找子字符串
返回指向 str1 中第一次出现的 str2 的指针,如果 str2 不是 str1 的一部分,则返回空指针。
匹配过程不包括终止空字符,但它止步于此。
模拟实现
char* my_strstr(const char* str1, const char* str2) { assert(str1 && str2); char* p2 = str2;//P2是记录需查找子字符串的首地址,便于后续循环更新使用 while (*str1 != '\0') { char* p1 = str1;//p1可以理解为一个活动指针,每一次当得到与母字符串的地址相等时,我们记录这个地址,并进行后续的比较 while (*p1 == *str2) { char* ret = str1;//ret记录待查找的母字符串第一个匹配的地址; if (*str1 != '\0') {//进行后续判断 p1++; str2++; } if (*str2 == '\0') {//当str达到'\0',说明匹配成功了,返回ret return ret; } } str2 = p2;//匹配失败,刷新str2的值; str1++;//str1向后移动,继续判断 } return NULL;//遍历了整个母字符串,没有找到,返回空指针; }
这里还有一种很厉害得算法叫kmp算法,大家可以看一下 比特大博哥
2.6 strtok
char * strtok ( char * str, const char * delimiters );
将字符串拆分为标记
对此函数的一系列调用将 str 拆分为标记,这些标记是由作为分隔符一部分的任何字符分隔的连续字符序列。
在第一次调用时,该函数需要一个 C 字符串作为 str 的参数,其第一个字符用作扫描令牌的起始位置。在后续调用中,该函数需要一个空指针,并将紧跟在最后一个令牌结束之后的位置用作扫描的新起始位置。
若要确定令牌的开头和结尾,该函数首先从起始位置扫描分隔符中未包含的第一个字符(该字符将成为令牌的开头)。然后从令牌的开头开始扫描分隔符中包含的第一个字符,该字符将成为令牌的结尾。如果找到终止空字符,扫描也会停止。
令牌的此末尾将自动替换为空字符,令牌的开头由函数返回。
在对 strtok 的调用中找到 str 的终止空字符后,对此函数的所有后续调用(以空指针作为第一个参数)将返回空指针。
找到最后一个令牌的点由函数在内部保留,以便在下一次调用时使用(不需要特定的库实现来避免数据争用)。
模拟实现:
static char* ret2 = NULL;//设置一个全局的指针变量;记录母字符串当前的位置 char* my_strtok(char* str1, const char* str2) { if (str1!=NULL) { char* ret = str1; char* p2 = str2;//理解为一个活动指针,记录起始位置 begin: while (*str1 != '\0') { while (*str1 != *p2) {//一一匹配在当前str1指向得情况下,是否有匹配字符 if (*p2 != '\0') { p2++; } else { str1++; p2 = str2; goto begin; } } *str1 = '\0'; ret2 = str1; return ret; } return ret; } else {//传入参数为NULL时得情况,这种情况和第一次得情况很像,有点递归得意思,但地址变换比较复杂,要时刻记住,自己变得什么,存得什么,要复原什么,第一感觉可以递归实现,但没写出来,也没去看库函数的源码,欢迎大佬指教。 str1 = ret2+1; if (*str1 == '\0') { return NULL; } char* ret = str1; char* p2 = str2; begin1: while (*str1 != '\0') { while (*str1 != *p2) {//一一匹配在当前str1指向得情况下,是否有匹配字符 if (*p2 != '\0') { p2++; } else { str1++; p2 = str2; goto begin1; } } *str1 = '\0'; ret2 = str1; return ret; } ret2 = str1 - 1; return ret; } }
总体实现思路:分两大类情况,第一类是参数不为NULL时,在str1指向的字符串中去查找str2指向的字符串是否包含其丹个字符,第一印象就是循环的嵌套了,第一层,str1指向目标,第二层,str2指向目标,通过基本的控制语句实现简单的逻辑就可以了;到最后返回的时候,我们利用全局指针变量去记录上一个我们达到目标的位置;第二类是指针为空指针时,这时候需要我们第一次记录的地址去变换得到新的str1,后面的步骤就有点类似了;但是要注意一些细节,特殊情况的考虑;但由余小编水平有限,可能会有很多考虑不周的情况,大家有其它好的想法也可以评论区留言哦。
附一张vs2019测试图:
2.7 [strerror](斯特雷罗尔 - C++参考 (cplusplus.com))
char * strerror ( int errnum );
获取指向错误消息字符串的指针
解释 errnum 的值,生成一个字符串,其中包含一条消息,该消息描述错误条件,就好像库的函数设置为 errno 一样。
返回的指针指向静态分配的字符串,程序不得修改该字符串。对此函数的进一步调用可能会覆盖其内容(不需要特定的库实现来避免数据争用)。
strerror
生成的错误字符串可能特定于每个系统和库实现
2.8 字符分类函数
2.9 memcpy
void * memcpy ( void * destination, const void * source, size_t num );
复制内存块
将 num 字节值从源指向的位置直接复制到目标指向的内存块。
源指针和目标指针所指向的对象的基础类型与此函数无关;结果是数据的二进制副本。
该函数不检查源中是否有任何终止空字符 - 它始终精确地复制数字字节。
为避免溢出,目标参数和源参数所指向的数组的大小应至少为 num 个字节,并且不应重叠(对于重叠的内存块,memmove 是一种更安全的方法)。
- 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
- 这个函数在遇到 ‘\0’ 的时候并不会停下来
- 如果source和destination有任何的重叠,复制的结果都是未定义的
模拟实现:
void my_memcpy(void* dest, const void* src, size_t num) {//需要用到void*,因为我们不知道用户需要操作什么类型的数据 while (num--) { *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } }
2.10 memmove
void * memmove ( void * destination, const void * source, size_t num );
移动内存块
将 num 字节值从源指向的位置复制到目标指向的内存块。复制就像使用中间缓冲区一样进行,从而允许目标和源重叠。
源指针和目标指针所指向的对象的基础类型与此函数无关;结果是数据的二进制副本。
该函数不检查源中是否有任何终止空字符 - 它始终精确地复制数字字节。
为避免溢出,目标参数和源参数所指向的数组的大小应至少为 num 个字节。
模拟实现:
void my_memmove(void* dest, const void* src, size_t num) { char c[100];//设置缓冲池 my_memcpy(c, src, num); my_memcpy(dest, c, num); }
毒鸡汤
念念不忘,必有回响,知识也一样,时常想起积累的小知识点,总有一天你会发现自己的思维居然真的能实现成代码,但在实现的路上可能会有无数bug,这也是见怪不怪的,哈哈。但总的来说就是思维不清晰或者基础知识不扎实,前者靠锻炼,后者靠积累,大家加油!最后关注小编不迷路,后期不定时更新c的知识哦!