内容概要

  • 分析三种函数的存在原因
  • 编译器对它们的自动构造
  • C++中各种类的类型(表现形式)
  • 模板

存在的原因

通常而言,析构函数是为了避免内存泄露,复制构造(赋值)函数是为了深复制,移动构造(赋值)函数是为了避免过多的复制操作造成的开销。它们往往都围绕着一个前提,那就是类的成员变量存在原始指针,引用着堆空间,所以它们往往是一起出现的。

于是不难理解在《C++ Primer 5th》中,作者给出这样的建议

All five copy-control members should be thought of as a unit: Ordinarily, if a class defines any of these operations, it usually should define them all. As we’ve seen, some classes must define the copy constructor, copy-assignment operator, and destructor to work correctly . Such classes typically have a resource that the copy members must copy. Ordinarily, copying a resource entails some amount of overhead. Classes that define the move constructor and move-assignment operator can avoid this overhead in those circumstances where a copy isn’t necessary.

编译器自动构造的版本

  • 默认析构函数什么都不做
  • 编译器总是生成复制构造(赋值)函数,除了以下情况:
    1. 类有无法复制的成员变量,如unique_ptr实例、显式对复制构造(赋值)函数指明deleted等
    2. 父类不能复制
  • 一旦类作者自定义了析构函数、复制构造(赋值)函数,那么编译器都不会自动生成移动构造(赋值)函数;而且复制构造(赋值)函数一样,受限于成员变量和父类

可以看到,析构函数和复制构造(赋值)函数主要确保程序的正确运行;而移动构造(赋值)函数是为了效率

类的表现形式

在《C++ Primer 5th》中,作者大致描述了三种类型的类,一种表现得和值一样,另一种和指针一样,最后一种两者都不是:

Classes that behave like values have their own state. When we copy a value like object, the copy and the original are independent of each other. Changes made to the copy have no effect on the original, and vice versa.

Classes that act like pointers share state. When we copy objects of such classes, the copy and the original use the same underlying data. Changes made to the copy also change the original, and vice versa. Of the library classes we’ve used, the library containers and string class have valuelike behavior. Not surprisingly, the shared_ptr class provides pointerlike behavior.

The IO types and unique_ptr do not allow copying or assignment, so they provide neither valuelike nor pointerlike behavior.

当定义一个类时,不妨仔细考虑它的状态是私有的还是共享的——如果是value like,那么在改变自身状态时(比如通过赋值),要先释放自己的资源,而且要注意赋值的源对象是自己的特殊情况;

如果是pointer like,因为资源是共享的,所以不需要释放。

模板

class Foo {
public:
    ...
    
    //析构函数
    ~Foo() {
        ...
    }
    
    //复制构造
    Foo(const Foo &foo) {
        ...
    }
    
    //复制赋值
    Foo& operator=(const Foo &foo) {
        //如果Foo是value like,那么要注意形参就是自己本身的特殊情况,最佳实践如下:
        auto temp = new XX(*foo.ptr); //先拷贝再释放
        delete ptr; //避免了ptr就是foo.ptr的极端情况
        ptr = temp;
        ...
        //当然也可以用 this != &foo 的方法
    }
    
    //移动构造 
    Foo(const Foo &&foo) {
        // 本质其实就是让自己的指针指向一个已存在的资源
        // 不要忘了源对象的指针要置空,避免它释放资源时把移动出去的资源给释放了
        ptr = foo.ptr;
        foo.ptr = nullptr;
    }
    
    //移动赋值
    Foo& operator=(const Foo &&foo) {
        if (this != &foo) {
            ... //同上构造
        }
        return *this;
    }
};



Zhu

有问题欢迎发邮件交流