目录
如何比较两个字符串的内容是否相等:
我们先创建代码进行尝试:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
char arr1[20] = "zhangsan";
char arr2[20] = "zhangsanfeng";
if (arr1 == arr2)
{
printf("==");
}
else
{
printf("!=");
}
return 0;
}
我们进行编译
答案是不相等,很多人就有疑问:两个字符串的确不相等啊,所以我们写的代码是对的。
究竟是对的吗?我们再进行尝试
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
char arr1[20] = "zhangsan";
char arr2[20] = "zhangsan";
if (arr1 == arr2)
{
printf("==");
}
else
{
printf("!=");
}
return 0;
}
我们把两个字符串内容写成相等的,再进行编译
可以发现,依旧是不相等,原因是什么呢?
答:arr1和arr2都是数组名,数组名表示首元素的地址,这两个数组的首元素并不是在同一个空间,所以arr1!=arr2.
我们可以发现,我们用这种方法进行比较的时候,压根比的不是字符串的内容,而是比的数组首元素的地址,所以这种方法肯定是错的。
那么究竟该如何比较两个字符串相等呢?
我们引出函数strcmp
strcmp函数的两个参数是两个字符指针,这两个字符指针对应的就是字符串首字符的地址,返回值是整型,这个返回值是什么呢?
由图标可知,当第一个字符串小于第二个字符串,返回小于0的数字
当第一个字符串等于第二个字符串,返回等于0的数字。
当第一个字符串大于第二个字符串,返回大于0的数字。
我们可以这样书写
#define _CRT_SECURE_NO_WARNINGS 1
#include<string.h>
#include<stdio.h>
int main()
{
char arr1[20] = "zhangsan";
char arr2[20] = "zhangsanfeng";
int ret = strcmp(arr1, arr2);
if (ret < 0)
printf("<\n");
else if (ret == 0)
printf("=\n");
else
printf(">\n");
return 0;
}
这里我们有一个疑问?
字符串是如何比较大小的呢?
注意:字符串比较大小的时候,比较的是内容而不是字符串的长度,例如:zhangsan和zhangsanfeng进行比较,首先比较各自的第一位,都为z,相等,再比较各自的第二位,都为h,相等-------,直到n,都相等,比较下一位,zhangsan的字符串的下一位是\0,而zhangsanfeng的字符串的下一位是f,f对应的ascii码值大于zhangsan,所以对应字符串zhangsanfeng就大于zhangsan。
我们再举一个例子,比如abcdef和abq进行比较
前两位都相等,比较第三位,因为q对应的ascii码值大于c,所以字符串abq大于字符串abcdef。
接下来,我们来模拟实现strcmp函数
#define _CRT_SECURE_NO_WARNINGS 1
#include<string.h>
#include<stdio.h>
#include<assert.h>
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;
}
int main()
{
char arr1[20] = "zhangsan";
char arr2[20] = "zhangsanfeng";
int ret = my_strcmp(arr1, arr2);
if (ret < 0)
printf("<\n");
else if (ret == 0)
printf("=\n");
else
printf(">\n");
return 0;
}
第一个字符串小于第二个字符串,所以应该是小于号
我们对代码提出一些问题:
1:const char*str1, const char*str2)为什么两个字符指针都需要加上const
答:因为我们只是要比较字符串而已,所以不想让字符串内容发生改变,让代码更加健壮。
2:assert(str1&&str2);断言函数有什么目的?
答:在进行解引用之前,一定要加上断言函数,断言函数能够防止空指针的出现,空指针不能解引用,否则直接报错。
3:为什么main函数的if else语句有三部分,而函数定义中的if else语句只有两部分
答:因为函数定义部分中的一个语句*str1==*str2在函数定义的开端,也就是说,函数只要能够运行到if else语句,就没有相等的可能性了。
我们可以对代码进行简化:我们直接删除函数定义部分的if else语句,直接返回两个字符串指针解引用的差值
int my_strcmp(const char*str1, const char*str2)
{
assert(str1&&str2);
while (*str1 == *str2)
{
if (*str1 == '\0')
return 0;
str1++;
str2++;
}
return(*str1 - *str2);
}
如图所示
strcpy
strcmp
strcat
这些函数都是长度不受限制的字符串函数,使用时会造成一些问题,例如
int main()
{
char arr1[4] = { 0 };
strcpy(arr1, "hello bit");
printf("%s", arr1);
return 0;
}
我们可以发现,我们的数组arr1只能存放四个元素,但是我们把hello bit整个都存放进去,正常情况下是存放不进去的,我们进行编译
代码也的确报错了,但是我们的显示器上依旧打印了”hello bit“,说明这个字符串已经拷贝,数组明明放不下,但是字符串依旧拷贝,造成代码越界访问,这就是这个函数的问题。
这是我们加入#define _CRT_SECURE_NO_WARNINGS 1的结果,假如我们把他屏蔽掉
程序报错,并且提示我们用strcpy_s函数,虽然这个函数相对于strcpy更加安全,但是这个函数只能够在vs上使用,所以没有可移植性,所以我们不采纳。
上面的三个函数都是长度不受限制的字符串函数,接下来,我们写一些长度受限制的字符串函数
strncpy
strncat
strncmp
我们举一个例子
我们先使用strncpy函数
我们可以发现:strncpy函数比strcpy函数的参数多了一个整型,我们写一个strncpy函数的代码
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = "abcdef";
char arr2[] = "hello bit";
strncpy(arr1, arr2, 5);
printf("%s", arr1);
return 0;
}
这里的参数5的意思是只拷贝五个字符,我们进行编译
strncpy实现了字符串拷贝,并且只拷贝了数组arr2[]的五个字符给数组arr1[]。
这时候,有人提出一个问题:假如我们的数组中的字符串只有三个字符,而我们却要拷贝五个会发生什么?
答:我们进行实验
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = "abcdef";
char arr2[] = "bit";
strncpy(arr1, arr2, 5);
printf("%s", arr1);
return 0;
}
我们进行编译
只打印了bit,原因是什么呢?我们进行调试解答。
我们可以发现,当数组元素不够时,函数会把\0拷贝到目的地数组中去,虽然我们的字符f并没有消失,但是我们提前拷贝了\0,字符串的结束标志是\0,所以只打印了bit。
接下来,我们分析一下strncat
可以发现,strncat只是比strcat多了一个参数。
我们进行实验
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = "hello ";
char arr2[] = "bit";
strncat(arr1, arr2, 3);
return 0;
}
我们进行调试,
这是strncat函数调用完毕之后的结果 ,我们可以发现,bit追加到了数组arr1的内部,这里我们有一个问题,这个arr[9]对应的\0是怎么来的?是数组arr1自动添加的,还是strncat追加的?
答:我们进行实验
char arr1[20] = "hell\0xxxxxxxx ";
我们把数组arr1[]修改成这样,我们的追加应该是覆盖了一个\0和两个x,我们进行调试
假如是数组arr1后面的\0,那么应该在x的最后面,所以这里的\0是函数strncat追加的,由此可得:strncat函数在追加时,会在字符串的末尾追加\0。
那么假如我们数组arr2只有三个字符,但是我们调用strncat函数对arr1追加6个字符,是不是想strncpy一样追加\0呢?
答:我们进行试验
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = "hell\0xxxxxxxx ";
char arr2[] = "bit";
strncat(arr1, arr2, 6);
return 0;
}
当调用strncat函数之后
我们可以发现,strncat并没有再多追加\0.
接下来,我们介绍一个strstr:查找子串的一个函数。
简单的说:给你一个字符串,从字符串中查找另外一个字符串。
strstr函数的参数是两个字符指针,返回值也是一个字符指针。
strstr函数是这样使用的
#include<stdio.h>
#include<string.h>
int main()
{
char email[] = "zpw@bitejiuyeke.com";
char substr[] = "bitejiuyeke";
char*ret = strstr(email, substr);
if (*ret == NULL)
printf("字符串不存在\n");
else
printf("%s", ret);
return 0;
}
strstr是这样作用的:看email数组中对应的字符串时候有substr数组对应的字符串,如果有的话,返回email对应的字符串中从substr字符串首元素开始的字符串,如果没有的话,返回空指针。
由代码可知,这emai字符串是包含substr对应的字符串的。我们进行运行
接下来,我们来模拟实现strstr函数
#include<stdio.h>
#include<string.h>
#include<assert.h>
char*my_strstr(const char*str1, const char*str2)
{
assert(str1&&str2);
const char*s1 = str1;
const char*s2 = str2;
const char*p = str1;
while (*p)
{
s1 = p;
s2 = str2;
while (*s1 != '\0'&&*s2 != '\0'&&*s1 == *s2)
{
s1++;
s2++;
}
if (*s2 == '\0')
{
return p;
}
p++;
}
return NULL;
}
int main()
{
char arr1[] = "abcdef";
char arr2[] = "bcd";
char*ret = my_strstr(arr1, arr2);
if (ret == NULL)
{
printf("字符串不存在\n");
}
else
{
printf("%s", ret);
}
return 0;
}
提出几个问题:while (*p)在这里有什么作用?
答:当p解引用是空指针的时候,就证明数组arr1里的字符串直到\0都没有匹配到数组arr2内的全部字符,所以就证明字符串不存在,当p解引用不是空指针,我们就进入循环。
2:*s1 != '\0'&&*s2 != '\0'&&*s1 == *s2为什么while循环的进入条件需要同时满足以上三个?
答:最简单的条件:*s1 == *s2因为我们while循环就是为了筛选字符串的,筛选的前提就是解引用后对应的字符必须相等。
*s1 != '\0'这个条件:如果s1解引用后结果等于\0,表示已经到第一个字符串的末尾也没有结束循环,所以该字符串不存在
*s2 != '\0'这个条件:假如s2解引用后结果等于\0,就表示数组arr2内部的字符串出现在arr1内部,所以该字符串满足条件,所以结束该循环。
下一个函数:strtok
我们看这个函数的结构:参数是两个指针,第二个指针对应的汉语意思是分隔符,返回值是字符指针。
这个函数的意思是切割字符串,这个函数的实现步骤比较复杂。
1:delimiters参数是个字符串,用来定义分隔符的字符集合
2:第一个参数指定一个字符串,该字符串包含0个或多个由deli字符串中一个或多个分隔符分割的标记。
3:strtok会找到str中的下一个标记,并将其用\0结尾,返回一个指向这个标记的指针。
(strtok函数会改变字符串的内容,所以一般用strtok切割被拷贝过的字符串并且可以被修改。
4:strtok的第一个参数不是NULL,他会找到str中的第一个标记,然后保存它在字符串中的位置
5:strtok的第二个参数是NULL,函数将在同一个字符串被保存的位置开始,寻找他的下一个标记。
6:如果字符串中,不存在更多的标记,我们就返回空指针。
首先,我们分析第一个。
#include<stdio.h>
#include<string.h>
int main()
{
char arr1 = "zsk@1390281278.com";
const char*sep = "@.";
}
sep就是我们设置的第二个参数,我们通过这个参数创建一个字符串,字符串内部的元素是我们自己设计的分割符。
第二个:
strtok(arr1, sep);
arr1就是我们对应的参数,这个参数指向字符串"zsk@1390281278.com",并且含有我们所设计的分割符。
第三个:
函数首先找到下一个标记@,把这个@改成\0,这时候就分割出来了一个字符串了,然后我们返回这个字符串首元素的地址,也就是z的地址。
因为strtok函数会改变字符串的内容,所以我们要对字符串进行拷贝,拷贝之后再调用函数
#include<stdio.h>
#include<string.h>
int main()
{
char arr1 = "zsk@1390281278.com";
const char*sep = "@.";
char sp[30] = { 0 };
strcpy(sp, arr1);
strtok(sp, sep);
}
意思就是这样:我们要调用strtok函数修改分割字符串,因为我们分割之后会改变字符串的内容,所以我们先拷贝一个字符串,用该字符串来调用函数来实现分割字符串,又不会改变字符串的内容。
4:第一个参数不是空指针,我们直接找到第一个分割符@,把@改成\0,分割出一个字符串,返回这个字符串首元素的地址,然后保存我们找到第一个分割符的位置。
5:第二个参数是空指针,我们要在我们之前保存的分割符的位置开始,寻找他的下一个标志。
6:运行到最后,没有分割符了,我们调用函数,返回的就是空指针了。
我们实现这个完整的函数
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "zsk@1390281278.com";
const char*sep = "@.";
char sp[30] = { 0 };
strcpy(sp, arr1);
char*ret=strtok(sp, sep);
printf("%s\n", ret);
ret=strtok(NULL, sep);
printf("%s\n", ret);
ret=strtok(NULL, sep);
printf("%s\n", ret);
return 0;
}
我们发现,这种写法效率太低,假如我们有大量的分割符在字符串中,我们不能一个语句一个语句的写,接下来,我们介绍一种for循环的方法解决这个问题。
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[] = "zsk@1390281278.com";
const char*sep = "@.";
char sp[30] = { 0 };
strcpy(sp, arr1);
char*ret = NULL;
for (ret = strtok(sp, sep); ret != NULL; ret = strtok(NULL, sep))
{
printf("%s\n", ret);
}
return 0;
}
我们在for循环语句这样写,首先执行ret = strtok(sp, sep),然后进行判断,假如ret != NULL,那么我们就printf("%s\n", sp)打印出分割的字符串,接下来我们再执行ret = strtok(NULL, sep),因为我们不是第一次调用函数,所以我们的第一个参数是空指针,然后一直执行循环,直到ret等于空指针时,for循环结束。
我们进行编译
strerror
参数是一个整型,返回值是一个char类型的指针。
函数的作用:返回错误码对应的错误信息。
c语言的库函数在执行失败的时候,都会返回失败的时候,都会返回错误码。
每个错误码对应不同的错误信息。
返回值是错误码对应的错误信息的字符串的首字符的地址。
先举一个例子
#include<string.h>
#include<stdio.h>
int main()
{
printf("%s\n", strerror(0));
printf("%s\n", strerror(1));
printf("%s\n", strerror(2));
printf("%s\n", strerror(3));
return 0;
}
我们进行编译
这就是错误码对应的错误信息。
举一个例子
#include<errno.h>
int main()
{
FILE*pf=fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s", strerror(errno));
}
return 0;
}
其中,errno是C语言设置的一个全局的错误码存放的变量。
fopen是打开文件夹的意思,第一个参数是文件夹的名字,第二个参数r的意思是读,这里的意思是以读的形式打开文件夹test.txt,然后把文件夹的地址传递给pf指针,假如pf是空指针,就证明产生了错误,这时候我们在if语句中打印错误信息。
我们进行编译:
没有文件或者没有文件夹,证明我们并没有文件test.txt。
字符分类函数:
这么多函数,我们先实验一下iscntrl函数
为什么参数是整型?
答:因为字符的ASCII码值是整数。
这个函数的意思是:如果c是一个空白字符,那么我们返回一个非0的数字,如果c不是一个空白字符,那么我们返回0.
我们测试一下。
#include<ctype.h>
int main()
{
int a = iscntrl('w');
printf("%d", a);
}
这个函数的头文件是#include<ctype.h>
字符w并不是空白字符,我们的函数iscntrl的返回值应该为0,我们进行编译。
结果正确。
我们换一个函数isdigit,进行实验。
int main()
{
int a = isdigit('0');
printf("%d", a);
}
我们进行编译
结果不为0.
isdigit判断是不是数字字符(这些字符是1,2,3,4,5,6,7,8,9)
两个字符转换函数
int tolower(int c)
int toupper(int c)
这两个函数是大小写转化函数,第一个函数能把大写字母转化为小写字母,第二个函数能把小写字母转化为大写字母。
我们举一个例子
int main()
{
int a = tolower('W');
printf("%c", a);
return 0;
}
我们进行编译吗,可以发现,大写字母转化为小写字母
注意:对于 int tolower(int c)函数,假如参数为非大写字母的任意其他字符,字符不会发生任何改变。int toupper(int c)也是同理。
memcpy:内存拷贝。
strcpy和strncpy都是字符串拷贝,不能够拷贝其他类型,例如
int arr1[] = { 1, 2, 3, 4, 5 };
int arr2[30] = { 0 };
假如我们要把数组arr1内部的元素拷贝到数组arr2,上述两个函数就不起作用了。
memcpy的结构
函数的参数有三个:第一个是指向目的地空间的无类型指针,第二个是指向来源空间的无类型指针,第三个是无符号整型,返回值是一个无类型指针。
我们进行实验
int main()
{
int arr1[] = { 1, 2, 3, 4, 5 ,6,7};
int arr2[30] = { 0 };
memcpy(arr2, arr1, 28);
return 0;
}
进行调试
我们可以发现,数组arr1内部的元素全部被拷贝到数组arr2中了。
总结memcpy的用法:将第二个参数指向的数组 的元素拷贝到第一个参数指向的数组,第三个参数代表拷贝的数量(单位是字节)。
这时候,我们再回看这个函数的类型可以发现
这个函数参数的两个指针为什么是void*?
答:因为void*的指针能够接受任意类型的参数,无论是int*还是char*还是float*。
我们进行模拟实现memcpy。
#include<string.h>
#include<assert.h>
void*memcpy(void*dest, const void*src, size_t num)
{
assert(dest&&src);
void*ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int arr1[] = { 1, 2, 3, 4, 5 ,6,7};
int arr2[30] = { 0 };
memcpy(arr2, arr1, 28);
return 0;
}
memcpy函数的返回值是一个void型的指针,指向的元素是数组arr1首元素的地址。
memcpy负责拷贝两个独立空间中的数据。,不能拷贝两个相同空间的数据。