函数 —— strncpy() (内存重叠) memcpy() memmove()

时间:2023-01-20 16:55:51
char *strncpy(char *dest, const char *src, size_t n)

/*功能:
 * C 库函数 char *strncpy(char *dest, const char *src, size_t n) 把 src 所指向的字符串复制到 dest,最多复制 n 个字符。当 src 的长度小>于 n 时,dest 的剩余部分将用空字节填充。*/
/*参数:
 * dest -- 指向用于存储复制内容的目标数组。
 * src -- 要复制的字符串。
 * n -- 要从源中复制的字符数。*/
/*返回值:

 * 该函数返回最终复制的字符串。*/

  
  说明:
        如果src的前n个字节不含NULL字符,则结果不会以NULL字符结束。
        如果src的长度小于n个字节,则以NULL填充dest直到复制完n个字节。
        src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。
        返回指向dest的指针。

不考虑内存重叠

int main(void)
{
        char src[40];
        char dst[12];
        char dst_less[25];

        strcpy(src,"This is runoob.com");

        printf("strlen(src) = %d\n",strlen(src)); //strlen(src) = 18

误解一:如果src 长度大于等于 n, 那么 strncpy 会拷贝 n – 1 各字符到 dest, 然后补 0?

错,大错特错,罚抄上面的 DESCRIPTION ,直到看到:

if there is no null byte among the first n bytes of src, the result will not be null-terminated.

这就可能导致了不安全的因素。

如果待拷贝字符串长度大于了 n, 那么 dest 是不会有结尾字符 0 的。假设这样一种情况:

1
2
3
char s[] =  "hello world" ;
strncpy (s,  "shit!" , 5);
puts (s);

输出的结果是 “shit” 还是 “shit! world” ?

这种情况只是导致了输出结果错误,严重的,如果 dest n 字节后面一直没有 0,那么就会导致程序段错误。

strncpy 最开始引入标准库是用来处理结构体中固定长度的字符串,比如路径名,而这些字符串的用法不同于 C 中带结尾字 0 的字符串。所以 strncpy 的初衷并不是一个安全的 strcpy.

        //src 的长度     大于 或者 等于     len的长度(18>10)
        //把src所指向的字符串复制到dest,最多赋值10个,注意它的结果不会以NUL字节结尾(NUL即'\0')
        memset(dst,'\0',sizeof(dst));
        strncpy(dst,src,10);
        printf("最终的目标字符串是:%s\n",dst);//最终的目标字符串是:This is ru

误解二:如果 src 长度小于 n, 那么strncpy 和 strcpy 效果一样?

错,事实上,strncpy 还会把 dest 剩下的部分全部置为 0!

        //src的长度      小于      len的长度(18<20)
        //dest_less数组 的剩余部分将用空字节填充 到len的长度。
        printf("sizeof(dst_less)=%d\n",sizeof(dst_less)); //sizeof(dst_less)=25
        memset(dst_less,'\0',sizeof(dst_less));
        strncpy(dst_less,src,20);
        printf("最终的目标字符串是: %s\n",dst_less);//最终的目标字符串是: This is runoob.com

一直认为 strncpy 只是比 strcpy 多了长度校验,确不知道 strncpy 会把剩下的部分全置为 0(粗体部分)。

char *strncpy(char *dest, const char *src, size_t n);

DESCRIPTION
The strcpy() function copies the string pointed to by src (including the terminating `\0′ character) to the array pointed to by dest. The strings may
not overlap, and the destination string dest must be large enough to receive the copy.
The strncpy() function is similar, except that not more than n bytes of src are copied. Thus, if there is no null byte among the first n bytes of src,
the result will not be null-terminated.
In the case where the length of src is less than that of n, the remainder of dest will be padded with null bytes.

这会导致什么后果呢?

首先,如果 strncpy 的长度填错了,比如比实际的长,那么就可能会把其他数据清 0 了。我就遇到过这个问题,在后来检查代码看到这个问题时,也并不以为然,因为拷贝的字符串不可能超过缓冲区的长度。

另外,假设 dest 的长度为 1024, 而待拷贝的字符串长度只有 24,strncpy 会把余下的 1000 各字节全部置为 0. 这就可能会导致性能问题,这也是我


网上很多博客也写了这个函数,面试也常常会遇到,但是,我发现网上的很多代码都是有问题的,我们先看下大部分网上博客的实现:
[plain]  view plain  copy
  1. char *strncpy(char *dst, const char *src, size_t len)  
  2. {  
  3.     assert(dst != NULL && src != NULL);  
  4.     char *res = dst;  
  5.     while (len--)  
  6.     {  
  7.         *dst++ = *src++;  
  8.     }  
  9.     return res;  
  10. }  

看着好像没啥问题,但是,当src的长度小于len呢?这份代码没有处理这个问题。当src的长度小于len时,应该如何处理?《C和指针》p179给出的答案是:
和strcpy一样,strncpy把源字符串的字符复制到目标数组。然而,它总是正好向dst写入len个字符。如果strlen(src)的值小于len,dst数组就用
额外的NUL字节填充到len长度,如果strlen(src)的值大于或等于len,那么只有len个字符被复制到dst中。”
注意!它的结果将不会以NUL字节结尾。(NUL即‘\0’).

由此可见,我们还需要判断strlen(src)是否小于len,如果是,还需要在dst后面添加NUL,因此,正确的代码应该如下:
[plain]  view plain  copy
  1. char *strncpy(char *dest, const char *src, size_t len)  
  2. {  
  3.     assert(dest != NULL && src != NULL);  
  4.     char *res = dest;  
  5.     int offset = 0;  
  6.     if (strlen(src) < len)//src长度小于len  
  7.     {  
  8.         offset = len - strlen(src);  
  9.         len = strlen(src);  
  10.     }  
  11.   
  12.     while (len--)  
  13.     {  
  14.         *dest++ = *src++;  
  15.     }  
  16.     while (offset--)  
  17.     {  
  18.         *dest++ = '\0';  
  19.     }  
  20.     return res;  
  21. }  



使用这个函数,尤其需要注意,不要出现len>strlen(dst)的情况,如果len>strlen(dst),那么会破坏dst后面的内存:
函数 —— strncpy() (内存重叠) memcpy() memmove()
我们假设前面红色部分是dst,然后strncpy(dst,src,10);那么后面黄色部分的内存就被破坏了。strncpy是不负责检测len是否大于dst长度的。

总的来说,strncpy总是复制len个字符到dst指向的内存!!!

所以,还会出现下面的情况:

[plain]  view plain  copy
  1. char message[] = "abcd";  
  2. strncpy(message, "abcde",5);  
  3. cout << message;  
输出是abcde烫烫烫烫烫烫烫烫烫烫烫烫烫烫  (结果不唯一)

message的内存是有5个字节的,但是将abcde拷贝过去时,最后面的‘\0’被覆盖了,strncpy并不会负责添加‘\0’到dst结尾,因此,输出该字符串是,会在e字符后面一直找到‘\0’才结束,因此就会出现乱码。

考虑内存重叠

        char message[] = "qwertyu";
        printf("sizeof(message)=%d\n",sizeof(message)); //sizeof(message)=8
        strncpy(message,"abc",3);
        printf("message=%s\n",message); //message=abcrtyu

        char *p = NULL;
        p=(char*)malloc(100);
        memcpy(p,"123456789",strlen("123456789")); //会等到错误的结果,有一个长度参数,只能拷>贝cnt个字节就结束了
        printf("before p =%s\n",p); //before p =123456789
        strcpy(p+1,p); //注意:这里重叠了,而strcpy是根据判断原串中的'\0'
        printf("after p =%s\n",p);//after p =1123456789
        free(p);
        return(0);
}

面试中经常会遇到让你写一个能够处理内存重叠的strncpy,标准库中的strncpy是不考虑内存重叠的,如果出现内存重叠,结果将是未定义的。

网上的很多博客也有这个代码的实现,其实很多也是有问题的,没有考虑src长度小于len的问题:
[plain]  view plain  copy
  1. char *strncpy(char *dst, const char *src, size_t len)  
  2. {  
  3.     assert(dst != NULL && src != NULL);  
  4.     char *res = dst;  
  5.     if (dst >= src && dst <= src + len - 1)//重叠,从后向前复制  
  6.     {  
  7.         dst = dst + len - 1;  
  8.         src = src + len - 1;  
  9.         while (len--)  
  10.             *dst-- = *src--;  
  11.     }  
  12.     else  
  13.     {  
  14.         while (len--)  
  15.             *dst++ = *src++;  
  16.     }  
  17.     return res;  
  18. }  


那么,如果要处理内存重叠,该怎么办?如果内存重叠和src的长度小于len这两种情况同时出现,又如何处理?

函数 —— strncpy() (内存重叠) memcpy() memmove()
见图,假设红色部分为src,黄色为dst。如果出现内存重叠,我们很容易想到:从后往前拷贝。如果src的长度小于len,则在后面补NUL。

[plain]  view plain  copy
  1. char *strncpy(char *dst, const char *src, size_t len)  
  2. {  
  3.     assert(dst != NULL && src != NULL);  
  4.     char *res = dst;  
  5.     int offset = 0;  
  6.     char *tmp;  
  7.     if (strlen(src) < len)//src长度小于len  
  8.     {  
  9.         offset = len - strlen(src);  
  10.         len = strlen(src);  
  11.     }  
  12.   
  13.     if (dst >= src && dst <= src + len - 1)//重叠,从后向前复制  
  14.     {  
  15.         dst = dst + len - 1;  
  16.         src = src + len - 1;  
  17.         tmp = dst;  
  18.         while (len--)  
  19.             *dst-- = *src--;  
  20.     }  
  21.     else  
  22.     {  
  23.         while (len--)  
  24.             *dst++ = *src++;  
  25.         tmp = dst;  
  26.     }  
  27.     while (offset--)  
  28.     {  
  29.         *tmp++ = '\0';  
  30.     }  
  31.     return res;  
  32. }  

那么,如果len的值大于dst的值,就会破坏dst后面的内存空间,这应该是要避免的。

最后,我们看一个有意思的东西:(此处strncpy是考虑内存重叠的版本)
函数 —— strncpy() (内存重叠) memcpy() memmove()
message的长度增加了0.0  当然  ,它后面的内存被破坏了,这可能带来严重的后果。


最后,使用strncpy时,最好自动添加‘\0’在结尾:
[plain]  view plain  copy
  1. char buffer[BSIZE];  
  2. .  
  3. .  
  4. strncpy(buffer,name,BSIZE);  
  5. buffer[BSIZE-1]='\0'; 

解决问题:

int main(void)
{
        char string[8] = {'\0'};
        char *str1 = "abcde";
        int i =0;
        strncpy(string, str1, 3);
        string[3] = '\0';

        for(i=0;i<sizeof(string);i++)
        {
                printf("sting[%d] = %c\n",i,string[i]);
        }
        printf("%s\n", string);
        return 0;
}
函数 —— strncpy() (内存重叠) memcpy() memmove()

内存重叠:拷贝的目的地址在源地址范围内。所谓内存重叠就是拷贝的目的地址和源地址有重叠。

在函数strcpy和函数memcpy都没有对内存重叠做处理的,使用这两个函数的时候只有程序员自己保证源地址和目标地址不重叠,或者使用memmove函数进行内存拷贝。

memmove函数对内存重叠做了处理。

函数原型:

1
2
void  *memcpy(  void  *dest,  const  void  *src, size_t count );
void  *memmove(  void * dest,  const  void * src, size_t count );

 1.memcpy和memmove相同点
都是用于从src拷贝count个字节到dest。

2.memcpy和memmove区别
如果目标区域和源区域有重叠的话:
memcpy不能够确保源串所在重叠区域在拷贝之前被覆盖。
memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,复制后src内容会被更改,当目标区域与源区域没有重叠则和memcpy函数功能相同。

但当源内存和目标内存存在重叠时,memcpy会出现错误,而memmove能正确地实施拷贝,但这也增加了一点点开销。


memmove的处理措施:
(1)当源内存的首地址等于目标内存的首地址时,不进行任何拷贝
(2)当源内存的首地址大于目标内存的首地址时,实行正向拷贝
(3)当源内存的首地址小于目标内存的首地址时,实行反向拷贝

3.Linux下的实现过程
linux下,两个函数都在头文件string.h中定义,函数原型为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void  * __cdecl memcpy (  void  * dst, const  void  * src,size_t count);
void  * __cdecl memmove (  void  * dst, const  void  * src,size_t count);
 
//实现代码如下:
 
void  * __cdecl memcpy (  void  * dst, const  void  * src,size_t count){
          void  * ret = dst;
 
          while  (count--) {
         // 注意, memcpy函数没有处理dst和src区域是否重叠的问题
                    *( char  *)dst = *( char  *)src;
                    dst = ( char  *)dst + 1;
                    src = ( char  *)src + 1;
          }
 
          return (ret);
}
  
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);
}

当src和dst区域没有重叠时,两个函数是完全一样的。否则,memcpy不能正常工作的,memmove是可以正常工作的。

4.Windows平台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <string.h>
 
/* VS2010, Windows XP, Debug模式下运行 */
int  main( void )
{  
     void  test_memfunc( void );
     test_memfunc();
 
     return  0;
}
 
void  test_memfunc( void ){
 
     char  s1[] =  "abcdefgefghijklmnopq" ; //首地址:0x0012ff48
     char  s2[] =  "123456789" ; //首地址:0x0012ff34
     char  *c =  NULL ;
 
     int  l =  sizeof (s1); //数组s1长度为21
 
      /*
      内存重叠 : s2覆盖了原s1()的一部分空间。
      即: s2(0x0012ff34-0x0012ff49)   的内存地址范围和s1(0x0012ff48-0x0012ff5c)的内存地址范围发生重叠
      */
      c = memcpy(s2,s1, sizeof (s1)); //改用memmove同样运行出错
 
      /*
      运行出错:变量s1损坏。
      Run-Time Check Failure #2 - Stack around the variable 's2' was corrupted.
      */
      printf( "%s" ,s1);
}

 windows平台下,当发生内存重叠的时候,都不能正常运行,运行栈被破坏,提示错误:Run-Time Check Failure #2 - Stack around the variable 's2' was corrupted.



参考链接:

//https://blog.csdn.net/sinat_30071459/article/details/72771137
//http://www.kuqin.com/clib/string/strncpy.html
//http://blog.haipo.me/?p=1065
//https://www.cnblogs.com/Benoly/p/3845000.html
//http://www.kuqin.com/clib/string/strncpy.html