目录
注:
长度受限制的字符串函数
strlen
工作原理
实例
模拟实现strlen函数
strcpy
工作原理
解析工作原理
strcat - 字符串追加/连接
工作原理
实例
模拟实现
注意
strcmp - 字符串比较函数
工作原理
实例
模拟实现
优化my_strcmp函数
长度受限制的字符串函数
strncpy
实例
strncat
实例
strncmp
实例
strstr - 寻找字符串的子串
实例
模拟实现
strtok - 切割字符串
实例
strerror
实例
注:
本笔记参考B站up鹏哥C语言的视频
长度受限制的字符串函数
strlen
工作原理
- 字符串 ' ' 作为结束标志,strlen函数返回的是字符串中 ' ' 前面出现的字符串个数(不包含 ' ' )。
- 参数指向的字符串必须要以 ' ' 结束。
- 注意函数的返回值是size_t,是无符号的(unsigned int)。
解释第3点
int main()
{
if (strlen("abc") - strlen("abcdef") > 0)
{
printf(">n");
}
else
{
printf("<=n");
}
return 0;
}
打印结果为:>
分析:这里是无符号整型相减,虽然 3 - 6 = -3 ,但这里的 -3 的补码会直接被解析成原码,故-3在被解析后就是一个很大的正数。
实例
#include
#include
int main()
{
char arr[] = "abc";
int len = strlen(arr);
printf("%dn", len);
return 0;
}
- 字符串 ' ' 作为结束标志,strlen函数返回的是字符串中 ' ' 前面出现的字符串个数(不包含 ' ' )。
- 参数指向的字符串必须要以 ' ' 结束。
- 注意函数的返回值是size_t,是无符号的(unsigned int)。
解释第3点
int main()
{
if (strlen("abc") - strlen("abcdef") > 0)
{
printf(">n");
}
else
{
printf("<=n");
}
return 0;
}
打印结果为:>
分析:这里是无符号整型相减,虽然 3 - 6 = -3 ,但这里的 -3 的补码会直接被解析成原码,故-3在被解析后就是一个很大的正数。
#include#include int main() { char arr[] = "abc"; int len = strlen(arr); printf("%dn", len); return 0; }
打印结果是 3 。此时没有计算 ' ' 。
而如果将数组arr改为:char arr[] = { 'a', 'b', 'c' }; 此时数组内没有放入 ' ' ,打印结果就是随机值。
这里之所以是74,是因为strlen函数在内存中持续往后查找,在某个位置找到了 ' ' 。
模拟实现strlen函数
提供一种写法
#include#include int my_strlen(const char* str)//加上const,使代码更加“健壮” { assert(str != NULL);//记得断言 int count = 0;//计算器 while (*str != ' ') { count++; str++; } return count; } int main() { char arr[] = "abc"; int len = my_strlen(arr); printf("%dn", len); return 0; }
strcpy
工作原理
把指针变量source所指向的空间内的数据拷贝到指针变量destination所指向的空间内。
- 源字符串(source)必须以 ' ' 结束。
- 会将源字符串(source)中的 ' ' 拷贝到目标空间。
- 目标空间必须足够大,以确保能存放源字符串(source)。
- 目标空间必须可变。
存放字符串的错误示范
int main()
{
char arr[20] = { 0 };
arr = "Hello";
return 0;
}
这是行不通的,因为 arr 是数组首元素的地址。
解析工作原理
- 源字符串必须以 ' ' 结束。
- 会将源字符串中的 ' ' 拷贝到目标空间。
把指针变量source所指向的空间内的数据拷贝到指针变量destination所指向的空间内。
- 源字符串(source)必须以 ' ' 结束。
- 会将源字符串(source)中的 ' ' 拷贝到目标空间。
- 目标空间必须足够大,以确保能存放源字符串(source)。
- 目标空间必须可变。
存放字符串的错误示范
int main()
{
char arr[20] = { 0 };
arr = "Hello";
return 0;
}
这是行不通的,因为 arr 是数组首元素的地址。
- 源字符串必须以 ' ' 结束。
- 会将源字符串中的 ' ' 拷贝到目标空间。
使用strcpy的正确示范
#include#include int main() { char arr[20] = "#########"; strcpy(arr, "Hello");//字符串"Hello"在被使用时,使用的是"H"的地址 printf("%sn", arr); return 0; }
打印结果:
执行调试,看见
这里 ' ' 也被带过去了。
---
如果被拷贝的字符串没有 ' ' 结尾,如:
#include#include int main() { char arr_1[20] = "#########"; char arr_2[] = { 'a', 'b', 'c' }; strcpy(arr_1, arr_2); printf("%sn", arr_1); return 0; }
执行代码,发现
程序挂了。
分析:数组arr_2最后没有 ' ' 结尾,strcpy函数会从数组arr_2的首地址出发,寻找 ' ' ,在找到 ' ' 之前,无法确认strcpy函数在内存中找到了什么。
---
- 目标空间必须足够大,以确保能存放源字符串。
还有一种情况
#include#include int main() { char arr_1[5] = "#####"; char* p = "Hello World"; strcpy(arr_1, p); printf("%sn", arr_1); return 0; }
这种情况执行代码会出现
这是因为目标数组(arr_1)空间太小,强行将字符串拷贝后发生溢出,但是程序崩溃了 - 数组arr_1被撑爆了。
---
- 目标空间必须可变。
#include#include int main() { char* str = "xxxxxxxxxxxxxxxx"; char* p = "Hello World"; strcpy(str, p); printf("%sn", str); return 0; }
执行代码,发现:
程序崩溃了。
分析:目标空间必须可以修改。指针变量str内存放的是一个常量字符串的地址,常量字符串无法修改。
strcat - 字符串追加/连接
工作原理实例把指针变量source所指向的空间内的数据追加到指针变量destination所指向的空间内。
- 源字符串(source)必须以 ' ' 结束。
- 目标空间必须足够大,能容纳下源字符串(source)的内容。
- 目标空间必须可以修改。
- 字符串无法自己追加自己。
#include#include int main() { char arr_1[20] = "Hello "; char arr_2[] = "World"; strcat(arr_1, arr_2);//字符串追加/连接 printf("%sn", arr_1); return 0; }
打印结果为:
运行调试,发现
注意,原本的数组arr_1内存放的那个 ' ' 没有了。
分析:
一种测试方法(测试是否会追加arr_2中的 ' ' )
将数组arr_1改为 char arr_1[20] = "Hello ##########"; 注意:arr_1内原本就有一个 ' ' ,这里多放了一个 ' ' 进去,并且在后面追加了#号,如果strcat函数执行,如果拿取了arr_2的 ' ' ,则arr_1中的一个#号会被覆盖掉。
现在开始调试,发现:
说明数组arr_2的 ' ' 也会被抓取,同时,这也说明指针变量source所指向的空间内必须存在 ' ' 。
模拟实现
分析:
参考库函数写法
void my_strcat(char* dest,const char* src)
src指向的空间不需要改变,加入const 。
注意:
字符数组被初始化后,如果存在没有被初始化的部分,这部分会自动被放入 ' ' 。
对于指针变量dest和src,存在
要把src指向的 'W' 追加到dest指向的 ' ' 的位置上:
- 找到目标字符串(arr_1)中的' ' ;
- 源数据(arr_2)追加过去,包含' ' 。
- 注意返回类型,返回目标空间(destion)的起始地址。
#include#include char* my_strcat(char* dest,const char* src) { char* ret = dest; assert(dest && src); //找到目标字符串(arr_1)中的' ' while (*dest) { dest++; } //while循环停下时,dest指向 ' ',即0 //源数据(arr_2)追加过去,包含' ' while (*dest++ = *src++) { ; } //当*++dest = *src++拿取' '时,表达式的结果就是' ' return ret;//返回目标空间的起始地址 } int main() { char arr_1[20] = "Hello ##########"; char arr_2[] = "World"; printf("%sn", my_strcat(arr_1, arr_2)); //由于返回值(char*),所以可以直接打印 return 0; }
库函数的实现参考
注意
对strcat函数而言,无法将目标字符串拷贝到其自身后面,如:
int main()
{
char arr[20] = "abcd";
strcat(arr, arr);
printf("%sn", arr);
return 0;
}
运行代码,发现
代码挂掉了。
原因:数组arr内原本被放入的是 'a' 'b' 'c' 'd' ' ' ,在追加字符串时,字符'a'会把原本的 ' ' 覆盖掉,导致最后无法找到 ' ' ,又开始拿取 字符'a',陷入死循环。
(ps:笔者在VS2022上用64位环境测试该库函数时,代码没有挂掉。而如果测试自己编写的my_strcat函数,发现代码挂掉了,推测这里是编译器本身导致的问题。)
以下为一些推测
开始调试,打开反汇编和寄存器
当执行到这一步时,在寄存器rdx和rcx内部分别存入了一个数组arr首元素的地址
接下来继续执行反汇编
此时再看寄存器
此时寄存器RCX存储的地址变成了0000000000000004,而寄存器RDX存储的地址与原本相比,向后了4个字节,打开内存
ASCII码的61对应的数值就是字符'a',而原本字符'a'所在的位置应该在往前4个字节,
这说明此时已经复制成功。再看寄存器
发现寄存器R11存储了数组arr首元素的地址,结合源代码,也就是说,R11应该对应返回的地址。
而此时寄存器R9和R10也被调用了(如果使用my_strcat函数,这两个寄存器是不会被调用的。)
查询资料分析寄存器R10被用作数据存储,在使用之前会保存原值。
------
猜测:在函数strcat被调用时,编译器会先在寄存器R10内保存数组arr内的数据,在数组arr被改变时,source实际是调用了寄存器R10内的数据,被改变的数组本身则在寄存器R11内被存储起来,这样就达成了函数执行的目的。
strcmp - 字符串比较函数
工作原理实例字符串比较和长度无关,两个字符串从首地址开始比较:
▲如果字符相同,比较下一位;
▲如果字符不同,则认为该位字符的ASCII值大的字符串更大。
而如果两个字符串完全相同,则当比较完 ' ' 后,函数结束,认为两个字符串相等。
- 标准规定:
○第一个字符串大于第二个字符串,则返回大于0的数字;
○第一个字符串等于第二个字符串,则返回0;
○第一个字符串小于第二个字符串,则返回小于0的数字。注意
int main() { char* p = "OBC"; char* q = "ABCDEF"; if (p > q) printf(">n"); else printf("<=n"); return 0; }这种写法是不行的。p和q分别是指向两块不同空间的指针:
调试可以看出地址的不同。
上述代码是在比较两个地址的大小,并不是比较字符串的大小。
同理,if ("OBC" > "ABCDEF") 这种写法也是在比较字符串首地址大小,也不行。
#include#include int main() { int ret = strcmp("abbb", "abc"); printf("%dn", ret); return 0; }
程序执行结果:-1
#include#include int main() { char* p = "abcdef"; char* q = "abbb"; int ret = strcmp(p, q); if (ret > 0) { printf("p > qn"); } else if (ret < 0) { printf("p < qn"); } else { printf("p == qn"); } return 0; }
程序执行结果:
模拟实现#include#include int my_strcmp(const char* s1, const char* s2) { assert(s1 && s2); while (*s1 == *s2) { if (*s1 == '0') { return 0; } s1++; s2++; } if (*s1 > *s2) { return 1; } else { return -1; } } int main() { char* p = "abcdef"; char* q = "abbb"; int ret = my_strcmp(p, q); if (ret > 0) { printf("p > qn"); } else if (ret < 0) { printf("p < qn"); } else { printf("p == qn"); } return 0; }
分析:
当函数内部传入数据时,指针s1和s2指向首字符的地址。
首先首字符'a'进行比较,发现相同,向后寻找;字符'b'也相同;再找,找到字符'b'和'c'不同,开始比较。
优化my_strcmp函数
my_strcmp函数中
if (*s1 > *s2)
{
return 1;
}
else
{
return -1;
}
可以被改为 return *s1 - *s2;
库函数的实现参考
长度受限制的字符串函数
长度不受限制的字符串函数
- strcpy
- strcat
- strcmp
与之对应,存在长度受限制的字符串函数
- strncpy
- strncat
- strncmp
strncpy
长度不受限制的字符串函数
- strcpy
- strcat
- strcmp
与之对应,存在长度受限制的字符串函数
- strncpy
- strncat
- strncmp
实例拷贝source内开始(num)的字符到destination内。(存在字符限制 - num限制了可以拷贝的字符的个数)
三个参数
- 目标字符串(destination)的首元素地址;
- 源字符串(source)的首元素地址;
- 要拷贝的字符数目(num)。
这个函数的好处是可以控制拷贝字符的个数,可以使字符数组不容易被撑爆,不容易出现警告。
当源字符串(source)长度不满足字符数目(num)的要求时,会自动补上 ' ' 。
#include#include int main() { char arr1[20] = "abcdef"; char arr2[] = "qwer"; strncpy(arr1, arr2, 2); printf("%sn", arr1); return 0; }
打印结果:
注意
如果strncpy函数内部变成 strncpy(arr1, arr2, 6); 的形式,开始调试,发现:
这里确实拷贝了6个字符,最后两个是 ' ' 。
库函数实现参考
strncat
实例追加字符串,从源字符串(source)的第一个字符开始,追加到目标字符串(destination)的末尾。
如果源字符串(source)的长度(num)少于要求,只追加到源字符串(source) ' ' 的位置。
#include#include int main() { char arr1[20] = "Hello "; char arr2[] = "World"; strncat(arr1, arr2, 3); printf("%sn", arr1); return 0; }
打印结果:
同时,如果把长度(num)改为6,也可以正常打印:
库函数实现参考
strncmp
实例从指针str1和str2的指向字符串的首字符的地址开始比较。存在三种情况使函数结束:
- 比较字符,发现字符不同;
- 找到终止空字符 ' ' ;
- 被比较的字符数目和指定的比对数目(num)匹配。
#include#include int main() { char* p = "abcdef"; char* q = "abcqwert"; int ret = strncmp(p, q, 3); printf("%dn", ret); return 0; }
打印结果:
而如果把比对数目(num)改为4,打印结果:
strstr - 寻找字符串的子串
实例
- 如果str2是str1的子串,则返回str1中第一次出现子串str2的地址;
- 如果str2不是str1的子串,则返回一个空指针。
#include#include int main() { char arr1[] = "abcdefabcdef"; char arr2[] = "bcd"; //在arr1中查找是否包含arr2数组 char* ret = strstr(arr1, arr2); if (ret == NULL) printf("没找到n"); else printf("找到了:%sn", ret); return 0; }
打印结果:
模拟实现#include#include char* my_strstr(const char* str1, const char* str2) { assert(str1 && str2); //存储起始位置 const char* s1 = NULL; const char* s2 = NULL; char* cp = str1;//指针cp一开始是维护str1的 #include char* my_strstr(const char* str1, const char* str2) { assert(str1 && str2); //存储起始位置 const char* s1 = NULL; const char* s2 = NULL; const char* cp = str1;//指针cp一开始是维护str1的 //匹配 if (*str2 == ' ') { return (char*)str1; } while (*cp) { //赋值与回正 s1 = cp; s2 = str2; while (*s1 && *s2 && (*s1 == *s2)) { s1++; s2++; } if (*s2 == ' ') { return cp; } cp++;//cp向后找一个元素 } return NULL; } int main() { char arr1[] = "abbbcde"; char arr2[] = "bbc"; //在arr1中查找是否包含arr2数组 char* ret = my_strstr(arr1, arr2); if (ret == NULL) printf("没找到n"); else printf("找到了:%sn", ret); return 0; }
分析:
对于传来的两个地址,函数内部只进行比较,加上 const 保护数据。
在比较过程中,存在两种情况:
情况一
- 比较字符'a'和字符'c',发现字符不相等,str1继续寻找下一个元素;
- 比较字符'b'和'c',又发现不相等,继续向后寻找;
- 比较字符'c'和'c',发现相等;
- str1和str2分别向后一个元素并进行比对,发现相等;
- 继续向后比对……;
- str2找到 ' ' ,说明查找完毕,说明找到子串。
情况二
1.字符'a'和'b'不相等,str1往后寻找一个元素;
2.str1找到字符'b',字符'b'和字符'b'相等,开始向后匹配,找到:3.发现字符'b'和'c'不相等,接下来str1会向前一个元素 - 回正(即原本开始匹配位置的下一个位置),再进行一次比对(此时str2也要回正):
4.循环步骤,最后发现str2指向的字符串不是str1指向的字符串。
所以如果想要回正,就需要新的指针s1和s2。但是这样是不够的,因为这样只是解决了起始位置的问题,但是str1的回正并不一定是回到起始位置,所以还需要一个指针cp。
故函数开始时,存在:
库函数实现参考
strtok - 切割字符串
实例
- sep参数是个字符串,定义了用作分隔符的字符集合;
- 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中的一个或者多个分隔符分割的标记;
- strtok函数找到str中的下一个标记,并将其用 ' ' 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分字符串时,一般都是临时拷贝的内容,并且该内容可以被修改。)
char tmp[20] = { 0 }; strcpy(tmp, arr); char* ret = strtok(tmp, p);其中arr就是原本的字符串。
三种情况
- strtok函数的第一个参数不为NULL,函数找到str中的一个标记,strtok函数将保存它在字符串中的位置;
- strtok函数的第一个参数是NULL,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
- 如果字符串中不存在更多的标记,则返回NULL指针。
注意:strtok函数需要记录 ' ' 的位置,即需要有记录功能(静态变量 - static 修饰局部变量)。
借用cplusplus的例子
#include#include int main () { char str[] ="- This, a sample string."; char * pch; printf ("Splitting string "%s" into tokens:n",str); pch = strtok (str," ,.-"); while (pch != NULL) { printf ("%sn",pch); pch = strtok (NULL, " ,.-"); } return 0; }
代码执行的结果:
例子2
#include#include int main() { char arr[] = "255@255.255-255"; char* p = "@.-"; char tmp[20] = { 0 }; strcpy(tmp, arr); char* ret = NULL; for (ret = strtok(tmp, p); ret != NULL; ret = strtok(NULL, p)) { printf("%sn", ret); } return 0; }
打印结果:
分析:
数组tmp内原本存放:"255@255.255-255"
- 第一次使用strtok函数,数组tmp内发生改变:"255 255.255-255"
- 第二次使用strtok函数:"255 255 255-255"
- 第三次使用strtok函数:"255 255 255 255"
strerror
实例使用库函数的时候,有可能出现调用失败
调用失败时,都会设置错误码
一般地,错误码都会被放入变量 errno (int errno)
errno中一般存储着整数,需要使用函数strerror进行翻译才能变成可以查看的信息注意:errno是一个全局的错误码。想要使用,需要引用头文件
例1
#include#include #include int main() { printf("%sn", strerror(0)); printf("%sn", strerror(1)); printf("%sn", strerror(2)); printf("%sn", strerror(3)); printf("%sn", strerror(4)); return 0; }
打印结果:
例2
#include#include #include int main() { FILE* pf = fopen("test.txt", "r"); //打开文件test.txt 以只读的形式打开 if (pf == NULL)//如果文件不存在,pf就是有一个空指针 { printf("%sn", strerror(errno)); } //... //fclose(pf);//关闭文件 //pf = NULL; return 0; }
打印结果:
如果对应文件夹下没有文件test.txt,则



