内存拷贝问题

内存移动函数详解
本文详细介绍了如何编写一个内存移动函数,从最初的版本逐步改进至专业级别,并提供了全面的测试用例。

写一个函数,完成内存移动,并为其写一个简单的测试用例来进行测试。

 够简单的吧?有的同学很快就写出了答案,详见程序清单1与程序清单2

程序清单 1 V0.1版程序

[cpp]  view plain copy
  1. void MyMemMove(char *dst,char *src,int count)  
  2. {  
  3.     while(count--)  
  4.     {  
  5.         *dst++ = *src++;  
  6.     }  
  7. }  

程序清单 测试用例

[cpp]  view plain copy
  1. void Test()  
  2. {  
  3.    char p1[256] = ”hello,world!”;  
  4.    char p2[256] = {0};  
  5.    MyMemMove(p2,p1,strlen(p1));  
  6.    printf(“%s”,p2);  
  7. }  

客观地讲,相比那些交白卷或者函数声明都不会写的同学来说,能够写出这段代码的同学已经非常不错了,至少在C语言这门课程上已经达到了现行高校的教育目标,但是离企业的用人要求还有一定的距离。我们不妨将上面的程序称为V0.1版本,看看还有没有什么地方可以改进。

 首先我们看看函数声明是否合理,V0.1版的程序将源地址和目的地址都用char *来表示,这样当然也没有什么问题,但是让其他人使用起来却很不方便,假如现在要将count个连续的结构体对象移动到另外一个地方去,如果要使用v0.1的程序的话,正确的写法如下:

 MyMemMove((char *)dst,(char *)src,sizeof(TheStruct)*count)

 也就是说我们需要将结构体指针强制转换成char * 才能够正常工作,这样除了字符串以外其它的类型都不可避免地要进行指针强制转换,否则编译器就会呱呱叫,比如在VC++2008下就会出现这样的错误:

 error C2664: 'MyMemMove' : cannot convert parameter 1 from 'TheStruct *' to 'char *'

 那么如何解决这个问题呢?其实很简单,我们知道有一种特别的指针,任何类型的指针都可以对它赋值,那就是void *,所以应该将源地址和目的地址都用void*来表示。当然函数体的内容也要作相应的改变,这样我们就得到了V0.2版的程序。

程序清单 3 V0.2版程序

[cpp]  view plain copy
  1. void MyMemMove(void *dst,void *src,int count)  
  2. {  
  3.    while (count--)  
  4.    {  
  5.       *(char *)dst = *(char *)src;  
  6.       dst = (char *)dst + 1;  
  7.       src = (char *)src + 1;  
  8.    }  
[cpp]  view plain copy
  1. }  
[cpp]  view plain copy
  1.   
 

 有的同学可能会问,这里面不是还有指针强制转换吗?只不过是换了地方。没错,强制指针转换确实是从使用者的代码转移到了库的代码里,但我们可以将MyMemMove理解为库,而将Test理解为使用者,事实上通过调整之后的效果却有天壤之别,V0.1是一逸永劳,而V0.2是一劳永逸!

 还有几个细节需要注意,为了实现链式表达式,我们应该将返回值也改为void *。此外,如果我们不小心将“*(char *)dst = *(char *)src;”写反了,写成“*(char *)src = *(char *)dst;”编译照样通过,而为了找出这个错误又得花费不少时间。注意到src所指向的内容在这个函数内不应该被改变,所有对src所指的内容赋值都应该被禁止,所以这个参数应该用const修饰,如果有类似的错误在编译时就能够被发现:

 error C3892: 'src' : you cannot assign to a variable that is const

 作为程序员犯错误在所难免,但是我们可以利用相对难犯错误的机器,也就是编译器来降低犯错误的概率,这样我们就得到了V0.3版的程序。

程序清单 4 V0.3版程序

