为什么很多人禁用拷贝(复制)构造函数

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

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

为什么禁用拷贝构造函数

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

浅拷贝问题

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

 
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对象指向同一个缓 冲区,这会导致析构的时候两次删除同一片区域的问题(这个问题又叫双杀问题)。

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

  1. 自己编写拷贝构造函数,然后在拷贝构造函数中创建新的buf_,不过拷贝构造函数的 编写需要考虑异常安全的问题,所以编写起来有一定的难度。

  2. 使用 shared_ptr 这样的智能指针,让所有的 Widget 对象共享一片 buf_,并 让 shared_ptr 的引用计数机制帮你智能的处理删除问题。

  3. 禁用拷贝构造函数和赋值操作符。如果你根本没有打算让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++中没有拷贝构造函数,这 个模式实现可能比较困难。

如何禁用拷贝构造

  1. 如果你的编译器支持 C++11,直接使用 delete

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

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

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的类都不能使用拷贝构造函数和赋值运算符。
 

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

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

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

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 当你禁用拷贝构造函数时,你可以使用智能指针来构造 std::vector 的元素。 你可以使用 std::unique_ptr 来保存 vector 中的元素,这样你就可以避免使用拷贝构造函数了。 例如: ``` #include <vector> #include <memory> class MyClass { public: MyClass() {} MyClass(const MyClass&) = delete; // other methods... }; int main() { std::vector<std::unique_ptr<MyClass>> v; v.push_back(std::make_unique<MyClass>()); // other operations... } ``` 这样,你就可以在禁用拷贝构造函数的情况下使用 std::vector 了。 ### 回答2: C++中的智能指针可以很方便地管理指针资源,其中最常用的智能指针是std::shared_ptr和std::unique_ptr。 禁用拷贝构造函数的类不能直接作为vector元素,因为vector会在内部执行元素的复制操作。此时,我们可以使用std::unique_ptr智能指针来构造vector元素。 禁用拷贝构造函数的类可以使用std::unique_ptr作为成员变量,将其指向的对象内存资源转移给std::unique_ptr。这样,在将该类对象添加到vector时,可以使用std::make_unique函数来创建std::unique_ptr对象,并将其移动到vector中。 下面是一个示例: ```cpp #include <iostream> #include <memory> #include <vector> class MyClass { public: // 禁用拷贝构造函数 MyClass(const MyClass&) = delete; // 定义移动构造函数 MyClass(MyClass&&) = default; MyClass(int data) : data_(data) {} void printData() { std::cout << "Data: " << data_ << std::endl; } private: int data_; }; int main() { std::vector<std::unique_ptr<MyClass>> vec; vec.push_back(std::make_unique<MyClass>(1)); vec.push_back(std::make_unique<MyClass>(2)); vec.push_back(std::make_unique<MyClass>(3)); for (const auto& ptr : vec) { ptr->printData(); } return 0; } ``` 在上面的示例中,禁用拷贝构造函数的类`MyClass`使用了std::unique_ptr来管理内存资源,然后通过std::make_unique函数创建std::unique_ptr,并将其移动到了vector容器中。最后,通过循环遍历vector元素,调用类的成员函数进行打印输出。 这样,即使禁用拷贝构造函数,也可以使用std::unique_ptr智能指针来构造vector元素,实现了对资源的正确管理。 ### 回答3: 禁用拷贝构造函数的类可以使用智能指针构造vector元素,具体方法如下。 智能指针(Smart Pointer)是C++中的一个重要概念,通过它我们可以管理动态分配的内存资源,避免内存泄漏和悬挂指针等问题。在C++标准库中,提供了两种常用的智能指针:shared_ptr和unique_ptr。 首先,由于禁用拷贝构造函数,因此无法直接通过拷贝构造函数将对象插入vector中。一个简单的解决方法是使用unique_ptr,它的特点是所有权唯一且不可共享。 在向vector中插入元素时,可以使用make_unique函数创建一个unique_ptr,将该指针的所有权交给vector管理,具体示例代码如下: ```cpp #include <iostream> #include <vector> #include <memory> class MyClass { private: int data; public: MyClass(int value) : data(value) {} // 禁用拷贝构造函数 MyClass(const MyClass& other) = delete; // 禁用拷贝赋值运算符 MyClass& operator=(const MyClass& other) = delete; }; int main() { std::vector<std::unique_ptr<MyClass>> myVector; myVector.push_back(std::make_unique<MyClass>(10)); myVector.push_back(std::make_unique<MyClass>(20)); // 输出vector中元素的data值 for (const auto& element : myVector) { std::cout << element->getData() << " "; } return 0; } ``` 以上代码创建了一个名为MyClass的类,禁用拷贝构造函数。在main函数中,我们使用了std::unique_ptr来保存MyClass的对象,并使用std::make_unique函数创建对象并将其插入到vector中。最后,我们通过for循环打印了vector中元素的data值。 总结来说,禁用拷贝构造函数的类可以使用unique_ptr来构造vector元素,以确保对象的所有权唯一性。这样既保证了按值传递的效果,又避免了复制的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值