C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常
量字符串中或者字符数组中。字符串常量适用于那些对它不能做修改的字符串函数。本章节重点介
绍处理字符和字符串的库函数的使用和注意事项。
在讲字符串函数使用之前我们先学一下const,和assert。
1:const的意思是不可修改的意思。
2:assert他是个断言函数,头文件是include
strlen的函数形式:
size_t strlen(const char* string)
头文件是include
这里size_t的意思是无符号整形,其实是用typedef重命名typedef unsigned int size_t由来的。
我们来简单的使用一下。
这个函数使用起来是比较简单的,但是我们不光要学会使用还用知道这个函数是怎么实现的。但是在模拟实现这个函数的时候,我们必须知道这个函数的实现原理,即这个函数是从哪开始的,又是从哪里结束的。这里我们可以借鉴一下“注意事项”。
我们看下面一段代码:
我们会发现,怎么计算的是74一个这么大的随机数数啊?这个正确答案明显是6。
我们再看这段代码:
我们会发现它只计算了’\0’之前的的三个字符,那现在我们知道了strlen函数是从首个字符开始计算,知道遇到\0停止。
我们再看一段代码:
int main()
{
if (strlen("abc") - strlen("abcdef") > 0)
{
printf("hehe\n");
}
else
{
printf("haha\n");
}
return 0;
}
你觉得答案是什么呢?相信大多数人的第一眼都认为答案是haha,但是我要告诉你们的是,答案是hehe。为什么呢?这我们就要回到strlen函数的返回值,返回值是size_t,这是个什么呢?这个其实是无符号整形的意思,其实是用来typedef重命名得来的 typedef unsigned int size_t。
所以上面的3-6=-3,无符号整型-无符号整型得到的还是无符号整型,所以得吧-3看成无符号整型来看,这就说的过去为什么是hehe了。
现在我们知道了strlen是怎么实现的,那我们就来模拟实现一下吧。接下来我会使用三种方法来模拟实现strlen函数。
1:计数器:
#include<stdio.h>
#include<string.h>
#include<assert.h>
size_t My_strlen(const char* str)//str是不可修改的,所以用const修饰。
{
int count=0;
assert(str!=NULL);//因为传过来的是指针,所以用assert断言判断指针的有效性。
while(*str!='\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[]="abcdef";
int ret=My_strlen(arr);
printf("%d",ret);
return 0;
}
2:递归
#include<stdio.h>
#include<string.h>
#include<assert.h>
size_t My_strlen(const char* str)
{
int count=0;
assert(str!=NULL);
while(*str!='\0')
{
return 1 + My_strlen(str + 1);
}
}
int main()
{
char arr[]="abcdef";
int ret=My_strlen(arr);
printf("%d",ret);
return 0;
}
3:指针-指针
#include<stdio.h>
#include<string.h>
#include<assert.h>
size_t My_strlen(const char* str)
{
char* source=str;
assert(str!=NULL);
while(*str!='\0')
{
str++;
}
return str-source;
}
int main()
{
char arr[]="abcdef";
int ret=My_strlen(arr);
printf("%d",ret);
return 0;
}
总结:
1 :字符串以'\0'作为结束标志,strlen函数返回的是在字符串中'\0'前面出现的字符个(不包含'\0')。
2:参数指向的字符串必须要以'\0'结束。
3:注意函数的返回值为size_t,是无符号的。
strcpy的函数形式:
char* strcpy(char* destination,const char* source)
头文件include
strcpy名为字符拷贝,顾名思义就是将source的字符拷贝到destination里面去。
我们来间的的使用一下。
这里我们发现’\0’也拷贝过去了,那我们就可大胆的猜测一下,是不是他就是以’\0‘为结束标志呢?答案是肯定的。
那如果将arr1里面的字符拷贝到arr2行不行呢?
这里我们的发现虽然可以打印出来,但是会报错误。其实原理很简单,你可以模拟一下穿鞋子,你的脚是41码的,但是你非要穿36的鞋子,你说会不会出问题吧,所以,拷贝的目标空间必须足够大,起码要大于源字符串的空间。
那我们看一下下面的代码。
我们就是吧刚才的arr1改成了*p,改完后p就是个常量字符串,是可修改的,这个时候我发现系统有报错了,并且也没法运行,也就出不了结果来。
这就说明了,我们的目标空间必须时可变的。
现在我们知道了他是怎开始和结束的,也知道了他的使用规范,那我们就来模拟实现一下strcpy函数。
#include<stdio.h>
#include<string.h>
#include<assert.h>
char* My_strcpy(char* dest,const char* src)
{
assert(dest!=NULL);
assert(src!=NULL);
char* bigan=dest;
while(*src!='\0')
{
*dest=*src;
dest++;
src++;
}
*dest=*src;//拷贝\0;
return bigan;//返回起始空间的地址,因为他的返回类型是char*指针。
}
int main()
{
char arr1[]="abcdefg";
char arr2[]="hijk";
char* ret=My_strcpy(arr1,arr2);
printf("%s",ret);
return 0;
}
这里我们可以升级一下:
#include<stdio.h>
#include<string.h>
#include<assert.h>
char* My_strcpy(char* dest,const char* src)
{
assert(dest!=NULL);
assert(src!=NULL);
char* bigan=dest;
while(*dest++=*src++)
{
;
}
return bigan;
}
int main()
{
char arr1[]="abcdefg";
char arr2[]="hijk";
char* ret=My_strcpy(arr1,arr2);
printf("%s",ret);
return 0;
}
总结:
1:源字符串必须以'\0'结束。
错误示范:char arr[]={'a','b','c'};
2:会将源字符串中的'\0'拷贝到目标空间。
3:目标空间足够大,以确保能存放源字符串。
错误示范:char arr1="a",char arr2="abcdefg";
4:目标空间必须可变。
strcat的函数形式:
char* strcat(char* destination,char* source)
头文件include
strcat名为字符串追加,顾名思义就是将source里面的字符串追加到destination里面去。
我们来简单的使用一下:
这里我们可以很清楚的知道,再追加的时候他是从destination的\0开始追加的,并且source里面的\0也会追加过去,也就是说字符串追加是从第一个遇到的’\0’开始追加的,并且还会把\0追加过去。
同样的道理这里的目标空间必须有足够大的空间,能容纳源字符串的内容。
这张图出现了和strcpy相同的错误,同样也说明了目标空间必须可修改。
现在我们知道了他是怎开始和结束的,也知道了他的使用规范,那我们就来模拟实现一下strcat函数。
#include<stdio.h>
#include<string.h>
#include<assert.h>
char* My_strcat(char* dest,const char* src)
{
char* bigan=dest;
assert(dest!=NULL);
assert(src!=NULL);
while(dest!='\0')
{
dest++;
}
while(*dest++=*src)
{
;
}
return bigan;
}
int main()
{
char arr1[30]="hello";
char arr2[]="world";
char* ret=My_strcat(arr1,arr2);
printf("%s",ret);
return 0;
}
那如果字符串追自己给自己追加会是怎么样呢?我们来试一试
int main()
{
char arr1[30] = "hello";
strcat(arr1, arr1);
printf("%s", arr1);
return 0;
}
注:这里其实是不能够实行的,但是有些编译器是可以编译出来的,但是实际上这样是错误的写法的,报错。因为再找到arr1的\0并开始追加arr1的时候,arr1就是在变化的,那么source也就是arr1就是在变化的,因为你在给自己追加自己,这样的话,那你的结束标记也就是找到arr1的\0就永远找不到,就会报错。
总结:
1:源字符串必须以'\0'结束。
2:目标空间必须有足够大的空间,能容纳源字符串的内容。
3:a目标空间必须可修改。
strcmp的函数形式:
int strcmp(const char* str1,const char* str2)
头文件:include
strcmp名为字符串比较,其实比较的就是字符的ASCLL码值。
他的返回特点:
1:第一个字符串大于第二个字符串,返回大于0的数字。
2:第一个字符串小于得二个字符串,返回小于0的数字。
3:第一个字符串等于第二个字符串,返回0。
我们来简单的使用一下:
这里返回的是-1小于0的数,那就说明p1小于p2的,但是我们发现p1由6个字符,p2只有5个字符,这就说明了strcmp比较的不是字符的个数。其实strcmp是对字符串里面的字符是一对一对的进行比较的。举个例子。
char* p1="abcdef";
char* p2="abcghi";
这里首先是p1里面的a和p2里面的a进行比较,发现他们相同,这个时候就跳过a进行下一对进行比较,就是p1里面的b和p2里面的b比较,以此类推直到遇到不同的字符进行比较或者两者都遇到\0就会停止。而这里遇到p1的d和p2的g比较,发现p2大于p1,就返回小于0的数。
现在我们来模拟实现一下:
#include<stdio.h>
#include<string.h>
#include<assert.h>
int My_strcmp(const char* str1, const char* str2)
{
assert(str1 != NULL);
assert(str2 != NULL);
//比较
while (*str1 == *str2)
{
if (*str1 == '\0')
return 0;
str1++;
str2++;
}
return *str1 - *str2;
}
int main()
{
const char* p1 = "abcdef";
const char* p2 = "sqwer";
int ret = My_strcmp(p1, p2);
printf("%d", ret);
return 0;
}
总结:
1:字符串比较的是字符的ASCLL码值,不是字符的个数。
2:比较的时候是一对一对的比较的,直到遇到不相等的或者遇到\0才会停止。
strncpy的函数形式:
char* strncpy(char* destination,const char* source,size_t num)
头文件:include
可以发现strncpy就比strcpy多了一个参数size_t num,而strncpy的作用就是将source里面的num个字符拷贝到destination里面。
我们来简单的使用一下:
那如果num的个数大于source的字符个数会怎么样呢?
我们会发现这里arr2没有7个字符,只有5个字符,但是他拷贝的时候却拷贝了7个过去,剩下的两个用\0充当了。
现在我们模拟实现一下:
#include<stdio.h>
#include<string.h>
#include<assert.h>
char* My_strncpy(char* dest, const char* source, int num)
{
assert(dest && source);
char* ret = dest;
while (num != 0 && (*dest++ = *source++))
{
num--;
}
if (num)//源字符串长度小于num
{
while (num)
{
*dest++ = '\0';
num--;
}
}
}
int main()
{
char arr1[12] = "abcdefghij";
char arr2[] = "hello";
My_strncpy(arr1, arr2, 5);
printf("%s", arr1);
return 0;
}
总结:(其实strncpy的特点和strcpy的特点是相似的)
1:源字符串必须以'\0'结束。
错误示范:char arr[]={'a','b','c'};
2:会将源字符串中的'\0'拷贝到目标空间。
3:目标空间足够大,以确保能存放源字符串。
错误示范:char arr1="a",char arr2="abcdefg";
4:目标空间必须可变。
5:拷贝num个字符从源字符串到目标空间
6:如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后面追加\0,直到num个。
strncat的函数形式:
char* strncat(char* destination,const char* source,size_t num)
头文件:include
这里也是一样的strncat就比strcat多了size_t num这一个参数,他也是将source里面的num个字符追加到destination里面去。
我们来简单的实现一下:
我们再看一下下面一段代码:
我们就会发现它和strcat一样遇到第一个\0后开始追加的。
那我我们来尝试一下,当num大于source里面的字符个数会不会像strncpy一样把多余的用\0充当。
这里我们发下他并没有将多余的用\0充当,也就是说无论你多了多少没追加的,他都只会追加一个\0过去。
现在我们来模拟实现一下:
#include<stdio.h>
#include<stdio.h>
#include<assert.h>
char* my_strncat(char* dest, const char* src, int num)
{
assert(dest != NULL);
assert(src != NULL);
char* bigan = dest;
while (*dest != '\0')
{
dest++;
}
while (num-- &&*src!='\0')
{
*dest++ = *src++;
}
*dest = '\0';
return bigan;
}
int main()
{
char arr1[20] = "hell\0oxxxx";
char arr2[] = "world";
char* ret = my_strncat(arr1, arr2, 3);
printf("%s\n", ret);
return 0;
}
总结:
1:源字符串必须以’\0’结束。
2:目标空间必须有足够大的空间,能容纳源字符串的内容。
3:a目标空间必须可修改。
3:如果num大于source的字符个数,剩余的空间只追加一个\0。
strncmp的函数形式:
size_t strncmp(const char* string1,const char* string2,int num)
strncmp的大概意思就是比较num个字符。
来简单的实现一下:
这个比较方法和strcmp是一样的,只是多了个比较的个数。
现在来模拟实现一下:
#include<stdio.h>
#include<string.h>
#include<assert.h>
My_strncmp(const char* arr1, const char* arr2, int num)
{
assert(arr1 && arr2);
while (--num && (*arr1) && (*arr1 == *arr2))
{
arr1++;
arr2++;
}
return *arr1 - *arr2;
}
int main()
{
const char* p1 = "hello";
const char* p2 = "world";
int ret = My_strncmp(p1, p2,1);
printf("%d", ret);
return 0;
}
strstr的函数形式:
char* strstr(const char* p1,const char* p2)
他的功能是在p1里面找p2,如果找到了就返回找到的字符的首地址回去,如果没找到就返回NULL。
我们来实现一下:
我们再看一段代码
这里虽然找到了def但是打印出来的却是defghi,这是因为找到了def他就会返回arr1中d的地址,打印的时候就会从d开始打印,直到遇到\0停止。
那我们再看一段代码:
这里arr1中虽然有两个def,但是他还是会从第一次出现的def开始打印。
现在我们来模拟实现一下:
#include<stdio.h>
#include<string.h>
#include<assert.h>
char* My_strstr(const char* p1,const char* p2)
{
assert(p1!=NULL);
assert(p2!=NULL);
if(*p1=='\0')
{
return p1;
}
while(*p1)
{
while((*p1==*p2)&&(*p1!='\0')&&(*p2!='\0'))
{
p1++;
p2++;
}
if(*p2=='\0')
{
//一般这是我们的正常思路,但是你会发现当找到了全部的p2的时候,找不到返回的
//地址了,所以这样是错误的代码,那现在我们就可以设置一个变量来代替p1和p2,
//而p1和p2不变
}
p1++;
}
return NULL;
}
int main()
{
char* arr1 = "abbbcdef";
char* arr2 = "bbc";
char* ret = My_strstr(arr1, arr2);
if (ret == NULL)
{
printf("子串不存在\n");
}
else
{
printf("%s", ret);
}
return 0;
}
另外有种特殊情况:
char* arr1[]="abcdddef";
char* arr2[]="dde";
当这种情况的时候,看上去么有找到,其实是可以找到的,那这个时候s2就要回到p2,而s1只向后退一步,再重新开始就可以找到了,这个时候我们就得再要一个变量ret来储存这个变量。
现在我们来写正确的代码:
#include<stdio.h>
#include<string.h>
#include<assert.h>
char* My_strstr(const char* p1,const char* p2)
{
assert(p1!=NULL);
assert(p2!=NULL);
char* s1=NULL;
char* s2=NULL;
char* ret=p1;
if(*p1=='\0')
{
return p1;
}
while(*ret)
{
s1=ret;
s2=p2;
while((*s1 == *s2) && (s2 != '\0') && (*s1 != '\0'))
{
s1++;
s2++;
}
if(*s2=='\0')
{
return ret;
}
ret++;
}
return NULL;
}
int main()
{
char* arr1 = "abbbcdef";
char* arr2 = "bbc";
char* ret = My_strstr(arr1, arr2);
if (ret == NULL)
{
printf("子串不存在\n");
}
else
{
printf("%s", ret);
}
return 0;
}
strtok的函数形式:
char* strtok(char* str,const char* sep)
1:sep参数是个字符串,定义了用作分隔符的字符集合
2:第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中的一个或者多个分隔符分割的标记
3:strtok函数找到str中的下一个标记,并将其用\0结尾,返回一个指向这个标记的指针。(注:strtok
函数会改变操作的字符串,所以使用strtok函数分割的字符串一般都是临时拷贝的内容并且可修改)
4:strtok函数的第一个参数不是NULL,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
5: strtok函数的第一个为NULL,函数在同一个字符串中被保存的位置开始,查找下一个标记
6:如果字符串中不存在更多的标记,则返回NULL指针。
下来简单的使用一下:
我们看到这里只打印了123后面的却没有出来,这是因为是声明了一个库函数。
这样声明了三个函数,虽然都显示出来了,但是不难想象一下如果由几百个怎么办,所以我们来优化一下:
这样是不是更加简便而且还通用。
strerror的函数形式:
char* strerror(int errnum)
作用:将错误码转换成错误信息。
上面的数字就是错误码,输出的结果就是错误信息。
但是我们使用的时候肯定不是直接的输入数字的。
#include<stdio.h>
#include<errno.h>
int main()
{
char* str=strerror(errno);//errno 是一个全局的错误码的变量,当C语言中的库函数在执行
//过程中发生了错误,就会把对应的错误码赋值到errno中。
//头文件include
printf("%s",str);
}
另外我们还有有些字符分类函数:
字符分类函数:
函数 如果它的参数符合下列条件就返回真
iscntrl 任何控制字符
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
ispunct 标点符号,任何不属于数字或字母的图像字符(可打印符号)
isgraph 任何图像字符
字符转换:
int tolower ( int c ); //大写字符转小写字符
int toupper ( int c ); //小写字符转大写字符
像之前的
strcpy
strncpy
strcat
strncat
strcmpy
strncmp
····
这些字符串函数,他们的操作对象都是字符串,并且都必须以\0结尾,但是他们不适用于整形数组,浮点数组,结构体数组等其他的数组,只单独对字符数组有用。所以这就有了内存操作数组,他是针对都有类型的。
例如:
我们原本想把arr1中的12345拷贝到arr2中,但是只有1拷贝过去了,这个时候我们就要注意了,之前的所有字符串函数对应的都是一个字节,这里的strcpy也是,他是一个一个字节的拷贝的,这里arr1在内存里的存放方式是:
01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00
而拷贝的时候是一个一个字节的拷贝过去的,而遇到’\0’(0)就停止了,所以遇到1后面的0就停止拷贝了,这就是为什么只拷贝了一个1过去的原因。
memcpy的函数形式:
void* memcpy(void* destination,const viod* source,size_t num)
头文件:include
之所以可以针对所有类型的数组,就在于void*。如果还有不明白void*的,可以去 详解之进阶指针中的qsort函数冒泡函数模拟实现里面了解。
void* My_memcpy(void* dest, const void* src, size_t num)
{
assert(dest != NULL);
assert(src != NULL);
void* ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;
++(char*)dest;
++(char*)src;
}
return ret;
}
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[5] = { 0 };
int i=0;
char* ret=My_memcpy(arr2, arr1, sizeof(arr1));
for(i=0;i<5;i++)
{
printf("%d ",arr1[i]);
}
return 0;
}
memmove函数的形式:
void* My_memcpy(void* dest, const void* src, size_t num)
这里的num指的是拷贝多少个字节。
头文件:include
memmove的功能是处理重叠拷贝。
就比如:
本来我们要的是1 2 1 2 3 4 5 6 7 8 9 10,但是他给我们却不是这样,这个原理其实和strcpy自己拷贝字节的原理是一样的。
注:这里用的不是库函数里面的memcpy是因为,memcpy其实在有些编译器里面是可以进行重叠拷贝的。但是:
C语言标准:
memcpy 只要处理 不重叠的内存拷贝就可以了
memmove 是处理重叠内存拷贝
这个时候memmove就派上用场了
我们现在来简单的实现一下:
那现在我们来分析一下:
像这样的情况 dest
像这样的情况 dest>src 我们就可以将src里面的字符按照从后到前的方式一一拷贝过去。
这样,我们就可以进行模拟实现了:
void* My_memmove(void* dest,const void* src,size_t num )
{
assert(dest!=NULL);
assert(src!=NULL);
char* ret=dest;
if(dest<src)//前->后
{
while(num--)
{
*(char*)dest=*(char*)src;
++(char*)dest;
++(char*)src;
}
}
else//后->前
{
while(num--)
{
*((char*)dest+num)=*((char*)+num);
}
}
return ret;
}
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
My_memmove(arr1+2, arr1, 20);
for (i = 0; i < 10; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
memset函数形式:
void* memset(void* dest,int c,size_t)
c指的是设置多少个符。
功能:将dest的num个字节,用c替换。
在一段内存中填充某个给定的值,它是比较大的结构体或数组进行清零操作的一种最快的方法。
void* My_memset(void* src, int n, size_t num)
{
assert(src != NULL);
void* bigan = src;
while (num--)
{
*(char*)src = (char)n;
++(char*)src;
}
return bigan;
}
int main()
{
char arr[] = "abcdef";
char* ret=My_memset(arr, '#', 4);
printf("%s", arr);
return 0;
}
memcmp的函数形式:
int memmove(const void* ptr1,const void* ptr2,size_t num)
num指的是待比较的字节。
这个函数不会因为 遇到\0就停止比较了。
头文件:include
他的返回之特点和strcmp是一样的。
1:第一个字符串大于第二个字符串,返回大于0的数字。
2:第一个字符串小于得二个字符串,返回小于0的数字。
3:第一个字符串等于第二个字符串,返回0。
我们来简单的实现一下:
刚才我们知道了,num是比较多少个字节,这里是一个一个字节的比较,知道比较到两个内存数组不相等,或者num个直接都相同返回0。
现在我们来模拟实现一下:
#include<stdio.h>
#include<string.h>
#include<assert.h>
int My_memcmp(const void* ptr1, const void* ptr2, size_t num)
{
assert(ptr1 != NULL);
assert(ptr2 != NULL);
const char* s1 = (char*)ptr1;
const char* s2 = (char*)ptr2;
if (!num)
return 0;
while (--num && *s1 == *s2)
{
s1++;
s2++;
}
return *s1 - *s2;
}
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 1,2,5,6,3 };
int ret = My_memcmp(arr1, arr2, 8);
printf("%d", ret);
}
好了到这里,学习也就完毕了,能看到这的小伙伴们你们是我最大的动力,博主我也是尽我最大的努力,用最平常的话来讲明白知识点,我也是花了大量的时间来做这个,望大家多多支持。