[cpp]  view plain copy
  1. void * MyMemMove(void *dst,const void *src,int count)  
  2. {  
  3.     void *ret=dst;  
  4.     while (count--)  
  5.     {  
  6.        *(char *)dst = *(char *)src;  
  7.        dst = (char *)dst + 1;  
  8.        src = (char *)src + 1;  
  9.     }  
  10.     return ret;  
  11. }  

 现在再来考虑这样一种情况,有使用者这样调用库: MyMemMove(NULL,src, count),这是完全可能的,因为一般来说这些地址都是程序计算出来的,那就难免会算错,出现零地址或者其它的非法地址也不足为奇。可以预料的是,如果出现这种情况的话,则程序马上就会down掉,更糟糕的是你不知道错误出在哪里,于是不得不投入大量的精力在浩瀚的代码中寻找bug解决这类问题的通用办法是对输入参数作合法性检查,也就是V0.4版程序。

程序清单 5 V0.4版程序

[cpp]  view plain copy
  1. void * MyMemMove(void *dst,const void *src,int count)  
  2. {  
  3.     void *ret=dst;  
  4.     if (NULL==dst||NULL ==src)  
  5.     {  
  6.         return dst;  
  7.     }  
  8.     while (count--)  
  9.     {  
  10.         *(char *)dst = *(char *)src;  
  11.         dst = (char *)dst + 1;  
  12.         src = (char *)src + 1;  
  13.     }  
  14.     return ret;  
  15. }  

 上面之所以写成“if (NULL==dst||NULL ==src)”而不是写成“if (dst == NULL || src == NULL)”,也是为了降低犯错误的概率。我们知道,在C语言里面“==”“=”都是合法的运算符,如果我们不小心写成了“if (dst = NULL || src = NULL)”还是可以编译通过,而意思却完全不一样了,但是如果写成“if (NULL=dst||NULL =src)”,则编译的时候就通不过了,所以我们要养成良好的程序设计习惯:常量与变量作条件判断时应该把常量写在前面。

 V0.4版的代码首先对参数进行合法性检查,如果不合法就直接返回,这样虽然程序dwon掉的可能性降低了,但是性能却大打折扣了,因为每次调用都会进行一次判断,特别是频繁的调用和性能要求比较高的场合,它在性能上的损失就不可小觑。

 如果通过长期的严格测试,能够保证使用者不会使用零地址作为参数调用MyMemMove函数,则希望有简单的方法关掉参数合法性检查。我们知道宏就有这种开关的作用,所以V0.5版程序也就出来了。

程序清单 6 V0.5版程序

[cpp]  view plain copy
  1. void * MyMemMove(void *dst,const void *src,int count)  
  2. {  
  3.     void *ret=dst;  
  4.     #ifdef DEBUG  
  5.     if (NULL==dst||NULL ==src)  
  6.     {  
  7.         return dst;  
  8.     }  
  9.     #endif  
  10.     while (count--)  
  11.     {  
  12.         *(char *)dst = *(char *)src;  
  13.         dst = (char *)dst + 1;  
  14.         src = (char *)src + 1;  
  15.     }  
  16.     return ret;  
  17. }  

 如果在调试时我们加入 “#define DEBUG”语句,增强程序的健壮性,那么在调试通过后我们再改为“#undef DEBUG”语句,提高程序的性能。事实上在标准库里已经存在类似功能的宏:assert而且更加好用,它还可以在定义DEBUG时指出代码在那一行检查失败,而在没有定义DEBUG时完全可以把它当作不存在。assert(_Expression)的使用非常简单,当_Expression0时,调试器就可以出现一个调试错误,有了这个好东西代码就容易多了。

程序清单 7 V0.6版程序

[cpp]  view plain copy
  1. void * MyMemMove(void *dst,const void *src,int count)  
  2. {  
  3.     assert(dst);  
  4.     assert(src);  
  5.     void *ret=dst;  
  6.     while (count--)  
  7.     {   
  8.         *(char *)dst = *(char *)src;  
  9.         dst = (char *)dst + 1;  
  10.         src = (char *)src + 1;  
  11.     }  
  12.     return ret;  
  13. }  

 一旦调用者的两个指针参数其中一个为零,就会出现如图1所示的错误,而且指示了哪一行非常容易查错。

 

 图 1 assertNULL)时,显示错误

 到目前为止,在语言层面上,我们的程序基本上没有什么问题了,那么是否真的就没有问题了呢?这就要求程序员从逻辑上考虑了,这也是优秀程序员必须具备的素质,那就是思维的严谨性,否则程序就会有非常隐藏的bug,就这个例子来说,如果用户用下面的代码来调用你的程序。

