当前位置:网站首页>三说 拷贝构造之禁用
三说 拷贝构造之禁用
2022-07-01 06:26:00 【HUSTER593】
关于C++的拷贝构造函数,很多的建议是直接禁用。为什么大家会这么建议呢?没有拷贝构造函数会有什么限制呢?如何禁用拷贝构造呢?这篇文章对这些问题做一个简单的总结。
这里讨论的问题以拷贝构造函数为例子,但是通常赋值操作符是通过拷贝构造函数来实现 的( copy-and-swap 技术,详见《Exceptional C++》一书),所以这里讨论也适用于赋 值操作符,通常来说禁用拷贝构造函数的同时也会禁用赋值操作符。
1 为什么禁用拷贝构造函数
关于拷贝构造函数的禁用原因,我目前了解的主要是两个原因。第一是浅拷贝问题,第二 个则是基类拷贝问题。
1.1 浅拷贝问题
编译器默认生成的构造函数,是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支持拷贝,你完全可 以直接禁用这两操作,这样一来,前面提到的这些问题就都不是问题了。
1.2 基类拷贝构造问题
如果我们不去自己编写拷贝构造函数,编译器默认生成的版本会自动调用基类的拷贝构造函数完成基类的拷贝:
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);
}
上面这段代码的输出如下:
Base Default Constructor
Drived Default Constructor
Base Default Constructor // 调用了基类的默认构造函数
Drived Copy Constructor
这当然不是我们想要看到的结果,为了能够得到正确的结果,我们需要自己手动调用基类 的对应版本拷贝基类对象。
Drived(const Drived& d) : Base(d) {
cout << "Drived Copy Constructor" << endl;
}
这本来不是什么问题,只不过有些人编写拷贝构造函数的时候会忘记这一点,所以导致基类子对象没有正常复制,造成很难察觉的BUG。所以为了一劳永逸的解决这些蛋疼的问题,干脆就直接禁用拷贝构造和赋值操作符。
2 没有拷贝构造的限制
在C++11之前对象必须有正常的拷贝语义才能放入容器中,禁用拷贝构造的对象无法直接放入容器中,当然你可以使用指针来规避这一点,但是你又落入了自己管理指针的困境之中 (或许使用智能指针可以缓解这一问题)。
C++11中存在移动语义,你可以通过移动而不是拷贝把数据放入容器中。
拷贝构造函数的另一个应用在于设计模式中的原型模式,在C++中没有拷贝构造函数,这 个模式实现可能比较困难。
3 如何禁用拷贝构造
如果你的编译器支持 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++对象模型》 一 书。
4 总结
禁用原因主要是两个:
- 浅拷贝问题,也就是上面提到的二次析构。
- 自定义了基类和派生类的拷贝构造函数,但派生类对象拷贝时,调用了派生类的拷贝,没有调用自定义的基类拷贝而是调用默认的基类拷贝。这样可能造成不安全,比如出现二次析构问题时,因为不会调用我们自定义的基类深拷贝,还是默认的浅拷贝。
5 附录
Effective C++条款 6 规定,如果不想用编译器自动生成的函数,就应该明确拒绝。方法一般有三种:
- C++11对函数声明加
delete关键字:Base(const Base& obj) = delete;,不必有函数体,这时再调用拷贝构造会报错尝试引用已删除的函数。 - 最简单的方法是将拷贝构造函数声明为
private - 条款 6 给出了更好的处理方法:创建一个基类,声明拷贝构造函数,但访问权限是
private,使用的类都继承自这个基类。默认拷贝构造函数会自动调用基类的拷贝构造函数,而基类的拷贝构造函数是private,那么它无法访问,也就无法正常生成拷贝构造函数。
Qt就是这样做的,QObject 定义中有这样一段,三条都利用了:
第一种方法:最简单的方法是将拷贝构造函数声明为private:
private:
Q_DISABLE_COPY(QMainWindow)
#define Q_DISABLE_COPY(Class) \ Class(const Class &) Q_DECL_EQ_DELETE;\ Class &operator=(const Class &) Q_DECL_EQ_DELETE;
类的不可拷贝特性是可以继承的,例如凡是继承自QObject的类都不能使用拷贝构造函数和赋值运算符。
第二种方法 继承一个uncopyable类
C++的编译在链接之前,如果我们能在编译期解决这个问题,会节省不少的时间,要想在编译期解决问题,就需要人为制造一些bug。我们声明一个专门阻止拷贝的基类uncopyable。
class uncopyable{
protected:
uncopyable(){
}
~uncopyable(){
}
private:
uncopyable(const uncopyable&);
uncopyable& operator=(const uncopyable&);
}
接下来,我们的类只要继承uncopyable,如果要发生拷贝,编译器都会尝试调用基类的拷贝构造函数或者赋值运算符,但是因为这两者是私有的,会出现编译错误。
边栏推荐
- C语言课设图书信息管理系统(大作业)
- [ManageEngine Zhuohao] what is network operation and maintenance management and what is the use of network operation and maintenance platform
- C语言课设职工信息管理系统(大作业)
- Top 10 Free 3D modeling software for beginners in 2022
- Tidb database characteristics summary
- [unity shader ablation effect _ case sharing]
- Student attendance system for C language course (big homework)
- To sort out the anomaly detection methods, just read this article!
- B-树系列
- C语言课设物业费管理系统(大作业)
猜你喜欢

VS2019如何永久配置本地OpenCV4.5.5使用
![[summary of knowledge points] chi square distribution, t distribution, F distribution](/img/a6/bb5cabbfffb0edc9449c4c251354ae.png)
[summary of knowledge points] chi square distribution, t distribution, F distribution

lxml模块(数据提取)
![[file system] how to run squashfs on UBI](/img/d7/a4769420c510c47f3c2a615b514a8e.png)
[file system] how to run squashfs on UBI

SQL学习笔记九种连接2

【KV260】利用XADC生成芯片温度曲线图

C语言课设销售管理系统设计(大作业)
![[enterprise data security] upgrade backup strategy to ensure enterprise data security](/img/59/e44c6533aa546e8854ef434aa64113.png)
[enterprise data security] upgrade backup strategy to ensure enterprise data security

Forkjoin and stream flow test

Design of sales management system for C language course (big homework)
随机推荐
华福证券开户是安全可靠的么?怎么开华福证券账户
Distributed lock implementation
How did ManageEngine Zhuohao achieve the goal of being selected into Gartner Magic Quadrant for four consecutive years?
【ManageEngine卓豪】助力黄石爱康医院实现智能批量化网络设备配置管理
SQL学习笔记2
B-树系列
C语言课设工资管理系统(大作业)
C语言课设学生信息管理系统(大作业)
H5网页判断是否安装了某个APP,安装则跳转未安装则下载的方案总结
数据库对象:视图学习记录
启牛学堂合作的证券公司是哪家?开户安全吗?
【Unity Shader 描边效果_案例分享第一篇】
SQL学习笔记九种连接2
[ManageEngine Zhuohao] helps Huangshi Aikang hospital realize intelligent batch network equipment configuration management
阿里OSS Postman Invalid according to Policy: Policy Condition failed: [“starts-with“, “$key“, “test/“]
To sort out the anomaly detection methods, just read this article!
sci-hub如何使用
C language course set up property fee management system (big work)
【#Unity Shader#自定义材质面板_第一篇】
Mysql 表分区创建方法