前言

本文仅对函数本身进行分析,不考虑编译环境的差异。

例如关于strcpy,memcpy和memmove关于内存重叠的问题,各个编译器和库都对其进行了各不相同的优化。

strcpy

strcpy提供了字符串的复制。即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符。

要求:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。

基本实现

strcpy函数:

1
2
3
4
5
6
7
char * strcpy(char *dst,const char *src)   //[1]
{
	assert(dst != NULL && src != NULL)  //此句在源码中不存在   [2]
    char *ret = dst;  //[3]
    while ((*dst++=*src++)!='\0'); //[4]
    return ret;
}

[1]const修饰

源字符串参数用const修饰,防止修改源字符串。

[2]空指针检查

(A)不检查指针的有效性,说明不注重代码的健壮性。

(B)检查指针的有效性时使用assert(!dst && !src);

char *转换为bool即是类型隐式转换,这种功能虽然灵活,但更多的是导致出错概率增大和维护成本升高。

(C)检查指针的有效性时使用assert(dst != 0 && src != 0);

直接使用常量(如本例中的0)会减少程序的可维护性。而使用NULL代替0,如果出现拼写错误,编译器就会检查出来。

[3]返回目标地址

(A)忘记保存原始的strdst值。

[4]’\0’

(A)循环写成while (dst++=src++);明显是错误的。

(B)循环写成while (src!=’\0’) dst++=*src++;

循环体结束后,dst字符串的末尾没有正确地加上’\0’。

*dst++=*src++

此处如果是

int i
i++ =5;

则不能通过编译,因为i++不能当作左值使用。

但如果是指针则可行,因为指针的解引用会提取指针中的内容,而不需要指针本身的地址。

为什么要返回char *?

返回dst的原始值使函数能够支持链式表达式。

链式表达式的形式如:

int l=strlen(strcpy(strA,strB));

又如:

char * strA=strcpy(new char[10],strB);

返回strSrc的原始值是错误的。

  1. 源字符串肯定是已知的,返回它没有意义。

  2. 不能支持形如第二例的表达式。

  3. 把const char *作为char *返回,类型不符,编译报错。

注意:strcpy没有考虑内存重叠的情况,下面的测试用例就能使调用strcpy函数的程序崩溃:

1
2
char str[10]="abc";
strcpy(str+1,str);

strncpy

定义

在 ANSI C 中,strcpy 的安全版本是 strncpy。利用strncpy替代strcpy来防止缓冲区越界.

strncpy与strcpy的不同之处就在于复制n个字符,而不是把所有字符拷贝(包括结尾’\0’)

原型:char strncpy(char dst, char *src, size_t n);

功能:将字符串src中最多n个字符复制到字符数组dest中(它并不像strcpy一样遇到NULL才停止复制,而是等凑够n个字符才开始复制),返回指向dest的指针。

说明:

如果n > dest串长度,dest栈空间溢出产生崩溃异常。

否则:

  1. src串长度<=dest串长度,(这里的串长度包含串尾NULL字符)

    1. 如果n < src串长度,src的前n个字符复制到dst中。但是由于没有NULL字符,所以直接访问dst串会发生栈溢出的异常情况。

    2. 如果n >= src串长度,先将src串完全拷贝到dst串中,dst内的未复制空间用’\0’填充.

    3. 如果n = dest串长度,[0,src串长度]处存放于desk字串,(src串长度, dest串长度]处存放NULL。   

  2. src串长度>dest串长度

    如果n <=dest串长度,则dest串没有NULL字符,会导致输出会有乱码。

   综上,一般情况下,使用strncpy时,建议将n置为dest串长度(除非你将多个src串都复制到dest数组,并且从dest尾部反向操作),复制完毕后,为保险起见,将dest串最后一字符置NULL,避免发生在第2)种情况下的输出乱码问题。当然喽,无论是strcpy还是strncpy,保证src串长度<dest串长度才是最重要的。