程序清单 重叠的内存测试

[cpp]  view plain copy
  1. void Test()  
  2. {  
  3.    char p [256]= "hello,world!";  
  4.    MyMemMove(p+1,p,strlen(p)+1);  
  5.    printf("%s\n",p);  
  6. }  

 如果你身边有电脑,你可以试一下,你会发现输出并不是我们期待的 “hhello,world!”(在“hello world前加个h),而是“hhhhhhhhhhhhhh”,这是什么原因呢?原因出在源地址区间和目的地址区间有重叠的地方,V0.6版的程序无意之中将源地址区间的内容修改了!有些反映快的同学马上会说我从高地址开始拷贝。粗略地看,似乎能解决这个问题,虽然区间是重叠了,但是在修改以前已经拷贝了,所以不影响结果。但是仔细一想,这其实是犯了和上面一样的思维不严谨的错误,因为用户这样调用还是会出错:

 MyMemMove( p, p+1, strlen(p)+1);

 所以最完美的解决方案还是判断源地址和目的地址的大小,才决定到底是从高地址开始拷贝还是低地址开始拷贝,所以V0.7顺利成章地出来了。

程序清单 9 V0.7版程序

[cpp]  view plain copy
  1. void * MyMemMove(void *dst,const void *src,int count)  
  2. {  
  3.    assert(dst);  
  4.    assert(src);  
  5.    void * ret = dst;  
  6.    if (dst <= src || (char *)dst >= ((char *)src + count)) 
  7.     {  
  8.        while (count--)
  9.        {  
  10.            *(char *)dst = *(char *)src;  
  11.            dst = (char *)dst + 1;  
  12.            src = (char *)src + 1;  
  13.        }  
  14.     }  
  15.     else 
  16.     {  
  17.         dst = (char *)dst + count - 1;  
  18.         src = (char *)src + count - 1;  
  19.         while (count--) 
  20.         {  
  21.             *(char *)dst = *(char *)src;  
  22.             dst = (char *)dst - 1;  
  23.             src = (char *)src - 1;  
  24.         }  
  25.     }  
  26.     return(ret);  
  27. }
  28.  经过以上7个版本的修改,我们的程序终于可以算是业级了。回头再来看看前面的测试用例,就会发现那根本就算不上是测试用例,因为它只调用了最正常的一种情况,根本达不到测试的目的。有了上面的经历,测试用例也就相应地出现了,我们不妨用字符数组来模拟内存。
  29. 程序清单相对全面的测试用例
  30. void Test()  
  31. {  
  32.    char p1[256] = "hello,world!";  
  33.    char p2[256] = {0};  
  34.    MyMemMove(p2,p1,strlen(p1)+1);  
  35.    printf("%s\n",p2);  
  36.    MyMemMove(NULL,p1,strlen(p1)+1);  
  37.    MyMemMove(p2,NULL,strlen(p1)+1);  
  38.    MyMemMove(p1+1,p1,strlen(p1)+1);  
  39.    printf("%s\n",p1);  
  40.    MyMemMove(p1,p1+1,strlen(p1)+1);  
  41.    printf("%s\n",p1);  
  42. }
  43.  初写代码的时候,往往考虑的是程序正常工作的情况该怎么处理。当你有了几年经验,写了几万行代码后就会发现,处理异常部分的分支代码有时比正常的主干线代码还要多,而这也正是高质量程序和一般程序拉开差距的地方。如果把软件产品当作一台机器,那么这样一个个细小的函数和类就是零部件,只有当这些零部件质量都很高时,整个软件产品的质量才会高,不然就会像前几年的国产轿车一样,今天这个零件罢工明天那个零件休息。而作为检验这些零部件的测试用例,一定要模拟各种恶劣的环境,将零部件隐藏的缺陷暴露出来,从这意义上说,编写测试用例的程序员要比软件设计的程序员思维要更严谨才行。
