string.h
C 标准库 – <string.h> | 菜鸟教程 (runoob.com)
1 void *memchr(const void *str, int c, size_t n)
在参数 str 所指向的字符串的前 n 个字节中搜索第一次出现字符 c(一个无符号字符)的位置。
在这个函数中,可以看到有void *作为形参,void *作为返回值。
这代表什么呢?
void *作为形参表示可以传入任意类型的指针;如果是返回值,则表示可以转成任意类型的指针。但是,通常情况下,输入输出的都是字符指针,传入其他类型的指针无操作意义。
输入时可以直接传入,但是输出时通常需要将void *强转成其他所需类型的指针。因为函数输出的结果总要是一个具体的类型。
这里的位置,指的就是目标字符所在的地址,即指针。
#include <stdio.h> #include <string.h> int main () { const char str[] = "be.happy.everyday"; const char ch = 'p'; //虽然第二个参数是int,但是可以直接传入char char *ret; ret = (char *)memchr(str, ch, strlen(str)); printf("result is %s\n", ret); printf("result is %c\n", *ret); return(0); }
运行结果:
这里说的位置不是直接返回第几个位置,而是地址值。
2 | int memcmp(const void *str1, const void *str2, size_t n) 把 str1 和 str2 的前 n 个字节进行比较。 |
你还在用‘>’‘<’‘=’等比较字符串吗?
事实上,用大于小于比较字符串的方法是不对的。我们看一下两种常见的错误方法。
#include<stdio.h> int main() { char arr1[] = "abcdef"; char arr2[] = "abc"; if (arr1 < arr2) { } if ("abcdef" < "abc") { } }
为什么两个字符串不能直接用大于小于号比较?
因为用大于小于号,比较的是字符串的首地址大小,而不是字符串的大小。如上述例子,比较的是"abcdef"中a的地址和"abc"中a的地址。为什么不能用数组名直接比较
因为数组名代表首元素地址(一般情况下),用数组名比较跟用字符串直接比较性质是一样的,所以用数组名比较也是不对的。
字符串比较大小的实质比较字符串的大小就是比较每个字符串中对应位置上的字符大小(ASC II码值大小),如果相同,就比较下一对,直到不同或者都遇到'\0'。
3 | void *memcpy(void *dest, const void *src, size_t n) 从 src 复制 n 个字符到 dest。 |
该函数返回一个指向目标存储区 str1 的指针,即返回结果是指向目标字符串。
我有个疑问,复制后是接续还是覆盖?
直接说结论:是覆盖。
这里的关键其实不是接续还是覆盖,只要指定了源和目标的地址,那么,就会从源的地址开始处开始复制,放到以目的地址为起始的连续空间中。
通常,dest是一个未初始化的字符数组。
4 | void *memmove(void *dest, const void *src, size_t n) 另一个用于从 src 复制 n 个字符到 dest 的函数。 |
memmove和memcpy的作用都是内存拷贝,唯一的区别是,当内存发生局部重叠时:memmove保证了拷贝的结果是正确的,但是memcpy不一定是正确的。
但是memcpy比memmove速度快。
考虑下面两种内存重叠的情况:
- 第一种情况下,拷贝重叠的区域不会出现问题,内容均可以正确的被拷贝。
- 第二种情况下,问题出现在右边的两个字节,这两个字节的原来的内容首先就被覆盖了,而且没有保存。所以接下来拷贝的时候,拷贝的是已经被覆盖的内容,显然这是有问题的。
注意:是一个字符一个字符地复制的,所以第二种情况会出错。
在实际使用时,使用memmove是比memcpy更安全的。
更多参考:
碰到个问题,我刚想把两个函数操作放一起比较,结果发现结果不对,原来是我两次都是基于原数据来计算的,事实上,运行第一个函数后,内存中的数据就已经变了。
#include <stdio.h> #include <string.h> int main() { char s[] = "123456789"; char *p1 = s; char *p2 = s+2; char *rp1 = (char *)memcpy(p2, p1, 5); //运行一次后,原来的数据已经变了 char *rp2 = (char *)memmove(p2, p1, 5); //第二次运行是在第一次基础上操作的 printf("Hello, World! %s\n", rp1); printf("Hello, World! %s\n", rp2); return 0; }
5 | void *memset(void *str, int c, size_t n) 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。 |
复制到前n个字符,还是第n个字符?
#include <stdio.h> #include <string.h> int main() { char s[] = "behappy"; printf("Hello, World! %s\n", memset(s, 'a', 4)); return 0; }
注意,该值返回一个指向存储区 str 的指针。输入时str是什么类型的指针,输出的就是什么类型的指针,这里不必特意强制类型转换。
memset用来对一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初始化。
作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作。
memset的使用有个需要注意的地方。
那就是,memset只能把所有的内存都赋值成同一个字节数,即在字符的范围内。
事实上,string.h中基本都是针对字符的操作。
所以memset才常常用于初始化,或者给某段内容赋值某个字节数。
想要通过memset把半字或者字类型的数据一次性赋值,是不行的,一定要十分注意。
这个错误我之前就在数组批量赋值时遇到过,我定义一个short类型的数组,然后想着通过memset给这个数组批量赋值short类型的数据,省得用循环去一个一个地赋值了。
但实际得到的效果和想要的想过并不一样,如下所示:
我本来想要的是数组中全是6,但运行结果并不是。
这个非预期的结果咋来的呢?
我想要赋值的是16位的,但实际这个memset函数只会截取低8位,即一个字节一个字节地复制数据,所以实际情况是这样的:
因此,要十分注意这个问题。
C语言中,如果想要给数组批量赋予非单字节的数据,就只能使用循环来进行,没什么更简便的方式。
重要:
在某一天又试了下这个函数:
结果愣是提示段错误。
这程序就两句话,超级简单,哪里出问题了呢?
终于想明白了。
直接用字符串“yallhappy”,默认是char *,也就是说,这是一个常量,是不能改变的。
这种也会触发段错误。
那么,就延伸出一个重要的问题,在这些字符串处理函数中,只要涉及到对字符串的修改,那么所对应的字符串必然是字符数组形式来定义的。
以下为正确用法:
6 | char *strcat(char *dest, const char *src) 把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。 |
该函数返回一个指向最终的目标字符串 dest 的指针。
- dest -- 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串。
- src -- 指向要追加的字符串,该字符串不会覆盖目标字符串。
以上提到了两个关键点,一是目标数组要包含一个字符串,二是要用足够的容量来容纳。
经过测试,我们发现strcat的实现模式是将src中的所有字符(连同字符串最后的’\0’一起)加到dest字符串中第一个‘\0’的位置。
![]()
值得注意的是:
1.追加的字符串src和目标字符串dest中都必须要带有字符’\0’,并且追加字符串src必须以‘\0’结尾,否则追加过程无法顺利实现。
2.目标字符串空间必须足够大(足够容纳下追加字符串src的内容)。
3.目标空间必须可修改(前面不能加const并且不能说常量字符串)但是,在进行测试的时候,发现了一些问题。
1、
直接定义字符串可以追加。理论分析,s并没有足够的容量去容纳。但确实是追加上了。
#include <stdio.h> #include <string.h> int main() { char s[] = "behappy"; char t[] = "everyday"; printf("%s\n", strcat(s, t)); //behappyeveryday return 0; }
2、
目标字符串未初始化,且容量不足容纳源字符串,但还是追加上了
#include <stdio.h> #include <string.h> int main() { char s[1]; char t[] = "everyday"; printf("%s\n", strcat(s, t)); //everyday return 0; }
这是什么原因?
因为strcat函数是不安全的。然数组s的长度是1,但将t连接到s的后面时,不会进行越界检查,而是直接将s追加到t的后面。这样,就会占用不属于s的内存,所以运行程序时可能出现多种情况,比如:
1.
程序奔溃
2.
表现正常
3.
没奔溃,但程序出现莫名其妙的现象这3种可能都有可能出现,出现2实属侥幸,说不定下次运行时就会出现情况1或3。
最好是保证第一个空间长度足够,不足也会尝试继续往后放第二个字符串的字符,但是这种做法是十分危险的,可能会覆盖掉一些有用的数据,导致程序的不可预知的错误,也可能是尝试往一个可读不可写的存储空间写入数据则程序直接崩溃。
先要肯定一点:教程没有说错! 你在编程时必须要注意这一点:保证strcat的第一个参数有足够的空间!为什么没报错呢? 因为:这个函数的参数是指针类型,函数中也只是通过指针来读写这些内存,编译器无从得知这几块内存到底多大,所以编译阶段编译器不会报错。函数的行为大致就是先找到第一个参数所指的内存中字符串结尾的位置,然后从此处开始依次写入第二个参数中的字符...直到写完。函数也根本不知道第一个参数所指的内存空间到底够不够大,所以函数本身也不会对此检查。而在运行时,如果向第一个参数处写入了过多的字符,则有可能会引起问题,也有可能不会:假如第一个参数所指的内存空间后面的内存刚好也是可读写的,那么函数就继续向其中写入字符,虽然这已经是算写“越界”了,但函数不知道啊。 这样后面输出时就会把这字符串完整地输出。但假如第一个参数所指内存后面不可写,到函数写越界时就会引起运行时错误。所以这样的代码在很多时候不会触发运行时错误而运行出看似正确的结果,但这是非常错误的代码! 即便后续的内存是可写的,但也许其中存着其他数据,写越界就有可能覆盖这些数据,引起其他bug,这是严重的安全隐患! 其实所谓“缓冲区溢出漏洞”大致就是这样来的。
7 | char *strncat(char *dest, const char *src, size_t n) 把 src 所指向的字符串追加到 dest 所指向的字符串的结尾,直到 n 字符长度为止。 |
n字符长度是指追加后整个的长度,还是指src要追加的长度?
#include <stdio.h> #include <string.h> int main() { char d[20] = "be happy"; char s[9] = " everyday"; printf("%s\n", strncat(d, s, 5)); //be happy ever return 0; }
经测试,n是指要源字符串要追加的字符长度。
strncat和strcat有何区别?
因为strcat是以'\0'作为结束标志的,所以strcat无法完成对自身的追加。
![]()
问题原因:在第一次循环的时候就自身的‘\0’就已经被第一个元素覆盖了,导致后面找不到'\0'所以无法退出循环,进入了死循环中。
这时候,就需要用到strncat,strncat的原理不同于strcat,strcat是利用'\0'来退出循环,strncat则是用长度来退出,因为有长度的限制,所以只要长度达到了就能结束,安全性有了更好的保证。
延伸:这些处理函数中,有些是以'\0'为结束条件,有些是以长度为结束条件,注意区分。
一般都是实现两个字符串的拼接,长度就是sizeof(str),不能忘了最后的'\0',要不字符串没有结束符了。
8 | char *strchr(const char *str, int c) 在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。 |
该函数返回在字符串 str 中第一次出现字符 c 的位置,如果未找到该字符则返回 NULL。
和memchr相比,strchr不限制前n个字符了,而是在整个目标字符串中搜索。
如果一段内存中有\0字符作为有效数据的话,显然不能用strchr去查找的,因为strchr是以'\0'作为结束条件的。strchr会停在\0,memchr以长度作为结束条件,所以不会停在'\0'。
strchr期望第一个参数以空值终止,因此不需要长度参数。 memchr的工作方式类似,但不期望内存块以空值终止,因此可以成功搜索\0字符。
9 | int strcmp(const char *str1, const char *str2) 把 str1 所指向的字符串和 str2 所指向的字符串进行比较。 |
10 | int strncmp(const char *str1, const char *str2, size_t n) 把 str1 和 str2 进行比较,最多比较前 n 个字节。 |
该函数返回值如下:
- 如果返回值 < 0,则表示 str1 小于 str2。
- 如果返回值 > 0,则表示 str1 大于 str2。
- 如果返回值 = 0,则表示 str1 等于 str2。
12 | char *strcpy(char *dest, const char *src) 需要注意的是如果目标数组 dest 不够大,而源字符串的长度又太长,可能会造成缓冲溢出的情况。 |
13 | char *strncpy(char *dest, const char *src, size_t n) 当 src 的长度小于 n 时,dest 的剩余部分将用空字节填充。 |
可用于给某声明过的数组进行初始化。
或者给某个数组整个地改变其内容。
#include <stdio.h> #include <string.h> int main() { char s[20] = "everydayyyy"; char t[] = "behappy"; printf("%s\n", strcpy(s, t)); //behappy return 0; }
直接复制到s,不管s里面有什么内容。
strcpy()函数是将一个字符串复制到另一块空间地址中的函数,‘\0’是停止拷贝的终止条件,同时也会将 '\0' 也复制到目标空间。
16 | size_t strlen(const char *str) 计算字符串 str 的长度,直到空结束字符,但不包括空结束字符。 |
计算字符串 str 的长度,直到空结束字符,但不包括空结束字符。
![]()
注意,strlen只能得到字符串的长度,包括传入字符串或者字符数组,不能传入其他类型的数据。
18 | char *strrchr(const char *str, int c) 在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。 |
strchr是从第一次出现的位置,strrchr是最后一次出现的位置。注意区分。
注意事项
为什么都把字符串存在字符数组里,而不存在字符指针形式的字符串呢?因为字符指针形式的字符串压根就不能被改变,是在代码区存着的。
men系列和str系列有什么区别?
str开头的函数只能用于字符数据的操作,遇到NULL'\0'会停止。
mem开头的函数用于操作内存内容,可以处理NULL,操作的基本单位是字节操作。
如果mem操作单位是字节,那基本也只适合用来操作字符。如果是数值,通常就可以直接运算或者操作,没必要用这些函数。我觉得二者最大的区别在于,mem以字符数n为操作限制,而str以’\0’作为操作限制。
以strcpy和memcpy为例总结区别:
1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等(虽然一般不会这么操作)。
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy。
再延伸一下,其实str开头的也不全是以’\0’作为判断条件的。有的str带了n的,也是以字符长度n作为结束条件的。比如和strcat类似的另一个strncat.
c语⾔数组复制两种⽅法可以实现。
为方便说明,定义两个整型数组a,b,并实现将a中的值赋值到b中。
int a[4] = {1,2,3,4}, b[4];
1、 通过数组遍历,逐个赋值。
定义循环变量int i;
for(i = 0; i < 4; i ++)
b[i] = a[i];
该程序功能为遍历数组a,逐个赋值到数组b对应元素中。
2、 借助内存复制函数memcpy,整体赋值。
void *memcpy(void *dst, void *src, size_t size);
这个函数的功能为将src上,⼤⼩为size字节的数据赋值到dst上。
赋值数组的代码为
memcpy(b,a,sizeof(a));当我们要比较两个int型的数组怎么办呢?
我们可以借助于memcmp()函数
memset初始化
memset是直接操作内存的,所以也可以用来初始化任意类型数组和结构体
初始化数组参考:C语言 数组初始化的三种常用方法({0}, memset, for循环赋值)以及原理_谁吃薄荷糖的博客-优快云博客
初始化结构体:
在C里面经常使用memset来把一个结构体的内容全部设置为0。
strdup函数
1.函数原型:
#include <string.h> char *strdup(const char *s);
2.功能:
strdup()函数主要是拷贝字符串s的一个副本,由函数返回值返回,这个副本有自己的内存空间,和s没有关联。strdup函数复制一个字符串,使用完后,要使用delete函数删除在函数中动态申请的内存,strdup函数的参数不能为NULL,一旦为NULL,就会报段错误,因为该函数包括了strlen函数,而该函数参数不能是NULL。
C 库函数 -strlcpy
参考:C 库函数 - strlcpy() 和 strncpy() - 我家有只江小白 - 博客园 (cnblogs.com)
strlcpy函数创建的目的主要是针对strcpy函数缓冲区溢出的问题,因为一旦strcpy调用中src的字符串长度超过了dst的缓冲区大小,就会造成缓冲区安全漏洞问题,这是一类常见也严重的计算机安全漏洞。
函数原型声明:
size_t strlcpy(char *dest, const char *src, size_t size)
头文件引用:#include <string.h>
功能: 在已知dest缓冲区大小并不会造成缓冲区溢出前提下,将src地址开始的字符串复制到以dest开始的地址空间
返回值:src字符串的大小
函数参数:参数dest为目的字符串开始的指针,src为源字符串的开始地址,参数size代表dest字符串的大小
!!!!!!注意,size表示的是目标字符串的大小,而不是src源字符串的大小。
sprintf、snprintf、vsprintf、vsnprintf格式化函数分析
sprintf、snprintf、vsprintf、vsnprintf格式化函数分析_c++ sprintf vsprintf详解-优快云博客
strstr函数
strstr函数是C语言中用于查找字符串的函数。
strstr函数是一个在C语言标准库中定义的函数,它的作用是在主字符串(称为haystack)中搜索子字符串(称为needle),并返回指向子字符串第一次出现位置的指针。如果子字符串没有出现在主字符串中,则函数返回NULL。这个函数的定义可以在头文件string.h中找到。
strstr函数的原型如下:
char *strstr(const char *haystack, const char *needle);
其中,
haystack
是要搜索的目标字符串,needle
是要查找的子字符串。在使用strstr函数时,需要注意以下几点:
函数返回的指针指向的是主字符串中匹配到的位置,因此可以通过指针的偏移量来得到具体的位置。
如果需要查找字符串的最后一次出现,可以配合使用strstr函数。
函数的参数都应该是以'\0'结尾的字符串,否则可能会导致不可预料的结果。
总的来说,strstr函数是一个非常实用的字符串处理工具,在C语言编程中经常被用来进行字符串的模式匹配和查找操作。