关于C++的拷贝构造函数,很多的建议是直接禁用。为什么大家会这么建议呢?没有拷贝构 造函数会有什么限制呢?如何禁用拷贝构造呢?这篇文章对这些问题做一个简单的总结。

这里讨论的问题以拷贝构造函数为例子,但是通常赋值操作符是通过拷贝构造函数来实现 的( copy-and-swap 技术,详见《Exceptional C++》一书),所以这里讨论也适用于赋 值操作符,通常来说禁用拷贝构造函数的同时也会禁用赋值操作符。

为什么禁用拷贝构造函数
关于拷贝构造函数的禁用原因,我目前了解的主要是两个原因。第一是浅拷贝问题,第二 个则是基类拷贝问题。

浅拷贝问题
编译器默认生成的构造函数,是memberwise拷贝,也就是逐个拷贝成员变量,对于 下面这个类的定义:

class Widget {public:Widget(const std::string &name) : name_(name), buf_(new char[10]) {}~Widget() { delete buf_; }
private:std::string name_;char *buf_;
};

默认生成的拷贝构造函数,会直接拷贝buf_的值,导致两个Widget对象指向同一个缓 冲区,这会导致析构的时候两次删除同一片区域的问题(这个问题又叫双杀问题)。

解决这个问题的方式有很多:

  • 自己编写拷贝构造函数,然后在拷贝构造函数中创建新的buf_,不过拷贝构造函数的 编写需要考虑异常安全的问题,所以编写起来有一定的难度;
  • 使用 shared_ptr 这样的智能指针,让所有的 Widget 对象共享一片 buf_,并 让 shared_ptr的引用计数机制帮你智能的处理删除问题;
  • 禁用拷贝构造函数和赋值操作符。如果你根本没有打算让Widget支持拷贝,你完全可
    以直接禁用这两操作,这样一来,前面提到的这些问题就都不是问题了。

基类拷贝构造问题
如果我们不去自己编写拷贝构造函数,编译器默认生成的版本会自动调用基类的拷贝构造 函数完成基类的拷贝:

class Base {public:Base() { cout << "Base Default Constructor" << endl; }Base(const Base &) { cout << "Base Copy Constructor" << endl; }
};
class Drived : public Base {public:Drived() { cout << "Drived Default Constructor" << endl; }
};
int main(void) {Drived d1;Drived d2(d1);
}

上面这段代码的输出如下:

Base Default Constructor
Drived Default Constructor
Base Copy Constructor // 自动调用了基类的拷贝构造函数

但是如果我们出于某种原因编写了,自己编写了拷贝构造函数(比如因为上文中提到的浅 拷贝问题),编译器不会帮我们安插基类的拷贝构造函数,它只会在必要的时候帮我们安 插基类的默认构造函数:

class Base {public:Base() { cout << "Base Default Constructor" << endl; }Base(const Base &) { cout << "Base Copy Constructor" << endl; }
};
class Drived : public Base {public:Drived() { cout << "Drived Default Constructor" << endl; }Drived(const Drived& d) {cout << "Drived Copy Constructor" << endl;}
};
int main(void) {Drived d1;Drived d2(d1);
}

上面这段代码的输出如下:

1
2
3
4
5
Base Default Constructor
Drived Default Constructor
Base Default Constructor // 调用了基类的默认构造函数
Drived Copy Constructor

这当然不是我们想要看到的结果,为了能够得到正确的结果,我们需要自己手动调用基类 的对应版本拷贝基类对象。

Drived(const Drived& d) : Base(d) {cout << "Drived Copy Constructor" << endl;
}

这本来不是什么问题,只不过有些人编写拷贝构造函数的时候会忘记这一点,所以导致基 类子对象没有正常复制,造成很难察觉的BUG。所以为了一劳永逸的解决这些蛋疼的问题, 干脆就直接禁用拷贝构造和赋值操作符。

没有拷贝构造的限制
在C++11之前对象必须有正常的拷贝语义才能放入容器中,禁用拷贝构造的对象无法直接放 入容器中,当然你可以使用指针来规避这一点,但是你又落入了自己管理指针的困境之中 (或许使用智能指针可以缓解这一问题)。

C++11中存在移动语义,你可以通过移动而不是拷贝把数据放入容器中。

拷贝构造函数的另一个应用在于设计模式中的原型模式,在C++中没有拷贝构造函数,这 个模式实现可能比较困难。

如何禁用拷贝构造
如果你的编译器支持 C++11,直接使用 delete

否则你可以把拷贝构造函数和赋值操作符声明成private同时不提供实现。

你可以通过一个基类来封装第二步,因为默认生成的拷贝构造函数会自动调用基类的拷 贝构造函数,如果基类的拷贝构造函数是 private,那么它无法访问,也就无法正常 生成拷贝构造函数。

class NonCopyable {protected:
~NonCopyable() {} // 关于为什么声明成为 protected,参考
// 《Exceptional C++ Style》
private:
NonCopyable(const NonCopyable&);
}
class Widget : private NonCopyable { // 关于为什么使用 private 继承
// 参考《Effective C++》第三版
}
Widget widget(Widget()); // 错误

上不会生成memberwise的拷贝构造函数,详细内容可以参考《深度探索C++对象模型》一 书
禁用拷贝
禁用原因主要是两个:

  1. 浅拷贝问题,也就是上面提到的二次析构。
  2. 自定义了基类和派生类的拷贝构造函数,但派生类对象拷贝时,调用了派生类的拷贝,没有调用自定义的基类拷贝而是调用默认的基类拷贝。这样可能造成不安全,比如出现二次析构问题时,因为不会调用我们自定义的基类深拷贝,还是默认的浅拷贝。