<think> 我们参考上述引用内容,用户需要了解C语言中内存拷贝的方法和相关函数。根据引用,主要涉及memcpy和memmove函数。 引用[1]提到memcpy是内存拷贝函数,并提到一些原理和注意事项。 引用[2]展示了memcpy的使用示例,并提到需要包含头文件string.h。 引用[3]则介绍了内存函数,并展示了重叠内存拷贝问题,这里引出了memmove函数(虽然引用中只写了memcpy,但实际在重叠内存拷贝时应该用memmove)。 引用[4]给出了memcpy的函数声明:void *memcpy(void *str1, const void *str2, size_t n),并说明了参数和返回值。 因此,我们计划回答: 1. 介绍memcpy函数:功能、声明、使用示例、注意事项(不能处理重叠内存)。 2. 介绍memmove函数:为什么需要memmove(解决重叠内存拷贝问题)、函数声明、使用示例。 3. 对比memcpy和memmove,说明使用场景。 4. 简单提及其他内存函数(如memset)以扩展知识(但用户问题主要关注拷贝,所以简要提及即可)。 注意:根据系统指令,数学公式(如果有)使用$...$,但这里主要是代码和文字描述。独立代码块使用```...```格式。 同时,生成相关问题。 回答结构: 一、memcpy函数 二、memmove函数 三、memcpy与memmove的区别 四、其他内存函数(简要) 五、相关问题 </think> ### C语言内存拷贝函数详解 在C语言中,内存拷贝主要通过`memcpy()`和`memmove()`两个标准库函数实现,它们都定义在`<string.h>`头文件中。 #### 一、memcpy()函数 `memcpy()`用于非重叠内存块的拷贝,其函数声明为: ```c void *memcpy(void *dest, const void *src, size_t n); ``` - **参数**: - `dest`:目标内存地址(类型为`void*`) - `src`:源内存地址(类型为`const void*`) - `n`:拷贝的字节数 - **返回值**:目标内存地址`dest` - **特点**: 1. 不处理内存重叠(源和目标内存重叠时行为未定义) 2. 效率高于手动循环拷贝 **使用示例**: ```c #include <stdio.h> #include <string.h> int main() { int arr1[] = {1,2,3,4,5}; int arr2[5] = {0}; memcpy(arr2, arr1, 5 * sizeof(int)); // 拷贝20字节(假设int为4字节) for(int i=0; i<5; i++) printf("%d ", arr2[i]); // 输出: 1 2 3 4 5 return 0; } ``` [^2] #### 二、memmove()函数 `memmove()`用于处理重叠内存拷贝,其函数声明为: ```c void *memmove(void *dest, const void *src, size_t n); ``` - **参数与返回值**:同`memcpy()` - **特点**: 1. 安全处理内存重叠 2. 通过临时缓冲区实现安全拷贝 **使用示例(重叠内存)**: ```c #include <stdio.h> #include <string.h> int main() { int arr[] = {1,2,3,4,5,6,7,8,9,10}; memmove(arr, arr+3, 5 * sizeof(int)); // 将4,5,6,7,8移到前5位 for(int i=0; i<10; i++) printf("%d ", arr[i]); // 输出: 4 5 6 7 8 6 7 8 9 10 return 0; } ``` [^3] #### 三、关键区别 | 特性 | memcpy() | memmove() | |--------------|------------------------|------------------------| | **内存重叠** | 不支持(未定义行为) | 支持 | | **效率** | 更高(直接拷贝) | 稍低(需检查重叠) | | **使用场景** | 非重叠内存块 | 重叠或不确定的内存区域 | > **重要原则**:当源和目标内存区域可能重叠时强制使用`memmove()`,否则优先使用`memcpy()`[^4]。 #### 四、实现原理(简化版) ```c // memcpy基础实现 void* memcpy(void* dest, const void* src, size_t n) { char* d = dest; const char* s = src; while(n--) *d++ = *s++; return dest; } // memmove基础实现 void* memmove(void* dest, const void* src, size_t n) { char* d = dest; const char* s = src; if(d < s) while(n--) *d++ = *s++; // 正向拷贝 else while(n--) d[n] = s[n]; // 反向拷贝 return dest; } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值