char* 赋值 const char* 释放_错误更正(拷贝赋值函数的正确使用姿势)

这篇博客纠正了关于C++中The Rule of Three的错误,强调了在拷贝赋值函数中释放内存、返回值及自赋值检查的重要性。文章通过实例分析了未正确实现拷贝赋值函数导致的内存泄露问题,并提供了修复方案,以避免自赋值导致的编译错误。
摘要由CSDN通过智能技术生成

这是一篇对什么是C++的The Rule of Three的错误更正和详细说明。

阅读时间7分钟。难度⭐⭐⭐

12930f191cc19d83fe5ae58dcd2c8ba5.png

虽然上一篇文章的阅读量只有凄惨的两位数17e3a25034c31baeb3e34f0ecd4dd342.gif,但是怀着对小伙伴负责的目的,必须保证代码的正确性。这是大厨做技术自媒体的态度。

前文最后一段代码是这样的:

class Dog {
 private:
   char* name;
   int age;
 public:
   '...省略构造和拷贝构造函数...'
    //拷贝赋值函数
   Dog& operator=(const Dog& that) {
     name = new char[strlen(that.name)+1];
     strcpy(name, that.name);
     age = that.age;
   }
   '...省略析构函数...'
};

先不谈异常安全,这段拷贝赋值函数的代码本身有什么问题?

有3个问题:

  • 没有释放原对象指针成员指向的内容

  • 没有返回值

  • 没有自赋值检查

下面我们一个一个分析。

 1   没有释放原对象指针

这个问题很严重,因为一定会造成内存泄露

原因是指针所指的内存未被释放,而指针又指向了别处。

例子如下,我们写了一个main函数,长这样:

int main(int argc, char* argv[]) {
    Dog D1("Bobby", 2);
    Dog D2("Teddy", 3);
    Dog D2 = D1;
}

D1和D2分别是是Dog的对象。根据构造函数的定义,D2中的name指针指向了字符数组“Teddy”。而当进行D2 = D1操作时,name = new char[strlen(that.name)+1]这一步会在D2中重新创建一个名字为name且指向“Bobby”的指针。

这么做也许编译器不会报错,但是会有问题。

因为在new一个name指针之前,原本的name指针指向的内存并没有被释放。而新的name指针只对新创建的内存负责,老的内存已经变成无主之地。看来内存泄露是逃不掉了。

这个问题看着复杂,解决的办法倒是简单,只需要在拷贝赋值函数体第一行加上 delete[] name就可以了。

class Dog {
 private:
   char* name;
   int age;
 public:
   '...省略构造和拷贝构造函数...'
    //拷贝赋值函数
   Dog& operator=(const Dog& that) {
     delete[] name; //释放原对象指针成员指向的内容
     name = new char[strlen(that.name)+1];
     strcpy(name, that.name);
     age = that.age;
   }
   '...省略析构函数...'
};
 2   没有返回值

第二个问题犯的错很低级a9dee05b293385cbe448817a3d006044.png,拷贝赋值函数的行为和普通函数一样需要一个返回值。而返回值的类型通常是类的对象的引用。

参照常用的写法,这里返回*this(this是C++类的隐藏成员,表示对象本身)。

class Dog {
 private:
   char* name;
   int age;
 public:
   '...省略构造和拷贝构造函数...'
    //拷贝赋值函数
   Dog& operator=(const Dog& that) {
     delete[] name; //释放原对象指针成员指向的内容
     name = new char[strlen(that.name)+1];
     strcpy(name, that.name);
     age = that.age;
     return *this; //返回对象引用
   }
   '...省略析构函数...'
};

另外大家可能有疑问为什么返回值是一个引用而不是一个值呢?

答案是只有引用才能进行连续赋值。

假设有3个Dog对象:D1、D2、D3,如果返回值不是引用,那么类似D1 = D2 = D3将不能通过编译。

 3   没有自赋值检查

什么叫做自赋值?

就是两个相同对象之间用等号连接,比如:

int main(int argc, char* argv[]) {
    Dog D1("Bobby", 2);
    Dog D1 = D1; //同一个D1相互赋值
}

当然,一般不会有人写出这样的代码来。这里只是举个简单的例子,但是如果在大型项目中不同开发者对同一对象取了不同的别名,那么自赋值的情况是有可能发生的。

对于上面的Dog类而言,如果执行D1 = D1,那么会发生下面的事情:

首先,对象D1中的name指针被析构,name指向的内存被释放;

然后,下一行中的strlen(that.name)又用到了D1的name所指向的内存。

重点来了:这时你会惊讶地发现编译器提示你name已经不存在了!!!

6741a223ca7e1da01791bc4570b40e6d.gif

因为在编译器看来,你在做对同一对象先释放了内存再使用的非法事情!

就好比你是拆迁大队的,你没有确认拆的是不是自己的房子就不管三七二十一直接拆了,然而你晚上还要回家住......

9e35a6a4cbafce01148d7891347a0fef.gif

C++真的烧脑,仅仅是不小心把自己赋值给了自己就把自己的一部分给搞丢了,这在其他语言中似乎是天方夜谭。但是C++似乎很情愿把事情搞复杂。

幸好,自赋值问题也很容易修复,只需要在delete指针之前做一个自赋值的判断。

完整代码如下:

class Dog {
 private:
   char* name;
   int age;
 public:
   '...省略构造和拷贝构造函数...'
    //拷贝赋值函数
   Dog& operator=(const Dog& that) {
     if(this != &that) { //判断是否自赋值
         delete[] name; //释放原对象的指针指向的内容
         name = new char[strlen(that.name)+1];
         strcpy(name, that.name);
         age = that.age;
     }
     return *this;
   }
   '...省略析构函数...'
};

this != &that这个判断的写法看上去莫名其妙,大厨来给大家分析一下:

this代表D1=D1中等号左边的D1,&that代表等号右边的D1的引用(本质上还是D1)。this和&that二者如果相等就说明是同一个对象,那么拷贝赋值函数就直接返回对象的引用。

至此,三个问题终于都解决了d4eb701707cee8c3ef088e0fbd2bc171.png

 4   总结时刻

通过以上问题的剖析可以发现,C++一大半奇奇怪怪行为的背后都有一个处理不当的指针。

另外,写一个正确的类真的一点都不简单,需要考虑内存泄露,返回值类型,自赋值等等情况。

打住,再说下去大厨真的转行成C++专业劝退师了。

6de789a7b78d9bb7ae199f5d5ac9eac2.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值