内存拷贝函数的深入思考

内存拷贝函数

转帖自周立功博客http://blog.sina.com.cn/s/blog_5e8facd20100eorv.html


编者按:我们知道,优秀的运动员除了自身的天赋和努力之外,出色的教练必不可少。一个成功的企业除了拥有出类拔萃的员工之外,同样需要一位出色的教练,那就是企业的CEO。由此可见,如果我们要想成为一位优秀的程序员,毫无疑问卓越的教练是致关重要的。无数事实告诉我们,成功者之所以成功,是因为成功地选择导师的缘故。

 周立功公司之所以在一定程度上取得了一些成功,绝对不是运气所为,除了不断努力选拔优秀人才之外,且不惜代价投入人力和资本培训员工,更重要的是有效的方法,从而保证了初学者的投入与产出的最佳效果,让具备一定潜质的应届大学生脱颖而出。

 优秀与平庸全在一念之间,关键在于你的首次择业而非就业,良好的习惯与平和的心态跟人们的第一份工作和第一位导师有很大的关系。当下的利益固然重要,但从某种程度上来看,机会、环境、远景比眼前利益更重要,因为机会、环境和远景等因素会最大限度地将你锻造成为一个在未来有绝对竞争力的人才。同样是赚钱,你是否参与了公司由默默无闻迈向卓越的过程?你是否收获了更多的快乐?你是否找到了前所未有的成就感?而很多人仅仅赚到了一些钱,幸福而不快乐。

 你将希望成为哪一类人,何去何从?最初的选择尤其重要。


 本文是我们训练程序员的系列文章之一,仅供参考!

 这几年在全国巡回招聘应届毕业生的过程中,经常会遇到这样的现象:有些同学对自己的笔试比较满意,可是最后却得不到面试的机会,心里大为不解,颇有死不瞑目的味道。那么问题到底出现在哪里呢?让我们来看一个例子,这是我们招聘过程中一道常见的题目。

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

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

程序清单 1 V0.1版程序

void MyMemMove(char *dst,char *src,int count)
{
    while(count--)
    {
        *dst++ = *src++;
    }
}

程序清单 测试用例

void Test()
{
   char p1[256] = ”hello,world!”;
   char p2[256] = {0};
   MyMemMove(p2,p1,strlen(p1));
   printf(“%s”,p2);
}

客观地讲,相比那些交白卷或者函数声明都不会写的同学来说,能够写出这段代码的同学已经非常不错了,至少在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版程序

