废话不多说,进入正题。字符串操作几乎是我们在C或C++学习的过程中都会遇到困惑的地方,我们得不到自己想要的输出结果, 或者得到了,但没完全得到。就比如我们得到了我们想要的字符串,但是紧接着也会输出一些乱码。类似如图所示的情况:
那么我们应该如何去搞定这些字符串操作的bug呢?接下来咱们来细细讨论:
对于一个字符串数组或者char型指针,我们一定应该给它初始化,因为不初始化的话,里面都是一些默认初始化的值,可以认为里面是一些“垃圾”数据,那么我们首先来介绍我们的第一个函数:
1. 内存初始化memset
我一般用memset这个函数来给动态申请的内存进行初始化,因为如果是栈区内存,我们一般就在变量声明的同时就进行初始化,所以在堆区申请的内存,我们需要手动来给它初始化,我们来看一下函数的声明:
void * memset ( void *Dest, int Value, ACPI_SIZE Count)
从函数的声明来看,总共有三个参数,我们来解释一下这三个参数:
void *Dest:这是我们要进行初始化的指针,在这里被默认为void*指针 int Value:一个int型变量,也就是我们需要给这片内存初始化的值,一般传0 ACPI_SIZE Count:这是我们需要初始化的内存长度,一般是和申请内存的大小相同
我们一般把一块申请到的内存,使用之前,初始化为0,以便后续使用,特别说明:memset 不止针对字符串指针,其它的指针也可以使用。我们来看一个简单的使用举例:
//对于栈区内存,我们在申请到的时候,就对其初始化 char Des[15] = { 0 }; //堆区内存,我们申请到之后,使用memset初始化为0 char* ptr1 = (char*)malloc(15); memset(ptr1, 0, 15);//将我们申请到的15个字节初始化为0
对于栈区内存初始化,我们看一下这个图:
对于堆区内存初始化,我们来看一下初始化执行的效果:
2. 字符串长度计算strlen
一个字符串长度的计算会被用来干什么呢?也许拷贝字符串的时候会用到的吧,或者说来计算一下输入了多少字符?反正不管怎么说,这都是非常通用的字符串操作函数,来看一下它的声明:
ACPI_SIZE strlen ( const char *String)
我们可以看到返回值是一个数值类型,而这个函数的参数只有一个,多少有点敷衍了。。。但是流程还是要走的哈,所以我们再来看一下参数代表的意义:
const char *String:特别注意是一个const char*。有关这个的概念, 在我之前讲C语言指针的时候讲过,意思就是字符串内容不允许修改,这是肯定的,我们传入字符串指针是为了让它计算长度,肯定不能被修改。
其实strlen的实现非常简单,在内部申请了一个局部变量用来计数,然后用一个while循环来判断,如果这个字符串不为 ‘\0’ ,那就指针后移,计数器++,否则,字符串长度计算结束,返回这个计数器变量的值。
现在我们来用它计算一个字符串的长度来看一下:
//初始化为HelloWorld char Des[] = "HelloWorld";//注意这是11个字节,因为还有 \0 结尾 //计算字符串的长度 size_t StringLength = strlen(Des); return 0;
计算这个字符串的结果:
从上图中我们应该能看到两个问题,首先我们默认初始化的Des字符串,里面有11个字符,但是最后我们发现计算出来的StringLength只算出了10,这个原因就是因为在strlen实现的时候,没有把 \0 的长度计算进去,所以需要注意:strlen计算出来的字符串长度不包含 \0 ,这个要切记,以后在写代码过程中会经常用到的。
3. 字符串复制strcpy
当我们想要将一个源字符串拷贝一个备份出来的时候,我们依然有可以使用的字符串操作函数,我们也可以预想的到,如果想要实现一个复制操作,那最起码得有源字符串和目标字符串,这样才能说是拷贝吧,我们在这里先说strcpy函数,我们还是惯例先来看看它的声明:
char * strcpy ( char *DstString, const char *SrcString)
我们看到这个函数的返回值是一个char型指针,但是我们一般不取它的返回值,因为在传参时候的DstString指向的就是我们要传回的字符串值,来看一下这两个参数分别代表什么:
char *DstString:目标字符串指针,需要拷贝到的地方 const char *SrcString:源字符串指针,被拷贝的对象
看一下使用举例:
//对于栈区内存,我们在申请到的时候,就对其初始化 char SrcString[] = "HelloWorld"; //堆区内存,15个字节的长度 char* DstString = (char*)malloc(15); memset(DstString, 0, 15);//将我们申请到的15个字节初始化为0 strcpy(DstString, SrcString);
再来看一下拷贝字符串的效果:
在使用strcpy的过程中需要注意:那就是对于目标内存长度的申请,因为在之前strlen计算长度的时候,并没有把字符串的结尾null给计算进去,所以申请内存时候要多申请一个字节,用来存放字符串结尾字符。这是因为strcpy在内部实现的时候,就会在字符串复制结束之后,在DstString的最后不上\0,也就是null字符来结尾,这个需要占用一个字节。
4. 字符串复制memcpy
在使用strcpy的过程中,我们发现我们只能按照字符串的null字符结尾来控制复制操作的停止,这显得有点被动了,那我们能不能变得主动出击呢?当然是可以的,我们接下来介绍的memcpy就是可以控制我们对于复制长度的控制:
void * memcpy ( void *Dest, const void *Src, ACPI_SIZE Count)
我们还是来看一下各个参数的详细意思吧:
void *Dest:需要拷贝到的目标指针首地址 const void *Src:被拷贝的指针首地址 ACPI_SIZE Count:当前指针类型被拷贝的个数
比如说我们指向从源字符串里面拷贝5个字符过来,那我们只需要将第三参数传个5就OK了,看一下下面这个例子:
//对于栈区内存,我们在申请到的时候,就对其初始化 char SrcString[] = "HelloWorld"; //堆区内存,15个字节的长度 char* DstString = (char*)malloc(15); memset(DstString, 0, 15);//将我们申请到的15个字节初始化为0 //我们只想复制前五个字节 Hello,所以这样写 memcpy(DstString, SrcString,5);
我们可以使用memcpy只复制Hello,但是不能使用strcpy来达到这个效果:
但是这个函数就不会像strcpy一样,在复制完成后给补上 \0 来作为结尾,因为它不止针对字符串操作,所以不会默认给添加\0结尾。
那对于这个函数一定要注意,它不仅仅针对的是字符串系列的操作,我们可以从参数类型看出来,它并不是和str系列的函数一样,str系列都是char型参数,这个函数的参数都是void型参数,那就意味着什么类型,它都可以接受。其实不止当前函数,所有的mem系列函数都不是仅仅针对字符串操作。
5. 字符串复制memmove
好的,在这里我们来提出新的问题,因为有的同学喜欢玩,现在各个领域内卷态势严重,那字符串拷贝当然也要来帮帮场子,如果我们想实现一个自己拷贝自己,那我们使用什么函数呢?如果使用memcpy会有什么样的效果呢?我记得在很久之前一直被大家津津乐道的内存重叠问题,memcpy就是典型的例子,但是因为我很久没测试了,今天测试时候,居然发现memcpy内存重叠问题被解决掉了??
//对于栈区内存,我们在申请到的时候,就对其初始化 char SrcString[] = "HelloWorld"; //我希望拷贝HelloWorld 中的前8个字节给字符串本身 //也就是我希望拷贝之后字符串变为HeHelloWor //但是我预计它会出现HeHeHeHeHe的情况 memcpy(SrcString + 2, SrcString, 8);
本来这里我是预计自己搞自己,然后会发生内存重叠,输出HeHeHeHeHe,然后再引出memmove来解决这个问题,没想到。。。它居然行了??也就是说正确输出了结果:
我都无语了,没想到微软居然把这个问题解决了,而我浑然不知!!!但是,但是,我肯定不甘心,我得告诉大家它之前是怎么实现的:
void * my_memcpy( void *Dest, const void *Src, size_t Count) { char *New = (char *)Dest; char *Old = (char *)Src; while (Count) { *New = *Old; New++; Old++; Count--; } return (Dest); }
这就是它原本的实现逻辑,而按照这个函数的实现逻辑,我们再来测试一番:
OK!!nice!达到我要的错误示范的效果了,而且,它确实之前就是这么实现的!
接下来我们来“顺理成章”的引出我们的主角memmove吧!先来看看声明:
void * memmove ( void *Dest, const void *Src, ACPI_SIZE Count)
对于这个函数的三个参数我们就不解释了,因为这三个参数和memcpy的三个参数是一样的,所以我们直接来看使用举例以及效果图吧:
这里也给大家看一下这个memmove的实现逻辑,来帮助大家更好的理解为什么能够避免内存重叠:
void * memmove ( void *Dest, const void *Src, ACPI_SIZE Count) { char *New = (char *) Dest; char *Old = (char *) Src; //在这里对于两个地址的大小进行了比较,用来确定该如何去复制 if (Old > New) { /*从头部复制*/ while (Count) { *New = *Old; New++; Old++; Count--; } } else if (Old < New) { /*从尾部开始复制*/ New = New + Count - 1; Old = Old + Count - 1; while (Count) { *New = *Old; New--; Old--; Count--; } } return (Dest); }
6. 字符串比较strcmp
在两个字符串出现的时候,有时候我们需要比较两个字符串是否相同,或者看一下从哪儿开始不同的,那我们就用到了strcmp函数:
int strcmp ( const char *String1, const char *String2)
这个函数的返回值是我们需要注意的地方,它的两个参数倒也容易理解:
返回值:如果两个字符串相等,返回0,若String1>String2则返回 1,若String1<String2,则返回 -1 const char *String1:第一个需要比较的字符串 const char *String2:第二个需要比较的字符串
这个函数相对来说比较好理解,让我们来看一个小例子:
//初始化为HelloWorld char String1[] = "HelloWorld"; //初始化为HelloChina char String2[] = "HelloChina"; //预期返回1 int index = strcmp(String1, String2);
比较的办法,就是从两个传入的指针第一个字符开始比较,然后比较标准就是按照ASCII值的大小,比如’W’就大于’C’,这个就根据ASCII表比较了。如上的代码我们预期index值是1:
7. 字符串比较memcmp
我们使用strcmp比较的时候也是有个不好的地方,那就是无法控制比较字符串的长短,字符串长短还是自己能够控制比较舒服,毕竟有时候不需要比较太长。那现在提供给了大家机会,那还愣着干什么,赶紧来看看memcmp:
int __cdecl memcmp( const void *s1, const void *s2, size_t n)
mem系列的函数我们之前就说过了不止针对字符串,从它的参数就可以看出,那这三个参数又是什么意思呢?其实也和memcpy差不多:
返回值:如果两个字符串相等,返回0,若String1>String2则返回 1,若String1<String2,则返回 -1 void *s1:第一个需要比较的指针地址 const void *s2:第二个需要比较的指针地址 size_t n:比较的个数
它的返回值和strcmp是一样的,0,-1或者1,对于我们来说,我们就想比较两个字符串的前5个字节,因为前5个字符是一样的,我们来看看效果:
//初始化为HelloWorld char String1[] = "HelloWorld"; //初始化为HelloChina char String2[] = "HelloChina"; //预期输出0 int index = memcmp(String1, String2, 5);
对于以上两种字符串比较函数,这个需要我们去衡量,在什么时候用哪个,如果说我们需要比较两个完整的字符串,可以选择使用strcmp,如果我们比较两个字符串的一部分,那我们可以使用memcmp。
8. 字符串拼接strcat
我们总希望能有改变自己的机会,字符串又何必不是这样呢??它也许也想变得强大,变的无坚不摧,但是苦于没有办法,一直在沉淀自己,终于,等到了strcat函数的到来:
char * strcat ( char *DstString, const char *SrcString)
我们可以看到这个函数的两个参数和strcpy的参数的一样的,第一参数是个char型指针,第二参数是个const char*指针,也就是说第二个参数指向的字符串不允许被修改:
char *DstString:目标字符串指针,需要拼接到的目标字符串地址 const char *SrcString:源字符串指针,拼接在DstString之后
返回值是一个指针,指向目标字符串的首地址,我们来看看经过拼接之后,能不能顺利的让字符串完成变身,强大自我呢?
//初始化为Hello char String1[] = "Hello"; //初始化为KookNut39 char String2[] = "KookNut39"; //预期最终String1变为HelloKookNut39 strcat(String1, String2);
但是!!!必须要说的是,这样做是不对的,这是一个栈区内存,我们本身只有6个字节的空间,现在加上这个字符串之后,很明显栈区内存越界了!!!这是肯定不被允许的。
想要强大自己,这没错,但是得看有没有这个能力,所以我们现在需要让它首先具备强大自己的能力,那就是具有足够的内存空间!!
//动态申请20个字节,并且初始化为0 char* String1 = (char*)malloc(20); memset(String1, 0, 20); //修改String1为Hello strcpy(String1, "Hello"); //初始化为KookNut39 char String2[] = "KookNut39"; //预期最终String1变为HelloKookNut39 strcat(String1, String2);
稍作修改,动态申请内存,让我们拥有足够的内存空间来存放加上来的字符串,结果如下:
9. 字符串中搜索字符strchr
字符串中搜索特定字符也是我们也许会用到的小功能,其实这个函数相比前面的那些函数来说,也简单很多,我们先来看一下这个函数的声明:
char * strchr ( const char *String, int ch)
对于其中的两个参数和返回值,我们这么理解它:
返回值,当字符ch与源串String第一次匹配的时候的地址,如果串中所有字符都不匹配,那么返回NULL地址 const char *String:源串,匹配的来源 int ch:目标字符,用在再源串中匹配的
我们写一个简单的小例子来结束这个函数的讲解,我们涉及到了匹配成功和不成功两种情况:
char String1[] = "KookNut39"; //字符串中没有 Y 预期返回NULL char* ptr1 = strchr(String1, 'Y'); //预期返回 N 第一次出现的地址 char* ptr2 = strchr(String1, 'N');
看一下运行结果:
10. 字符串中搜索子串strstr
字符串中可以搜索匹配指定字符?那如果我要搜索一个字符串,有没有什么办法来实现呢?当然有啦,直接来一个strstr就可以找到字符串中子串出现的位置啦:
char * strstr ( const char *String1, const char *String2)
我们来看一下这个字符串中这些参数的具体意思:
返回值:如果String2是String1的子串,那么返回第一次匹配的地址,否则返回NULL const char *String1:目标字符串 const char *String2:需要在目标串中寻找的子串
我们还是一样,在这里进行能匹配的子串和不能匹配的子串进行两次测试,看一下返回值是不是符合我们的预期:
//初始化为HelloKookNut39 char String1[] = "HelloKookNut39"; //寻找我的名字KookNut39 char String2[] = "KookNut39"; //预期返回第一次匹配到的地址 char* ptr1 = strstr(String1, String2); //预期返回NULL ptr1 = strstr(String1, "Kt39"); return 0;
行文至此,终于结束了有关常用字符串操作函数的介绍,授人以鱼不如授人以渔,这句话我觉得始终是没问题的,在这里我希望通过给大家介绍简单的使用例子,能够让大家在以后的学习日子里,对于字符串操作可以游刃有余!!如果这篇博文能够给您的学习带来帮助,麻烦您赏博主一个点赞+评论+收藏,给博主继续创作提供动力,谢谢!
今日份与君共勉:“待到秋来九月八,我花开后百花杀”