拷贝控制 ============== .. highlight:: cpp 拷贝控制(copy control) - 拷贝构造函数(copy constructor) - 拷贝赋值运算符(copy-assignment operator) - 移动构造函数(move constructor) - 移动赋值运算符(move-assignment operator) - 析构函数(destructor) 拷贝构造函数 -------------- **拷贝构造函数的第一个参数必须是引用类型** 。 在函数调用中,具有非引用类型的参数要进行拷贝初始化。类似地,当一个函数具有非引用类型的返回类型时,返回值会被用来初始化调用方的结果。 拷贝构造函数被用来初始化 **非引用类类型** (被初始化的是类的非引用对象)参数,如果拷贝构造函数的参数不是引用类型,为了调用拷贝构造函数, 我们必须拷贝它的实参,然而拷贝实参又需要调用拷贝构造函数,如此无限循环。 如果我们没有为类定义拷贝控制函数,编译器会为我们定义一个。与合成默认构造函数不同(如果定义了其他构造函数,则需要我们再显式定义默认构造函数), 即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。 如果有指针类型的成员数据,编译器合成的拷贝函数就只会复制指针(而不是复制指针指向的内容),从而得到一个相同指向的复制品(浅复制)。 很可能这并不是我们想要的,这时需要考虑自己动手定义拷贝构造函数。 拷贝构造函数的调用时机有如下三种: - 当用类的一个对象去初始化该类的另一个对象时。 - 当函数的形参是类的对象,调用函数时。 - 当函数的返回值是类的对象,函数执行完成返回时。 default 和 delete --------------------- 使用 ``=default`` 将拷贝控制成员定义为 ``=default`` ,显式要求编译器生成合成的版本。 - 类内使用 ``=default`` 修饰成员的声明,则合成的函数隐式地声明为内联函数(注:定义在类内的函数自动为内联函数)。 - 类外使用 ``=default`` 修饰成员的定义,则合成的成员不是内联函数。 - 只能对默认构造函数或拷贝控制成员使用 ``=default`` 。 使用 ``=delete`` 在函数参数列表之后加上 ``=delete`` 定义为 **删除的函数** :虽然有声明,但是不能以任何形式使用它们。 将拷贝构造函数和拷贝赋值运算符定义为删除的函数,从而阻止拷贝操作。 直接初始化和拷贝初始化 ------------------------- 直接初始化直接调用与实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。 .. code-block:: cpp :linenos: string dots(10, '.'); // 直接初始化 string s(dots); // 直接初始化 string s2 = dots; // 拷贝初始化 string s3 = "999-9999"; // 拷贝初始化 string s4 = string(100, '9'); // 拷贝初始化 .. code-block:: cpp :linenos: class ClassTest { public: ClassTest() { c[0] = '\0'; cout << "ClassTest()" << endl; } ClassTest& operator=(const ClassTest &ct) { strcpy(c, ct.c); cout << "ClassTest& operator=(const ClassTest &ct)" << endl; return *this; } ClassTest(const char *pc) { strcpy(c, pc); cout << "ClassTest (const char *pc)" << endl; } // private: ClassTest(const ClassTest& ct) { strcpy(c, ct.c); cout << "ClassTest(const ClassTest& ct)" << endl; } private: char c[256]; }; 调用: .. code-block:: cpp :linenos: ClassTest ct1("ab"); // 直接初始化 // 输出: ClassTest (const char *pc) ClassTest ct2 = "ab"; // 拷贝初始化 // 输出: ClassTest (const char *pc) // 首先调用构造函数 ClassTest(const char *pc) 函数创建一个临时对象;然后调用拷贝构造函数,把这个临时对象作为参数,构造对象ct2 // 然而结果并没有输出 ClassTest(const ClassTest& ct)。有说法是编译器优化之后,直接匹配了 ClassTest(const char *pc),不再调用拷贝构造函数 ClassTest ct3 = ct1; // 拷贝初始化 // 输出: ClassTest(const ClassTest& ct) // ct1 已经存在,直接调用拷贝构造函数 ClassTest ct4(ct1); // 直接初始化 // 输出: ClassTest(const ClassTest& ct) // ct1 已经存在,直接调用拷贝构造函数 ClassTest ct5 = ClassTest(); // 拷贝初始化 // 输出: ClassTest() // 首先调用默认构造函数产生一个临时对象;然后调用拷贝构造函数,把这个临时对象作为参数,构造对象ct5 ct3 = ct2; // 赋值 // 输出: ClassTest& operator=(const ClassTest &ct) 当把拷贝构造函数设置为 ``private`` ,ct3、ct4、ct5的初始化都无法完成。 explicit ------------ :: This keyword is a declaration specifier that can only be applied to in-class constructor declaration. An explicit constructor cannot take part in implicit conversions. It can only be used to explicitly construct an object. 单个参数的构造函数(或者除了第一个参数外其余参数都有缺省值的多参构造函数)承担了两个角色: - 用于构建单参数的类对象; - 隐含的类型转换操作符。 ``explicit`` 指定转换函数(C++11 起)或构造函数为显式,即它不能用于隐式转换和拷贝初始化。 声明为 ``explicit`` 的构造函数不能在隐式转换中使用,只能显式调用去构造一个类对象。其好处在于可以禁止编译器执行非预期(往往也不被期望)的类型转换。 但是将拷贝构造函数声明成 ``explicit`` 并不是良好的设计。 .. code-block:: cpp :linenos: struct A { A(int) { } // 转换构造函数 A(int, int) { } // 转换构造函数 (C++11) operator bool() const { return true; } // 类型转换函数 }; struct B { explicit B(int) { } explicit B(int, int) { } explicit operator bool() const { return true; } }; int main() { A a1 = 1; // OK:复制初始化选择 A::A(int) A a2(2); // OK:直接初始化选择 A::A(int) A a3 {4, 5}; // OK:直接列表初始化选择 A::A(int, int) A a4 = {4, 5}; // OK:复制列表初始化选择 A::A(int, int) A a5 = (A)1; // OK:显式转型 if (a1) ; // OK:A::operator bool() bool na1 = a1; // OK:复制初始化选择 A::operator bool() bool na2 = static_cast(a1); // OK:static_cast 进行直接初始化 // B b1 = 1; // 错误:复制初始化不考虑 B::B(int) B b2(2); // OK:直接初始化选择 B::B(int) B b3 {4, 5}; // OK:直接列表初始化选择 B::B(int, int) // B b4 = {4, 5}; // 错误:复制列表初始化不考虑 B::B(int, int) B b5 = (B)1; // OK:显式转型 if (b2) ; // OK:B::operator bool() // bool nb1 = b2; // 错误:复制初始化不考虑 B::operator bool() bool nb2 = static_cast(b2); // OK:static_cast 进行直接初始化 } initializer_list ------------------------- :: #include ``initializer_list`` 类的对象是一个访问 ``const T`` 类型对象数组的轻量代理对象。 以下场景会发生 initializer_list 对象的自动构造: - 用花括号列表初始化一个对象(列表初始化),对应的构造函数接受一个 initializer_list 参数。 - 以花括号列表为赋值运算符的右运算数,或作为函数调用的参数,而对应的赋值运算符/函数接受 initializer_list 参数。 - 绑定花括号列表到 ``auto`` ,在范围 for 循环(Range For Loop)中使用。 initializer_list 可由一对指针(分别指向列表首、尾)或指针+长度实现, 复制一个 initializer_list 不会复制其底层对象。 成员函数包括: ``size`` ``begin`` ``end`` 。 .. code-block:: cpp :linenos: #include #include #include using namespace std; template class Vec { public: vector v; Vec(initializer_list); void append(initializer_list); pair c_arr() const; }; template Vec::Vec(initializer_list lt): v(lt){} template void Vec::append(initializer_list lt) { v.insert(v.end(), lt.begin(), lt.end()); } template pair Vec::c_arr() const { return {&v[0], v.size()}; } template void print(T val_list) { for(auto& val: val_list) cout << val << "#"; // range for loop cout << endl; } int main() { Vec va = {1,2,3}; // 拷贝初始化 va.append({4,5}); print(va.v); // 1#2#3#4#5# auto p = va.c_arr(); cout << *p.first << " " << p.second << endl; // 1 5 // print({1,2,3,4,5}); // 编译错误,{1,2,3,4,5} 不是表达式,无类型,因此 T 无法推导 print>({1,2,3,4,5}); // 1#2#3#4#5# print>({1,2,3,4,5}); // 1#2#3#4#5# for(auto n: {1,2,3,4,5}) cout << n << ends; return 0; } push 和 emplace --------------------------- 在 19 章提到了 push 和 emplace 的区别,这里用一个例子说明。 .. container:: toggle .. container:: header :math:`\color{darkgreen}{Example}` .. code-block:: cpp :linenos: #include #include // std::move class Foo { public: Foo(std::string str) : name(str) { std::cout << "constructor" << std::endl; } Foo(const Foo& f) : name(f.name) { std::cout << "copy constructor" << std::endl; } Foo(Foo&& f) : name(std::move(f.name)) { std::cout << "move constructor" << std::endl; } private: std::string name; }; int main(int argc, char ** argv) { std::vector v; int count = 10000000; v.reserve(count); { Foo temp("test"); // constructor v.push_back(temp);// push_back(const T&),参数是左值引用 // copy constructor } v.clear(); { Foo temp("test"); // constructor v.push_back(std::move(temp));// push_back(T &&), 参数是右值引用 // move constructor } v.clear(); { v.push_back(Foo("test"));// push_back(T &&), 参数是右值引用 // constructor // move constructor } v.clear(); { std::string temp = "test"; v.push_back(temp);// 构造临时对象,push_back(T &&), 参数是右值引用 // constructor // move constructor } v.clear(); { std::string temp = "test"; v.emplace_back(temp);// 只有一次构造函数,不调用拷贝构造函数,速度最快 // constructor } return 0; } .. note:: ``std::move`` 并不会真正地移动对象(真正的移动操作是移动构造函数、移动赋值运算符等完成的), ``std::move`` 只是将参数转换为右值引用。 我们可以销毁一个移动之后的源对象(moved-from),也可以赋予它新值,但是不能使用一个移后源对象的值。 如:上例中的 temp 被移动后,就不能再取它的值来使用。 参考资料 ------------- 1.《C++ Primer 第5版 中文版》 Page 440 -- 442,449,470 -- 475。 2. C++的一大误区——深入解释直接初始化与复制初始化的区别 https://blog.csdn.net/ljianhui/article/details/9245661 3. C++11使用emplace_back代替push_back https://blog.csdn.net/yockie/article/details/52674366 4. explicit 说明符 https://zh.cppreference.com/w/cpp/language/explicit 5. initializer_list https://en.cppreference.com/w/cpp/utility/initializer_list