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