C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在 常量字符串 中
或者 字符数组 中。 字符串常量 适用于那些对它不做修改的字符串函数.
因此此篇文章便是介绍字符和字符串函数的使用与注意事项.
strlen
strlen
的使用(接收地址)#include
#include
int main()
{
char arr1[] = "abcdefg";
char arr2[] = {
'a','b','c','d','e','f','g'};
printf("%d",strlen(arr1));
printf("%d",strlen(arr2));
return 0;
}
上面的结果是:
7
随机值 (因为
strlen
接收首元素地址然后向后查找到\0
停止,但是arr2
并没有\0
结尾,随意是随机值)
strlen
的小坑下面一段代码,你觉得答案是多少??
#include
#include int main() { const char*str1 = "abcdef"; const char*str2 = "bbb"; if(strlen(str2)-strlen(str1)>0) { printf("str2>str1\n"); } else { printf("srt1>str2\n"); } return 0; } 结果是什么呢???
答案是:
str2>str1
因为
strlen
的返回值是无符号整型
,所以无符号减去无符号一定是大于等于0的
strlen
的模拟(计数法 递归 指针相减)size_t my_strlen(char* str)
{
unsigned int count = 0;
while(*str)
{
count++;
str++;
}
return count;
}
size_t my_strlen(char* str)
{
if (*str != 0)
{
return 1 + my_strlen(++str);
}
return 0;
}
size_t my_strlen(char* str)
{
char* ret = str;
while(*str) str++;
return str-ret;
}
strcpy
官方写法
char* strcpy(char* strDestination, const char* strSource);
即第一个参数是目的地地址,第二个参数源地址,
例子:
#include
#include int main() { char arr1[] = "abcdef"; char arr2[] = "FF"; strcpy(arr1,arr2); printf("%s",arr1); return 0; } 结果:
FF
注意点: 这里的复制其实并不是真正的复制,准确的说是覆盖,即把arr2的所有内容(包括
\0
)都覆盖到arr1
对应位置,也就是说虽然打印出来
arr1
是FF
,但是实质上arr1
等于FF\0def
不信看下面的图
注意事项:
源字符串必须以 ‘\0’ 结束。,如果原字符串没有\0,就会一直拷贝原字符串地址后面的所有内容,直到找到值为0
会将源字符串中的也 ‘\0’ 拷贝到目标空间。
**目标空间必须足够大,以确保能存放源字符串。**如果目标空间不够大,则会导致源字符串拷贝不进去;
目标空间必须可变,即目标空间没有
const
修饰
strcpy
char* my_strcpy(const char* destination,const char* source)
{
assert(destination && source);
while(*(char*)destination++ = *(char*)source++) ;
return destination;
}
strcat
官方写法
char* strcat( char* strDestination, const char *strSource);
即第一个参数是目的空间 第二个参数是源码空间
使用例子:
#include
#include int main() { char arr1[10] = "abcd"; char arr3[] = "ABCD"; my_strcpy(arr1, arr3); printf("%s", arr1); return 0; } 答案:
abcdABCD
但是如果这样呢
#include
#include int main() { char arr1[] = "abcd"; char arr3[] = "ABCD"; my_strcpy(arr1, arr3); printf("%s", arr1); return 0; } 答案:
报错!!! 因为目标空间
arr1
大小不够装下再追加,所以我么在使用strcat
时候一定要注意目标空间的大小,同时源字符串一定要\0
结尾提醒: 追加原理是首先找到destination的\0,然后在\0上追加source
比如下面
#include
#include int main() { char arr1[11] = "ab\0dxxxxxx"; char arr3[] = "ABCD"; strcat(arr1,arr3); printf("%s", arr1); return 0; } 我们可以看到
arr1
从ab\0dxxxxxx
变成了abABCD\0xxx
因为他是找到第一个\0,然后在\0上追加.
提醒:如果自己给自己追加会崩溃.因为\0被覆盖了
总结:
源字符串必须以 ‘\0’ 结束。
目标空间必须有足够的大,能容纳下源字符串的内容。
目标空间必须可修改。
strcat
的模拟实现char* ,my_strcat(char* strDestination, const char* strSource)
{
char* ret = strDestination;
/*先找到目标空间的\0*/
while(*strDestination) strDestination++;
/*开始追加*/
while(*strDestination++= *(char*)strSource++);
return ret;
}
strcmp
如果有下面的题:
#include
int main()
{
char* p1 = "abcdef";
char* p2 = "aqwer";
if(p1 < p2) printf("-1\n");
if(p1 == p2) printf("0\n");
if(p1 > p2) printf("1\n");
return 0;
}
答案: 0
你会发现,
p1
与p2
并没有真正的再比较全部字符,而是比较了第一个字符部分.因此我们想要真正的比较字符串就需要一个函数: ----->
strcmp
int strcmp ( const char * str1, const char * str2 );
- 如果第一个字符串大于第二个字符串,则返回大于0数字
- 如果第一个字符串等于第二个字符串,则返回0
- 如果第一个字符串小于第二个字符串,则返回小于0数字
例如:
#include
int main() { char* p1 = "abcdef"; char* p2 = "aqwer"; char* p3 = "aavd"; char* p4 = "abcdef"; printf("%d\n", strcmp(p1, p2)); printf("%d\n", strcmp(p1, p3)); printf("%d\n", strcmp(p1, p4)); return 0; }
strcmp
模拟实现int my_strcmp(const char* arr1, const char* arr2)
{
while (*(char*)arr1 && (*(char*)arr1 == *(char*)arr2))
{
(char*)arr1++;
(char*)arr2++;
}
if (!*(char*)arr1) return 0;
/*当跳出循环就表示碰到不一样的了,所以返回差值*/
return *(char*)arr1 - *(char*)arr2;
}
前面的三个不受长度限制的函数有局限性:
比如strcpy
:------->源字符串需要末尾有\0
,且必须拷贝到\0
结束,不关心是否目的地装得下源字符串,撑爆就报错
strcat
:------->源字符串需要末尾有\0
,且必须追加到\0
结束,不关心是否目的地装得下源字符串,撑爆就报错
strcmp
:------->源字符串需要末尾有\0
,且必须比较到\0
结束,不关心是否目的地装得下源字符串,撑爆就报错
因此,变有了下面的与前三者功能相似,但是可以控制数量的长度受限函数
strncpy
用法与strcpy
一样,但是多了个参数 size_t count
,即需要拷贝的字符数量
char * strncpy ( char * destination, const char * source, size_t count );
用法:
#include
#include
int main()
{
char arr1[] = "abcdefg";
char arr2[] = "XXXX";
strncpy(arr1,arr2,3);
printf("%s",arr1);
return 0;
}
答案:
XXXdefg
你会看到,并没有再把
arr2
的\0
给放进arr1
**注意点:**当count
的数量大于arr2
的长度时候,会自动用\0
补数,比如:
#include
#include
int main()
{
char arr1[] = "abcdefghijklmnopq";
char arr2[] = "XXXX";
strncpy(arr1,arr2,15);
printf("%s",arr1);
return 0;
}
可以清楚的看到arr1
里面的内容:自动补充了很多\0
strncpy
char* my_strncpy(char* str1, const char* str2, size_t count)
{
char* ret = str1;
while (count&& (*str1++ = *(char*)str2++)) count--;
if (count)
/*--count 是因为str1已经为\0,不再需要继续赋值\0,所以先减一下*/
while (--count) *str1++ = '\0';
return ret;
}
strncat
用法与
strcat
一样,但是多了个参数size_t count
,即需要追加的字符数量
char * strncat ( char * destination, const char * source, size_t count );
用法示例:
#include
#include
int main()
{
char arr1[50] = "abcdef\0abcdefghilj";
char arr2[] = "XXXX";
strncat(arr1, arr2, 15);
printf("%s", arr1);
return 0;
}
答案:
abcdefXXXX
而当
count>arr2
时,就不再管
strncat
char* my_strncat(char* str1, const char* str2, size_t count)
{
char* ret = str1;
while (*str1) str1++;
while (count-- && (*str1++ = *(char*)str2++));
return ret;
}
strncmp
int strncmp ( const char * str1, const char * str2, size_t num );
用法与前面一样,我就不再赘述怎么用,直接开始模拟
strncmp
的模拟实现int my_strncmp(const char* str1, const char* str2, size_t num)
{
while (num-- && *str1 && (*str1 == *str2))
{
str1++;
str2++;
}
return *(char*)str1 - *(char*)str2;
}
strstr
功能是查找一个字符串是否是另一个字符串的子串.例如 :
#include
#include int main () { char str[] ="This is a simple string"; char * pch; pch = strstr (str,"simple"); if (pch != NULL) strncpy (pch,"sample",6); puts (str); return 0; } 结果:
This is a simple string
,可以看见,虽然前面调用了strncpy
但是对str
好像并没有影响,因为pch
是将接收的是str
中的simple
的首地址.即S
strstr
char* my_strstr(const char* dest,const char* src)
{
/*第一步,一一比较.当src都比较完了(*src等于\0),则说明是子串*/
while(*dest) //确保dest每个字符后面的字符串与src匹配
{
char* ret = dest;
//一一比对,当src等于\0,说明全部比对完成.
while((*ret == *src) && (*src!='\0'))
{
ret++;
src++;
}
if(!*src) return dest;
(char*)dest++;
}
return NULL;
}
像上面这样真的完毕了吗??? 我们似乎只处理了 像下面一样的例子:
目标:
"abcdefg"
源串:
"cdef"
那么,还有什么不一样的例子我们没有考虑到呢???哦??好像是这个
目标:
"abcdefg"
源串:
"defghi"
而且好像还有这个
目标:
"ABCDDDEFGH"
源串:
"DDEFG"
这种情况如果只用最开始模拟的情况,则会返回空指针.
所以针对第二种与第三种情况,内层循环还应该有另外一个条件,即*dest ! = '\0'
.
并且,每个字符后面的字符串比较完毕,指针又返回到所比较字符串开头.因此,添加以下代码
char* my_strstr(const char* dest, const char* src)
{
char* s1;
char* s2;
char* cur = (char*)dest;
/*第一步,一一比较.当src都比较完了(*src等于\0),则说明是子串*/
while (*cur) //确保dest每个字符后面的字符串与src匹配
{
s1 = (char*)cur;
s2 = (char*)src;
//一一比对,当src等于\0,说明全部比对完成
while ((*s1 != '\0') && (*s2 != '\0')&&(*s1 == *s2))
{
s1++;
s2++;
}
if (!*s2) return cur;
cur++;
}
return NULL;
}
另外,对于求子字符串的算法还有KMP
,可以参考一位大佬的文章KMP
strtok
字符串分割函数,即目标字符串通过设置的分割符进行分割出来
官方写法:
char * strtok ( char * str, const char * delimiters );
第一个参数是目标**
被分割字符串
,第二个参数是分隔符集合
**用法:
- 传入一份临时拷贝的字符串给
str
,因为不想要源字符串被真正的分割.- 对于同一个字符串,第一次调用,必须传入地址,第二次以及多次传入
NULL
- 对于
strtok
的妙用一般是使用for循环.- 未分割完时,返回值是被分割的小段字符串的首地址,分割完时,返回NULL
#include
#include
int main()
{
char str[] = "[email protected]/cn";
char delimiters[] = ".@/";
printf("%s\n",strtok(str,delimiters));
printf("%s\n",strtok(NULL,delimiters));
printf("%s\n",strtok(NULL,delimiters));
printf("%s\n",strtok(NULL,delimiters));
return 0;
}
#include
#include
int main()
{
char str[] = "[email protected]/cn";
char delimiters[] = ".@/";
char* p;
for(p = strtok(str,delimiters);p != NULL;p = strtok(NULL,delimiters))
{
printf("%s\n",p);
}
return 0;
}
strerror
官方写法: char * strerror ( int errnum )
即输入一个代表错误码的整数,然后会返回错误信息
#include
#include
int main()
{
printf("%s\n",strerror(0));
printf("%s\n",strerror(1));
printf("%s\n",strerror(2));
printf("%s\n",strerror(3));
return 0;
}
但是这个函数我们经常用在哪里呢???,答案是配合errno
使用
errno
是一个全局的错误码变量errno
中.#include
#include
#include
int main()
{
FILE* pf = fopen("text.txt","r");//随便写的一个文件名,并不存在.
if(pf == NULL) printf("%s",strerror(errno));
else printf("打开成功!!!!!!!");
return 0;
}
)iscntrl
任何可控字符isspace
空白字符:空格, \f(换页), \n(换行),\t(水平制表符),\r(回车),\v(锤子制表符)isdigit
检验十进制数字 0 ~ 9isxdigit
检验16进制数字 0 ~ Fislower
检验是否是小写字母 a ~ zisupper
检验是否是大写字母 A ~ Zisalpha
检验是都是字母 a ~ z 和 A ~ Zisalnum
检验是否是数字和字符 0~9 a~z A~Z#include
#include
int main()
{
char num[] = "abEF 123";
printf("%d\n",islower(num[0]));
printf("%d\n",isupper(num[2]));
printf("%d\n",isdigit(num[5]));
return 0;
}
结果:
2 1 4
但是不同电脑可能不同结果.因为只要符合就返回的是 非零数
tolower
和toupper
)#include
#include
#include
int main()
{
char a[] = "abcdef";
char b[] = "ASDFG";
for (int i = 0; i < strlen(a); i++)
{
printf("%c",toupper(a[i]));
}
printf("\n");
for (int i = 0; i < strlen(b); i++)
{
printf("%c", tolower(b[i]));
}
printf("\n");
return 0;
}
上面我已经叙述了很多的关于字符串的处理函数
包括以下:
strlen
strcpy
strcat
strcmp
strncpy
strncat
strncmp
strstr
strtok
字符分类函数 字符转换 函数
但是我们能够明显的发现,我们都是局限在处理 字符串 上面,并不能处理其他的类型.
比如我需要复制一份数组,如果我们用strcpy
试试.
#include
#include
int main()
{
int num1[] = {
1,3,5,7,9};
int num2[5] = {
0}; //初始化为0
strcpy(num2,num1);
for(int i= 0;i<5;i++)
{
printf("%d\n", num[i]);
}
return 0;
}
结果:
1
0
0
0
0
原因: strcpy
是一个字节一个字节进行复制的,而我们的计算机大部分都是小端存储.
即数组num1
的存放结构如图;
所以当strcpy
复制了01 之后,后面就是0,便停止了复制.而目的数组num2
本就是全部为0,所以当第一个字节被复制一份01以后,num2[0]
的值就是1,而后面都没有复制,所以全是0
因此,我们引入了内存处理函数,主要是下面几个 :
memcpy
memmove
memcmp
memset
memcpy
的使用与模拟官方文档:
void * memcpy ( void * destination, const void * source, size_t num );
第一个参数: 目的地地址
第二个参数: 源字符串地址
第三个参数: 字节数量
memcpy
使用例子;#include
#include
int main()
{
int num1[5] = {
8,8,8,8,8 }; //初始化为0
int num2[5] = {
1,3,5,7,9 };
memcpy(num1, num2, 12); // 12个字节就是3个整数
for (int i = 0; i < 5; i++)
{
printf("%d ", num1[i]);
}
return 0;
}
成功复制!!!
memcpy
的模拟实现其实这个模拟还是比较简单的(相比于
qsort
的模拟),因为都用到了 万能指针void*
,接收一切
在我么实现这个函数之前,我们需要首先确定他是怎么复制的…前面已经说过,是一个字节一个字节复制的.那么我们肯定需要一个循环,且循环20次,但是接收的参数都是指针,而且 目的地还是 void*
指针,那么怎么连接上 第三个参数与 第一个参数呢?,答案是 字符指针
因为一个字符指针的跳跃性就是 一个字节,刚好符合 size_t num
void* my_memcpy(void* dest, const void* src, size_t num)
{
char* ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;
++(char*)dest;
//之所以前置++,是因为结合性++高于(类型转化),而dest与src都是void*
//不能加减,所以就会报错,所以变成前置++
++(char*)src;
}
return ret;
}
memmove
的使用与模拟官方文档:
void * memcpy ( void * destination, const void * source, size_t num );
用法与
memcpy
一模一样.这里就不再赘述.那么他的功能是什么呢?? 他的功能也与
memcpy
一模一样,那我们还学习什么呢??那么,如果我们用自己写的函数
my_memcpy
实现把自己的一部分复制到另一部分去呢???比如下面例子:
int arr[] = { 1,2,3,4,5,6,7,8,9,10};
如果我想把 1 2 3 4 5 放到 4 5 6 7 8的位置呢??如果使用自己实现的
my_memcpy
答案是这样的1 2 3 1 2 3 1 2 9 10
为什么呢??因为当空间重叠时候,自己去复制自己时候,有一部分就会被覆盖,如图
而memmove
的作用就是来实现重叠空间的复制
那么重叠空间我们怎么来实现进行复制呢???
很简单,我们倒着来放.比如刚才的1 2 3 4 5 放到 4 5 6 7 8,
我们从后开始,先把5 放到 8位置-------> 4放到 7位置----->3放到6位置------>2放到5位置------->1放到4位置
所有的都可以这样吗?? 那我如果想要把4 5 6 7 8 放到1 2 3 4 5位置呢???如果倒着放,就会又混乱.因此我们是需要分情况进行设计的.
目的位置在源位置之后,比如1 2 3 4 5是源位置,1就是源首地址. 而4 5 6 7 8是目的地址,而4就是目的首地址.这符合目的位置在原位置之后,就采用 从后向前复制
而这种情况的难点就是怎么找最后一个元素的最后一个字节位置
答案是
dest+num-1
和src + num -1
,为何??见图:
除去
情况1
的情况,就都采用 挨着顺序复制
void* my_memmove(void* dest,const void* src,size_t num)
{
char* ret = dest;
if(dest > src)
{
while(num--)
{
//因为这里已经减了一次,所以不再需要num-1;
*((char*)dest + num) = *((char*)src + num);
}
}
else
{
while(num--)
{
*(char*)dest = *(char*)src;
++(char*)dest;
++(char*)src;
}
}
return ret;
}
memcpy
进行重叠空间试试,行嘛??行!!!在最底层,
memcpy
实际上是与memmove
一模一样的.都可以复制 重叠行与非重叠性的空间,但是为什么我们要弄两个呢>解释:
在c语言标准里面:
memcpy
只需要实现不重叠空间复制
memmove
只需要实现重叠空间复制懂了吗???也就是说,他们都是超额完成了自己任务,我们去模拟,只是为了掌握算法与思想
所以才分开了memcpy
与memmove
进行模拟
官方文档:
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
第一个参数: 目的地地址
第二个参数: 源目标地址
第三个参数: 直接数量
使用: 与前面所讲的大致一样.不再一 一赘述,此处只讲模拟.在模拟之前建议再看看此文上面的
strcmp
,有异曲同工之妙
memcmp
模拟int my_memcmp(const void * ptr1, const void * ptr2, size_t num)
{
while(num-- && (*(char*)ptr1 == *(char*)ptr2))
{
++(char*)ptr1;
++(char*)ptr2; //为什么前置++,之前strcmp讲解过
}
return *(char*)ptr1 - *(char*)ptr2;
}
memset
的使用及注意事项官方文档:
void * memset ( void * ptr, int value, size_t num );
第一个参数: 目标地址
第二个参数: 某个确定的字符
第三个参数: 字节数量.
#include
#include
int main()
{
char str[10];
memset(str,'*',sizeof(str));
for(int i = 0;i<10;i++)
{
printf("%c ",str[i]);
}
return 0;
}
#include
#include
int main()
{
char str[10];
int num[10];
memset(str,10,sizeof(str));
memset(num,10,sizeof(str));
printf("这是字符数组内容--------------------------------\n")
for(int i = 0;i<10;i++)
{
printf("%d ",str[i]);
}
printf("这是整型数组内容--------------------------------\n")
for(int i = 0;i<10;i++)
{
printf("%d ",num[i]);
}
return 0;
}
会发现整型数组与我们想象的不一样,因为一个整型数组是4个字节.
而memset
是针对字节操作的.所以注意,一般我们只是用于整型数组初始化为0
,或者-1
,这才是准确的