常用字符串函数和内存函数
本文介绍常见的字符串函数和内存函数,模拟实现这些函数以及交代这些函数使用时需要注意的地方
一.字符串函数
1.1.strlen函数
函数声明:
size_t strlen ( const char * str );
功能:求字符串的长度。
这个函数大家应该经常使用,但有以下几点需要特别注意
- 字符串已经 ‘\0’ 作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包含 ‘\0’ )。
- size_t strlen ( const char * str );参数指向的字符串必须要以 ‘\0’ 结束。
- 注意函数的返回值为size_t,是无符号的
关于第三点举个例子
#include <string.h>
int main()
{
if (strlen("abc") - strlen("abcd") > 0)
{
printf(">");
}
else
{
printf("<=");
}
return 0;
}
最终结果是”>“,因为strlen(“abc”)是无符号的3,strlen(“abcd”)是无符号的4,二者相减,得到的还是一个无符号数,结果>0。
实现strlen
实现strlen有三种方式:计数器,递归,指针相减
递归:
int MyStrlen(const char * str)
{
if(*str == '\0')
return 0;
else
return 1+my_strlen(str+1);
}
计数器
int MyStrlen(const char * str)
{
int count = 0;
while(*str)
{
count++;
str++;
}
return count;
}
指针相减
int MyStrlen(char *s)
{
char *p = s;
while(*p != ‘\0’ )
p++;
return p-s;
}
1.2.strcpy函数
函数声明:
char* strcpy(char * destination, const char * source);
功能是把源头空间的字符串拷贝到目标字符串中去。
注意:
- 源字符串必须以 ‘\0’ 结束。
- 会将源字符串中的 ‘\0’ 拷贝到目标空间。
- 目标空间必须足够大,以确保能存放源字符串。
- 目标空间必须可变。
关于第四点:例如
const char arr[10] = "abcdef";
arr就不能作为destination传参,因为arr数组的元素是不能改变的。
模拟实现:
#include <assert.h>
char* MyStrcpy(char* dest, const char* src)
{
assert(dest != NULL && src != NULL);
char* ret = dest;
while ((*dest++ = *src++) != '\0')
{
;
}
return ret;
}
这里的assert()是一个库函数,头文件<assert.h>。功能是断言括号里的内容一定成立,如果不成立程序就会停止运行。因为NULL是不能解引用的,加上断言后如果程序出现这个错误,编译器就会告诉你断言失败的位置,方便我们定位错误,所以这个函数对程序员来说是非常友好的,应当多多使用。
这里的while ((*dest++ = *src++) != '\0')
写法非常简洁,值得仔细品味。
1.3.strcat函数
函数声明:
char * strcat ( char * destination, const char * source );
功能是把源头字符串追加目标字符串的后面
注意:
- 源字符串必须以 ‘\0’ 结束。
- 目标空间必须有足够的大,能容纳下源字符串的内容。
- 目标空间必须可修改。
模拟实现
char* mystrcat(char* dest, const char* src)
{
assert(dest && src);
char* ret = dest;
//找到目标字符串的\0
while (*dest != '\0')
{
dest++;
}
//拷贝
while ((*dest++ = *src++) != '\0')
{
;
}
return ret;
}
思路就是先找到目标字符串的‘\0’,从这个位置开始执行拷贝,直到遇到源字符串的‘\0’停下,‘\0’也要拷贝。
思考一个问题,使用strcat,自己给自己追加可以吗?
char arr[20] = "abcdef";
strcat(arr, arr);
我们预期达到的效果是拼接成”abcdefabcdef“这样一个字符串,但是当你这样做时,会发现程序陷入死循环最终崩了。
你会发现,子又生孙,孙又生子,永远都找不到‘\0’,子子孙孙,无穷匮也…………
要想自己给自己追加,可以使用strncat这个函数,感兴趣可自行研究
1.4.strstr函数
函数声明:
char * strstr ( const char *str1, const char * str2);
这个函数的比较有趣,它的功能是在str1这个字符串中找str2这个字符串,如果找到了就返回第一次出现的地址,如果找不到就返回空指针。
模拟实现
char* MyStrstr(const char* str1, const char* str2)
{
assert(str1 && str2);
char* s1 = NULL;
char* s2 = NULL;
char* cp = (char*)str1;
while (*cp != '\0')
{
s1 = cp;
s2 = (char*)str2;
while (*s1 == *s2 && *s1 != '\0' && *s2 != '\0')
{
if (*s2 == '\0')
{
return cp;
}
s2++;
s2++;
}
if (*s2 == '\0')
{
return cp;
}
cp++;
}
return NULL;
}
指针cp是可能满足条件的地址,cp最开始指向str1字符串的第一个字符,然后用s1从cp开始往后遍历,s2从str2首字符开始往后遍历,如果*s1 == *s2,就让他们继续往后移动,如果还是相等,继续移动。
若二者始终相等,一直到把str2遍历完了,说明找到了子字符串,返回最初记录的位置cp;
若在遍历的过程中出现不相等的情况,说明cp不是我们要找的地址,所以让cp往后移动,s1回到cp位置,s2也回到起始位置,重新开始遍历。
如果cp把str1遍历完了还没有被返回,说明找不到了,返回NULL。
二.内存函数
字符串函数只能处理字符串,而内存函数可以处理所有类型的数据
2.1.memcpy函数
函数声明如下:
void * memcpy ( void * destination, const void * source, size_t num );
该函数的功能是把source指向的那块内存中的数据,拷贝到destination指向的那块空间,拷贝多少数据呢?拷贝num个字节的数据。
首先看看使用的例子:
int arr1[] = { 1,2,3,4,5,6,7,8,9,10};
int arr2[8] = { 0 };
memcpy(arr2, arr1, 20);
这段代码就表示把arr1数组的前20个字节的数据,拷贝到arr2数组中去。
拷贝后的结果如下
memcpy函数相较于strcpy函数,它的优点在于能拷贝任意类型的数据,浮点型,整型甚至结构体,万物皆能拷。
我们可以尝试自己实现这个函数:
首先看看这个函数的参数,destination的类型是void*
,void*是一个通用类型的指针,他能接收任意类型数据的地址,但是如果要对他解引用,或者进行加减运算,就必须先要强制类型转换。因为只有这样编译器才知道解引用访问几个字节,+1要跳过几个字节。
还可以发现source指针前加了一个const
,这表示不能通过指针来更改指针指向的那块空间的数据。因为既然是拷贝,那么源头的数据肯定时不需要更改的,在实现这个函数前我就先在参数列表中加上const,如果到时候头脑发热把源头数据给改了,编译器就会报警告提示我。
实现如下
#include <assert.h>
void* MyMemcpy(void* dest, void* src, size_t num)
{
assert(dest != NULL && src != NULL);
void* ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
核心思路就是把void* 强制转换为char*,一次访问一个字节,拷贝num次。
2.2.memmove函数
void * memmove ( void * destination, const void * source, size_t num );
该函数的功能是,把source指向的那块内存中的num个字节的数据,拷贝destination到指向的那块空间。
通过仔细对比你会发现,memmove的功能不是和memcp一样吗,而且函数声明也是一毛一样,那二者的不同之处在哪呢?
在内存重叠的时候,使用memcpy可能会出现意想不到的效果,此时建议使用memove
举例说明:
int arr[20] = { 1,2,3,4,5,6,7,8 };
memcpy(arr + 2, arr, 16);
我的本意是用1,2,3,4来覆盖3,4,5,6,最终得到{1,2,1,2,3,4,7,8}这样一个数组,但结果却是{1,2,1,2,1,2,7,8}。这是因为重叠的那片区域,既是源头数据,也是目标空间的数据,在从前向后拷贝的过程中,重叠部分的数据被改成1,2,而3,4根本就没有机会拷贝。
这是memcpy函数的一个不足之处,而memmove就弥补了这一缺陷。
下面我们自己来实现memmove
memmcpy之所以会出现问题,是因为它总是从前向后拷贝数据,当内存重叠时就可能会出现问题,既然如此,那我们就从后向前拷贝呗!从上面的例子来看,从后向前拷贝确实能解决问题。但上面的情况是重叠部分在源头空间的后部,若重叠部分在源头空间的前部时就需要从前往后拷贝了。而如果两块内存没有重叠区域,从后往前和从前往后均可。因此,需要分类讨论。
1.dest > src从后往前拷贝
2.dest < src从前往后拷贝
实现代码:
#include <assert.h>
void* MyMemmove(void* dest, const void* src, size_t num)
{
assert(dest != NULL && src != NULL);
void* ret = dest;
//前->后
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);
}
}
}
还有一点需要说明,当我们使用VS编译器去测试memcpy时,发现即使内存重叠时也不会出现问题,这是为什么呢?
其实C语言本身只是规定了各种库函数的功能,它规定memcpy的拷贝数据的功能,前提是内存不重叠。同时又规定一个功能更加强大的库函数memmove,它可以处理内存重叠的情况。
规定下来后,各大编译器厂商实现自家的库函数,只需完成函数的基本功能。而开发VS的程序员在写memcpy时显然就超额完成了任务,就连内存重叠也能完成拷贝。但是话说回来,C语言本身并没有要求memcpy有这么强大的功能,所以如果你在别的编译器底下运行时可能就会出现问题。
简单来说,C语言要求memmove拿100分,而memcpy只要打60分就行,但是VS底下的memcpy和memmove都是100分。