Effective C++条款6规定,如果不想用编译器自动生成的函数,就应该明确拒绝。方法一般有三种:

  1. C++11对函数声明加delete关键字:Base(const Base& obj) = delete;,不必有函数体,这时再调用拷贝构造会报错尝试引用已删除的函数。
  2. 最简单的方法是将拷贝构造函数声明为private
  3. 条款6给出了更好的处理方法:创建一个基类,声明拷贝构造函数,但访问权限是private,使用的类都继承自这个基类。默认拷贝构造函数会自动调用基类的拷贝构造函数,而基类的拷贝构造函数是private,那么它无法访问,也就无法正常生成拷贝构造函数。

(2)第二种方法 继承一个uncopyable类
C++的编译在链接之前,如果我们能在编译期解决这个问题,会节省不少的时间,要想在编译期解决问题,就需要人为制造一些bug。我们声明一个专门阻止拷贝的基类uncopyable。

class uncopyable{protected:uncopyable(){}~uncopyable(){}
private:uncopyable(const uncopyable&);uncopyable& operator=(const uncopyable&);
}

接下来,我们的类只要继承uncopyable,如果要发生拷贝,编译器都会尝试调用基类的拷贝构造函数或者赋值运算符,但是因为这两者是私有的,会出现编译错误。

为什么禁用拷贝(复制)构造函数相关推荐

  1. C++何时调用拷贝(复制)构造函数

    StringBad ditto (motto); StringBad metoo = motto; StringBad also = StringBad(motto); StringBad * pSt ...

  2. C++ Copy Constructor (拷贝构造函数,复制构造函数)

    1.什么是Copy Constructor? Copy Constructor 是一个特殊的构造函数,一般只有一个参数,这个参数一般是用const修饰的,对自己类的一个引用(reference).什么 ...

  3. C++ 复制构造函数或者拷贝构造函数

    复制构造函数 是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象. 复制构造函数通常用于: 通过使用另一个同类型的对象来初始化新创建的对象. 复制对象把它作为参数传 ...

  4. 复制构造函数(拷贝构造函数)

    也许很多C++的初学者都知道什么是构造函数,但是对复制构造函数(copy constructor)却还很陌生.对于我来说,在写代码的时候能用得上复制构造函数的机会并不多,不过这并不说明复制构造函数没什 ...

  5. C++——构造函数(拷贝构造,拷贝复制),析构函数,操作符重载

    C++--构造函数(拷贝构造,拷贝复制),析构函数,操作符重载 构造函数与析构函数:: 涉及构造函数还可以看这篇文章C++搞懂深拷贝初始化=与赋值=的区别 1.声明和定义构造函数和析构函数 构造函数在 ...

  6. C++深复制(深拷贝)、浅复制(浅拷贝)和复制构造函数(拷贝构造函数)详解+实例

    转载出处:https://blog.csdn.net/sxhelijian/article/details/23209967 对象的复制 对于普通类型的对象来说,它们之间的复制是很简单的,例如: in ...

  7. C++拷贝构造函数(复制构造函数)详解

    link 复制构造函数是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用. 如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数.大多数情况下,其作用是实现从源对象到目 ...

  8. 拷贝构造函数c语言,C++拷贝构造函数(复制构造函数)详解

    复制构造函数是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用. 复制构造函数的参数可以是 const 引用,也可以是非 const 引用. 一般使用前者,这样既能以常量对象(初 ...

  9. [ C++ ] — 拷贝构造函数(复制构造函数)

    拷贝构造函数 拷贝构造函数就是用 同一类型的对象复制成员值来初始化对象(当出现类的 "=" 赋值时,就会调用拷贝构造函数) 简单来说,拷贝构造函数就是来复制对象的 默认拷贝构造函数 ...

最新文章

  1. 在Linux下如何安装配置SVN服务
  2. 实现php实现价格的排序,PHP实现二维数组排序(按照数组中的某个字段)
  3. 云时代 揭开性能监测战略的隐秘优势
  4. 计算机网络「二」—— 物理层(多图详解)
  5. jQuery操作Table学习总结(转)
  6. R语言do.call函数简单说明
  7. swift学习_xcode6搭建
  8. C++接收字符串数组_C语言处理字符串的7个函数
  9. Nagios监控数据脚本记录一下。
  10. linux硬件 软件raid,linux学习之路之磁盘阵列RAID及硬件RAID和软件RAID的区别
  11. 新手起步:通达信怎么编写指标公式以及通达信公式的使用方法
  12. 久期方程 matlab,有限差分法解薛定谔方程与MATLAB实现
  13. 集团类企业信息化原则与思路
  14. VB→C++→C#→VB.NET,语言的共性和个性
  15. 搭建一个自己的电影网站?如何做呢
  16. 第十届ACM山东省赛总结
  17. webpack配置缓存
  18. 新浪微博 [异常问题] 414 Request-URL Too Large
  19. 【Colab】Colab使用教程(跑本地文件)
  20. 离散傅里叶变换的算法实现

热门文章

  1. Linux环境下搭建区块链私有链+部署智能合约
  2. farpoint支持python_Farpoint-中文手册
  3. FarPoint.Win.Spread 表格 鼠标悬停 展示表格数据 并且控制每行字数 代码备忘
  4. git规范代码提交格式:commitlint+husky安装
  5. 超级网管员——网络管理
  6. 大数据Hadoop生态圈介绍
  7. 我的世界java1.15更新了什么动物_Minecraft我的世界Java版1.16更新内容
  8. 深蓝学院-视觉SLAM十四讲-第四章作业
  9. CreateFile 函数详细解析
  10. android 自定义滑块,Android自定义View实现滑块SeekBar