“C++ Primer”


拷贝构造函数

定义了当用同类型的另一个对象初始化本对象时做什么

只要是第一个参数是此类对象的引用并且所有其它参数都是默认值就是拷贝构造函数。其第一个参数必须是引用类型,并且几乎总是const引用类型,尽管也可以使用非const引用。拷贝构造函数将隐式用于不少场景下,因此,拷贝构造函数不应该是explicit的。

当初始化标准化容器或是调用其insertpush成员时,容器会对其元素进行拷贝初始化,相反,使用emplace成员创建的元素都进行直接初始化

拷贝赋值运算符

定义了将一个对象赋予同类型的另一个对象时做什么

重载运算符本质上是函数,如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数

析构函数

析构函数的名字是~ 后接类名,其没有返回值,没有参数。由于其没有参数,所以不能被重载,一个类只有一个析构函数。

析构函数分为两部份:函数体和析构部分。在构造函数中,成员先于函数体被初始化。在析构函数中,函数体先执行,然后成员按照在类定义中出现的相反顺序进行析构。

why: 如果一个类需要析构函数,那么几乎可以肯定需要拷贝构造函数和拷贝赋值操作符 reason: 如果类 V 里面有指针数据,则默认的拷贝赋值是浅拷贝,两个变量 v1、v2 里的指针指向同一个地方,析构的时候会释放两遍,或者 v1 析构了,v2 里的引用的指针就非法了

需要拷贝构造函数的通常意味着需要拷贝赋值操作符,反之一样

调用析构函数的时机

default

可以通过将拷贝控制成员定义为=default来显式的要求编译器生成合成的版本

阻止拷贝

删除的函数:虽然定义了它,但是不能以任何方式使用他们,通过在参数列表后添加=delete表示

=delete 只能放在类定义内的成员函数声明处,不能放在定义处。原因在于,调用成员函数通常需要知道成员函数的声明。而类外的定义处则是生成函数代码的地方。

所有成员函数都可以被定义为被删除的函数,除了析构函数

如果一个类的成员不能被拷贝、赋值或销毁时,其对应的拷贝控制成员将被合成为被删除的。

交换操作

定义swap函数对于希望改变元素的顺序的算法来说特别重要。这种算法在需要交换元素的顺序时调用swap函数。如果一个类定义了自己的swap函数,算法将使用类定义的版本。否则,将使用库中定义的版本。

定义swap函数并不是必须的,然而定义swap是对分配了资源的类的重大优化。

对象移动

库容器、stringshared_ptr 支持拷贝和移动,IOunique_ptr 则只能移动不能拷贝。

右值引用

必须绑定到右值的引用,右值引用使用 && 符号,右值引用只能绑定到即将销毁的对象上,因而,可以自由的移动右值引用对象中的资源。

一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。不能将左值引用(lvalue reference)绑定到需要转型的值、字面量或者返回右值的表达式上。右值引用则刚好相反:可以将右值引用绑定到以上的值,但不能直接将右值引用绑定到左值。如:

1
2
3
4
5
6
int i = 42;
int &r = i;
int &&rr = i; //错误:不能将右值引用绑定到左值上
int &r2 = i * 42; //错误:不能将左值引用绑定到右值上
const int &r3 = i * 42; //可以将 const 左值引用绑定到任何类型的值上(const/非 const 的左/右值)
int &&rr2 = i * 42; //将右值引用绑定到右值上

左值持久;右值短暂

这两个特性意味着:使用右值引用的代码可以自由的移动右值引用所绑定对象的资源

一个变量就是一个左值;不能直接将右值引用绑定到一个变量上,即使这个变量被定义为右值引用类型也不可以。因而,不能将右值引用绑定到一个定义为右值引用的变量上

move 函数

可以显式将左值强转为对应的右值引用类型,也可以通过调用 move 库函数来获取绑定到左值的右值引用,其被定义在 utility 头文件中。如:

1
int &&rr3 = std::move(rr1);

move 告知编译器: 我们有一个左值,但我们希望像一个右值一样处理它,意味着:除了对rr1赋值或销毁它以外,我们将不再使用它。

使用move的代码应该使用std::move,而不是move,这样做可以避免潜在的名字冲突。

移动构造函数和移动赋值运算符

1
2
3
4
5
StrVec::StrVec(StrVec &&s) noexcept : // 移动操作不应抛出任何异常
    elements(s.elements), first_free(s.first_free), cap(s.cap)
{
    s.elements = s.first_free = s.cap = nullptr; // 确保移动后源对象不再指向被移动的资源 
}

` noexcept:通过在函数参数列表后加上noexcept,在构造函数时noexcept出现在参数列表后到冒号之间,来告知编译器一个函数不会抛出异常,必须同时在类体内的声明处和定义处同时指定noexcept`。

移动迭代器

在新标准中,定义了移动迭代器(move iterator)适配器。移动迭代器通过改变迭代器的解引用操作来适配给定的迭代器。通常,迭代器解引用返回元素的左值引用,与其它迭代器不同,解引用移动迭代器返回右值引用。调用函数 make_move_iterator 将常规迭代器变成移动迭代器,移动迭代器的操作与原始迭代器操作基本一样,因而可以将移动迭代器传给 uninitialized_copy 函数。如:

1
uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()));

右值引用和成员函数

1
2
void push_back(const X&); //拷贝:绑定到任何类型的 X
void push_back(X&&); //移动:绑定到可修改的右值 X

有些成员函数是只允许左值调用的,右值是不能调用的,如:在新标准前可以给两个字符串拼接的结果赋值:s1 + s2 = "wow!"; ,在新标准中可以强制要求赋值操作符的左操作数是左值,通过在参数列表后放置引用修饰符(reference qualifier)可以指示this的左值/右值特性。如:

1
2
3
4
5
6
class Foo {
public:
    Foo& operator=(const Foo&) &;
};
Foo& Foo::operator=(const Foo& rhs) &
{ return *this; }

引用修饰符可以是&或者&&用于表示this指向左值或右值。与const修饰符一样,引用修饰符必须出现在非 static 成员函数的声明和定义处。被&修饰的函数只能被左值调用,被&&修饰的函数只能被右值调用。

一个函数既可以有const也可以有引用修饰符,在这种情况下,引用修饰符在const修复符的后面。如:

1
2
3
4
class Foo {
public:
    Foo someMem() const &;
};

引用限定符可以用来区分重载,且可以综合引用限定符和const来区分一个成员函数的重载版本

当定义具有相同名字和相同参数列表的成员函数时,必须同时提供引用修饰符或者都不提供引用修饰符,如果只在其中一些提供,而另外一些不提供就是编译错误。如:

1
2
Foo sorted() &&;
Foo sorted() const; //错误:必须提供引用修饰符