基本实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
char * strncpy (char * dst, const char * src, size_t count)
{
		assert(dst != NULL && src != NULL)	//此句在源码中不存在
        char *ret = dst;

        while (count && (*dst++ = *src++)!='\0')    /* copy string */
		//保证src没到'\0'和count非0
                count--;

        if (count)                              /* pad out with zeroes */
		//如果count大于零,此时src已经复制完,'\0'已写入,不够的字符数都填充\0
                while (--count)
                        *dst++ = '\0';
        return(ret);
}

strlcpy

strcpy是最不安全拷贝字符串函数,因为src串的长度有时会很长。随后strncpy函数为了解决这个问题出现了,但这个函数有些地方实现得也很诡异,它对于字符串末尾的’\0’处理不是很好。

char str[11];
strncpy(str, "hello world", 11);

在例1中只会把str数组填满,但这个字符串却没有’\0’的结束符。

char str[20];
strncpy(str, "sample", 15);

在例2中,15远远大于了字符串”sample”的长度,这时strncpy要在剩下的部分补充’\0’。首先说这样会影响效率,其次对于static或者calloc这样已经初始化的数组根本不需要填写’\0’。

因此在使用strncpy拷贝字符串时,通常要这样写,

strncpy(path, homedir, sizeof(path) – 1);
path[sizeof(path) – 1] = ‘\0’;

而strlcpy可以自动处理末尾’\0’的问题

size_t strlcpy(char *dst, const char *src, size_t size);

然而strlcpy不是ANSI C函数,一般在linux下使用。

memcpy

memcpy提供了一般内存的复制。即memcpy对于需要复制的内容没有限制,因此用途更广。

基本实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void * memcpy(void *dst,const void *src,int count)
{
   assert(dst != NULL && src != NULL)		//此句在源码中不存在

   void * ret = dst;//保存dst首指针
   while (count--)
   {
      *(char *)dst = *(char *)src;
      dst = (char *)dst + 1;
      src = (char *)src + 1;
   }
   return(ret);
}

注意:

  1. 按照ANSI(American National Standards Institute)标准。不能对void指针进行算法操作。即不能对void指针进行如p++的操作,所以需要转换为具体的类型指针来操作,例如char *。

  2. dst 指针要分配足够的空间,也即大于等于 num 字节的空间。如果没有分配空间,会出现断错误。

  3. dst 和 src 所指的内存空间不能重叠(如果发生了重叠,使用 memmove() 会更加安全)。

strcpy和memcpy的区别

strcpy和memcpy主要有以下3方面的区别。

  1. 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。

  2. 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符”\0”才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。

  3. 用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

memmove

memmovey原型:

void *memmove(void *dest, const void *src, size_t n);

描述:

memmove() 函数从src内存中拷贝n个字节到dest内存区域,但是源和目的的内存可以重叠。

返回值:

memmove函数返回一个指向dest的指针。

从上面的描述中可以看出两者的唯一区别就是在对待重叠区域的时候,memmove可以正确的完成对应的拷贝,而memcpy不能。

内存覆盖的情形有以下两种,

从实现中可以看出memcpy()是从内存左侧一个字节一个字节地将src中的内容拷贝到dest的内存中,这种实现方式导致了对于图中第二种内存重叠情形下,最后两个字节的拷贝值明显不是原先的值了,新的值是变成了src的最开始的2个字节了。

而对于第一种内存覆盖情况,memcpy的这种拷贝方式是可以的。

而memmove就是针对第二种内存覆盖情形,对memcpy进行了改进。

基本实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void * __cdecl memmove  void * dst,const void * src,size_t count){
         void * ret = dst;
         if (dst <= src || (char *)dst >= ((char *)src + count)) {
                   // 若dst和src区域没有重叠,则从起始处开始逐一拷贝
                   while (count--){
                            *(char *)dst = *(char *)src;
                            dst = (char *)dst + 1;
                            src = (char *)src + 1;
                   }
         }
         else{
        // 若dst和src 区域交叉,则从尾部开始向起始位置拷贝,这样可以避免数据冲突
                   dst = (char *)dst + count - 1;
                   src = (char *)src + count - 1;

                   while (count--){
                            *(char *)dst = *(char *)src;
                            dst = (char *)dst - 1;
                            src = (char *)src - 1;
                   }
         }

         return(ret);
}