目录
本文重点:
重点介绍处理字符和字符串的库函数的使用和注意事项!
- 求字符串长度 strlen
- 长度不受限制的字符串函数 strcpy strcat strcmp
- 长度受限制的字符串函数介绍 strncpy strncat strncmp
- 字符串查找 strstr strtok
- 错误信息报告 strerror
- 字符操作 内存操作函数 memcpy memmove memset memcmp
1. 前言
在处理字符串时,往往需要我们进行一系列的操作,比如复制,找字串等等。
字符串一般存储在常量区或字符数组中,倘若我们想对字符串实现某些操作,可以尝试运用字符串库函数来解决,这样不仅可以减少代码里,同时也可以让处理统一起来。
另外,内存函数是直接对内存的数字进行直接操控的函数,这类函数处理的数据类型十分广泛,在需要统一修改内存数据或者进行两个未知类型的数据的操作时,我们可以使用内存函数。
下面开始正式介绍字符串函数与内存函数。
2. 字符串函数
2.1 strlen
strlen函数一般用于计算有效字符串的长度,比如abcd\0的长度用strlen来计算,其返回值为4。
下面介绍其原理:
我们需要传一个指针给strlen函数,由指针指向的字符开始,读到的字符若不是\0,那么计数器就加1,遇到\0则计数结束。
以下是strlen的模拟实现:
size_t my_strlen(const char* str)
{
size_t len = 0;
while (*str++)
{
len++;
}
return len;
}
2.2 strcpy
strcpy函数用于复制字符串到指定地址,复制的对象可以是常量字符串,也可以是字符数组中的字符串。而粘贴的对象只能是字符数组。
strcpy的使用样例:
int main()
{
a[5]="abcd";
b[6]="xxxxx";
//把a的字符串复制到b去。
strcpy(b,a);
return 0;
}
结果是b存的字符串变为"abcdx\0"。
这里我们需要注意的是:
- b数组的size必须比a数组的大,否则会报错。
- a与b中的字符串必须以\0结尾,否者复制时可能会造成越界访问。
- a可以是常量字符串,但b必须为字符数组或其他合法的地址。
以下为strcpy的模拟实现:
char* my_strcpy(char* dest, const char* src)
{
while (*dest++ = *src++)
{
;
}
}
2.3 strcmp
strcmp是比较两个字符串的函数,通常不会比较字符串的长度,strcmp的参数是两个字符串的起始地址,从该地址开始比较每个字符的ASCII码,到\0结束,途中若某一个字符比较出了大小,那么将根据情况返回整数或者负数代表大于或小于。若两个字符串相同那么返回0;
下面是strcmp的模拟实现:
int my_strcmp(const char* p1,const char* p2)
{
while (*p1 == *p2)
{
if (*p1 == '\0')
{
return 0;
}
p1++,p2++;
}
return *p1 - *p2;
}
与原strcmp对比,一般在VS上面*p1>*p2会返回1,换了不同的编译器会有不同的值。
总之*p1>*p2会返回正数,否则返回负数,相同返回0;
2.4 strcat
char* strcat(char* dest,const char* src);
strcat函数的作用是在dest字符串的末端接上src字符串。所以需要两个参数,1是目标字符串的地址,以及另一个字符串的地址。
strcat的模拟实现:
char* my_strcat(char* dest,const char* src)
{
char* ret = dest;
while (*dest)
{
dest++;
}
while (*dest++ = *src++)
{
;
}
return ret;
}
可以看到,strcat是先找到字符串dest的\0位置,然后将src的首字符覆盖\0,然后进行strcopy。
那么这时,我们就需要注意:
strcat的两个参数不可以是同一个地址的字符串,也就是该字符串不可以自己给自己追加字符。因为strcat会将原字符串的\0覆盖,两个指针同时移动,永远不可能停下。
一旦开始追加,那么src永远找不到\0所以追加永远不会结束。
2.5 strstr
char* strstr(const char* p1,const char* p2);
我曾经在KMP算法中讲过这个函数,在字符串p1中寻找p2子串。其实我目前了解到的找子串的算法有两种KMP算法、字符串哈希、暴力算法。
如果是暴力算法那思路很简单,从p1每一个字符开始,与p2进行比对,如果比对失败,则进行下一个字符的比对。这样的算法时间复杂度为O(N^2)。
KMP算法在我的另一文中有所介绍(适用于strstr)。这里就直接模拟暴力解法了。
char* my_strstr(const char* p1,const char* p2)
{
while (*p1)
{
if (*p1 == *p2)
{
const char* cur1 = p1, * cur2 = p2;
while (*cur1 == *cur2 || *cur2 == '\0')
{
if (*cur2 == '\0')
{
return p1;
}
cur1++, cur2++;
}
}
p1++;
}
return NULL;
}
那么字符串函数我们就先讲到这里,接下来看看内存函数。
3. 内存函数
基本概念:
内存函数是对内存(16进制数)进行直接操作的函数,通常用来进行初始化,无差别修改等操作。
假如我们想交换a,b的值,那么我仅需要把a中每一个十六进制数与b中的每一个十六进制数进行交换即可。有了这个基本概念,我们就可以进行内存函数的操作了。
3.1memcpy
void* memcpy(void* dest ,void* src, size_t size);
这个函数其实很像我们的strncpy函数,后面会有有讲到。
由于是进行内存上的操作,那么无论何种类型的参数,内存函数都应该能接收,通过计算好的size来对size个字节的内存进行修改,这里便是对size个字节的空间进行copy操作。
先上模拟的函数:
void* my_memcpy(void* dest ,void* src, size_t size)
{
void* ret = dest;
while (size--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
这里把类型强制转换成char* 就是为了能够以字节为单位进行操作,同时需要注意一下dest与src指针递增的方式,这是比较标准的,因为void指针不能进行++操作,因为不同的编译器可能会存在语法上不支持的操作。为了代码的兼容性更高,我们采用这种方式进行。
3.2 memmove
这个函数是加强版的memcpy函数,如果想要把数组本身的内容复制给本身,那么我们就要用到memmove函数,比如 12345678,想要修改成 12567878,仅需把2作为目的地,把5678copy到2345即可。
那有人可能会问,这样memcpy就不能实现了吗?其实memcpy也可以实现,但是memmove与memcopy本身定位的功能就是这么分的而已。
下面是memmove的模拟实现:
void* my_memmove(void* dest,void* src,size_t size)
{
assert(dest && src);
void* ret = dest;
if (dest <= src)
{
while (size--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else
{
while (size--)
{
*((char*)dest + size) = *((char*)src + size);
}
}
return ret;
}
3.3 (补充)memset
void* memset(char* dest, int val, size_t size);
memset函数是将size个字节的十六进制数修改成val值,val常用0,-1,ff等,根据需要来初始化内存空间,一般独立使用不接收返回的指针。
4. 其他好用的字符串函数
4.1 strn系列
strncpy | 复制n个字符 |
strncmp | 比较n个字符 |
strncat | 追加n个字符 |
需要3个参数,前两个与原本的相同,第三个需要一个整形n。不再赘述。
4.2 strtok
假如我需要把一个邮箱分为3个部分,比如 123456@qq.com 分为123456 、qq、com。
这时候就需要用到字符串分割函数strtok。
char * strtok ( char * str, const char * sep );
sep中是一个只含分隔符标志的字符串,比如“@.”。str就是需要被分割的字符串。这个函数会把第一个遇到的分隔符变成\0,返回起始地址。这个函数有记忆功能,需要用到循环配合使用。
str1[] = "123456@qq.com";
seq[] = "@.";
char* p1 = strtok(str1,seq);
p1拿到的是1的地址。
char* p2 = strtok(NULL,seq);
p2拿到的是q的地址。
char* p3 = strtok(NULL,seq);
p3拿到的是c的地址
后面再用strtok将不起作用。
4.3 strerror
char * strerror ( int errnum );
strerror需要搭配errno变量来实现,这个变量是C语言自带的,使用时需要#include <errno,h>
errno是错误信息码,比如我们遇到的404 NOT FOUND 中的404也是错误码。
而strerror(errno)则能够返回错误码所对应的错误信息。可以以%s的方式打印出错误信息。
printf ("Error opening file unexist.ent: %s\n",strerror(errno));
而perror则是printf + strerror(errno)的结合体。
perror("自定义信息");
4.4 字符分类函数
函数 | 如果他的参数符合下列条件就返回真 |
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 |
salnum | 字母或者数字,a~z,A~Z,0~9 |
ispunct | 标点符号,任何不属于数字或者字母的图形字符(可打印) |
isgraph | 任何图形字符 |
isprint | 任何可打印字符,包括图形字符和空白字符 |
5. 结语
以上便是字符函数与内存函数的库函数介绍,因为经常用到,我们这里就进行了还算详细的介绍以及模拟实现。希望能帮得上大家。