面向对象编程基础¶
本课程入选教育部产学合作协同育人项目 课程主页:http://cpp.njuer.org 课程老师:陈明 http://cv.mchen.org
ppt和代码下载地址
git clone https://gitee.com/cpp-njuer-org/book
第III部分 类设计者的工具¶
第13章拷贝控制 第14章操作重载与类型转换 第15章面向对象程序设计 第16章模板与泛型编程
第13章¶
拷贝控制¶
拷贝控制¶
当定义一个类时,我们显式地或隐式地指定
在此类型的对象拷贝、移动、赋值和销毁时做什么。
一个类通过定义五种特殊的成员函数来控制这些操作,包括:
- 拷贝构造函数(copy constructor)、
- 拷贝赋值运算符(copy-assignmentoperator)、
- 移动构造函数(move constructor)、
- 移动赋值运算符(move-assignment operator)和
- 析构函数(destructor)。
拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。
拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。
析构函数定义了当此类型对象销毁时做什么。
我们称这些操作为拷贝控制操作(copy control)。
拷贝控制¶
如果一个类没有定义所有这些拷贝控制成员,编译器会自动为它定义缺失的操作。
因此,很多类会忽略这些拷贝控制操作。
但是,对一些类来说,依赖这些操作的默认定义会导致灾难。
通常,实现拷贝控制操作最困难的地方是首先认识到什么时候需要定义这些操作。
在定义任何C++类时,拷贝控制操作都是必要部分。
必须定义对象拷贝、移动、赋值或销毁时做什么,这常常令人感到困惑。
如果我们不显式定义这些操作,编译器也会为我们定义,
- 但编译器定义的版本的行为可能并非我们所想。
拷贝、赋值与销毁¶
拷贝构造函数¶
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,
则此构造函数是拷贝构造函数。
class Foo {
public:
Foo(); // default constructor
Foo(const Foo&); // copy constructor
// ...
};
拷贝构造函数的第一个参数必须是一个引用类型
- 虽然我们可以定义一个接受非const引用的拷贝构造函数,
但此参数几乎总是一个const的引用。
拷贝构造函数在几种情况下都会被隐式地使用。
因此,拷贝构造函数通常不应该是explicit的
合成拷贝构造函数¶
如果我们没有为一个类定义拷贝构造函数,编译器会为我们定义一个。
与合成默认构造函数不同,
即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。
对某些类来说,合成拷贝构造函数(synthesized copy constructor)
用来阻止我们拷贝该类类型的对象。
而一般情况,合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。
编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中。
合成拷贝构造函数¶
每个成员的类型决定了它如何拷贝:对类类型的成员,会使用其拷贝构造函数来拷贝;
内置类型的成员则直接拷贝。
虽然我们不能直接拷贝一个数组,但合成拷贝构造函数会逐元素地拷贝一个数组类型的成员。
- 如果数组元素是类类型,则使用元素的拷贝构造函数来进行拷贝。
class Sales_data {
public:
// other members and constructors as before
// declaration equivalent to the synthesized copy constructor
Sales_data(const Sales_data&);
private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0;
};
// equivalent to the copy constructor that would be synthesized for Sales_data
Sales_data::Sales_data(const Sales_data &orig):
bookNo(orig.bookNo), // uses the string copy constructor
units_sold(orig.units_sold), // copies orig.units_sold
revenue(orig.revenue) // copies orig.revenue
{ } // empty body
拷贝初始化¶
现在,我们可以完全理解直接初始化和拷贝初始化之间的差异了
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
当使用直接初始化时,我们实际上是
- 要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。
当我们使用拷贝初始化(copy initialization)时,我们
- 要求编译器将右侧运算对象拷贝到正在创建的对象中,
- 如果需要的话还要进行类型转换
拷贝初始化通常使用拷贝构造函数来完成。但是,如果一个类有一个移动构造函数,
- 则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。
- 但现在,我们只需了解拷贝初始化何时发生,以及
- 拷贝初始化是依靠拷贝构造函数或移动构造函数来完成的就可以了。
拷贝初始化¶
拷贝初始化不仅在我们用=定义变量时会发生,在下列情况下也会发生·
- 将一个对象作为实参传递给一个非引用类型的形参·
- 从一个返回类型为非引用类型的函数返回一个对象·
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
- 某些类类型还会对它们所分配的对象使用拷贝初始化。
- 例如,当我们初始化标准库容器或是调用其insert或push成员时,
容器会对其元素进行拷贝初始化。
与之相对,用emplace成员创建的元素都进行直接初始化
参数和返回值¶
在函数调用过程中,具有非引用类型的参数要进行拷贝初始化
类似的,当一个函数具有非引用的返回类型时,返回值会被用来初始化调用方的结果
拷贝构造函数被用来初始化非引用类类型参数,
- 这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。
- 如果其参数不是引用类型,则调用永远也不会成功——为了调用拷贝构造函数,
我们必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数,
如此无限循环。
拷贝初始化的限制¶
如果我们使用的初始化值要求通过一个explicit的构造函数来进行类型转换
那么使用拷贝初始化还是直接初始化就不是无关紧要的了:
vector<int> v1(10); // ok: direct initialization
vector<int> v2 = 10; // error: constructor that takes a size is explicit
void f(vector<int>); // f's parameter is copy initialized
f(10); // error: can't use an explicit constructor to copy an argument
f(vector<int>(10)); // ok: directly construct a temporary vector from an int
//直接初始化v1是合法的,但看起来与之等价的拷贝初始化v2则是错误的,
//因为vector的接受单一大小参数的构造函数是explicit的。
//出于同样的原因,当传递一个实参或从函数返回一个值时,
//我们不能隐式使用一个explicit构造函数。如果我们希望使用一个explicit构造函数
//就必须显式地使用,像此代码中最后一行那样。
编译器可以绕过拷贝构造函数¶
在拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象
即,编译器被允许将下面的代码
string null_book = "9-999-99999-9"; // copy initialization
改写为
string null_book("9-999-99999-9");//compiler omits the copy constructor
但是,即使编译器略过了拷贝/移动构造函数,但在这个程序点上,
拷贝/移动构造函数必须是存在且可访问的(例如,不能是private的)
练习¶
拷贝构造函数是什么?什么时候使用它?
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,
则此构造函数是拷贝构造函数。
当使用拷贝初始化时,我们会用到拷贝构造函数。
练习¶
解释为什么下面的声明是非法的:
Sales_data::Sales_data(Sales_data rhs);
参数类型应该是引用类型。
练习¶
当我们拷贝一个StrBlob时,会发生什么?拷贝一个StrBlobPtr呢?
当我们拷贝StrBlob时,会使 shared_ptr 的引用计数加1。
当我们拷贝 StrBlobPtr 时,引用计数不会变化。
练习¶
假定 Point 是一个类类型,它有一个public的拷贝构造函数,
指出下面程序片段中哪些地方使用了拷贝构造函数:
Point global;
Point foo_bar(Point arg) // 1
{
Point local = arg, *heap = new Point(global);
// 2: Point local = arg, 3: Point *heap = new Point(global)
*heap = local;
Point pa[4] = { local, *heap }; // 4, 5
return *heap; // 6
}
上面有6处地方使用了拷贝构造函数。
练习¶
给定下面的类框架,编写一个拷贝构造函数,拷贝所有成员。你的构造函数应该
动态分配一个新的string,并将对象拷贝到ps所指向的位置,而不是拷贝ps本身:
class HasPtr {
public:
HasPtr(const std::string& s = std::string()):
ps(new std::string(s)), i(0) { }
private:
std::string *ps;
int i;
}
#include <string>
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) :
ps(new std::string(s)), i(0) { }
HasPtr(const HasPtr& hp) : ps(new std::string(*hp.ps)), i(hp.i) { }
private:
std::string *ps;
int i;
};
拷贝赋值运算符¶
类也可以控制其对象如何赋值:
Sales_data trans, accum;
trans = accum; // uses the Sales_data copy-assignment operator
与拷贝构造函数一样,如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。
重载赋值运算符¶
重载运算符(overloadedoperator)
- 本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号组成。
- 因此,赋值运算符就是一个名为operator=的函数。
类似于任何其他函数,运算符函数也有一个返回类型和一个参数列表。
重载运算符的参数表示运算符的运算对象。
某些运算符,包括赋值运算符,必须定义为成员函数。
如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。
对于一个二元运算符,例如赋值运算符,其右侧运算对象作为显式参数传递。
重载赋值运算符¶
拷贝赋值运算符接受一个与其所在类相同类型的参数:
class Foo {
public:
Foo& operator=(const Foo&); // assignment operator
// ...
};
为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。
标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。
赋值运算符通常应该返回一个指向其左侧运算对象的引用。
合成拷贝赋值运算符¶
与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为它
- 生成一个合成拷贝赋值运算符(synthesized copy-assignment operator)。
类似拷贝构造函数,对于某些类,合成拷贝赋值运算符用来禁止该类型对象的赋值。
如果拷贝赋值运算符并非出于此目的,它会将右侧运算对象的每个非static成员赋予
左侧运算对象的对应成员,这一工作是通过成员类型的拷贝赋值运算符来完成的。
对于数组类型的成员,逐个赋值数组元素。
合成拷贝赋值运算符返回一个指向其左侧运算对象的引用。
下面的代码等价于Sales_data的合成拷贝赋值运算符:
// equivalent to the synthesized copy-assignment operator
Sales_data&
Sales_data::operator=(const Sales_data &rhs)
{
bookNo = rhs.bookNo; // calls the string::operator=
units_sold = rhs.units_sold; // uses the built-in int assignment
revenue = rhs.revenue; // uses the built-in double assignment
return *this; // return a reference to this object
}
练习¶
拷贝赋值运算符是什么?什么时候使用它?合成拷贝赋值运算符完成什么工作?
什么时候会生成合成拷贝赋值运算符?
拷贝赋值运算符是一个名为 operator= 的函数。当赋值运算发生时就会用到它。
合成拷贝赋值运算符可以用来禁止该类型对象的赋值。
如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符。
练习¶
当我们将一个 StrBlob 赋值给另一个 StrBlob 时,会发生什么?赋值 StrBlobPtr 呢?
会发生浅层复制。
练习¶
为HasPtr 类编写赋值运算符。
类似拷贝构造函数,你的赋值运算符应该将对象拷贝到ps指向的位置。
#include <string>
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) :
ps(new std::string(s)), i(0) { }
HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) { }
HasPtr& operator=(const HasPtr &rhs_hp) {
if(this != &rhs_hp){
std::string *temp_ps = new std::string(*rhs_hp.ps);
delete ps;
ps = temp_ps;
i = rhs_hp.i;
}
return *this;
}
private:
std::string *ps;
int i;
};
析构函数¶
析构函数执行与构造函数相反的操作:
- 构造函数初始化对象的非static数据成员,还可能做一些其他工作;
- 析构函数释放对象使用的资源,并销毁对象的非static数据成员。
析构函数是类的一个成员函数,名字由波浪号接类名构成。
它没有返回值,也不接受参数:
class Foo {
public:
~Foo(); // destructor
// ...
};
由于析构函数不接受参数,因此它不能被重载。
对一个给定类,只会有唯一一个析构函数。
析构函数完成什么工作¶
如同构造函数有一个初始化部分和一个函数体,析构函数也有一个函数体和一个析构部分。
在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中
出现的顺序进行初始化。
在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。
在对象最后一次使用之后,析构函数的函数体可执行类设计者希望执行的任何收尾工作。
通常,析构函数释放对象在生存期分配的所有资源。
在一个析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,
析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型。
销毁类类型的成员需要执行成员自己的析构函数。
内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。
隐式销毁一个内置指针类型的成员不会delete它所指向的对象。
与普通指针不同,智能指针是类类型,所以具有析构函数。
因此,与普通指针不同,智能指针成员在析构阶段会被自动销毁。
什么时候会调用析构函数¶
无论何时一个对象被销毁,就会自动调用其析构函数:·
- 变量在离开其作用域时被销毁。·
- 当一个对象被销毁时,其成员被销毁。·
- 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁。·
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁·
- 对于临时对象,当创建它的完整表达式结束时被销毁。
由于析构函数自动运行,我们的程序可以按需要分配资源,
而(通常)无须担心何时释放这些资源。
什么时候会调用析构函数¶
{ // new scope
// p and p2 point to dynamically allocated objects
Sales_data *p = new Sales_data; // p is a built-in pointer
auto p2 = make_shared<Sales_data>(); // p2 is a shared_ptr
Sales_data item(*p); // copy constructor copies *p into item
vector<Sales_data> vec; // local object
vec.push_back(*p2); // copies the object to which p2 points
delete p; // destructor called on the object pointed to by p
} // exit local scope; destructor called on item, p2, and vec
// destroying p2 decrements its use count; if the count goes to 0, the object is freed
// destroying vec destroys the elements in vec
当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
合成析构函数¶
当一个类未定义自己的析构函数时,编译器会为它
- 定义一个合成析构函数(synthesized destructor)。
类似拷贝构造函数和拷贝赋值运算符,对于某些类,合成析构函数被用来阻止该类型
的对象被销毁。如果不是这种情况,合成析构函数的函数体就为空。
下面的代码片段等价于Sales_data的合成析构函数:
class Sales_data {
public:
// no work to do other than destroying the members, which happens automatically
~Sales_data() { }
// other members as before
};
//在(空)析构函数体执行完毕后,成员会被自动销毁。
//特别的,string的析构函数会被调用,它将释放bookNo成员所用的内存。
认识到析构函数体自身并不直接销毁成员是非常重要的。
成员是在析构函数体之后隐含的析构阶段中被销毁的。
在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
练习¶
析构函数是什么?合成析构函数完成什么工作?什么时候会生成合成析构函数?
析构函数是类的一个成员函数,名字由波浪号接类名构成。
它没有返回值,也不接受参数。合成析构函数可被用来阻止该类型的对象被销毁。
当一个类未定义自己的析构函数时,编译器会为它生成一个合成析构函数。
练习¶
当一个 StrBlob 对象销毁时会发生什么?一个 StrBlobPtr 对象销毁时呢?
当一个 StrBlob 对象被销毁时,shared_ptr 的引用计数会减少。
当 StrBlobPtr 对象被销毁时,不影响引用计数。
练习¶
为前面练习中的 HasPtr 类添加一个析构函数。
#include <string>
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0) { }
HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) { }
HasPtr& operator=(const HasPtr &hp) {
std::string *new_ps = new std::string(*hp.ps);
delete ps;
ps = new_ps;
i = hp.i;
return *this;
}
~HasPtr() {
delete ps;
}
private:
std::string *ps;
int i;
};
练习¶
在下面的代码片段中会发生几次析构函数调用?
bool fcn(const Sales_data *trans, Sales_data accum)
{
Sales_data item1(*trans), item2(accum);
return item1.isbn() != item2.isbn();
}
三次,分别是 accum、item1和item2。
练习¶
理解拷贝控制成员和构造函数的一个好方法的定义一个简单的类,
为该类定义这些成员,每个成员都打印出自己的名字:
struct X {
X() {std::cout << "X()" << std::endl;}
X(const X&) {std::cout << "X(const X&)" << std::endl;}
}
给 X 添加拷贝赋值运算符和析构函数,并编写一个程序以不同的方式使用 X 的对象
:将它们作为非引用参数传递;动态分配它们;将它们存放于容器中;诸如此类。
观察程序的输出,直到你确认理解了什么时候会使用拷贝控制成员,以及为什么会
使用它们。当你观察程序输出时,记住编译器可以略过对拷贝构造函数的调用。
练习¶
#include <iostream>
#include <vector>
#include <initializer_list>
struct X {
X() { std::cout << "X()" << std::endl; }
X(const X&) { std::cout << "X(const X&)" << std::endl; }
X& operator=(const X&) { std::cout << "X& operator=(const X&)"
<< std::endl; return *this; }
~X() { std::cout << "~X()" << std::endl; }
};
void f(const X &rx, X x){
std::vector<X> vec;
vec.reserve(2);
vec.push_back(rx);
vec.push_back(x);}
int main(){
X *px = new X;
f(*px, *px);
delete px;
return 0;
}
三/五法则¶
有三个基本操作可以控制类的拷贝操作:
- 拷贝构造函数、拷贝赋值运算符和析构函数。
而且,在新标准下,一个类还可以定义一个移动构造函数和一个移动赋值运算符,
C++语言并不要求我们定义所有这些操作:可以只定义其中一个或两个,而不必定义所有。
但是,这些操作通常应该被看作一个整体。
通常,只需要其中一个操作,而不需要定义所有操作的情况是很少见的。
需要析构函数的类也需要拷贝和赋值操作¶
当我们决定一个类是否要定义它自己版本的拷贝控制成员时,一个基本原则是
- 首先确定这个类是否需要一个析构函数。
- 通常,对析构函数的需求要比对拷贝构造函数或赋值运算符的需求更为明显。
- 如果这个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数
和一个拷贝赋值运算符。
我们在练习中用过的HasPtr类是一个好例子。这个类在构造函数中分配动态内存。
- 合成析构函数不会delete一个指针数据成员。
- 因此,此类需要定义一个析构函数来释放构造函数分配的内存。
- 基本原则告诉我们,HasPtr也需要一个拷贝构造函数和一个拷贝赋值运算符。
需要析构函数的类也需要拷贝和赋值操作¶
如果我们为HasPtr定义一个析构函数,
但使用合成版本的拷贝构造函数和拷贝赋值运算符,考虑会发生什么:
class HasPtr {
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)), i(0) { }
~HasPtr() { delete ps; }
// WRONG: HasPtr needs a copy constructor and copy-assignment operator
// other members as before
};
需要析构函数的类也需要拷贝和赋值操作¶
在这个版本的类定义中,构造函数中分配的内存将在HasPtr对象销毁时被释放。
但不幸的是,我们引入了一个严重的错误!
这个版本的类使用了合成的拷贝构造函数和拷贝赋值运算符。
这些函数简单拷贝指针成员,这意味着多个HasPtr对象可能指向相同的内存:
HasPtr f(HasPtr hp) // HasPtr passed by value, so it is copied
{
HasPtr ret = hp; // copies the given HasPtr
// process ret
return ret; // ret and hp are destroyed
}
当f返回时,hp和ret都被销毁,在两个对象上都会调用HasPtr的析构函数。
此析构函数会delete ret和hp中的指针成员。但这两个对象包含相同的指针值。
此代码会导致此指针被delete两次,这显然是一个错误。将要发生什么是未定义的。
需要析构函数的类也需要拷贝和赋值操作¶
此外,f的调用者还会使用传递给f的对象:
HasPtr p("some values");
f(p); // when f completes, the memory to which p.ps points is freed
HasPtr q(p); // now both p and q point to invalid memory!
p(以及q)指向的内存不再有效,在hp(或ret!)销毁时它就被归还给系统了。
如果一个类需要自定义析构函数,几乎可以肯定
它也需要自定义拷贝赋值运算符和拷贝构造函数。
需要拷贝操作的类也需要赋值操作,反之亦然¶
虽然很多类需要定义所有(或是不需要定义任何)拷贝控制成员,
但某些类所要完成的工作,只需要拷贝或赋值操作,不需要析构函数。
- 作为一个例子,考虑一个类为每个对象分配一个独有的、唯一的序号。
这个类需要一个拷贝构造函数为每个新创建的对象生成一个新的、独一无二的序号。
除此之外,这个拷贝构造函数从给定对象拷贝所有其他数据成员。
这个类还需要自定义拷贝赋值运算符来避免将序号赋予目的对象。
但是,这个类不需要自定义析构函数。
这个例子引出了第二个基本原则:
如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符。
反之亦然
如果一个类需要一个拷贝赋值运算符,几乎可以肯定它也需要一个拷贝构造函数。
然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。
练习¶
假定 numbered 是一个类,它有一个默认构造函数,能为每个对象生成一个唯一的序号
保存在名为 mysn 的数据成员中。
假定 numbered 使用合成的拷贝控制成员,并给定如下函数:
void f (numbered s) { cout << s.mysn < endl; }
则下面代码输出什么内容?
numbered a, b = a, c = b;
f(a); f(b); f(c);
输出3个完全一样的数。
练习¶
假定numbered 定义了一个拷贝构造函数,能生成一个新的序列号。
这会改变上一题中调用的输出结果吗?如果会改变,为什么?新的输出结果是什么?
会输出3个不同的数。并且这3个数并不是a、b、c当中的数。
练习¶
如果 f 中的参数是 const numbered&,将会怎样?
这会改变输出结果吗?如果会改变,为什么?新的输出结果是什么?
会输出 a、b、c的数。
练习¶
分别编写前三题中所描述的 numbered 和 f,验证你是否正确预测了输出结果。
#include <iostream>
class numbered{
public:
numbered(){mysn = unique++;}
int mysn;
static int unique;
};
int numbered::unique = 10;
void f(numbered s){
std::cout << s.mysn << std::endl;
}
int main(){
numbered a, b = a, c = b;
f(a);
f(b);
f(c);
}
练习¶
#include <iostream>
class numbered {
public:
numbered() {mysn = unique++;}
numbered(const numbered& n){mysn = unique++;}
int mysn;
static int unique;
};
int numbered::unique = 10;
void f(numbered s) {
std::cout << s.mysn << std::endl;
}
int main(){
numbered a, b = a, c = b;
f(a);
f(b);
f(c);
}
练习¶
#include <iostream>
class numbered
{
public:
numbered() {mysn = unique++;}
numbered(const numbered& n){mysn = unique++;}
int mysn;
static int unique;
};
int numbered::unique = 10;
void f(const numbered& s){
std::cout << s.mysn << std::endl;
}
int main(){
numbered a, b = a, c = b;
f(a);
f(b);
f(c);
}
使用=default¶
我们可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本
class Sales_data {
public:
// copy control; use defaults
Sales_data() = default;
Sales_data(const Sales_data&) = default;
Sales_data& operator=(const Sales_data &);
~Sales_data() = default;
// other members as before
};
Sales_data&
Sales_data::operator=(const Sales_data&) = default;
当我们在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的
(就像任何其他类内声明的成员函数一样)。
如果我们不希望合成的成员是内联函数,应该只对成员的类外定义使用=default,
就像对拷贝赋值运算符所做的那样。
我们只能对具有合成版本的成员函数使用=default(即,默认构造函数或拷贝控制成员)
阻止拷贝¶
大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,
无论是隐式地还是显式地。
虽然大多数类应该定义(而且通常也的确定义了)拷贝构造函数和拷贝赋值运算符,
但对某些类来说,这些操作没有合理的意义。
在此情况下,定义类时必须采用某种机制阻止拷贝或赋值。
- 例如,iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。
为了阻止拷贝,看起来可能应该不定义拷贝控制成员。
但是,这种策略是无效的:如果我们的类未定义这些操作,编译器为它生成合成的版本。
定义删除的函数¶
在新标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数
(deleted function)来阻止拷贝。
删除的函数是这样一种函数:我们虽然声明了它们,但不能以任何方式使用它们。
在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的:
struct NoCopy {
NoCopy() = default; // use the synthesized default constructor
NoCopy(const NoCopy&) = delete; // no copy
NoCopy &operator=(const NoCopy&) = delete; // no assignment
~NoCopy() = default; // use the synthesized destructor
// other members
};
=delete通知编译器(以及我们代码的读者),我们不希望定义这些成员。
定义删除的函数¶
与=default不同,=delete必须出现在函数第一次声明的时候,
这个差异与这些声明的含义在逻辑上是吻合的。
一个默认的成员只影响为这个成员而生成的代码,
因此=default直到编译器生成代码时才需要。
而另一方面,编译器需要知道一个函数是删除的,以便禁止试图使用它的操作。
与=default的另一个不同之处是,我们可以对任何函数指定=delete
(我们只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default)。
虽然删除函数的主要用途是禁止拷贝控制成员,
但当我们希望引导函数匹配过程时,删除函数有时也是有用的。
析构函数不能是删除的成员¶
值得注意的是,我们不能删除析构函数。如果析构函数被删除,就无法销毁此类型的对象了。
对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或创建该类的临时对象。
而且,如果一个类有某个成员的类型删除了析构函数,我们也不能定义该类的变量或临时对象。
因为如果一个成员的析构函数是删除的,则该成员无法被销毁。
而如果一个成员无法被销毁,则对象整体也就无法被销毁了。
对于删除了析构函数的类型,虽然我们不能定义这种类型的变量或成员,
但可以动态分配这种类型的对象。但是,不能释放这些对象:
struct NoDtor {
NoDtor() = default; // use the synthesized default constructor
~NoDtor() = delete; // we can't destroy objects of type NoDtor
};
NoDtor nd; // error: NoDtor destructor is deleted
NoDtor *p = new NoDtor(); // ok: but we can't delete p
delete p; // error: NoDtor destructor is deleted
对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。
合成的拷贝控制成员可能是删除的¶
如前所述,如果我们未定义拷贝控制成员,编译器会为我们定义合成的版本。
类似的,如果一个类未定义构造函数,编译器会为其合成一个默认构造函数。
对某些类来说,编译器将这些合成的成员定义为删除的函数:
- 如果类的某个成员的析构函数是删除的或不可访问的(例如,是private的),
则类的合成析构函数被定义为删除的。·
- 如果类的某个成员的拷贝构造函数是删除的或不可访问的,
则类的合成拷贝构造函数被定义为删除的。
- 如果类的某个成员的析构函数是删除的或不可访问的,
则类合成的拷贝构造函数也被定义为删除的。·
- 如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,
或是类有一个const的或引用成员,则类的合成拷贝赋值运算符被定义为删除的。·
- 如果类的某个成员的析构函数是删除的或不可访问的,或是类有一个引用成员,
它没有类内初始化器,或是类有一个const成员,它没有类内初始化器且其类型未
显式定义默认构造函数,则该类的默认构造函数被定义为删除的。
这些规则的含义是:如果一个类有数据成员不能默认构造、拷贝、复制或销毁
则对应的成员函数将被定义为删除的。
合成的拷贝控制成员可能是删除的¶
一个成员有删除的或不可访问的析构函数会导致合成的默认和拷贝构造函数被定义
为删除的,这看起来可能有些奇怪。其原因是,如果没有这条规则,我们可能会创
建出无法销毁的对象。
对于具有引用成员或无法默认构造的const成员的类,编译器不会为其合成默认构造函数
如果一个类有const成员,则它不能使用合成的拷贝赋值运算符。
- 毕竟,此运算符试图赋值所有成员,而将一个新值赋予一个const对象是不可能的。
虽然我们可以将一个新值赋予一个引用成员,但这样做改变的是引用指向的对象的值,
而不是引用本身。如果为这样的类合成拷贝赋值运算符,则赋值后,左侧运算对象仍然
指向与赋值前一样的对象,而不会与右侧运算对象指向相同的对象。由于这种行为看起
来并不是我们所期望的,因此对于有引用成员的类,合成拷贝赋值运算符被定义为删除的。
本质上,当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
private拷贝控制¶
在新标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private的
来阻止拷贝:
class PrivateCopy {
// no access specifier; following members are private by default;
// copy control is private and so is inaccessible to ordinary user code
PrivateCopy(const PrivateCopy&);
PrivateCopy &operator=(const PrivateCopy&);
// other members
public:
PrivateCopy() = default; // use the synthesized default constructor
~PrivateCopy(); // users can define objects of this type but not copy them
};
由于析构函数是public的,用户可以定义PrivateCopy类型的对象。
由于拷贝构造函数和拷贝赋值运算符是private的,用户代码将不能拷贝这个类型的对象。
但是,友元和成员函数仍旧可以拷贝对象。
为了阻止友元和成员函数进行拷贝,
我们将这些拷贝控制成员声明为private的,但并不定义它们。
¶
声明但不定义一个成员函数是合法的,对此只有一个例外,后续介绍。
试图访问一个未定义的成员将导致一个链接时错误。
通过声明(但不定义)private的拷贝构造函数,可以预先阻止任何拷贝该类型对象的企图:
- 试图拷贝对象的用户代码将在编译阶段被标记为错误;
- 成员函数或友元函数中的拷贝操作将会导致链接时错误。
希望阻止拷贝的类应该使用=delete来定义它们自己的拷贝构造函数和拷贝赋值运算符,
而不应该将它们声明为private的。
练习¶
定义一个 Employee 类,它包含雇员的姓名和唯一的雇员证号。
为这个类定义默认构造函数,以及接受一个表示雇员姓名的 string 的构造函数。
每个构造函数应该通过递增一个 static 数据成员来生成一个唯一的证号。
#include <string>
using std::string;
class Employee{
public:
Employee();
Employee(const string& name);
const int id() const { return id_; }
private:
string name_;
int id_;
static int s_increment;
};
int Employee::s_increment = 0;
Employee::Employee(){id_ = s_increment++;}
Employee::Employee(const string& name){
id_ = s_increment++;
name_ = name;
}
练习¶
你的 Employee 类需要定义它自己的拷贝控制成员吗?如果需要,为什么?
如果不需要,为什么?实现你认为 Employee 需要的拷贝控制成员。
可以显式地阻止拷贝。
#include <string>
using std::string;
class Employee {
public:
Employee();
Employee(const string &name);
Employee(const Employee&) = delete;
Employee& operator=(const Employee&) = delete;
const int id() const { return id_; }
private:
string name_;
int id_;
static int s_increment;
};
练习¶
解释当我们拷贝、赋值或销毁 TextQuery 和 QueryResult 类对象时会发生什么?
成员会被复制。
练习¶
你认为 TextQuery 和 QueryResult 类需要定义它们自己版本的拷贝控制成员吗?
如果需要,为什么?实现你认为这两个类需要的拷贝控制操作。
合成的版本满足所有的需求。因此不需要自定义拷贝控制成员。
拷贝控制和资源管理¶
类的行为可以像一个值,也可以像一个指针。
- 像值:对象有自己的状态,副本和原对象是完全独立的。
- 标准库容器和string类
- 像指针:共享状态,拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。
- shared_ptr类
练习¶
假定我们希望HasPtr的行为像一个值。即,对于对象所指向的string成员,
每个对象都有一份自己的拷贝。为HasPtr编写拷贝构造函数和拷贝赋值运算符。
#include <string>
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) :
ps(new std::string(s)), i(0) { }
HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) { }
HasPtr& operator=(const HasPtr &hp) {
auto new_p = new std::string(*hp.ps);
delete ps;
ps = new_p;
i = hp.i;
return *this;
}
~HasPtr() {
delete ps;
}
private:
std::string *ps;
int i;
};
行为像值的类¶
//为了提供类值的行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝。
class HasPtr {
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)), i(0) { }
// each HasPtr has its own copy of the string to which ps points
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); // copy the underlying string
delete ps; // free the old memory
ps = newp; // copy data from rhs into this object
i = rhs.i;
return *this; // return this object
}
关键概念:赋值运算符¶
如果将一个对象赋予它自身,赋值运算符必须能正确工作。·
大多数赋值运算符组合了析构函数和拷贝构造函数的工作。
//防范自赋值操作的重要性,考虑如果赋值运算符如下编写将会发生什么
// WRONG way to write an assignment operator!
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
delete ps; // frees the string to which this object points
// if rhs and *this are the same object, we're copying from deleted memory!
ps = new string(*(rhs.ps));
i = rhs.i;
return *this;
}
//对于一个赋值运算符来说,正确工作是非常重要的,即使是将一个对象赋予它自身,
//也要能正确工作。一个好的方法是在销毁左侧运算对象资源之前拷贝右侧运算对象。
练习¶
比较上一节练习中你编写的拷贝控制成员和这一节中的代码。
确定你理解了你的代码和我们的代码之间的差异(如果有的话)。
练习¶
如果本节中的HasPtr版本未定义析构函数,将会发生什么?
如果未定义拷贝构造函数,将会发生什么?
如果未定义析构函数,将会发生内存泄漏。
如果未定义拷贝构造函数,将会拷贝指针的值,指向同一个地址。
练习¶
假定希望定义 StrBlob 的类值版本,而且希望继续使用 shared_ptr,
这样我们的 StrBlobPtr 类就仍能使用指向vector的 weak_ptr 了。
你修改后的类将需要一个拷贝的构造函数和一个拷贝赋值运算符,但不需要析构函数。
解释拷贝构造函数和拷贝赋值运算符必须要做什么。解释为什么不需要析构函数。
拷贝构造函数和拷贝赋值运算符要重新动态分配内存。
因为 StrBlob 使用的是智能指针,当引用计数为0时会自动释放对象,
因此不需要析构函数。
练习¶
对上一题中描述的 strBlob 类,编写你自己的版本。
头文件:
#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <exception>
using std::vector; using std::string;
class ConstStrBlobPtr;
class StrBlob {
public:
using size_type = vector<string>::size_type;
friend class ConstStrBlobPtr;
ConstStrBlobPtr begin() const;
ConstStrBlobPtr end() const;
练习¶
StrBlob():data(std::make_shared<vector<string>>()) { }
StrBlob(std::initializer_list<string> il):
data(std::make_shared<vector<string>>(il)) { }
// copy constructor
StrBlob(const StrBlob& sb) :
data(std::make_shared<vector<string>>(*sb.data)) { }
// copy-assignment operators
StrBlob& operator=(const StrBlob& sb);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
void push_back(const string &t) { data->push_back(t); }
void pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
string& front() {
check(0, "front on empty StrBlob");
return data->front();
}
练习¶
string& back() {
check(0, "back on empty StrBlob");
return data->back();
}
const string& front() const {
check(0, "front on empty StrBlob");
return data->front();
}
const string& back() const {
check(0, "back on empty StrBlob");
return data->back();
}
private:
void check(size_type i, const string &msg) const {
if (i >= data->size()) throw std::out_of_range(msg);
}
private:
std::shared_ptr<vector<string>> data;
};
练习¶
class ConstStrBlobPtr {
public:
ConstStrBlobPtr():curr(0) { }
ConstStrBlobPtr(const StrBlob &a, size_t sz = 0):wptr(a.data), curr(sz) { } // should add const
bool operator!=(ConstStrBlobPtr& p) { return p.curr != curr; }
const string& deref() const { // return value should add const
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
ConstStrBlobPtr& incr() {
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
private:
std::shared_ptr<vector<string>> check(size_t i, const string &msg) const {
auto ret = wptr.lock();
if (!ret) throw std::runtime_error("unbound StrBlobPtr");
if (i >= ret->size()) throw std::out_of_range(msg);
return ret;
}
std::weak_ptr<vector<string>> wptr;
size_t curr;
};
练习¶
主函数:
#include "ex_13_26.h"
ConstStrBlobPtr StrBlob::begin() const // should add const
{
return ConstStrBlobPtr(*this);
}
ConstStrBlobPtr StrBlob::end() const // should add const
{
return ConstStrBlobPtr(*this, data->size());
}
StrBlob& StrBlob::operator=(const StrBlob& sb)
{
data = std::make_shared<vector<string>>(*sb.data);
return *this;
}
int main()
{
return 0;
}
定义行为像指针的类¶
有时我们希望直接管理资源。在这种情况下,使用引用计数(referencecount)
- 除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,
用来记录有多少对象与正在创建的对象共享状态。
当我们创建一个对象时,只有一个对象共享状态,因此将计数器初始化为1。·
- 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。
拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户所共享。·
- 析构函数递减计数器,指出共享状态的用户少了一个。
如果计数器变为0,则析构函数释放状态。·
- 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。
如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,
拷贝赋值运算符就必须销毁状态。
引用计数¶
计数器不能直接作为HasPtr对象的成员。下面的例子说明了原因:
HasPtr p1("Hiya!");
HasPtr p2(p1); // p1 and p2 point to the same string
HasPtr p3(p1); // p1, p2, and p3 all point to the same string
解决此问题的一种方法是将计数器保存在动态内存中。
当创建一个对象时,我们也分配一个新的计数器。
当拷贝或赋值对象时,我们拷贝指向计数器的指针。
使用这种方法,副本和原对象都会指向相同的计数器。
定义一个使用引用计数的类¶
class HasPtr {
public:
// constructor allocates a new string and a new counter, which it sets to 1
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)), i(0), use(new std::size_t(1)){}
// copy constructor copies all three data members and increments the counter
HasPtr(const HasPtr &p):
ps(p.ps), i(p.i), use(p.use) { ++*use; }
HasPtr& operator=(const HasPtr&);
~HasPtr();
private:
std::string *ps;
int i;
std::size_t *use; // member to keep track of how many objects share
*ps
};
//在此,我们添加了一个名为use的数据成员,它记录有多少对象共享相同的string。
//接受string参数的构造函数分配新的计数器,并将其初始化为1,
//指出当前有一个用户使用本对象的string成员。
类指针的拷贝成员“篡改”引用计数¶
HasPtr::~HasPtr()
{
if (--*use == 0) { // if the reference count goes to 0
delete ps; // delete the string
delete use; // and the counter
}
}
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
++*rhs.use; // increment the use count of the right-hand operand
if (--*use == 0) { // then decrement this object's counter
delete ps; // if no other users
delete use; // free this object's allocated members
}
ps = rhs.ps; // copy data from rhs into this object
i = rhs.i;
use = rhs.use;
return *this; // return this object
}
练习¶
定义你自己的使用引用计数版本的 HasPtr。
#include <string>
class HasPtr {
public:
HasPtr(const std::string &s = std::string()) : ps(new std::string(s)), i(0), use(new size_t(1)) { }
HasPtr(const HasPtr &hp) : ps(hp.ps), i(hp.i), use(hp.use) { ++*use; }
HasPtr& operator=(const HasPtr &rhs) {
++*rhs.use;
if (--*use == 0) {
delete ps;
delete use;
}
ps = rhs.ps;
i = rhs.i;
use = rhs.use;
return *this;
}
练习¶
~HasPtr() {
if (--*use == 0) {
delete ps;
delete use;
}
}
private:
std::string *ps;
int i;
size_t *use;
};
练习¶
给定下面的类,为其实现一个默认构造函数和必要的拷贝控制成员。
(a)
class TreeNode {
pravite:
std::string value;
int count;
TreeNode *left;
TreeNode *right;
};
(b)
class BinStrTree{
pravite:
TreeNode *root;
};
练习¶
头文件:
#include <string>
using std::string;
class TreeNode {
public:
TreeNode() : value(string()), count(new int(1)),
left(nullptr), right(nullptr) { }
TreeNode(const TreeNode &rhs) : value(rhs.value), count(rhs.count),
left(rhs.left), right(rhs.right) { ++*count; }
TreeNode& operator=(const TreeNode &rhs);
~TreeNode() {
if (--*count == 0) {
delete left;
delete right;
delete count;
}
}
private:
std::string value;
int *count;
TreeNode *left;
TreeNode *right;
};
练习¶
class BinStrTree {
public:
BinStrTree() : root(new TreeNode()) { }
BinStrTree(const BinStrTree &bst) : root(new TreeNode(*bst.root)) { }
BinStrTree& operator=(const BinStrTree &bst);
~BinStrTree() { delete root; }
private:
TreeNode *root;
};
练习¶
实现和主函数:
#include "ex_13_28.h"
TreeNode& TreeNode::operator=(const TreeNode &rhs)
{
++*rhs.count;
if (--*count == 0) {
delete left;
delete right;
delete count;
}
value = rhs.value;
left = rhs.left;
right = rhs.right;
count = rhs.count;
return *this;
}
练习¶
BinStrTree& BinStrTree::operator=(const BinStrTree &bst)
{
TreeNode *new_root = new TreeNode(*bst.root);
delete root;
root = new_root;
return *this;
}
int main()
{
return 0;
}
交换操作¶
管理资源的类通常还定义一个名为swap的函数。
- 重排元素顺序的算法,在需要交换两个元素时会调用swap。
- 如果一个类定义了自己的swap,那么算法将使用类自定义版本。
否则,算法将使用标准库定义的swap。
//为了交换两个对象我们需要进行一次拷贝和两次赋值
HasPtr temp = v1; // make a temporary copy of the value of v1
v1 = v2; // assign the value of v2 to v1
v2 = temp; // assign the saved value of v1 to v2
//理论上,这些内存分配都是不必要的。
//我们更希望swap交换指针,而不是分配string的新副本
string *temp = v1.ps; // make a temporary copy of the pointer in v1.ps
v1.ps = v2.ps; // assign the pointer in v2.ps to v1.ps
v2.ps = temp; // assign the saved pointer in v1.ps to v2.ps
编写我们自己的swap函数¶
//在我们的类上定义一个自己版本的swap来重载swap的默认行为
class HasPtr {
friend void swap(HasPtr&, HasPtr&);
// ... other members as in § 13.2.1
};
inline void swap(HasPtr &lhs, HasPtr &rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps); // swap the pointers, not the string data
swap(lhs.i, rhs.i); // swap the int members
}
//与拷贝控制成员不同,swap并不是必要的。
//但是,对于分配了资源的类,定义swap可能是一种很重要的优化手段。
swap函数应该调用swap,而不是std::swap¶
void swap(Foo &lhs, Foo &rhs)
{
// WRONG: this function uses the library version of swap,
// not the HasPtr version
std::swap(lhs.h, rhs.h);
// swap other members of type Foo
}
//希望调用为HasPtr对象定义的版本
void swap(Foo &lhs, Foo &rhs)
{
using std::swap;
swap(lhs.h, rhs.h); // uses the HasPtr version of swap
// swap other members of type Foo
}
//用swap而不是std::swap。
//如果存在类型特定的swap版本,其匹配程度会优于std中定义的版本
在赋值运算符中使用swap¶
拷贝并交换(copy and swap)
- 将左侧运算对象与右侧运算对象的一个副本进行交换
// note rhs is passed by value, which means the HasPtr copy constructor
// copies the string in the right-hand operand into rhs
HasPtr& HasPtr::operator=(HasPtr rhs)
{
// swap the contents of the left-hand operand with the local variable rhs
swap(*this, rhs); // rhs now points to the memory this object had used
return *this; // rhs is destroyed, which deletes the pointer in rhs
}
//自动处理了自赋值情况且天然就是异常安全的。
// - 改变左侧运算对象之前拷贝右侧运算对象,保证了自赋值的正确
// - 唯一可能抛出异常的是拷贝构造函数中的new表达式
// - 如果真发生了异常,它也会在我们改变左侧运算对象之前发生。
//使用拷贝和交换的赋值运算符自动就是异常安全的,且能正确处理自赋值。
练习¶
解释 swap(HasPtr&, HasPtr&)中对 swap 的调用不会导致递归循环。
这其实是3个不同的函数,参数类型不一样,所以不会导致递归循环。
练习¶
为你的类值版本的 HasPtr 编写 swap 函数,并测试它。
为你的 swap 函数添加一个打印语句,指出函数什么时候执行。
#include <string>
#include <iostream>
class HasPtr {
public:
friend void swap(HasPtr&, HasPtr&);
HasPtr(const std::string &s = std::string()) :
ps(new std::string(s)), i(0) { }
HasPtr(const HasPtr &hp) : ps(new std::string(*hp.ps)), i(hp.i) { }
HasPtr& operator=(const HasPtr &hp) {
auto new_p = new std::string(*hp.ps);
delete ps;
ps = new_p;
i = hp.i;
return *this;
}
练习¶
~HasPtr() {
delete ps;
}
void show() { std::cout << *ps << std::endl; }
private:
std::string *ps;
int i;
};
inline
void swap(HasPtr& lhs, HasPtr& rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.i, rhs.i);
std::cout << "call swap(HasPtr& lhs, HasPtr& rhs)" << std::endl;
}
练习¶
为你的 HasPtr 类定义一个 < 运算符,并定义一个 HasPtr 的 vector。
为这个 vector 添加一些元素,并对它执行 sort。注意何时会调用 swap。
#include <string>
#include <iostream>
class HasPtr
{
public:
friend void swap(HasPtr&, HasPtr&);
friend bool operator<(const HasPtr &lhs, const HasPtr &rhs);
HasPtr(const std::string &s = std::string())
: ps(new std::string(s)), i(0)
{ }
HasPtr(const HasPtr &hp)
: ps(new std::string(*hp.ps)), i(hp.i)
{ }
练习¶
HasPtr& operator=(HasPtr tmp)
{
this->swap(tmp);
return *this;
}
~HasPtr()
{
delete ps;
}
void swap(HasPtr &rhs)
{
using std::swap;
swap(ps, rhs.ps);
swap(i, rhs.i);
std::cout << "call swap(HasPtr &rhs)" << std::endl;
}
练习¶
void show() const
{
std::cout << *ps << std::endl;
}
private:
std::string *ps;
int i;
};
void swap(HasPtr& lhs, HasPtr& rhs)
{
lhs.swap(rhs);
}
bool operator<(const HasPtr &lhs, const HasPtr &rhs)
{
return *lhs.ps < *rhs.ps;
}
练习¶
int main(){
std::vector<HasPtr> v={HasPtr("abc"),HasPtr("def"),HasPtr("bcd")};
for(auto &a:v){
a.show();
}
std::cout<<std::endl;
sort(v.begin(),v.end());
for(auto &a:v){
a.show();
}
return 0;
}
练习¶
类指针的 HasPtr 版本会从 swap 函数收益吗?
如果会,得到了什么益处?如果不是,为什么?
不会。类值的版本利用swap交换指针不用进行内存分配,因此得到了性能上的提升。
类指针的版本本来就不用进行内存分配,所以不会得到性能提升。
拷贝控制示例¶
定义自己的拷贝控制成员的原因
- 分配资源
- 簿记工作或其他操作
拷贝控制示例¶
两个类命名为Message和Folder,分别表示电子邮件消息和消息目录
- 每个Message都会保存一个它所在Folder的指针的set
- 每个Folder都保存一个它包含的Message的指针的set
析构函数和拷贝赋值运算符都必须从包含一条Message的所有Folder中删除它。
拷贝构造函数和拷贝赋值运算符都要将一个Message添加到给定的一组Folder中。
拷贝控制示例¶
拷贝赋值运算符通常执行拷贝构造函数和析构函数中也要做的工作。
这种情况下,公共的工作应该放在private的工具函数中完成。
Message类¶
class Message {
friend class Folder;
public:
// folders is implicitly initialized to the empty set
explicit Message(const std::string &str = ""):
contents(str) { }
// copy control to manage pointers to this Message
Message(const Message&); // copy constructor
Message& operator=(const Message&); // copy assignment
~Message(); // destructor
// add/remove this Message from the specified Folder's set of messages
void save(Folder&);
void remove(Folder&);
private:
std::string contents; // actual message text
std::set<Folder*> folders; // Folders that have this Message
// utility functions used by copy constructor, assignment, and destructor
// add this Message to the Folders that point to the parameter
void add_to_Folders(const Message&);
// remove this Message from every Folder in folders
void remove_from_Folders();
};
save和remove成员¶
void Message::save(Folder &f)
{
folders.insert(&f); // add the given Folder to our list of Folders
f.addMsg(this); // add this Message to f's set of Messages
}
void Message::remove(Folder &f)
{
folders.erase(&f); // take the given Folder out of our list of Folders
f.remMsg(this); // remove this Message to f's set of Messages
}
Message类的拷贝控制成员¶
// add this Message to Folders that point to m
void Message::add_to_Folders(const Message &m)
{
for (auto f : m.folders) // for each Folder that holds m
f->addMsg(this); // add a pointer to this Message to that Folder
}
//The Message copy constructor copies the data members of the given object
Message::Message(const Message &m):
contents(m.contents), folders(m.folders)
{
add_to_Folders(m); // add this Message to the Folders that point to m
}
Message的析构函数¶
// remove this Message from the corresponding Folders
void Message::remove_from_Folders()
{
for (auto f : folders) // for each pointer in folders
f->remMsg(this); // remove this Message from that Folder
}
Message::~Message()
{
remove_from_Folders();
}
Message的拷贝赋值运算符¶
//先从左侧运算对象的folders中删除此Message的指针,
//然后再将指针添加到右侧运算对象的folders中,从而实现了自赋值的正确处理
Message& Message::operator=(const Message &rhs)
{
// handle self-assignment by removing pointers before inserting them
remove_from_Folders(); // update existing Folders
contents = rhs.contents; // copy message contents from rhs
folders = rhs.folders; // copy Folder pointers from rhs
add_to_Folders(rhs); // add this Message to those Folders
return *this;
}
Message的swap函数¶
//定义一个Message特定版本的swap,
//我们可以避免对contents和folders成员进行不必要的拷贝
//swap函数必须管理指向被交换Message的Folder指针
//通过两遍扫描folders中每个成员来正确处理Folder指针
void swap(Message &lhs, Message &rhs)
{
using std::swap; // not strictly needed in this case, but good habit
// remove pointers to each Message from their (original) respective Folders
for (auto f: lhs.folders)
f->remMsg(&lhs);
for (auto f: rhs.folders)
f->remMsg(&rhs);
// swap the contents and Folder pointer sets
swap(lhs.folders, rhs.folders); // uses swap(set&, set&)
swap(lhs.contents, rhs.contents); // swap(string&, string&)
// add pointers to each Message to their (new) respective Folders
for (auto f: lhs.folders)
f->addMsg(&lhs);
for (auto f: rhs.folders)
f->addMsg(&rhs);
}
练习¶
为什么Message的成员save和remove的参数是一个 Folder&?
为什么我们不能将参数定义为 Folder 或是 const Folder?
因为 save 和 remove 操作需要更新指定 Folder。
练习¶
编写本节所描述的 Message。
头文件:
#include <string>
#include <set>
class Folder;
class Message {
friend void swap(Message &, Message &);
friend class Folder;
public:
explicit Message(const std::string &str = ""):contents(str) { }
Message(const Message&);
Message& operator=(const Message&);
~Message();
void save(Folder&);
void remove(Folder&);
void print_debug();
练习¶
private:
std::string contents;
std::set<Folder*> folders;
void add_to_Folders(const Message&);
void remove_from_Folders();
void addFldr(Folder *f) { folders.insert(f); }
void remFldr(Folder *f) { folders.erase(f); }
};
void swap(Message&, Message&);
练习¶
class Folder {
friend void swap(Folder &, Folder &);
friend class Message;
public:
Folder() = default;
Folder(const Folder &);
Folder& operator=(const Folder &);
~Folder();
void print_debug();
private:
std::set<Message*> msgs;
void add_to_Message(const Folder&);
void remove_from_Message();
void addMsg(Message *m) { msgs.insert(m); }
void remMsg(Message *m) { msgs.erase(m); }
};
void swap(Folder &, Folder &);
练习¶
实现和主函数:
#include "ex13_34_36_37.h"
#include <iostream>
void swap(Message &lhs, Message &rhs)
{
using std::swap;
lhs.remove_from_Folders(); // Use existing member function to avoid duplicate code.
rhs.remove_from_Folders(); // Use existing member function to avoid duplicate code.
swap(lhs.folders, rhs.folders);
swap(lhs.contents, rhs.contents);
lhs.add_to_Folders(lhs); // Use existing member function to avoid duplicate code.
rhs.add_to_Folders(rhs); // Use existing member function to avoid duplicate code.
}
练习¶
// Message Implementation
void Message::save(Folder &f)
{
addFldr(&f); // Use existing member function to avoid duplicate code.
f.addMsg(this);
}
void Message::remove(Folder &f)
{
remFldr(&f); // Use existing member function to avoid duplicate code.
f.remMsg(this);
}
void Message::add_to_Folders(const Message &m)
{
for (auto f : m.folders)
f->addMsg(this);
}
练习¶
Message::Message(const Message &m)
: contents(m.contents), folders(m.folders)
{
add_to_Folders(m);
}
void Message::remove_from_Folders()
{
for (auto f : folders)
f->remMsg(this);
}
练习¶
Message::~Message()
{
remove_from_Folders();
}
Message &Message::operator=(const Message &rhs)
{
remove_from_Folders();
contents = rhs.contents;
folders = rhs.folders;
add_to_Folders(rhs);
return *this;
}
void Message::print_debug()
{
std::cout << contents << std::endl;
}
练习¶
// Folder Implementation
void swap(Folder &lhs, Folder &rhs)
{
using std::swap;
lhs.remove_from_Message();
rhs.remove_from_Message();
swap(lhs.msgs, rhs.msgs);
lhs.add_to_Message(lhs);
rhs.add_to_Message(rhs);
}
void Folder::add_to_Message(const Folder &f)
{
for (auto m : f.msgs)
m->addFldr(this);
}
练习¶
Folder::Folder(const Folder &f)
: msgs(f.msgs)
{
add_to_Message(f);
}
void Folder::remove_from_Message()
{
for (auto m : msgs)
m->remFldr(this);
}
Folder::~Folder()
{
remove_from_Message();
}
Folder &Folder::operator=(const Folder &rhs)
{
remove_from_Message();
msgs = rhs.msgs;
add_to_Message(rhs);
return *this;
}
练习¶
void Folder::print_debug()
{
for (auto m : msgs)
std::cout << m->contents << " ";
std::cout << std::endl;
}
int main()
{
return 0;
}
练习¶
如果Message 使用合成的拷贝控制成员,将会发生什么?
在赋值后一些已存在的 Folders 将会与 Message 不同步。
练习¶
设计并实现对应的 Folder 类。
此类应该保存一个指向 Folder 中包含 Message 的 set
参考之前
练习¶
为 Message类添加成员,实现向 folders添加和删除一个给定的 Folder*。
这两个成员类似Folder 类的 addMsg 和 remMsg 操作。
参考之前
练习¶
我们并未使用拷贝交换方式来设计 Message 的赋值运算符。你认为其原因是什么?
对于动态分配内存的例子来说,拷贝交换方式是一种简洁的设计。
而这里的 Message 类并不需要动态分配内存,用拷贝交换方式只会增加实现的复杂度。
动态内存管理类¶
某些类需要在运行时分配可变大小的内存空间。
- 这种类通常可以使用标准库容器来保存它们的数据。
而某些类需要自己进行内存分配。
- 这些类一般来说必须定义自己的拷贝控制成员来管理所分配的内存。
StrVec类的设计¶
//实现标准库vector类的一个简化版本StrVec
//使用一个allocator来获得原始内存
//- 在需要添加新元素时用allocator的construct成员在原始内存中创建对象。
//- 当我们需要删除一个元素时,我们将使用destroy成员来销毁元素。
每个StrVec有三个指针成员指向其元素所使用的内存:·
- elements,指向分配的内存中的首元素·
- first_free,指向最后一个实际元素之后的位置·
- cap,指向分配的内存末尾之后的位置
0 1 2 3 4 未构造元素
^ ^ ^
elements first_free cap
StrVec类的设计¶
StrVec还有一个名为alloc的静态成员,其类型为allocator<string>。
- alloc成员会分配StrVec使用的内存。
我们的类还有4个工具函数:·
- alloc_n_copy会分配内存,并拷贝一个给定范围中的元素。·
- free会销毁构造的元素并释放内存。·
- chk_n_alloc保证StrVec至少有容纳一个新元素的空间。
如果没有空间添加新元素,chk_n_alloc会调用reallocate来分配更多内存。·
- reallocate在内存用完时为StrVec分配新内存。
StrVec类定义¶
// simplified implementation of the memory allocation strategy for a vector-like class
class StrVec {
public:
StrVec(): // the allocator member is default initialized
elements(nullptr), first_free(nullptr), cap(nullptr) {}
StrVec(const StrVec&); // copy constructor
StrVec &operator=(const StrVec&); // copy assignment
~StrVec(); // destructor
void push_back(const std::string&); // copy the element
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
std::string *begin() const { return elements; }
std::string *end() const { return first_free; }
// ...
StrVec类定义¶
private:
std::allocator<std::string> alloc; // allocates the elements
// used by the functions that add elements to the StrVec
void chk_n_alloc()
{ if (size() == capacity()) reallocate(); }
// utilities used by the copy constructor, assignment operator, and destructor
std::pair<std::string*, std::string*> alloc_n_copy
(const std::string*, const std::string*);
void free(); // destroy the elements and free the space
void reallocate(); // get more space and copy the existing elements
std::string *elements; // pointer to the first element in the array
std::string *first_free; // pointer to the first free element in the array
std::string *cap; // pointer to one past the end of the array
};
使用construct¶
函数push_back调用chk_n_alloc确保有空间容纳新元素。
void StrVec::push_back(const string& s)
{
chk_n_alloc(); // ensure that there is room for another element
// construct a copy of s in the element to which first_free points
alloc.construct(first_free++, s);
}
alloc_n_copy成员¶
//alloc_n_copy成员会分配足够的内存来保存给定范围的元素,
//并将这些元素拷贝到新分配的内存中
pair<string*, string*>
StrVec::alloc_n_copy(const string *b, const string *e)
{
// allocate space to hold as many elements as are in the range
auto data = alloc.allocate(e - b);
// initialize and return a pair constructed from data and
// the value returned by uninitialized_copy
return {data, uninitialized_copy(b, e, data)};
}
free成员¶
//首先destroy元素,然后释放StrVec自己分配的内存空间。
void StrVec::free()
{
// may not pass deallocate a 0 pointer; if elements is 0, there's no work to do
if (elements) {
// destroy the old elements in reverse order
for (auto p = first_free; p != elements; /* empty */)
alloc.destroy(--p);
alloc.deallocate(elements, cap - elements);
}
}
//destroy函数会运行string的析构函数
//deallocate来释放本StrVec对象分配的内存空间
拷贝控制成员¶
//拷贝构造函数调用alloc_n_copy:
StrVec::StrVec(const StrVec &s)
{
// call alloc_n_copy to allocate exactly as many elements as in s
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
//析构函数调用free
StrVec::~StrVec() { free(); }
//拷贝赋值运算符在释放已有元素之前调用alloc_n_copy
StrVec &StrVec::operator=(const StrVec &rhs)
{
// call alloc_n_copy to allocate exactly as many elements as in rhs
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
在重新分配内存的过程中移动而不是拷贝元素¶
reallocate成员函数应该做什么
- 为一个新的、更大的string数组分配内存·
- 在内存空间的前一部分构造对象,保存现有元素·
- 销毁原内存空间中的元素,并释放这块内存
在重新分配内存空间时,如果我们能避免分配和释放string的额外开销,
StrVec的性能会好得多。
移动构造函数和std::move¶
通过使用新标准库引入的两种机制,我们就可以避免string的拷贝。
- 有一些标准库类,包括string,都定义了所谓的“移动构造函数”。
移动构造函数通常是将资源从给定对象“移动”而不是拷贝到正在创建的对象
标准库保证“移后源”(moved-from)string仍然保持一个有效的、可析构的状态。
可以想象每个string都有一个指向char数组的指针。可以假定string的移动构造函数
进行了指针的拷贝,而不是为字符分配内存空间然后拷贝字符。
- 第二个机制是一个名为move的标准库函数,它定义在utility头文件中
- 当reallocate在新内存中构造string时,
它必须调用move来表示希望使用string的移动构造函数,
如果它漏掉了move调用,将会使用string的拷贝构造函数。
- 使用move时,直接调用std::move而不是move
reallocate成员¶
//每次重新分配内存时都会将StrVec的容量加倍。
//如果StrVec为空,我们将分配容纳一个元素的空间:
void StrVec::reallocate()
{
// we'll allocate space for twice as many elements as the current size
auto newcapacity = size() ? 2 * size() : 1;
// allocate new memory
auto newdata = alloc.allocate(newcapacity);
// move the data from the old memory to the new
auto dest = newdata; // points to the next free position in the new array
auto elem = elements; // points to the next element in the old array
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free(); // free the old space once we've moved the elements
// update our data structure to point to the new elements
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
//由于我们使用了移动构造函数,这些string管理的内存将不会被拷贝。
//相反,我们构造的每个string都会从elem指向的string那里接管内存的所有权。
练习¶
编写你自己版本的 StrVec,包括自己版本的 reserve、capacity 和resize。
头文件:
#include <memory>
#include <string>
// 类vector类内存分配策略的简化实现
class StrVec
{
public:
StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) { }
StrVec(const StrVec&); // 拷贝构造函数
StrVec& operator=(const StrVec&); // 拷贝赋值运算符
~StrVec(); // 析构函数
void push_back(const std::string&); // 添加元素时拷贝元素
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
std::string *begin() const { return elements; }
std::string *end() const { return first_free; }
void reserve(size_t new_cap);
void resize(size_t count);
void resize(size_t count, const std::string&);
练习¶
private:
// 工具函数,被拷贝构造函数、赋值运算符和析构函数所使用
std::pair<std::string*, std::string*> alloc_n_copy(const std::string*, const std::string*);
// 销毁元素并释放内存
void free();
// 工具函数,被添加元素的函数使用
void chk_n_alloc() { if (size() == capacity()) reallocate(); }
//获得更多内存并拷贝已有元素
void reallocate();
void alloc_n_move(size_t new_cap);
private:
std::string *elements; // 指向数组首元素的指针
std::string *first_free; // 指向数组第一个空闲元素的指针
std::string *cap; // 指向数组第一个空闲元素的指针
std::allocator<std::string> alloc; // 分配元素
};
练习¶
实现和主函数:
#include "ex_13_39.h"
void StrVec::push_back(const std::string &s)
{
chk_n_alloc();
alloc.construct(first_free++, s);
}
// 分配足够的内存来保存给定范围的元素,并将这些元素拷贝到新分配的内存中
std::pair<std::string*, std::string*>
StrVec::alloc_n_copy(const std::string *b, const std::string *e)
{
// 分配空间保存给定范围中的元素
auto data = alloc.allocate(e - b);
// 初始化并返回一个pair,该pair由data和uninitialized_copy的返回值构成
return{ data, std::uninitialized_copy(b, e, data) };
}
练习¶
void StrVec::free()
{
// 不能传递给deallocate一个空指针,如果elements为0,函数什么也不做
if (elements) {
// 逆序销毁元素
for (auto p = first_free; p != elements;)
alloc.destroy(--p);
alloc.deallocate(elements, cap - elements);
}
}
StrVec::StrVec(const StrVec &rhs)
{
// 调用alloc_n_copy分配空间以容纳与rhs中一样多的元素
auto newdata = alloc_n_copy(rhs.begin(), rhs.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::~StrVec()
{
free();
}
练习¶
StrVec& StrVec::operator = (const StrVec &rhs)
{
// 调用alloc_n_copy分配空间以容纳与rhs中一样多的元素
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
void StrVec::alloc_n_move(size_t new_cap)
{
auto newdata = alloc.allocate(new_cap);
auto dest = newdata;
auto elem = elements;
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free();
elements = newdata;
first_free = dest;
cap = elements + new_cap;
}
练习¶
void StrVec::reallocate()
{
auto newcapacity = size() ? 2 * size() : 1;
alloc_n_move(newcapacity);
}
void StrVec::reserve(size_t new_cap)
{
if (new_cap <= capacity()) return;
alloc_n_move(new_cap);
}
void StrVec::resize(size_t count)
{
resize(count, std::string());
}
练习¶
void StrVec::resize(size_t count, const std::string &s)
{
if (count > size()) {
if (count > capacity()) reserve(count * 2);
for (size_t i = size(); i != count; ++i)
alloc.construct(first_free++, s);
}
else if (count < size()) {
while (first_free != elements + count)
alloc.destroy(--first_free);
}
}
int main()
{
return 0;
}
练习¶
为你的 StrVec 类添加一个构造函数,它接受一个 initializer_list<string> 参数。
头文件:
StrVec(std::initializer_list<std::string>);
实现:
void StrVec::range_initialize(const std::string *first,
const std::string *last)
{
auto newdata = alloc_n_copy(first, last);
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::StrVec(std::initializer_list<std::string> il)
{
range_initialize(il.begin(), il.end());
}
练习¶
在 push_back 中,我们为什么在 construct 调用中使用后置递增运算?
如果使用前置递增运算的话,会发生什么?
会出现 unconstructed。
练习¶
在你的 TextQuery 和 QueryResult 类中用你的 StrVec 类代替vector<string>,
以此来测试你的 StrVec 类。
略
练习¶
重写 free 成员,用 for_each 和 lambda 来代替 for 循环 destroy 元素。
你更倾向于哪种实现,为什么?
for_each(elements, first_free,
[this](std::string &rhs){ alloc.destroy(&rhs); });
更倾向于函数式写法。
练习¶
编写标准库 string 类的简化版本,命名为 String。
你的类应该至少有一个默认构造函数和一个接受 C 风格字符串指针参数的构造函数。
使用 allocator 为你的 String类分配所需内存。
头文件:
#include <memory>
class String{
public:
String() : String("") { }
String(const char *);
String(const String&);
String& operator=(const String&);
~String();
const char *c_str() const { return elements; }
size_t size() const { return end - elements; }
size_t length() const { return end - elements - 1; }
private:
std::pair<char*, char*> alloc_n_copy(const char*, const char*);
void range_initializer(const char*, const char*);
void free();
private:
char *elements;
char *end;
std::allocator<char> alloc;
};
练习¶
实现:
#include "ex_13_44_47.h"
#include <algorithm>
#include <iostream>
std::pair<char*, char*>
String::alloc_n_copy(const char *b, const char *e)
{
auto str = alloc.allocate(e - b);
return{ str, std::uninitialized_copy(b, e, str) };
}
void String::range_initializer(const char *first, const char *last)
{
auto newstr = alloc_n_copy(first, last);
elements = newstr.first;
end = newstr.second;
}
String::String(const char *s)
{
char *sl = const_cast<char*>(s);
while (*sl)
++sl;
range_initializer(s, ++sl);
}
练习¶
String::String(const String& rhs)
{
range_initializer(rhs.elements, rhs.end);
std::cout << "copy constructor" << std::endl;
}
void String::free()
{
if (elements) {
std::for_each(elements, end, [this](char &c){ alloc.destroy(&c); });
alloc.deallocate(elements, end - elements);
}
}
String::~String()
{
free();
}
练习¶
String& String::operator = (const String &rhs)
{
auto newstr = alloc_n_copy(rhs.elements, rhs.end);
free();
elements = newstr.first;
end = newstr.second;
std::cout << "copy-assignment" << std::endl;
return *this;
}
练习¶
测试:
#include "ex13_44_47.h"
#include <vector>
#include <iostream>
void foo(String x)
{
std::cout << x.c_str() << std::endl;
}
void bar(const String& x)
{
std::cout << x.c_str() << std::endl;
}
String baz()
{
String ret("world");
return ret;
}
练习¶
int main()
{
char text[] = "world";
String s0;
String s1("hello");
String s2(s0);
String s3 = s1;
String s4(text);
s2 = s1;
foo(s1);
bar(s1);
foo("temporary");
bar("temporary");
String s5 = baz();
练习¶
std::vector<String> svec;
svec.reserve(8);
svec.push_back(s0);
svec.push_back(s1);
svec.push_back(s2);
svec.push_back(s3);
svec.push_back(s4);
svec.push_back(s5);
svec.push_back(baz());
svec.push_back("good job");
for (const auto &s : svec) {
std::cout << s.c_str() << std::endl;
}
}
对象移动¶
移动而非拷贝对象会大幅度提升性能
- 某些情况下,对象拷贝后就立即被销毁了。
某些类型的对象不能拷贝但可以移动。
- IO类或unique_ptr这样的类包含不能被共享的资源(如指针或IO缓冲)
在新标准中,我们可以用容器保存不可拷贝的类型,只要它们可以被移动即可。
标准库容器、string和shared_ptr类既支持移动也支持拷贝。
IO类和unique_ptr类可以移动但不能拷贝。
右值引用¶
为了支持移动操作,新标准引入了一种新的引用类型——右值引用(rvalue reference)。
- 所谓右值引用就是必须绑定到右值的引用。
- 通过&&而不是&来获得右值引用。
- 右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。
因此可以自由地将一个右值引用的资源“移动”到另一个对象中。
左值和右值是表达式的属性
- 一个左值表达式表示的是一个对象的身份
- 一个右值表达式表示的是对象的值
右值引用¶
对于常规引用(为了与右值引用区分开来,我们可以称之为左值引用(lvalue reference))
- 我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式
右值引用有着完全相反的绑定特性:
- 我们可以将一个右值引用绑定到这类表达式上,
但不能将一个右值引用直接绑定到一个左值上:
int i = 42;
int &r = i; // ok: r refers to i
int &&rr = i; // error: cannot bind an rvalue reference to an lvalue
int &r2 = i * 42; // error: i * 42 is an rvalue
const int &r3 = i * 42; // ok: we can bind a reference to const to an rvalue
int &&rr2 = i * 42; // ok: bind rr2 to the result of the multiplication
右值引用¶
我们可以将一个左值引用绑定到返回左值表达式的结果上。
- 返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,
都是返回左值的表达式的例子
我们不能将一个左值引用绑定到生成右值的表达式上
- 返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。
但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上
左值持久;右值短暂¶
左值有持久的状态,
而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
右值引用只能绑定到临时对象,我们得知·
- 所引用的对象将要被销毁·
- 该对象没有其他用户
这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
右值引用指向将要被销毁的对象。我们可以从绑定到右值引用的对象“窃取”状态。
变量是左值¶
变量可以看作只有一个运算对象而没有运算符的表达式
变量表达式都是左值
不能将一个右值引用绑定到一个右值引用类型的变量上
int &&rr1 = 42; // ok: literals are rvalues
int &&rr2 = rr1; // error: the expression rr1 is an lvalue!
//右值表示临时对象
//变量是持久的,直至离开作用域时才被销毁。
变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,
即使这个变量是右值引用类型也不行。
标准库move函数¶
虽然不能将一个右值引用直接绑定到一个左值上,
但我们可以显式地将一个左值转换为对应的右值引用类型
- 还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,
此函数定义在头文件utility中
- move函数返回给定对象的右值引用。
int &&rr3 = std::move(rr1); // ok
//move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它
//调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。
//在调用move之后,我们不能对移后源对象的值做任何假设。
我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
使用move的代码应该使用std::move而不是move。这样做可以避免潜在的名字冲突。
练习¶
解释左值引用和右值引用的区别?
* 常规引用被称为左值引用
* 绑定到右值的引用被称为右值引用。
练习¶
什么类型的引用可以绑定到下面的初始化器上?
int f();
vector<int> vi(100);
int? r1 = f();
int? r2 = vi[0];
int? r3 = r1;
int? r4 = vi[0] * f();
int f();
vector<int> vi(100);
int&& r1 = f();
int& r2 = vi[0];
int& r3 = r1;
int&& r4 = vi[0] * f();
练习¶
对你在练习中定义的 String类,
为它的拷贝构造函数和拷贝赋值运算符添加一条语句,在每次函数执行时打印一条信息。
参考之前
练习¶
定义一个vector<String> 并在其上多次调用 push_back。运行你的程序,
并观察 String 被拷贝了多少次。
#include "ex_13_44_47.h"
#include <vector>
#include <iostream>
void foo(String x)
{
std::cout << x.c_str() << std::endl;
}
void bar(const String& x)
{
std::cout << x.c_str() << std::endl;
}
String baz()
{
String ret("world");
return ret;
}
练习¶
int main()
{
char text[] = "world";
String s0;
String s1("hello");
String s2(s0);
String s3 = s1;
String s4(text);
s2 = s1;
foo(s1);
bar(s1);
foo("temporary");
bar("temporary");
String s5 = baz();
练习¶
std::vector<String> svec;
svec.reserve(8);
svec.push_back(s0);
svec.push_back(s1);
svec.push_back(s2);
svec.push_back(s3);
svec.push_back(s4);
svec.push_back(s5);
svec.push_back(baz());
svec.push_back("good job");
for (const auto &s : svec) {
std::cout << s.c_str() << std::endl;
}
}
移动构造函数和移动赋值运算符¶
为了让我们自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符。
这两个成员类似对应的拷贝操作,但它们从给定对象“窃取”资源而不是拷贝资源。
类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。
不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。
与拷贝构造函数一样,任何额外的参数都必须有默认实参。
移动构造函数
- 完成资源移动
- 确保移后销毁源对象是无害的
- 资源完成移动,源对象必须不再指向被移动的资源,所有权已经归属新创建的对象。
移动构造函数和移动赋值运算符¶
StrVec::StrVec(StrVec &&s) noexcept // move won't throw any exceptions
// member initializers take over the resources in s
: elements(s.elements),first_free(s.first_free), cap(s.cap)
{
// leave s in a state in which it is safe to run the destructor
s.elements = s.first_free = s.cap = nullptr;
}
//noexcept通知标准库我们的构造函数不抛出任何异常
与拷贝构造函数不同,移动构造函数不分配任何新内存;
它接管给定的StrVec中的内存。在接管内存之后,它将给定对象中的指针都置为nullptr。
这样就完成了从给定对象的移动操作,此对象将继续存在。
最终,移后源对象会被销毁,意味着将在其上运行析构函数。
移动操作、标准库容器和异常¶
由于移动操作“窃取”资源,它通常不分配任何资源。移动操作通常不会抛出任何异常。
当编写一个不抛出异常的移动操作时,我们应该将此事通知标准库。
noexcept是我们承诺一个函数不抛出异常的一种方法。
我们在一个函数的参数列表后指定noexcept。
在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间:
class StrVec {
public:
StrVec(StrVec&&) noexcept; // move constructor
// other members as before
};
StrVec::StrVec(StrVec &&s) noexcept : /* member initializers */
{ /* constructor body */ }
//不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。
移动操作、标准库容器和异常¶
除非vector知道元素类型的移动构造函数不会抛出异常,否则在重新分配内存的过程中,
- 它就必须使用拷贝构造函数而不是移动构造函数。
- 移动构造函数,在移动了部分而不是全部元素后抛出了一个异常
旧空间中的移动源元素已经被改变了,而新空间中未构造的元素可能尚不存在
vector将不能满足自身保持不变的要求
- 使用了拷贝构造函数且发生了异常
在新内存中构造元素时,旧元素保持不变
vector可以释放新分配的(但还未成功构造的)内存并返回
vector原有的元素仍然存在。
如果希望在vector重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝,
就必须显式地告诉标准库我们的移动构造函数可以安全使用。
我们通过将移动构造函数(及移动赋值运算符)标记为noexcept来做到这一点。
移动赋值运算符¶
移动赋值运算符执行与析构函数和移动构造函数相同的工作
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
// direct test for self-assignment 正确处理自赋值:
if (this != &rhs) {
free(); // free existing elements
elements = rhs.elements; // take over resources from rhs
first_free = rhs.first_free;
cap = rhs.cap;
// leave rhs in a destructible state
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
//此右值可能是move调用的返回结果。
//与其他任何赋值运算符一样,关键点是我们不能在使用右侧运算对象的资源之前
//就释放左侧运算对象的资源(可能是相同的资源)。
移后源对象必须可析构¶
从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁。
当我们编写一个移动操作时,必须确保移后源对象进入一个可析构的状态
- StrVec的移动操作满足这一要求,
这是通过将移后源对象的指针成员置为nullptr来实现的。
除了将移后源对象置为析构安全的状态之外,移动操作还必须保证对象仍然是有效的
- 指可以安全地为其赋予新值或者可以安全地使用而不依赖其当前值。
- 移动操作对移后源对象中留下的值没有任何要求。
我们的程序不应该依赖于移后源对象中的数据。
在移动操作之后,移后源对象必须保持有效的、可析构的状态,
但是用户不能对其值进行任何假设。
合成的移动操作¶
编译器也会合成移动构造函数和移动赋值运算符
- 与拷贝操作不同,编译器根本不会为某些类合成移动操作。
- 如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,
编译器就不会为它合成移动构造函数和移动赋值运算符了。
- 因此,某些类就没有移动构造函数或移动赋值运算符。
- 如果一个类没有移动操作,通过正常的函数匹配,
类会使用对应的拷贝操作来代替移动操作。
合成的移动操作¶
只有当一个类没有定义任何自己版本的拷贝控制成员,
且类的每个非static数据成员都可以移动时,
编译器才会为它合成移动构造函数或移动赋值运算符。
- 编译器可以移动内置类型的成员。
- 如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员
// the compiler will synthesize the move operations for X and hasX
struct X {
int i; // built-in types can be moved
std::string s; // string defines its own move operations
};
struct hasX {
X mem; // X has synthesized move operations
};
只有当一个类没有定义任何自己版本的拷贝控制成员,
且它的所有数据成员都能移动构造或移动赋值时,
编译器才会为它合成移动构造函数或移动赋值运算符。
合成的移动操作¶
与拷贝操作不同,移动操作永远不会隐式定义为删除的函数
- 但是,如果我们显式地要求编译器生成=default的移动操作,
且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。
除了一个重要例外,什么时候将合成的移动操作定义为删除的函数遵循与定义删除
的合成拷贝操作类似的原则:
- 与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:
有类成员定义了自己的拷贝构造函数且未定义移动构造函数,
或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。
移动赋值运算符的情况类似。
- 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,
则类的移动构造函数或移动赋值运算符被定义为删除的。
- 类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,
则类的移动构造函数被定义为删除的。
- 类似拷贝赋值运算符,如果有类成员是const的或是引用,
则类的移动赋值运算符被定义为删除的。
合成的移动操作¶
例如,假定Y是一个类,它定义了自己的拷贝构造函数但未定义自己的移动构造函数:
struct hasY {
hasY() = default;
hasY(hasY&&) = default;
Y mem; // hasY will have a deleted move constructor
};
hasY hy, hy2 = std::move(hy); // error: move constructor is deleted
//编译器可以拷贝类型为Y的对象,但不能移动它们。
//类hasY显式地要求一个移动构造函数,但编译器无法为其生成。
//因此,hasY会有一个删除的移动构造函数。
//如果hasY忽略了移动构造函数的声明,则编译器根本不能为它合成一个。
//如果移动操作可能被定义为删除的函数,编译器就不会合成它们。
如果类定义了一个移动构造函数和/或一个移动赋值运算符,
则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。
否则,这些成员默认地被定义为删除的。
移动右值,拷贝左值……¶
如果一个类既有移动构造函数,也有拷贝构造函数,
编译器使用普通的函数匹配规则来确定使用哪个构造函数
赋值操作的情况类似
//在我们的StrVec类中,拷贝构造函数接受一个const StrVec的引用。
//因此,它可以用于任何可以转换为StrVec的类型。
//而移动构造函数接受一个StrVec&&,因此只能用于实参是(非static)右值的情形:
StrVec v1, v2;
v1 = v2; // v2 is an lvalue; copy assignment
StrVec getVec(istream &); // getVec returns an rvalue
v2 = getVec(cin); // getVec(cin) is an rvalue; move assignment
//在第一个赋值中,我们将v2传递给赋值运算符。v2的类型是StrVec,表达式v2是一
//个左值。因此移动版本的赋值运算符是不可行的因为我们不能隐式地将一个右值引
//用绑定到一个左值。因此,这个赋值语句使用拷贝赋值运算符。
//在第二个赋值中,我们赋予v2的是getVec调用的结果。此表达式是一个右值。
//在此情况下,两个赋值运算符都是可行的——将getVec的结果绑定到两个运算符的
//参数都是允许的。调用拷贝赋值运算符需要进行一次到const的转换,而StrVec&&则
//是精确匹配。因此,第二个赋值会使用移动赋值运算符。
……但如果没有移动构造函数,右值也被拷贝¶
如果一个类有一个拷贝构造函数但未定义移动构造函数,编译器不会合成移动构造函数
如果一个类没有移动构造函数,函数匹配规则保证该类型的对象会被拷贝,
- 即使我们试图通过调用move来移动它们时也是如此:
class Foo {
public:
Foo() = default;
Foo(const Foo&); // copy constructor
// other members, but Foo does not define a move constructor
};
Foo x;
Foo y(x); // copy constructor; x is an lvalue
Foo z(std::move(x)); // copy constructor, because there is no move constructor
//在对z进行初始化时,我们调用了move(x),它返回一个绑定到x的Foo&&。
//Foo的拷贝构造函数是可行的,因为我们可以将一个Foo&&转换为一个const Foo&。
//因此,z的初始化将使用Foo的拷贝构造函数。
用拷贝构造函数代替移动构造函数几乎肯定是安全的(赋值运算符的情况类似)。
一般情况下,拷贝构造函数满足对应的移动构造函数的要求:
- 它会拷贝给定对象,并将原对象置于有效状态。
实际上,拷贝构造函数甚至都不会改变原对象的值。
如果一个类有一个可用的拷贝构造函数而没有移动构造函数,
则其对象是通过拷贝构造函数来“移动”的。拷贝赋值运算符和移动赋值运算符的情况类似。
拷贝并交换赋值运算符和移动操作¶
HasPtr版本定义了一个拷贝并交换赋值运算符,
为此类添加一个移动构造函数,它实际上也会获得一个移动赋值运算符
class HasPtr {
public:
// added move constructor
HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) {p.ps =0;}
// assignment operator is both the move- and copy-assignment operator
HasPtr& operator=(HasPtr rhs)
{ swap(*this, rhs); return *this; }
// other members as in § 13.2.1
};
//让我们观察赋值运算符。此运算符有一个非引用参数,这意味着此参数要进行
//拷贝初始化。依赖于实参的类型,拷贝初始化要么使用拷贝构造函数,要么使用移
//动构造函数——左值被拷贝,右值被移动。因此,单一的赋值运算符就实现了
//拷贝赋值运算符和移动赋值运算符两种功能。
拷贝并交换赋值运算符和移动操作¶
假定hp和hp2都是HasPtr对象
hp = hp2; // hp2 is an lvalue; copy constructor used to copy hp2
hp = std::move(hp2); // move constructor moves hp2
//第二个赋值中,我们调用std::move将一个右值引用绑定到hp2上。
//在此情况下,拷贝构造函数和移动构造函数都是可行的。
//但是,由于实参是一个右值引用,移动构造函数是精确匹配的。
//移动构造函数从hp2拷贝指针,而不会分配任何内存。
不管使用的是拷贝构造函数还是移动构造函数,赋值运算符的函数体都swap两个运算
对象的状态。交换HasPtr会交换两个对象的指针(及int)成员。
在swap之后,rhs中的指针将指向原来左侧运算对象所拥有的string。
当rhs离开其作用域时,这个string将被销毁。
建议:更新三/五法则¶
所有五个拷贝控制成员应该看作一个整体:
一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。
如前所述,某些类必须定义拷贝构造函数、拷贝赋值运算符和析构函数才能正确工作。
这些类通常拥有一个资源,而拷贝成员必须拷贝此资源。
一般来说,拷贝一个资源会导致一些额外开销。在这种拷贝并非必要的情况下,
定义了移动构造函数和移动赋值运算符的类就可以避免此问题。
Message类的移动操作¶
定义了自己的拷贝构造函数和拷贝赋值运算符的类通常也会从移动操作受益。
- 通过定义移动操作,Message类可以使用string和set的移动操作来避免
拷贝contents和folders成员的额外开销。
除了移动folders成员,我们还必须更新每个指向原Message的Folder。
我们必须删除指向旧Message的指针,并添加一个指向新Message的指针。
移动构造函数和移动赋值运算符都需要更新Folder指针,
因此我们首先定义一个操作来完成这一共同的工作:
// move the Folder pointers from m to this Message
void Message::move_Folders(Message *m)
{
folders = std::move(m->folders); // uses set move assignment
for (auto f : folders) { // for each Folder
f->remMsg(m); // remove the old Message from the Folder
f->addMsg(this); // add this Message to that Folder
}
m->folders.clear(); // ensure that destroying m is harmless
}
//此函数首先移动folders集合。通过调用move,我们使用了set的移动赋值运算符
//而不是它的拷贝赋值运算符。如果我们忽略了move调用,代码仍能正常工作,但带
//来了不必要的拷贝。函数然后遍历所有Folder,从其中删除指向原Message的指针
//并添加指向新Message的指针。
值得注意的是,向set插入一个元素可能会抛出一个异常
因此Message的移动构造函数和移动赋值运算符可能会抛出异常
因此我们未将它们标记为noexcept
Message类的移动操作¶
Message的移动构造函数调用move来移动contents,并默认初始化自己的folders成员:
Message::Message(Message &&m): contents(std::move(m.contents))
{
move_Folders(&m); // moves folders and updates the Folder pointers
}
移动赋值运算符直接检查自赋值情况:
Message& Message::operator=(Message &&rhs)
{
if (this != &rhs) { // direct check for self-assignment
remove_from_Folders();
contents = std::move(rhs.contents); // move assignment
move_Folders(&rhs); // reset the Folders to point to this Message
}
return *this;
}
//移动赋值运算符必须销毁左侧运算对象的旧状态
移动迭代器¶
StrVec的reallocate使用了一个for循环来调用construct从旧内存将元素拷贝到新内存中
如果我们能调用uninitialized_copy来构造新分配的内存,将比循环更为简单。
uninitialized_copy恰如其名:
- 它对元素进行拷贝操作。标准库中并没有类似的函数将对象“移动”到未构造的内存中。
新标准库中定义了一种移动迭代器(move iterator)适配器
一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。
一般来说,一个迭代器的解引用运算符返回一个指向元素的左值。
与其他迭代器不同,移动迭代器的解引用运算符生成一个右值引用。
调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。
- 此函数接受一个迭代器参数,返回一个移动迭代器。
¶
移动迭代器支持正常的迭代器操作
- 可以将移动迭代器传递给uninitialized_copy
void StrVec::reallocate()
{
// allocate space for twice as many elements as the current size
auto newcapacity = size() ? 2 * size() : 1;
auto first = alloc.allocate(newcapacity);
// move the elements
auto last = uninitialized_copy(make_move_iterator(begin()),
make_move_iterator(end()),
first);
free(); // free the old space
elements = first; // update the pointers
first_free = last;
cap = elements + newcapacity;
}
//uninitialized_copy对输入序列中的每个元素调用construct来将元素“拷贝”到目的位置
//由于我们传递给它的是移动迭代器,因此解引用运算符生成的是一个右值引用,
//这意味着construct将使用移动构造函数来构造元素。
由于移动一个对象可能销毁掉原对象,因此你只有在确信算法在为一个元素赋值或
将其传递给一个用户定义的函数后不再访问它时,才能将移动迭代器传递给算法。
建议:不要随意使用移动操作¶
由于一个移后源对象具有不确定的状态,对其调用std::move是危险的。
当我们调用move时,必须绝对确认移后源对象没有其他用户。
通过在类代码中小心地使用move,可以大幅度提升性能。
而如果随意在普通用户代码(与类实现代码相对)中使用移动操作,
很可能导致莫名其妙的、难以查找的错误,而难以提升应用程序性能。
在移动构造函数和移动赋值运算符这些类实现代码之外的地方,
只有当你确信需要进行移动操作且移动操作是安全的,才可以使用std::move。
练习¶
为你的 StrVec、String 和 Message 类添加一个移动构造函数和一个移动赋值运算符。
自行设计
练习¶
在你的 String 类的移动操作中添加打印语句,
并重新运行13.6.1节的练习13.48中的程序,
它使用了一个vector<String>,观察什么时候会避免拷贝。
String baz()
{
String ret("world");
return ret; // first avoided
}
String s5 = baz(); // second avoided
练习¶
虽然 unique_ptr 不能拷贝,但我们在12.1.5节中编写了一个 clone 函数,
它以值的方式返回一个 unique_ptr。
解释为什么函数是合法的,以及为什么它能正确工作。
在这里是移动的操作而不是拷贝操作,因此是合法的。
练习¶
详细解释 HasPtr 对象的赋值发生了什么?
特别是,一步一步描述 hp、hp2 以及 HasPtr 的赋值运算符
中的参数 rhs 的值发生了什么变化。
左值被拷贝,右值被移动。
练习¶
从底层效率的角度看,HasPtr 的赋值运算符并不理想,解释为什么?
为 HasPtr 实现一个拷贝赋值运算符和一个移动赋值运算符,
并比较你的新的移动赋值运算符中执行的操作和拷贝并交换版本中的执行的操作。
当使用HasPtr& operator=(HasPtr rhs)比如hp = std::move(hp2)
ps成员被拷贝了两次,一次从hp2到rhs,另一次 rhs到*this
而使用HasPtr& operator=(HasPtr&& rhs), ps只被拷贝一次
HasPtr& operator=(const HasPtr& rhs){ // copy assignment operator
auto newps = new string(*rhs.ps);
delete ps;
ps = newps;
i = rhs.i;
return *this;
}
inline HasPtr& operator=(HasPtr&& rhs) noexcept{ // move assignment operator
cout<<" move assignment operator "<<endl;
if(this!=rhs){
delete ps;
ps = rhs.ps;
rhs.ps = nullptr;
rhs.i = 0;
}
return *this;
}
练习¶
如果我们为 HasPtr 定义了移动赋值运算符,
但未改变拷贝并交换运算符,会发生什么?编写代码验证你的答案。
error: ambiguous overload for 'operator=' (operand types are 'HasPtr' and
'std::remove_reference<HasPtr&>::type { aka HasPtr }')
hp1 = std::move(*pH);
^
右值引用和成员函数¶
除了构造函数和赋值运算符之外,如果一个成员函数同时提供拷贝和移动版本,
- 它也能从中受益。
这种允许移动的成员函数通常使用与拷贝/移动构造函数和赋值运算符相同的参数模式
- 一个版本接受一个指向const的左值引用,
- 第二个版本接受一个指向非const的右值引用。
定义了push_back的标准库容器提供两个版本:
void push_back(const X&); // copy: binds to any kind of X
void push_back(X&&); // move: binds only to modifiable rvalues of type X
可以将能转换为类型X的任何对象传递给第一个版本的push_back。此版本从其参数拷贝数据。
对于第二个版本,我们只可以传递给它非const的右值。从此版本会从其参数窃取数据。
区分移动和拷贝的重载函数通常有一个版本接受一个constT&,而另一个版本接受一个T&&
右值引用和成员函数¶
为StrVec类定义另一个版本的push_back
class StrVec {
public:
void push_back(const std::string&); // copy the element
void push_back(std::string&&); // move the element
// other members as before
};
// unchanged from the original version in § 13.5 (p. 527)
void StrVec::push_back(const string& s)
{
chk_n_alloc(); // ensure that there is room for another element
// construct a copy of s in the element to which first_free points
alloc.construct(first_free++, s);
}
void StrVec::push_back(string &&s)
{
chk_n_alloc(); // reallocates the StrVec if necessary
alloc.construct(first_free++, std::move(s));
}
右值引用和成员函数¶
当我们调用push_back时,实参类型决定了新元素是拷贝还是移动到容器中:
StrVec vec; // empty StrVec
string s = "some string or another";
vec.push_back(s); // calls push_back(const string&)
vec.push_back("done"); // calls push_back(string&&)
//差别在于实参是一个左值还是一个右值(从"done"创建的临时string)
右值和左值引用成员函数¶
通常,我们在一个对象上调用成员函数,而不管该对象是一个左值还是一个右值。例如:
string s1 = "a value", s2 = "another";
auto n = (s1 + s2).find('a');
有时,右值的使用方式可能令人惊讶:
s1 + s2 = "wow!";
//此处我们对两个string的连接结果——一个右值,进行了赋值。
//在旧标准中,我们没有办法阻止这种使用方式
//为了维持向后兼容性,新标准库类仍然允许向右值赋值
右值和左值引用成员函数¶
但是,我们可能希望在自己的类中阻止这种用法。
- 在此情况下,我们希望强制左侧运算对象(即,this指向的对象)是一个左值。
- 我们指出this的左值/右值属性的方式与定义const成员函数相同
即,在参数列表后放置一个引用限定符(reference qualifier)
class Foo {
public:
Foo &operator=(const Foo&) &; // may assign only to modifiable lvalues
// other members of Foo
};
Foo &Foo::operator=(const Foo &rhs) &
{
// do whatever is needed to assign rhs to this object
return *this;
}
引用限定符可以是&或&&,分别指出this可以指向一个左值或右值。
类似const限定符,引用限定符只能用于(非static)成员函数,
且必须同时出现在函数的声明和定义中。
¶
对于&限定的函数,我们只能将它用于左值;对于&&限定的函数,只能用于右值:
Foo &retFoo(); // returns a reference; a call to retFoo is an lvalue
Foo retVal(); // returns by value; a call to retVal is an rvalue
Foo i, j; // i and j are lvalues
i = j; // ok: i is an lvalue
retFoo() = j; // ok: retFoo() returns an lvalue
retVal() = j; // error: retVal() returns an rvalue
i = retVal(); // ok: we can pass an rvalue as the right-hand operand to assignment
一个函数可以同时用const和引用限定。
- 在此情况下,引用限定符必须跟随在const限定符之后:
class Foo {
public:
Foo someMem() & const; // error: const qualifier must come first
Foo anotherMem() const &; // ok: const qualifier comes first
};
重载和引用函数¶
引用限定符也可以区分重载版本
- 而且,我们可以综合引用限定符和const来区分一个成员函数的重载版本
class Foo {
public:
Foo sorted() &&; // may run on modifiable rvalues
Foo sorted() const &; // may run on any kind of Foo
// other members of Foo
private:
vector<int> data;
};
// this object is an rvalue, so we can sort in place
Foo Foo::sorted() &&
{
sort(data.begin(), data.end());
return *this;
}
// this object is either const or it is an lvalue; either way we can't sort in place
Foo Foo::sorted() const & {
Foo ret(*this); // make a copy
sort(ret.data.begin(), ret.data.end()); // sort the copy
return ret; // return the copy
}
¶
当我们对一个右值执行sorted时,它可以安全地直接对data成员进行排序。
对象是一个右值,意味着没有其他用户,因此我们可以改变对象。
当对一个const右值或一个左值执行sorted时,我们不能改变对象,
因此就需要在排序前拷贝data。
编译器会根据调用sorted的对象的左值/右值属性来确定使用哪个sorted版本:
retVal().sorted(); // retVal() is an rvalue, calls Foo::sorted() &&
retFoo().sorted(); // retFoo() is an lvalue, calls Foo::sorted() const &
¶
当我们定义const成员函数时,
- 可以定义两个版本, 唯一的差别是一个版本有const限定而另一个没有。
引用限定的函数则不一样。
- 如果我们定义两个或两个以上具有相同名字和相同参数列表的成员函数,
就必须对所有函数都加上引用限定符,或者所有都不加:
class Foo {
public:
Foo sorted() &&;
Foo sorted() const; // error: must have reference qualifier
// Comp is type alias for the function type (see § 6.7 (p. 249))
// that can be used to compare int values
using Comp = bool(const int&, const int&);
Foo sorted(Comp*); // ok: different parameter list
Foo sorted(Comp*) const; // ok: neither version is reference qualified
};
//本例中声明了一个没有参数的const版本的sorted,此声明是错误的。
//因为Foo类中还有一个无参的sorted版本,它有一个引用限定符,
//因此const版本也必须有引用限定符。
//另一方面,接受一个比较操作指针的sorted版本是没问题的,
//因为两个函数都没有引用限定符。
如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符
练习¶
为你的 StrBlob 添加一个右值引用版本的 push_back。
void push_back(string &&s) { data->push_back(std::move(s)); }
练习¶
如果 sorted 定义如下,会发生什么?
Foo Foo::sorted() const & {
Foo ret(*this);
return ret.sorted();
}
会产生递归并且最终溢出。
练习¶
如果 sorted定义如下,会发生什么:
Foo Foo::sorted() const & { return Foo(*this).sorted(); }
没问题。会调用移动版本。
练习¶
编写新版本的 Foo 类,其 sorted 函数中有打印语句,
测试这个类,来验证你对前两题的答案是否正确。
#include <vector>
#include <iostream>
#include <algorithm>
using std::vector; using std::sort;
class Foo {
public:
Foo sorted() &&;
Foo sorted() const &;
private:
vector<int> data;
};
练习¶
Foo Foo::sorted() && {
sort(data.begin(), data.end());
std::cout << "&&" << std::endl; // debug
return *this;
}
Foo Foo::sorted() const & {
std::cout << "const &" << std::endl; // debug
return Foo(*this).sorted(); // Exercise 13.57
}
int main()
{
Foo().sorted(); // call "&&"
Foo f;
f.sorted(); // call "const &"
}
小结¶
类的五个成员函数定义了该类型对象拷贝、移动、赋值以及销毁时发生什么。
- 拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数。
移动构造函数和移动赋值运算符接受一个(通常是非const的)右值引用;
拷贝版本则接受一个(通常是const的)普通左值引用。
如果一个类未声明这些操作,编译器会自动为其生成。
如果这些操作未定义成删除的,它们会逐成员初始化、移动、赋值或销毁对象:
- 合成的操作依次处理每个非static数据成员,
根据成员类型确定如何移动、拷贝、赋值或销毁它。
分配了内存或其他资源的类几乎总是需要定义拷贝控制成员来管理分配的资源。
如果一个类需要析构函数,则它几乎肯定也需要定义
移动和拷贝构造函数及移动和拷贝赋值运算符。
实践课¶
- 从课程主页 cpp.njuer.org 打开 《面向对象编程基础》实验课实验课 十二 c++新特性 https://developer.aliyun.com/adc/scenario/14de95385d684af3acc3cbd2865b90ee
- 使用g++编译代码
- 编辑一个 readme.md 文档,键入本次实验心得.
- 使用git进行版本控制 可使用之前的gitee代码仓库
- 云服务器(elastic compute service,简称ecs) - aliyun linux 2是阿里云推出的 linux 发行版 - vim是从vi发展出来的一个文本编辑器. - g++ 是c++编译器
习题1
给定下面的类框架,编写它的:
1)拷贝构造函数,拷贝所有成员。你的构造函数应该动态分配一个新的string,
并将对象拷贝到ps所指向的位置,而不是拷贝ps本身:
2)赋值运算符。类似拷贝构造函数,你的赋值运算符应该将对象拷贝到ps指向的位置。
3)一个析构函数。
4)移动赋值运算符
5)移动构造函数
并编写main函数并观察何时会调用这些函数。
class HasPtr {
public:
HasPtr(const std::string& s = std::string()):
ps(new std::string(s)), i(0) { }
private:
std::string *ps;
int i;
}
编辑c++代码和markdown文档,使用git进行版本控制
yum install -y git gcc-c++
使用git工具进行版本控制
git clone你之前的网络git仓库test(或其它名字)
cd test 进入文件夹test
(clone的仓库,可移动旧文件到目录weekN: mkdir -p weekN ; mv 文件名 weekN;)
vim test1.cpp
g++ ./test1.cpp 编译
./a.out 执行程序
git add . 加入当前文件夹下所有文件到暂存区
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
vim readme.md 键入新内容(实验感想),按ESC 再按:wq退出
git add .
git commit –m "weekN" 表示提交到本地,备注weekN
git push 到你的git仓库
git log --oneline --graph 可看git记录
键入命令并截图或复制文字,并提交到群作业.
cat test* readme.md
提交¶
- 截图或复制文字,提交到群作业.
- 填写阿里云平台(本实验)的网页实验报告栏,发布保存.本次报告不需要分享提交
- 填写问卷调查 https://rnk6jc.aliwork.com/o/cppinfo
关于使用tmux¶
sudo yum install -y tmux
cd ~ && wget https://cpp.njuer.org/tmux && mv tmux .tmux.conf
tmux 进入会话 .
前缀按键prefix= ctrl+a,
prefix+c创建新面板,
prefix+"分屏,
prefix+k选上面,prefix+j选下面,
prefix+1选择第一,prefix+n选择第n,
prefix+d脱离会话
tmux attach-session -t 0 回到会话0
vim 共分为三种模式¶
- 命令模式
- 刚启动 vim,便进入了命令模式.其它模式下按ESC,可切换回命令模式
- i 切换到输入模式,以输入字符.
- x 删除当前光标所在处的字符.
- : 切换到底线命令模式,可输入命令.
- 输入模式
- 命令模式下按下i就进入了输入模式.
- ESC,退出输入模式,切换到命令模式
- 底线命令模式
- 命令模式下按下:(英文冒号)就进入了底线命令模式.
- wq 保存退出
vim 常用按键说明¶
除了 i, Esc, :wq 之外,其实 vim 还有非常多的按键可以使用.命令模式下:
- 光标移动
- j下 k上 h左 l右
- w前进一个词 b后退一个词
- Ctrl+d 向下半屏 ctrl+u 向上半屏
- G 移动到最后一行 gg 第一行 ngg 第n行
- 复制粘贴
- dd 删一行 ndd 删n行
- yy 复制一行 nyy复制n行
- p将复制的数据粘贴在下一行 P粘贴到上一行
- u恢复到前一个动作 ctrl+r重做上一个动作
- 搜索替换
- /word 向下找word ?word 向上找
- n重复搜索 N反向搜索
- :1,$s/word1/word2/g从第一行到最后一行寻找 word1 字符串,并将该字符串
取代为 word2
vim 常用按键说明¶
底线命令模式下:
- :set nu 显示行号
- :set nonu 取消行号
- :set paste 粘贴代码不乱序
【注:把caps lock按键映射为ctrl,能提高编辑效率.】
Markdown 文档语法¶
# 一级标题
## 二级标题
*斜体* **粗体**
- 列表项
- 子列表项
> 引用
[超链接](http://asdf.com)
![图片名](http://asdf.com/a.jpg)
|表格标题1|表格标题2|
|-|-|
|内容1|内容2|