C++拷贝控制之拷贝、赋值与销毁

本文主要是《C++ Primer Ed5》第13章内容,希望能够对C++的拷贝控制了解的更为深入一些。

概述

C++中的拷贝控制操作主要涉及的几个拷贝控制函数为:

  • 拷贝构造函数
  • 拷贝赋值运算符
  • 移动构造函数
  • 移动赋值运算符
  • 析构函数
    其中,
  • 1和3定义了当用同类型的另一个对象【初始化】本对象时做什么,2和4定义了当用同类型的另一个对象【赋值】本对象时做什么,5定义了此类型对象销毁时做什么
  • 如果一个类没有定义这些拷贝控制成员,编译器会自动为其定义缺失的操作

拷贝构造函数

  1. 关键点
    • 即便用户定义了其他构造函数,编译器也会合成一个拷贝构造函数(但默认构造函数则是用户定义了构造函数,则编译器不会再提供合成构造函数)
    • 拷贝构造函数传参必须是引用,如果传值,会陷入循环,一般是const的
    • 一个类中如果有移动构造函数,则拷贝初始化是通过移动构造实现的
    • 注意区分直接初始化拷贝初始化的区别
      • 前者相当于函数匹配的过程,后者是拷贝的过程,如vector中的push和emplace
  2. 调用拷贝构造初始化的几种情况
    • 用等号=定义变量时
    • 以非引用的形式给函数传参
    • 函数返回一个非引用形式的对象作为返回值(现在gcc已经做优化了,编译时加上-fno-elide-constructors才会调用)
    • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员

拷贝赋值运算符

  1. 重载赋值运算
    • operator关键字后接入要定义的运算符,赋值运算符即函数operator=
    • 重载运算符的参数宝石运算符的运算对象,对于某些运算符,必须定义为类的成员函数,这样的话,运算符左侧的运算对象就能够绑定到隐式的this参数上

析构函数

  1. 关键点
    • 一个类只能有唯一一个析构函数,且不能被重载
    • 析构时按初始化的顺序逆序析构
    • 隐式析构时内置的指针类型不会delete其指向的对象,需要手动释放资源
    • 当指向一个对象的引用或指针离开作用域时,不会调用析构
    • 析构函数本身并不是直接销毁成员,成员是在析构函数体之后隐含的析构阶段被销毁的,用户自定义的析构函数可以理解为不平凡的析构函数,可以用std::is_trivial<T>::value查看,见代码中的TestPOD,这部分暂不深入
  2. 执行析构函数的几种情况
    • 变量离开作用域
    • 对象被销毁,成员被销毁
    • 容器(STL或数组)被销毁时,其元素被销毁
    • 动态分配的对象,显示调用delete运算符时
    • 临时对象,创建它的完整表达式结束时

三/五法则

  1. 一个类有三个基本的控制类的操作:拷贝构造函数拷贝赋值运算符析构函数,新标准下,还可以定义移动构造函数移动赋值运算符
  2. 需要析构函数的类也需要拷贝和赋值操作
    • 这里可以这样理解:对于需要显示声明析构函数的类(即具有不平凡析构函数),说明类中具有需要手动操作或释放的资源(特别是类中的指针成员),如果这里使用合成拷贝构造函数,那么合成拷贝构造函数默认的执行操作只是把指针再指向新的对象,这就会导致两个指针指向同一个对象,最后出现double free的问题
  3. 需要拷贝操作的类也需要赋值操作,反之亦然

控制各个拷贝函数

  1. default关键字
    • 显示要求编译器生成合成版本的控制函数
    • 如果在类内使用,编译器会将其隐式声明为inline,如果不希望是inline,只能在类外定义时加入default
  2. delete
    • c++11标准可以在函数列表后添加=delete将函数定义为删除的,这样用户便不可再调用之
    • =delete必须出现在函数第一次声明时,而=default则不是
      • 编译器在一开始编译时就需要知道该函数是否可删除,而default是编译器在生成代码时才需要
    • 不能delete析构函数
      • 析构函数被delete的类,可以被动态分配,但是不能delete,见代码TestDefaultAndDelete
  3. private拷贝控制
    • 还可以将拷贝构造函数等定义为private,以阻止用户使用,但并不能阻止类内其他成员和友元使用
    • 如果为了不让其他成员和友元使用,可以声明为private,但是不定义
    • 如果需要阻止拷贝,建议还是使用delete

