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

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

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

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

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   没有返回值

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

参照常用的写法,这里返回*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已经不存在了!!!

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

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

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二者如果相等就说明是同一个对象,那么拷贝赋值函数就直接返回对象的引用。

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

4   总结时刻

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

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

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

strcpy函数_错误更正(拷贝赋值函数的正确使用姿势)相关推荐

  1. C++学习笔记day47-----C++98-继承中的构造函数,析构函数,拷贝构造函数,拷贝赋值函数,多重继承,虚继承

    继承中的构造函数 当通过一个子类创建一个新的对象时,编译器会根据子类在初始化表中指明的基类的初始化方式去调用基类相应的构造函数.如果子类的初始化表中,并没有指明基类的初始化方式,编译器将会调用基类的无 ...

  2. 拷贝赋值函数、拷贝构造函数

    拷贝构造函数 也是构造函数的一种,常用来以另一对象为模板创建新对象.如果对象中没有指针,可以直接使用数据库自带的该函数,但有指针就需要自己构建,这是为了避免造成浅拷贝,使两个指针指向同一内存空间. 例 ...

  3. 【C++】Big Five: 构造函数、拷贝构造函数、拷贝赋值函数、移动构造函数、析构函数

    前言 C++类的成员变量是否含有"指针类型"直接决定了"Big Five"函数(就是标题中的5个函数)的编写!有无指针类型"成员变量造成Big Fiv ...

  4. C++中的trivial和non-trivial构造/析构/拷贝/赋值函数及POD类型

    在侯捷的<STL源码剖析>里提到trivial和non-trivial及POD类型,相关知识整理如下. trivial意思是无意义,这个trivial和non-trivial是对类的四种函 ...

  5. python四大高阶函数_详谈Python高阶函数与函数装饰器(推荐)

    一.上节回顾 Python2与Python3字符编码问题,不管你是初学者还是已经对Python的项目了如指掌了,都会犯一些编码上面的错误.我在这里简单归纳Python3和Python2各自的区别. 首 ...

  6. python while函数_详解python while 函数及while和for的区别

    1.while循环(只有在条件表达式成立的时候才会进入while循环) while 条件表达式: pass while 条件表达式: pass else: pass 不知道循环次数,但确定循环条件的时 ...

  7. hive substr函数_数据分析工具篇——HQL函数及逻辑

    本篇文章我们梳理一下hive常用的函数,对于hive而言,常用的函数并不是特别多,往往记住关键几个,就可以解决80%的问题,这也是大家喜欢hive的原因,那么,常用的函数有哪些呢? 时间函数 1)时间 ...

  8. js 匿名函数_编写高质量箭头函数的5个最佳做法

    作者:Dmitri Pavlutin译者:前端小智 来源:dmitripavlutin.com 箭头功能值得流行.它的语法简洁明了,使用词法绑定绑定 this,它非常适合作为回调.在本文中,通过了解决 ...

  9. c++ max函数_「C/C++」函数:定义、调用、参数传递

    5.1基本概念 函数分为主函数和子函数,一个程序中主函数有且只有一个,是程序的入口,而函数(或称子函数)可以有很多.子函数的存在可以让主函数不臃肿.一目了然,增强代码可读性. 引入函数的意义:利用率高 ...

最新文章

  1. 印度交通部或禁止无人驾驶汽车进入本土市场
  2. python 功能 代码_让你的Python代码实现类型提示功能
  3. Community Server专题五:IHttpHandlerFactory
  4. 理解GO CONTEXT机制
  5. OC的项目网址(自己编写的项目)
  6. C语言二叉树曲折级顺序遍历(附完整源码)
  7. php背景图片随页面大小改变,css背景图根据屏幕大小自动缩放
  8. MySQL千万数据量深分页优化
  9. 特征码的使用办法_小脚的美丽与哀愁,34/35码的她们都是怎么买鞋的?
  10. Jquery第二篇【选择器、DOM相关API、事件API】
  11. springboot自动装配原理(通俗易懂)
  12. 个人博客存在的三种形式
  13. java插桩-javaassist
  14. x509证书cer格式转pem格式
  15. 利用WebSphere Edge Server构建冷轧系统负载均衡
  16. redhat安装wine教程_Ubuntu20.04LTS安装搜狗输入法
  17. flea-common使用之通用策略模式实现
  18. Android中,长度单位详解(dp、sp、px、in、pt、mm)具体解释与换算(1)
  19. [ARCGIS]带黑边的IMG格式影像如何消除黑边?
  20. 关于Java对接读卡器遇到的坑Process finished with exit code -1073740940 (0xC0000374)

热门文章

  1. matlab指定间隔符,在matlab中为.dat文件指定小数分隔符[复制]
  2. 帝国cms录入表单模板php,帝国cms7.5在线表单提交制作教程
  3. 用java写的教职工信息管理系统_基于Java的教师信息管理系统的设计与实现论文.doc...
  4. IIS-ShortName-Scanner使用
  5. 设置springboot日志级别_Spring Boot 日志框架实践
  6. 执行命令npm install XXX后仍然提示 Cannot find Module XXX
  7. HTML邮件制作规范
  8. seajs-require使用示例
  9. 《Web前端开发修炼之道》-读书笔记CSS部分
  10. Markdown 进阶