本文会介绍重点的字符串函数和内存函数的使用和模拟实现
求字符串长度
strlen
长度不受限制的字符串函数
strcpy strcat strcmp
长度受限制的字符串函数介绍
strncpy strncat strncmp
字符串查找
strstr strtok
错误信息报告
strerror perror
内存操作函数
memcpy memmove memset memcmp
函数介绍
1.1 strlen
size_t strlen ( const char * str );
字符串已经 '\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前面出现的字符个数(不包含 '\0' )。
参数指向的字符串必须要以 '\0' 结束。
注意函数的返回值为size_t,是无符号的
#include <stdio.h>
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;
}
按照常理说,strlen(str2)=3,strlen(str1)=6,3-6=-3,输出的应该是srt1>str2
但是实际上输出的是str2>str1,
这是因为函数的返回值为size_t,两个无符号数相减得到的值也是无符号数,所以得到的值一定是非负数的。
1.2 strcpy
char* strcpy(char * destination, const char * source);
源字符串必须以 '\0' 结束。
会将源字符串中的 '\0' 拷贝到目标空间。
目标空间必须足够大,以确保能存放源字符串。
目标空间必须可变
int main()
{
char str1[20]="abcdef";
char str2[20]="zzzzzzzz";
strcpy(str2,str1);
return 0;
}
int main()
{
char str1="abcdef";
char *p="zzzzzzzz";
strcpy(p,str1);
return 0;
}
上方代码没有满足第四个要求,即目标空间必须可变,char*p="zzzzzzzzzz"是一串常量字符串,常量字符串不能被修改,所以程序会崩溃。
1.3 strcat
char * strcat ( char * destination, const char * source );
源字符串必须以 '\0' 结束。
目标空间必须有足够的大,能容纳下源字符串的内容。
目标空间必须可修改。
int main()
{
char str1[20]="hello ";
char str2[20]="world";
strcat(str1,str2);
return 0;
}
如果自己给自己追加
strcat(str1,str1);
就会发生无限追加的情况,hello worldhello worldhello worldhello world……,至于原因,等一会讲解函数模拟实现的时候具体叙述。
1.4 strcmp
int strcmp ( const char * str1, const char * str2 );
将str1与str2中的每个对应字符的ascii值相比,比较到出现另个字符不一样或者一个字符串结束或者所有个字符全部比较完。
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字
1.5 strncpy
char * strncpy ( char * destination, const char * source, size_t num );
从source位置开始,往后数num个字符,将这num个字符复制到destination
如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。
char str1[]="zzzzzzzzzz";
char str2[]="hello";
strncpy(str1,str2,7);
//str1[]="hello/0/0zzz"
1.6 strncat
char * strncat ( char * destination, const char * source, size_t num );
从source位置开始,往后数num个字符,将这num个字符追加到destination后面。
/* strncat example */
#include <stdio.h>
#include <string.h>
int main ()
{
char str1[20];
char str2[20];
strcpy (str1,"To be ");
strcpy (str2,"or not to be");
strncat (str1, str2, 6);
puts (str1);//To be or not//
return 0;
}
1.7 strncmp
int strncmp ( const char * str1, const char * str2, size_t num );
比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完。
1.8 strstr
char * strstr ( const char *str1, const char * str2);
在str1串中寻找str2第一次出现的位置,如果找到了,就返回第一次出现的地址,如果没有找到(str2不是str1的子集)就返回NULL。
/* strstr example */
#include <stdio.h>
#include <string.h>
int main ()
{
char str[] ="This is a simple string";
char * pch;
pch = strstr (str,"simple");
if(pch!=NULL)
puts(pch);
else
printf("找不到");
}
输出结果:simple string
1.9 strtok
char * strtok ( char * str, const char * sep );
sep参数是个字符串,定义了用作分隔符的字符集合
第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
如果字符串中不存在更多的标记,则返回 NULL 指针。
/* strtok example */
#include <stdio.h>
#include <string.h>
int main ()
{
char str[] ="- This, a sample string.";
char * pch;
pch = strtok (str," ,.-");//分隔符包括:' ' ',' '.' '-'
//这里pch返回的是T的地址,把This后面的','换成了'\0'
while (pch != NULL)
{
printf ("%s\n",pch);
pch = strtok (NULL, " ,.-");
}
return 0;
}
圈出的这些都是被替换成'\0'的
最终输出为:
This
a
sample
string
#include <stdio.h>
int main()
{
char *p = "2023.1/13";
const char* sep = "./";
char arr[30];
char *str = NULL;
strcpy(arr, p);//将数据拷贝一份,处理arr数组的内容
for(str=strtok(arr, sep); str != NULL; str=strtok(NULL, sep))
{
printf("%s\n", str);
}
}
1.10 strerror
char * strerror ( int errnum );
返回错误码,所对应的错误信息。
C语言的库函数在运行的时候,如果发生错误,就会将错误码存在一个变量中,这个变量是errno,全局变量
错误码是一些数字:1 2 3 4 5
我们需要将错误码翻译成错误信息
#include <stdio.h>
#include <string.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));
printf("%s\n",strerror(5));
return 0;
}
这只是演示,但实际上在操作中,会将错误码放在errno中,
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
//打开文件
FILE* pf=fopen("test.txt","r");//如果打开成功,返回有效指针,如果打开失败,返回NULL。
if(pf==NULL)
{
//如果没有这个文件
printf("打开失败\n");
printf("%s\n",strerror(errno));//No such file or directory
return 1;
}
fcolse(pf);
pf=NULL;
return 0;
}
1.11 perror
void perror ( const char * str );
打印错误信息
#include <stdio.h>
int main()
{
//打开文件
FILE* pf=fopen("test.txt","r");//如果打开成功,返回有效指针,如果打开失败,返回NULL。
if(pf==NULL)
{
//如果没有这个文件
printf("打开失败\n");
perror("fopen");//fopen: No such file or directory
return 1;
}
fcolse(pf);
pf=NULL;
return 0;
}
会直接打印错误信息,再打印信息之前,会先打印自定义的信息
perror = printf + strerror
1.12 字符分类函数
函数 | 如果他的参数符合下列条件就返回真 |
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 | 任何图形字符 |
isprint | 任何可打印字符,包括图形字符和空白字符 |
1.13 字符转换函数
int toupper ( int c );
int tolower ( int c );
/* isupper example */
#include <stdio.h>
#include <ctype.h>
int main ()
{
int i=0;
char str[]="Test String.\n";
char c;
while (str[i])
{
c=str[i];
if (isupper(c))
c=tolower(c);
putchar (c);
i++;
}
return 0;
}
1.14 memcpy
void * memcpy ( void * destination, const void * source, size_t num );
函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
这个函数在遇到 '\0' 的时候并不会停下来。
如果source和destination有任何的重叠,复制的结果都是未定义的。
strcpy->字符串拷贝
memcpy可拷贝任意类型void*
int main()
{
int arr1[10] = { 0,1,2,3,4,5,6,7,8,9 };
int arr2[10] = { 0 };
//memcpy(arr2, arr1, 5 * sizeof(int));
//arr2:0,1,2,3,4
//memcpy(arr2, arr1+2, 5 * sizeof(int));
//arr2:2,3,4,5,6
memcpy(arr2, arr1+2, 17);
//arr2:2,3,4,5,6
//6只拷贝了一个字节,06 00 00 00,只拷贝了06(小端存放)
return 0;
}
这里如果是字符串自己拷贝自己:memcpy(arr1+2, arr1, 5 * sizeof(int));
会存在问题,具体情况在下面的函数模拟实现中会有提到
1.15 memmove
void * memmove ( void * destination, const void * source, size_t num );
和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
如果源空间和目标空间出现重叠,就得使用memmove函数处理。
#include <stdio.h>
#include <string.h>
int main ()
{
char str[] = "memmove can be very useful......";
memmove (str+20,str+15,11);
puts (str);//memmove can be very very useful.
return 0;
}
1.16 memcmp
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
比较从ptr1和ptr2指针开始的num个字节
返回值如下:
与strcmp差不多,只不过这个函数是比较字节的
库函数的模拟实现
2.1 strlen
有三种方式
//计数器方式
size_t my_strlen1(const char* str)
{
assert(str!=NULL);
size_t count = 0;
char* c = str;
while (*c)
{
c++;
count++;
}
return count;
}
//递归
size_t my_strlen3(const char* str)
{
assert(str != NULL);
if (*str != 0)
return 1 + my_strlen3(str + 1);
else
return 0;
}
//指针-指针
size_t my_strlen2(const char* str)
{
assert(str!=NULL);
char* c = str;
while (*c != '\0')
{
c++;
}
return c - str;
}
2.2 strcpy
char* my_strcpy(char* dest, const char* src)
{
char* ret = dest;//存住目标空间的开始地址,便于返回
assert(dest && scr);//保证指针的有效性
while ((*dest++ = *src++))//这个表达式的值是*dest,当*src==0时,*dest被赋值为0,*dest=0时为假,中断循环
{
;
}
return ret;
}
2.3 strcat
char* my_strcat(char* dest, const char* src)
{
char* ret = dest;//存住目标空间的开始地址,便于返回
assert(dest && src);//保证指针的有效性
//1.找到目标空间的\0
while (*dest)
{
dest++;
}
//2.追加
while ((*dest++ = *src++))//这个表达式的值是*dest,当*src==0时,*dest被赋值为0,*dest=0时为假,中断循环
{
;
}
return ret;
}
如果目标空间和源空间重叠
char str[20]="hello world";
strcat(str,str+6);
src中的\0被w取代,所以(*dest++ = *src++)这个表达式永远找不到\0,循环不会停下,所以会一直追加,出现bug。
2.4 strcmp
int my_strcmp(const char* str1, const char* str2)
{
assert(str1 && str2);//保证指针的有效性
while (*str1 == *str2)//便利相等的字符,不相等跳出
{
if (*str1 == 0)//*str1 == *str2 == 0
return 0;//字符全部比较后,依然相等,返回0,代表两字符串相等
str1++;//指针指向下一个字符
str2++;
}
return *str1 - *str2;//不相等跳到这里,
//如果*str1>*str2,返回一个>0的数
//如果*str1<*str2,返回一个<0的数
//所以直接返回他俩的差值就行
}
2.5 strstr
一种简单场景:abcdef中找bc,只需要找一次就可以
一种复杂场景:abbbcdef中找bc,需要重复寻找多次
先讨论简单情况
1.传进去str1和str2的地址,指针一定是指向首元素,先将首元素相比,str1:a str2:b,不相等,
2.那么str1的指针就像后挪一位,str2指针位置不变,此时str1:b,str2:b,相等,
3.两个指针都向后挪一位,此时str1:c,str2:c,相等,
4.两个指针都向后挪一位,此时str1:d,str2:\0,str2指向了\0,代表str2已经全部找到,
然后函数就返回str1中最开始与str2相等的地址
这就需要一个专门用来记录起始地址的指针,则第2步再加上:ret=str1记录
第二种情况
1.传进去str1和str2的地址,先将首元素相比,str1:a str2:b,不相等,
2.那么str1的指针就像后挪一位,str2指针位置不变,此时str1:b,str2:b,相等,定义一个指针ret=str1,记录相等的起始地址
3.两个指针都向后挪一位,此时str1:b,str2:c,不相等,
相等的起始地址改变,ret++,
str2指针回到最开始,所以这里就说明了个问题,在最开始需要有个指针c2=str2,记住str2的起始 地址。
str1的指针返回到ret指向的地方
4.重复3.的动作,直到str2指向了\0,查找结束,返回ret
char* my_strstr(const char* str1, const char* str2)//只需查找不用修改,可以加const保护
{
assert(str1 && str2);//保证指针的有效性
if (*str2 == 0)
return (char*)str1;//如果str2为空,直接返回str1就可
char* c1 = NULL;//c1来遍历str1
char* ret = str1;//记录两串相等的起始位置
char* c2 = NULL;//c2来遍历str2
while (*ret)//当两串相等的起始位置指向str1中的\0时证明不可能在str1中找到str2了,所以跳出
{
c2 = str2;//c2返回到str2的首元素地址
c1 = ret;//c1返回到两串相等的起始位置(的下一位)
while (*c1 == *c2&&*c2)//当两字符不相等或者*c2=0的时候跳出
{
//两字符相等时,指针都指向下一位
c1++;
c2++;
}
// 这里是判断跳出循环的原因,
if (*c2 == 0)//如果是因为*c2=0才跳出的(这里不论*c1是否等于*c2都没有影响),证明str2已经被遍历完并且str1中有相等的子串,就返回两串相等的起始位置ret
return ret;
if (*c1 == 0)//这个判断生效的条件是*c1 != *c2 && *c2!=0 && *c1 = 0,这就说明str2还没有被遍历完str1就没有字符了,此时str1里绝对不会找出与str2相同的子串了,所以直接跳出循环。
break;
ret++;//能走到这里证明 str1在当前两串相等的起始位置开始不能找到str2,需跳到下一个位置继续遍历寻找
}
return NULL;//找不到就返回空指针。
}
2.6 memcpy
void* my_memcpy(void* dest, const void* src, size_t num)
{//内存函数,可以传任意类型的指针,返回类型也任意,所以都用void*,复制多少个字节呢,复制num个
assert(dest && src);
void* c = dest;//函数的返回类型就是void*所以
while (num--)//循环num次
{
*(char*)dest=*(char*)src;//void*的指针不能直接解引用,因为不知道需要解几个字节,所以需要强制类型转化为char*(因为char*指针可以一个字节一个字节的复制)
dest = (char*)dest + 1;//不能写成(char*)dest++,因为强制类型转化是临时的,当++的时候dest还是void*类型的//但是前置++(char*)dest可以
src = (char*)src + 1;
}
return c;
}
但是这里有一个小小的问题,如果source和destination有任何的重叠,例如:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
my_memcpy(arr+2, arr, 20);
return 0;
}
此时arr就会变成1,2,1,2,1,2,1,8,9,10
而不是我们想要的1,2,1,2,3,4,5,8,9,10
但是,只要我们从后向前拷贝,而不是从前向后拷贝,就可以解决这个问题
当然,从后往前拷也不是绝对好
my_memcpy(arr,arr+2, 20)
arr就变成了7,6,7,6,7,8,9,10
这时从前往后拷是正确的3,4,5,6,7,6,7,8,9,10
所以,显然是要分情况讨论的
1,2,3,4,5,6,7,8,9,10
假设src->3,copy3,4,5,6,7
当dest落在(src,src+num)时,必须从后向前copy
当dest<src时,必须从前向后copy
当dest>src+num,都可
但一般这种有重叠的问题,我们都用memmove来解决,而不是memcpy,以上分情况讨论的思想其实是memmove的实现思想
2.7 memmove
void* my_memmove(void* dest, const void* src, size_t num)
{
assert(dest && src);
if(dest<src)//从前向后
while (num--)
{
*(char*)dest=*(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
else//从后向前
while (num--)
{
*((char*)dest + num) = *((char*)src + num);//+(n-1),...,+1,+0
}
}