void MyMemMove(void *dst,void *src,int count)
{
   while (count--)
   {
      *(char *)dst = *(char *)src;
      dst = (char *)dst + 1;
      src = (char *)src + 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版程序

void * MyMemMove(void *dst,const void *src,int count)
{
    void *ret=dst;
    while (count--)
    {
       *(char *)dst = *(char *)src;
       dst = (char *)dst + 1;
       src = (char *)src + 1;
    }
    return ret;
}

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

程序清单 5 V0.4版程序

void * MyMemMove(void *dst,const void *src,int count)
{
    void *ret=dst;
    if (NULL==dst||NULL ==src)
    {
        return dst;
    }
    while (count--)
    {
        *(char *)dst = *(char *)src;
        dst = (char *)dst + 1;
        src = (char *)src + 1;
    }
    return ret;
}

 上面之所以写成“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版程序

void * MyMemMove(void *dst,const void *src,int count)
{
    void *ret=dst;
    #ifdef DEBUG
    if (NULL==dst||NULL ==src)
    {
        return dst;
    }
    #endif
    while (count--)
    {
        *(char *)dst = *(char *)src;
        dst = (char *)dst + 1;
        src = (char *)src + 1;
    }
    return ret;
}

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

程序清单 7 V0.6版程序

void * MyMemMove(void *dst,const void *src,int count)
{
    assert(dst);
    assert(src);
    void *ret=dst;
    while (count--)
    { 
        *(char *)dst = *(char *)src;
        dst = (char *)dst + 1;
        src = (char *)src + 1;
    }
    return ret;
}

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

 

 图 1 assertNULL)时,显示错误

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

程序清单 重叠的内存测试

void Test()
{
   char p [256]= "hello,world!";
   MyMemMove(p+1,p,strlen(p)+1);
   printf("%s\n",p);
}

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

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

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

程序清单 9 V0.7版程序

void * MyMemMove(void *dst,const void *src,int count)
{
   assert(dst);
   assert(src);
   void * ret = dst;
   if (dst <= src || (char *)dst >= ((char *)src + count)) 
   {
       while (count--) 
       {
           *(char *)dst = *(char *)src;
           dst = (char *)dst + 1;
           src = (char *)src + 1;
       }
    }
    else 
    {
        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);
}


 经过以上7个版本的修改,我们的程序终于可以算是工业级了。回头再来看看前面的测试用例,就会发现那根本就算不上是测试用例,因为它只调用了最正常的一种情况,根本达不到测试的目的。有了上面的经历,测试用例也就相应地出现了,我们不妨用字符数组来模拟内存。

程序清单 10 相对全面的测试用例

void Test()
{
   char p1[256] = "hello,world!";
   char p2[256] = {0};
   MyMemMove(p2,p1,strlen(p1)+1);
   printf("%s\n",p2);
   MyMemMove(NULL,p1,strlen(p1)+1);
   MyMemMove(p2,NULL,strlen(p1)+1);
   MyMemMove(p1+1,p1,strlen(p1)+1);
   printf("%s\n",p1);
   MyMemMove(p1,p1+1,strlen(p1)+1);
   printf("%s\n",p1);
}


 初写代码的时候,往往考虑的是程序正常工作的情况该怎么处理。当你有了几年经验,写了几万行代码后就会发现,处理异常部分的分支代码有时比正常的主干线代码还要多,而这也正是高质量程序和一般程序拉开差距的地方。如果把软件产品当作一台机器,那么这样一个个细小的函数和类就是零部件,只有当这些零部件质量都很高时,整个软件产品的质量才会高,不然就会像前几年的国产轿车一样,今天这个零件罢工明天那个零件休息。而作为检验这些零部件的测试用例,一定要模拟各种恶劣的环境,将零部件隐藏的缺陷暴露出来,从这意义上说,编写测试用例的程序员要比软件设计的程序员思维要更严谨才行。


<think>好的,我现在需要回答用户关于“char*的赋值函数”的问题。用户之前问过关于将std::string转换为char*或字符数组的方法,现在可能想进一步了解如何给char*赋值,特别是如何正确和安全地进行操作。 首先,我需要确定用户的问题具体指的是什么。"赋值函数"可能指的是如何将值赋给char*指针,或者如何编写一个函数来给char*赋值。用户可能对浅拷贝和深拷贝的区别不太清楚,或者对内存管理有疑问。 接下来,我要回顾char*的基本概念。char*是一个指向字符的指针,通常用于C风格的字符串。在C++中,处理char*需要特别注意内存管理,因为指针本身并不管理内存,容易导致内存泄漏或悬空指针。 用户可能遇到的问题包括:直接赋值导致的问题,比如两个指针指向同一块内存,修改一个会影响另一个;或者没有分配足够的内存导致缓冲区溢出。因此,需要解释不同的赋值方法,如浅拷贝和深拷贝,以及如何安全地进行内存分配和复制。 我需要考虑用户可能的误区,例如直接使用=运算符进行赋值,这实际上只是复制了指针地址,而没有复制字符串内容。应该指出正确的做法是使用strcpy、strncpy或者动态分配内存后再复制。 此外,用户可能需要了解如何编写一个函数来安全地给char*赋值,包括参数的传递、内存的分配和释放,以及避免内存泄漏的方法。还要提到使用智能指针或std::string来避免手动管理内存,但用户的问题集中在char*上,所以重点还是传统的方法。 另外,需要注意const char*的情况,比如字符串字面量是只读的,如果尝试修改会导致未定义行为。需要提醒用户在赋值时是否允许修改内容,并据此选择合适的方法。 还要考虑示例代码的正确性,确保示例中包含了必要的头文件,比如<cstring>用于字符串函数,以及使用new和delete进行内存管理。同时,提醒用户检查内存分配是否成功,虽然在现代C++中通常假设new不会失败,但在实际应用中可能需要处理异常或使用nothrow。 最后,总结不同方法的适用场景,比如浅拷贝适用于只读且原字符串生命周期足够长的情况,深拷贝适用于需要独立修改的情况,并强调内存管理的责任,避免常见错误如内存泄漏或访问已释放的内存。 可能还需要提到C++11之后的特性,比如使用strdup函数(虽然不是标准C++,但常见于某些实现),或者使用std::vector<char>作为替代方案,但用户的问题明确关于char*,所以可能不需要深入这些内容。 现在需要组织这些思考点,分步骤解释如何正确进行char*的赋值,包括直接赋值、浅拷贝、深拷贝,以及安全的方法如使用strcpy和动态内存分配,最后总结注意事项和最佳实践。</think>在 C/C++ 中,`char*` 是指向字符数据的指针,常用于操作 C 风格字符串。对 `char*` 的赋值需要特别注意**内存管理**和**数据复制**的语义。以下是常见方法和注意事项: --- ### **1. 直接赋值(浅拷贝)** 直接将 `char*` 指针指向另一个地址,**不复制数据**,仅共享同一内存: ```cpp char* str1 = "Hello"; // 字符串字面量(只读,可能存储在常量区) char* str2 = str1; // 直接赋值(str2 和 str1 指向同一内存) // 注意:修改 str2 可能导致未定义行为(若原数据在只读区) // str2[0] = 'h'; // 危险!尝试修改常量区的数据 ``` **适用场景**: - 仅需传递指针,无需修改内容(例如函数参数传递)。 --- ### **2. 使用 `strcpy` 复制内容(深拷贝)** 通过 `strcpy` 或 `strncpy` 将数据复制到新内存,实现**独立拷贝**: ```cpp #include <cstring> // 必须包含头文件 int main() { const char* source = "Hello"; char* dest = new char[strlen(source) + 1]; // 分配足够内存(+1 用于 '\0') strcpy(dest, source); // 深拷贝(复制内容) // 修改 dest 不影响 source dest[0] = 'h'; delete[] dest; // 必须手动释放内存 return 0; } ``` **注意**: - 必须确保目标指针已分配足够内存。 - 使用 `strncpy` 可限制复制长度,避免溢出: ```cpp char buffer[10]; strncpy(buffer, source, sizeof(buffer) - 1); // 最多复制 9 个字符 buffer[sizeof(buffer)-1] = '\0'; // 确保结尾是 '\0' ``` --- ### **3. 动态内存分配函数封装** 编写自定义函数实现安全赋值,管理内存生命周期: ```cpp #include <cstring> // 安全赋值函数 char* assign_char(const char* source) { if (!source) return nullptr; // 处理空指针 char* dest = new char[strlen(source) + 1]; strcpy(dest, source); return dest; } int main() { char* str = assign_char("Hello"); // 使用 str... delete[] str; // 必须释放内存 return 0; } ``` --- ### **4. 避免的常见错误** #### **错误 1:未分配内存直接复制** ```cpp char* dest; strcpy(dest, "Hello"); // 错误!dest 未指向有效内存 ``` #### **错误 2:忘记 `\0` 结尾** ```cpp char dest[5]; strncpy(dest, "Hello", 5); // 复制 5 个字符,但没有空间添加 '\0' std::cout << dest; // 可能输出乱码(未正确终止) ``` #### **错误 3:内存泄漏** ```cpp char* dest = new char[100]; dest = "Hello"; // 错误!原内存未释放,直接指向新地址(泄漏) ``` --- ### **5. 最佳实践** 1. **优先使用 `const char*`** 若无需修改内容,使用 `const char*` 避免意外修改: ```cpp const char* str = "Hello"; // 明确声明为只读 ``` 2. **使用智能指针管理内存** 通过 `std::unique_ptr` 自动释放内存: ```cpp #include <memory> #include <cstring> auto str = std::make_unique<char[]>(6); strcpy(str.get(), "Hello"); ``` 3. **尽可能用 `std::string` 替代** C++ 的 `std::string` 自动管理内存,更安全: ```cpp std::string str = "Hello"; const char* c_str = str.c_str(); // 临时访问 C 风格字符串 ``` --- ### **总结** | 方法 | 特点 | 适用场景 | |------|------|----------| | 直接赋值 | 浅拷贝,共享内存 | 只读、生命周期可控 | | `strcpy` | 深拷贝,独立内存 | 需修改数据或长期使用 | | 自定义函数 | 封装内存管理 | 复杂逻辑或复用代码 | **关键原则**: - 明确内存所有权(谁分配,谁释放)。 - 优先使用深拷贝避免意外修改。 - 用 `std::string` 或智能指针减少手动管理。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值