这篇文章主要介绍一些常用的字符串函数和内存操作函数,如果哪里写的有问题,欢迎评论区指正
目录
前言
C语言中的库函数,别人已经写好,我们可以直接拿来用,降低了开发门槛,提高了开发效率
这里的这些函数我们不仅要会用,还要知道它的内部是怎么实现的,也要会自己实现它。
学习库函数的时候应该要多查官方文档,看文档中对函数的说明,来更好的学习库函数
推荐两个C语言在线文档:
cplusplus.com - The C++ Resources Network
在学习C语言库函数的时候有什么不懂的都可以通过查询文档来帮助自己了解库函数
库函数分类:
- 标准库函数 (C语言自带的)
- 第三方库函数(要使用,需要额外下载安装)
一、strlen
这个函数用来求字符串长度
首先通过以下代码来体验一下
#include <stdio.h>
#include <string.h>
int main() {
char ch[] = "abcdef";
int len = strlen(ch);
printf("len = %d\n", len);
return 0;
}
运行结果:
函数声明
size_t strlen (const char* str);
头文件:<string.h>
字符串以'\0' 作为结束标志,strlen函数返回的是在字符串中'\0' 前面出现的字符个数(不包含'\0' ),参数str指向的字符串必须要以'\0' 结束,否则此操作就是标准未定义的,运行结果就是随机值
注意函数的返回值为size_t,是无符号的 ,如果我们在写代码中用到了size_t这种类型,一定要多加注意,不然很有可能就会写出bug
如下错误示范
#include <stdio.h>
#include <string.h>
int main() {
char ch[] = {'h','e','l','l','o'};
int len = strlen(ch);
printf("len = %d\n", len);
return 0;
}
分析:
这就是一个有问题的代码,所要求的字符数组中,没有'\0',那这就不是一个字符串,只是一个字符数组,不能用strlen来求长度,因为这就是标准未定义的行为,结果不可预知
strlen从参数ch所指向的字符‘h’开始往后数,没有遇见'\0',一直往后数,最后都已经访问越界了,还在往后数,直到后面可能某个值恰好为'\0'的ascii码0,才结束,导致所求得的这个值是错误的
运行结果:
要想不出错,我们可以手动的给里面添加一个'\0'进去,这样就不会出现越界访问的情况了
模拟实现
接下来,我们来动手自己模拟实现一下这个strlen函数
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* arr) {
assert(arr != NULL); //断言
int i = 0;
int len = 0;
while (arr[i] != '\0') {
i++;
len++;
}
return len;
}
int main() {
//模拟实现strlen
char arr[] = "abcdefgh";
printf("%d\n", my_strlen(arr));
return 0;
}
代码运行:
函数模拟分析:
在模拟实现的时候,函数的返回值我们仍然采用size_t类型,因为字符串长度肯定不可能是负数
在求字符串长度的时候,我们并不会改变字符串的长度,所以这里用const char*类型
函数开始先用assert断言,检验指针的有效性,然后我们就用str指针往后遍历整个字符串,这个过程中用len来计数,直到遇到'\0'结束,就求出了字符串的长度,然后将len作为返回值返回
关于const
const在这里共有三种情况
- const char* str
- char const* str
- char* const str
这三种情况中前两种情况都是一样的,使指针所指向的内容不发生改变,第三种情况是使指针变量本身不改变
在这个函数中,只是求字符串的长度,并不会改变指针所指向的内容,所以参数str加了const修饰
关于函数参数的有效性
在实现函数的时候,对于参数的有效性我们一定要进行检验(非常重要),有如下两种方式:
- if语句
- 断言
用断言会更好,使用断言的话,如果断言的条件为假,程序直接崩溃,并给出错误信息;若断言条件为真,则程序继续往下执行。
二、strcpy
这个函数用来字符串拷贝
通过以下代码先来体验一下
#include <stdio.h>
#include <string.h>
int main() {
char ch[20] = "abcd";
char ch2[] = "efg";
strcpy(ch, ch2);
printf("%s\n", ch);
return 0;
}
运行结果:
这段代码就是通过strcpy函数将ch2字符串里面的内容拷贝到ch里面(连同'\0'一起拷贝)
函数声明
char* strcpy (char* destination, const char* source);
从这个函数声明中我们我可以看到这个函数的信息
返回值:char* 返回拷贝过去之后的目标地址
参数:
destination:这个指针指向要拷贝的目标字符串
source:这个指针指向要被拷贝的源字符串 (在拷贝过程中,不会改变,所以加const修饰)
strcpy就是将source中的内容拷贝到destination中
模拟实现
#include <stdio.h>
#include <string.h>
#include <assert.h>
char* my_strcpy(char* dest, const char* src) {
assert(dest != NULL);
assert(src != NULL);
assert(*src != '\0');
int i = 0;
while (src[i] != '\0') {
dest[i] = src[i];
i++;
}
dest[i] = '\0';
return dest;
}
int main() {
char arr1[100] = "hello";
char arr2[100] = "abc";
my_strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
模拟代码分析:
首先关于函数的参数就不再多说了,和原函数保持一致
函数实现中,一进来先断言,检验指针的有效性
在拷贝过程中,要注意跳出循环的条件,*src != '\0';
在最后返回目标串的地址
为什么还要将目标串的地址返回:
为了能够进行链式访问
注意
- strcpy这个函数在拷贝的时候连同'\0'也一起拷贝过去
- 使用的时候,要求destination对应的内存空间足够大能够容纳下src所指向的字符串,如果destination内存空间不够,就会出现越界的情况,导致未定义行为的发生
三、strcat
这个函数用来进行字符串的拼接
通过以下代码先来体验体验
#include <stdio.h>
#include <string.h>int main() {
char ch[100] = "abcdef";
char ch2[] = "ghi";
strcat(ch, ch2); //将ch2里面的内容追加到ch后面
printf("%s\n", ch);
return 0;
}
运行结果
函数声明
char* strcat (char* destination, const char* source);
strcat的信息
返回值:返回完成字符串拼接之后的字符串地址
参数:
destination:指向目标字符串
source:指向要追加到dest后面的那个字符串,加const修饰,不可改
错误用法
#include <stdio.h>
#include <string.h>
int main() {
char ch[] = "abcdef";
char ch2[] = "hello";
strcat(ch, ch2);
printf("%s\n", ch);
return 0;
}
运行结果:
这就是因为ch只有7个字节大小,不能容纳下拼接之后的字符串
注意:
destination对应的内存空间要足够大,能够容纳下最终的拼接结果
模拟实现
#include <assert.h>
#include <stdio.h>
#include <string.h>
char* myStrcat(char* dest, const char* src) {
// dest要足够大
assert(dest != NULL);
assert(src != NULL); //加断言检验指针的有效性
int i = 0;
while (dest[i] != '\0') {
i++;
} //找dest的'\0'
int j = 0;
while (src[j] != '\0') {
dest[i] = src[j];
i++;
j++;
}
dest[i] = '\0';
return dest;
}
int main() {
char arr1[100] = "abcd";
char arr2[100] = "efg";
myStrcat(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
运行结果:
四、strcmp
这个函数用来比较两个字符串(比较规则:“字典序”---英文词典的单词排序方式)
从两个字符串的首字符开始比较,若相同,继续比较下一个,直到比较出结果
通过strcmp的返回值来确定比较结果
上代码:
#include <stdio.h>
#include <string.h>
int main() {
char ch[] = "abc";
char ch2[] = "ahello";
int ret = strcmp(ch, ch2);
printf("%d\n", ret);
return 0;
}
运行结果
分析
这里的用strcmp比较ch和ch2这两个字符串
首先比较首元素a和a,相等,继续比较下一个,发现b比h小,所以字符串比较结果就是ch比ch2小,返回-1
函数原型
int strcmp (const char* str1, const char* str2);
返回值:int类型
str1 > str2 返回1
str1 < str2 返回-1
str1 == str2 返回0
参数:str1和str2指向要比较的两个字符串(函数内部只对两个字符串进行比较,不改变字符串,所以加const)
注意:'\0'的ascii码值为0,所以比较的时候'\0'也会参与比较
模拟实现
#include <stdio.h>
#include <string.h>
#include <assert.h>
int myStrcmp(const char* str1, const char* str2) {
assert(str1 != NULL);
assert(str2 != NULL);
while (*str1 != '\0' && *str2 != '\0') {
if (*str1 > *str2) {
return 1;
}
else if (*str1 < *str2) {
return -1;
}
else {
str1++;
str2++;
}
}
if (*str1 > *str2) {
return 1;
}
else if (*str1 < *str2) {
return -1;
}
else {
return 0;
}
}
int main() {
char arr1[] = "abcd";
char arr2[] = "abcd";
printf("%d\n", myStrcmp(arr1, arr2));
return 0;
}
运行结果:
五、strstr
这个函数用来进行字符串匹配
就是从在一个字符串(主串)中去找另一个字符串(子串),并返回主串中子串第一次出现的位置
比如要去在"abcdef"找"de",那就从主串的首字符开始找,直到找到"de",然后就会返回主串中"de"的地址
示例代码
#include <stdio.h>
#include <string.h>
int main() {
char ch[] = "abcdef";
char ch2[] = "de";
printf("%p\n", ch);
printf("%p\n", strstr(ch, ch2));
return 0;
}
运行结果:
通过以上代码就可以看到strstr的功能
函数原型
const char* strstr ( const char* str1, const char* str2 );
返回值:const char*类型,返回在str1中str2首次出现的地址。 如果str1中不包含str2,则返回NULL
参数:
str1:指向主串
str2:指向子串
(只进行查找,不改变原来的字符串,所以加const)
模拟实现
暴力破解法(BF算法):
#include <assert.h>
#include <stdio.h>
#include <string.h>
const char* myStrstr(const char* str1, const char* str2) {
assert(str1 != NULL);
assert(str2 != NULL);
assert(*str1 != '\0');
assert(*str2 != '\0');
int i = 0;
int j = 0;
while (str2[j] != '\0' && str1[i] != '\0') {
if (str1[i] == str2[j]) {
i++;
j++;
}
else {
i = i - j + 1;
j = 0;
}
}
if (str2[j] == '\0') {
return &str1[i - j];
}
return NULL;
}
int main() {
char arr1[] = "abcdefg";
char arr2[] = "de";
printf("%p\n", arr1);
printf("%s\n", myStrstr(arr1, arr2));
return 0;
}
运行结果:
注意:
在模拟实现的时候要断言,判断str1和str2是否为空字符串
代码思路:
从str1的首字符开始查找,如果和str2的首字符相同,则比较下一个字符,继续往后比较,若不相同,则从str1第二个字符开始和str2比较,i回退到i - j + 1; j直接回退到0
六、strtok
这个函数用来做字符串切分----把一个字符串按照一定的分隔符切分成多个部分(切完之后还会保存每个部分的起始位置)
eg. 我们这里有个字符串"a,b,c,d,e",我们将这个字符串用,来切分,就可以得到5部分,"a","b","c","d","e"
函数声明
char* strtok (char* str, const char* delimiters);
一般情况下,库函数都是调用一次就可以达到我们的目的,但是这个函数不一样,通过查文档我们可以发现,这个函数调用一次,没啥用,需要连续调用很多次,才能完成切分,并且在连续调用的过程中每次传入的参数都不一样
代码演示
/* strtok example */
#include <stdio.h>
#include <string.h>
int main ()
{
char str[] ="This is a book";
char* pch;
pch = strtok(str," ");
while (pch != NULL)
{
printf ("%s\n",pch);
pch = strtok(NULL, " ");
}
return 0;
}
运行结果:
通过strtok将"This is a book"切分成了四部分并打印了出来
代码分析
在这段代码中,第一次调用strtok函数,第一个参数传字符串的起始地址,第二个参数传分隔符,然后进入循环,判断指针pch是否为空,若不为空,则切分未结束,继续调用strtok来切分(第一个参数为NULL,从上次切分的位置继续往后切分)
切分具体过程
代码中调用strtok用" "来切分字符串,
第一次调用strtok(str, " "); 则这个函数会从字符串起始位置往后找,找到" "之后,将" "改为'\0',同时返回指向T的指针,在循环中printf就打印出了第一部分
第二次调用strtok(NULL, " "); 由于第一个参数为NULL,于是就从上次切分的位置继续往后找" ",在is后面找到了" ",然后将其改为了'\0',同时返回指向i的指针,printf打印is
第三次调用,strtok(NULL, " "); 第一个参数为NULL,从上次切分位置继续往后,将其改为'\0',然后再返回指向a的指针,printf打印a
第四次调用,strtok(NULL, " "); 第一个参数为NULL,从上次切分位置继续往后找" ",没找到空格,但是遇到了'\0',这个时候函数也返回了book的指向'b'的指针,然后printf打印了book
第五次调用,strtok(NULL, " "); 第一个参数为NULL,从上次切分位置继续往后找空格,但上次已经找到了'\0',就没有再往后找的必要了,字符串的切分过程完成,返回NULL
如何做到从上次位置继续往后找呢?
在函数内部肯定是有一个变量来记录上次切分的位置,下次调用的时候,就能直到上次切分的位置了.
但是一个函数调用结束之后函数内部的变量都是局部变量,已经销毁,还怎么记住上次切分的位置呢?
这就是函数内部用到了static,用static来修饰局部变量,会延长局部变量的声明周期(变为整个程序的生命周期)
strtok的缺点
- 需要连续调用多次才能达到切分目的
- 多次调用,每次传入的参数不一致
- strtok内部记录了一个状态(上次切分的位置),就会导致线程不安全,即无法在多线程情况下使用strtok
上面的这一系列的字符串函数只能针对字符串进行相应的操作,比较局限,下面来看内存操作函数
七、memcpy
这个函数用来进行内存拷贝
函数声明
void* memcpy (void* destination, const void* source, size_t num );
返回值:拷贝之后返回destination (可进行链式访问)
参数:
destination: 要拷贝的目标地址
source: 指向将被拷贝的字符串 (不需要改变,加const)
num: 想要拷贝的字节数(size_t类型)
dest和src的类型均是void*,用void* 可以接受任意类型的指针
在函数内部也不需要考虑指针所指向空间的数据类型,直接按字节个数num进行拷贝
代码演示
#include <stdio.h>
#include <string.h>
int main() {
int arr[] = { 1,2,3,4 };
int arr2[] = { 5,6,7,8 };
memcpy(arr, arr2, sizeof(arr2));
for (int i = 0; i < 4; i++) {
printf("%d ", arr[i]);
}
return 0;
}
运行结果:
这段代码就是完成了将arr2里面内容拷贝到arr1当中去
模拟实现
#include <stdio.h>
#include <string.h>
void* myMemcpy(void* dest, const void* src, size_t num) {
assert(dest != NULL);
assert(src != NULL);
assert(num != 0);
char* pdest = (char*)dest;
char* psrc = (char*)src;
for (size_t i = 0; i < num; i++) {
pdest[i] = psrc[i];
}
return dest;
}
int main() {
char arr1[10] = "abcdefg";
char arr2[10] = "de";
myMemcpy(arr1, arr2, sizeof(arr2));
printf("%s\n", arr1);
return 0;
}
运行结果:
注意:
1. 在模拟实现的时候,void*类型的指针是不能进行解引用的,必须要转换为char*,达到每次访问一个字节的效果
2.dest的内存空间要足够大,能够容纳src的长度
八、memmove
这个函数和memcpy的功能基本一样,进行内存拷贝,不同的是,memcpy不支持内存重叠时的拷贝,但memmove可以识别内存重叠,使拷贝不会发生覆盖的现象
内存空间覆盖情况
如下图:
src和dest都是4个字节的空间,现在要将src里面的内容全部拷贝到dest里面,如果按照memcpy函数来拷贝的话,就是先将a拷贝到dest所指向的位置,依次往后拷贝,但是在拷贝a的时候就已经将src的最后一个空间里面的内容给覆盖了,导致拷贝出错,拷贝后dest里面就是abca
但是用memmove函数的话就会对内存覆盖情况作识别,如果内存重叠了,memmove函数就会倒着拷贝,从后往前拷贝,这样就不会出错了。
函数声明
void* memmove (void* destination, const void* source, size_t num);
返回值:返回拷贝之后的dest(便于链式访问)
参数:
destination: 要拷贝的目标地址
source: 指向将被拷贝的字符串 (不需要改变,加const)
num: 想要拷贝的字节数(size_t类型)
dest和src的类型均是void*,用void* 可以接受任意类型的指针
这个函数用法和memcpy相同,这里不再赘述
模拟实现
#include <stdio.h>
#include <string.h>
#include <assert.h>
void* my_memmove(void* dest, const void* src, size_t count) {
assert(dest != NULL);
assert(src != NULL);
assert(count != 0);
char* pdest = (char*)dest;
char* psrc = (char*)src;
if (pdest > psrc && pdest < psrc + count)
{
// 内存重叠
while (count > 0) {
pdest[count] = psrc[count];
count--;
}
}
else {
// 未重叠
int i = 0;
while (count > 0) {
pdest[i] = psrc[i];
count--;
i++;
}
}
return dest;
}
int main() {
char arr[100] = "abc";
char arr2[100] = "hello";
my_memmove(arr, arr2, 6);
printf("%s\n", arr);
return 0;
}
运行结果:
九、memcmp
这个函数用来比较指定大小的内存中的内容的大小
函数声明
int memcmp (const void* ptr1, const void* ptr2, size_t num);
分别比较numptr1和ptr2所指向的两个内存空间中num个字节的内存中的内容
返回值:
若ptr1 > ptr2 返回 1
ptr1 == ptr2 返回0
ptr1 < ptr2 返回-1
参数:
ptr1和ptr2指向要比较的两个内存空间
num表示要比较的内存空间大小(以字节为单位)
代码演示
#include <stdio.h>
#include <string.h>
int main() {
char ch[] = "abcde";
char ch2[] = "aaabce";
int ret = memcmp(ch, ch2, 1);
printf("ret = %d\n", ret);
return 0;
}
这段代码用来比较ch和ch2里面前一个字节的内容,ch和ch2里面每个字符占一个字节,前一个字节的内容均为'a', 相同,则memcmp的返回值为0
运行结果:
现在将memcmp的第三个参数改为2再来看结果:
这就是从第一个字符开始比较,一共比较两个字节的内容,用来比较ch和ch2这两个字符串,也就是比较前两个字符,第一个字符相等,然后来比较第二个字符,b比a大,所以返回值为1
模拟实现
#include <stdio.h>
#include <assert.h>
#include <string.h>
int my_memcmp(const void* ptr1, const void* ptr2, size_t num) {
assert(ptr1 != NULL);
assert(ptr2 != NULL);
assert(num != 0); // 注意断言传过来的num是不是0
char* pptr1 = (char*)ptr1;
char* pptr2 = (char*)ptr2;
int i = 0;
while (num > 0) {
if (pptr1[i] > pptr2[i]) {
return 1;
}
else if (pptr1[i] < pptr2[i]) {
return -1;
}
else {
i++;
num--;
}
}
return 0;
}
int main() {
char ch[] = "abcdef";
char ch2[] = "abcde";
printf("%d\n", my_memcmp(ch, ch2, sizeof(ch)));
return 0;
}
memcpy的模拟实现还是比较容易的
要注意的是void*类型的指针不能进行解引用,需要手动强转为char*类型,再进行解引用比较
十、memset
这个函数用来将指定内存空间中的前num个字节的内存空间内容修改为val值
函数声明
void* memset (void* ptr, int value, size_t num);
返回值:
将前num个字节内存的内容设置为val之后返回ptr
参数:
ptr: 指向要被修改的那块内存
value:要被设置为的值(以int类型进行传递,但在函数填充内存时,使用的是该值的unsigned char形式)
num: 表示要修改多少个字节的内存空间
代码演示
#include <stdio.h>
#include <string.h>
int main ()
{
char str[] = "almost every programmer should know memset!";
memset (str,'-',6);
printf("%s\n", str);
return 0;
}
通过这段代码将str这个字符串中的前6个字节的内容修改为'-'
运行结果:
模拟实现
void* my_memset(void* ptr, int val, size_t num) {
assert(ptr != NULL);
assert(num != 0);
char* pptr = (char*)ptr;
int i = 0;
while (num) {
pptr[i] = val;
num--;
i++;
}
return ptr;
}
以上就是关于字符串和内存操作的一些函数,这些函数我们都不仅要会用,也要会自己实现它,特别是实现的时候里面的检验操作不能缺
-----------------------------------------------------------------
-----------C语言字符串函数&&内存操作函数完结---------
欢迎大家关注!!!
一起学习交流 !!!
让我们将编程进行到底!!!
--------------整理不易,请三连支持------------------