【C++ Primer Chapter 13 总结】类的拷贝控制
类通过定义5个特殊的成员函数:复制构造函数、复制赋值操作符、移动构造函数、移动赋值操作符和析构函数,来控制对象的复制,移动,赋值和销毁。
-
copy constructors // 定义对象初始化另一个对象时会发生什么。
-
move constructors
-
copy-assignment operators // 定义将对象赋给另一个对象时会发生什么。
-
move-assignment operators
-
destructor // 定义对象不再存在时将发生什么。
1.拷贝构造函数。
第一个形参是类类型的引用,并且任何附加形参都有默认值,那么构造函数就是拷贝构造函数。
class Sales_data {
public:
Sales_data(const Sales_data&); // copy constructor
}
Sales_data::Sales_data(const Sales_data &orig):bookNo(orig.bookNo), units_sold(orig.units_sold), revenue(orig.revenue) { } // 初始化值列表
2. 当没有为类定义拷贝构造函数时,编译器会为我们合成一个(synthesized copy constructor)。
合成拷贝构造函数以memberwise copies方式将其参数的成员复制到被创建的对象中。
3. 使用直接初始化时,编译器使用普通函数匹配来选择与我们提供的实参最匹配的构造函数。
当拷贝初始化发生时,拷贝初始化需要拷贝构造函数或移动构造函数。
调用拷贝构造函数:
-
使用 = 定义变量
-
将对象作为实参传递给非引用类型的形参
-
从具有非引用返回类型的函数返回一个对象
-
标准库容器在初始化容器或调用insert或push成员时拷贝初始化其元素
string dots(10, '.'); // direct initialization string s(dots); // direct initialization string s2 = dots; // copy initialization string null_book = "9-999-99999-9"; // copy initialization string nines = string(100, '9'); // copy initialization
4.如果使用需要通过显式构造函数进行转换的初始化,那么使用复制还是直接初始化很重要。
vector<int> v1(10); // ok: 直接初始化 vector<int> v2 = 10; // error: 使用一个size作为参数的构造函数需要显示调用 void f(vector<int>); // f的参数使用拷贝初始化 f(10); // error: 不能使用显示构造函数拷贝实参,显示构造函数不允许其他类型隐式转换到类类型 f(vector<int>(10)); // ok: 使用int直接构造临时的vector对象
5.拷贝赋值操作符。
如果类没有定义自己的拷贝赋值操作符,编译器将为该类生成合成的拷贝赋值操作符。
Sales_data trans, accum; trans = accum; // 调用Sales_data的拷贝赋值操作符 Sales_data& Sales_data::operator=(const Sales_data &rhs) // 拷贝赋值操作符 { bookNo = rhs.bookNo; // 赋值操作 units_sold = rhs.units_sold; revenue = rhs.revenue; assignment return *this; }
6. 赋值操作符的操作是析构函数和拷贝构造函数的结合。
与析构函数一样,赋值函数销毁左操作数的内存。与拷贝构造函数一样,赋值函数从右操作数复制数据。
class HasPtr { public: HasPtr(const std::string &s = std::string()): ps(new std::string(s)), i(0) { } HasPtr(const HasPtr &p): ps(new std::string(*p.ps)), i(p.i) { } HasPtr& operator=(const HasPtr &); ~HasPtr() { delete ps; } private: std::string *ps; int i; }; HasPtr& HasPtr::operator=(const HasPtr &rhs) { auto newp = new string(*rhs.ps); // 按值复制时候,要连指针指向的对象一起复制。 delete ps; ps = newp; i = rhs.i; return *this; }
7.当操作符是成员函数时,左操作数被绑定到隐式的this形参。
class Foo { public: Foo& operator=(const Foo&); // 赋值操作符 };
8.在构造函数中,成员在执行函数体之前被初始化,并且成员以与类中出现的顺序相同的顺序被初始化。
在析构函数中,首先执行函数主体,然后销毁成员。成员以与初始化顺序相反的顺序销毁。
9.析构函数。
销毁部分是隐式的。销毁成员时会发生什么情况取决于成员的类型。
类类型的成员通过运行成员自己的析构函数销毁。
智能指针是class类型,所以在销毁阶段中会自动被销毁,而内置指针类型不会删除其指向的对象。
10. 如果类需要析构函数,那么几乎肯定也需要复制构造函数和复制赋值操作符。
如果类需要复制构造函数,则几乎肯定需要复制赋值操作符。
11. 通过将拷贝控制成员定义为 = default,可以显式地要求编译器生成合成版本的成员。
在函数体内使用=default的成员声明,默认为inline函数。同时在成员定义使用=default的不是inline函数。
class Sales_data { public: Sales_data() = default; Sales_data(const Sales_data&) = default; Sales_data& operator=(const Sales_data &); ~Sales_data() = default; }; Sales_data& Sales_data::operator=(const Sales_data&) = default;
12. 可以通过将拷贝构造函数和拷贝赋值操作符定义为 = deleted (已删除函数)来防止复制。被删除的函数是已声明但不能以任何其他方式使用的函数。
struct NoCopy { NoCopy() = default; NoCopy(const NoCopy&) = delete; NoCopy &operator=(const NoCopy&) = delete; // no assignment ~NoCopy() = default; };
13. 无法定义析构函数 =delete 的类变量,但是可以使用new动态分配内存,但是因为该类对象的析构函数=delete,所以无法delete该动态内存。
struct NoDtor { NoDtor() = default; ~NoDtor() = delete; }; NoDtor nd; // error NoDtor *p = new NoDtor(); // ok: but we can't delete p delete p; // error: NoDtor destructor is deleted
14. 如果类的数据成员不能默认构造、复制、赋值或撤销,则对应的成员将是已删除的函数。
15. 对于具有不能默认构造的引用成员或const成员的类,编译器不会合成默认构造函数。
16. 具有const成员的类不能使用合成复制赋值操作符:毕竟,该操作符会尝试给每个成员赋值。不能给const对象赋新值。
17.移动语义作用:
-
避免深拷贝。
-
有些类类型,例如IO类,无法进行拷贝。
18.右值引用。
右值引用是必须绑定到右值的引用。 右值引用只是对象的另一个名称。
右值引用有一个重要的属性,即它们只能绑定到一个将要销毁的对象。即:调用右值引用后,除了赋值和销毁,不应该再使用该右值引用的对象。
右值引用可以绑定的对象:1) 需要转换的表达式,2)字面值,3)返回右值的表达式。
返回左值的表达式:1)返回左值引用的函数,2)赋值、下标、解引用和前缀自增/自减操作符。
产生右值的表达式:1)返回非引用类型的函数,2)算术、关系、按位和后自增/自减操作符。
int i = 42; int &r = i; // 引用 const int &r3 = i * 42; // 指向const的引用可以绑定到右值对象上 int &&rr2 = i * 42; // 右值引用 19. 变量表达式是左值。右值引用变量是左值!因此无法绑定到右值引用! int &&rr1 = 42; // ok: literals are rvalues int &&rr2 = rr1; // error: the expression rr1 is an lvalue! 20. 将左值显式转换为其对应的右值引用类型。 调用move()告诉编译器将左值当作右值来处理。 int rr1 = 41; int &&rr3 = std::move(rr1);
19.移动构造函数和移动赋值操作符。
移动构造函数的初始参数,是对类类型的右值引用。
class StrVec { public: StrVec(StrVec&&); // 移动构造函数 StrVec& operator=(StrVec &&rhs); // 移动赋值操作符 }; StrVec::StrVec(StrVec &&s): elements(s.elements), first_free(s.first_free), cap(s.cap) { s.elements = s.first_free = s.cap = nullptr; } StrVec &StrVec::operator=(StrVec &&rhs) { if (this != &rhs) { free(); // free existing elements elements = rhs.elements; // take over resources from rhs first_free = rhs.first_free; cap = rhs.cap; rhs.elements = rhs.first_free = rhs.cap = nullptr; // leave rhs in a destructible state } return *this; }
20. 如果类定义了自己的复制构造函数、复制赋值操作符或析构函数,则不会合成移动构造函数和移动赋值操作符。
只有当类没有定义任何拷贝控制成员,并且类的每个非静态数据成员都可以被移动时,编译器才会合成移动构造函数或移动赋值操作符。
定义了移动构造函数或移动赋值操作符的类也必须定义自己的拷贝操作。否则,拷贝构造函数和拷贝赋值操作符默认 = deleted。
21.当一个类没有移动操作时,相应的复制操作将代替移动操作。即使用了move()也还是调用的复制构造函数。
class Foo { public: Foo() = default; Foo(const Foo&); // 拷贝构造函数 }; Foo x; Foo y(x); // 拷贝构造函数, x是左值 Foo z(std::move(x)); // 拷贝构造函数,因为没有移动构造函数
22.根据参数的类型,复制初始化使用复制构造函数或移动构造函数; 实参是lvalue则复制,实参是rvalue则移动。
class HasPtr { public: HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) {p.ps = 0;} HasPtr& operator=(HasPtr rhs) { swap(*this, rhs); return *this; } // 该运算符具有一个非引用形参,这意味着该形参将从实参复制初始化。 // 其他成员同6.点 }; hp = hp2; // hp2是左值,调用拷贝构造函数 hp = std::move(hp2); // 移动构造函数
23. 区分移动和复制形参的重载函数通常有一个版本的形参是const T&,另一个版本的形参是T&&。
class StrVec { public: void push_back(const std::string&); // 拷贝构造函数 void push_back(std::string&&); // 移动构造函数 }; void StrVec::push_back(const string& s) { chk_n_alloc(); alloc.construct(first_free++, s); } void StrVec::push_back(string &&s) { chk_n_alloc(); alloc.construct(first_free++, std::move(s)); }
24. 在形参表后面放置一个引用限定符表示this对象的左值/右值属性。
class Foo {
public:
Foo &operator=(const Foo&) & // this对象时左值
};
Foo &Foo::operator=(const Foo &rhs) & {
return *this;
}
Foo &retFoo();
Foo retVal();
Foo i, j;
i = j;
retFoo() = j;
i = retVal();
retVal() = j; // error: retVal返回右值,不能作为赋值操作符的左操作数
25. 可以根据引用限定符重载函数。
class Foo { public: Foo sorted() &&; Foo sorted() const &; private: vector<int> data; }; Foo Foo::sorted() && { // this对象是右值,可以原地修改。对象是右值,这意味着它没有其他用户,因此我们可以更改对象本身。 sort(data.begin(), data.end()); return *this; } Foo Foo::sorted() const & { // this对象是const或者左值,不能原地修改 Foo ret(*this); sort(ret.data.begin(), ret.data.end()); // sort the copy return ret; }
26.当我们定义两个或多个具有相同名称和相同参数列表的成员时,我们必须在所有这些函数上都提供或不提供引用限定符。
class Foo { public: Foo sorted() &&; Foo sorted() const; // error: must have reference qualifier Foo sorted() const &; // ok using Comp = bool(const int&, const int&); Foo sorted(Comp*); Foo sorted(Comp*) const; };