代码

#include <iostream>

#define PRINT_INFO(str) std::cout << str << std::endl;
#define PRINT_MEM_INFO(str, x) std::cout << str << x << std::endl;

class FDatas {
   public:
    FDatas(const std::string& str, int n) : id_(str), value_(n) {
        PRINT_MEM_INFO("default copy, id_ = ", this->id_);
    }
    FDatas(const FDatas& fd) {
        this->id_ = fd.id_;
        this->value_ = fd.value_;

        PRINT_MEM_INFO("this is copy constructor. id_ = ", this->id_);
    }

    // 重载运算符,必须定义为成员函数,这样运算符左侧的则能绑定到隐式的this参数中
    FDatas& operator=(const FDatas& fd) {
        this->id_ = fd.id_;
        this->value_ = fd.value_;
        PRINT_MEM_INFO("this is copy operator, id_ = ", fd.id_);
        return *this;
    }

    ~FDatas() { PRINT_INFO("this is deconstructor"); }

   public:
    void Print() { PRINT_MEM_INFO("Print id_ = ", this->id_); }

   private:
    std::string id_;
    int value_;
};

// gcc会做优化,返回临时对象时,不会构造临时对象了,加上-fno-elide-constructors才会
FDatas CopyData(FDatas fd) {
    PRINT_INFO("test CopyData");
    // 拷贝构造
    FDatas tmp_fd = fd;

    return tmp_fd;
}
void TestCopyConstructor() {
    FDatas fd("abc", 13);  // 直接初始化
    FDatas fd1 = fd;       // 拷贝初始化
    FDatas fd1_1(fd);      // 直接初始化

    PRINT_INFO("test copy data");

    // fd1拷贝给形参,调用拷贝构造函数
    FDatas fd2 = CopyData(fd1);

    FDatas fd3("ff", 12);
    fd3 = fd2;  // 拷贝赋值运算符

    PRINT_INFO("sdf");
    fd3.~FDatas();  // 即便显示调用了析构函数,最后还是会调用一次析构,因为这时候对象还是在内存中
    fd3.Print();
    PRINT_INFO("sdf111");
}

// POD
class A {};
class A1 {
    A1(const A& a) {}
};
class B {
    ~B();
};
// 用std::is_trivial<T>::value判断是否为平凡类型
void TestPOD() {
    PRINT_MEM_INFO("is_trival FDatas: ", std::is_trivial<FDatas>::value);
    PRINT_MEM_INFO("is_trival A: ", std::is_trivial<A>::value);    // 1
    PRINT_MEM_INFO("is_trival A1: ", std::is_trivial<A1>::value);  // 0, 有不平凡的构造函数
    PRINT_MEM_INFO("is_trival B: ", std::is_trivial<B>::value);    // 0, 有不平凡的析构函数
}

class FDataNew {
   public:
    FDataNew() = default;
    FDataNew(const FDataNew& fdn) = default;
    FDataNew& operator=(const FDataNew& fdn);

    ~FDataNew() = default;
};
FDataNew& FDataNew::operator=(const FDataNew& fdn) = default;

struct FD {
    FD() = default;
    ~FD() = delete;
};
void TestDefaultAndDelete() {
    FDataNew fdn;
    FDataNew fdn1(fdn);

    FD* fd = new FD();  // Ok
    // delete fd;          // error, 析构函数被delete了
}

int main(int argc, char* argv[]) {
    TestCopyConstructor();
    TestPOD();

    TestDefaultAndDelete();
    return 0;
}