您的位置:首页 > 编程语言 > C语言/C++

C++ primer阅读心得(第十三章)

2008-02-07 15:43 351 查看
1.类的复制控制(copy controll)包括:

拷贝构造函数
拷贝赋值操作符
析构函数
移动构造函数
移动赋值操作符

这五个成员和构造函数一起控制着类对象的构造、复制、移动和回收。

2.拷贝构造函数:拷贝构造函数是构造函数的一种,它的参数是本类型的引用(通常为const);拷贝构造函数会在几种情况下被隐式的调用,所以通常不应该被声明为explicit的。

class Foo {
public:
Foo(const char *);
Foo(const Foo &); //拷贝构造函数
....
};
Foo a = "abc"; //如果拷贝构造函数声明为explicit的,这句就不能通过编译
如果类没有定义拷贝构造函数,编译器会提供一个合成拷贝构造函数,将对每个数据成员进行自动复制(内置类型复制值,类类型调用拷贝构造函数,数组进行整体复制)。所以,当类含有指针数据成员时,使用默认拷贝构造函数将复制指针地址,从而造成“浅拷贝”,可以通过值处理或者智能指针来避免。

3.直接初始化与拷贝初始化:直接初始化指的是编译器根据传入的参数类型,调用对应的构造函数直接初始化一个对象。而拷贝初始化指的是编译器首先根据对应的参数构造一个临时对象,然后再调用拷贝构造函数初始化目标对象。因此,explicit的构造函数会阻止拷贝初始化,因为你禁止了编译器隐式构造临时对象了^_^。

string dots(10, '.'); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
string s3 = "999999"; //拷贝初始化
string s4 = string(10, '9'); //拷贝初始化

string FuncA(string s) {
return s;
}

string s5 = FuncA(dots); //传入dots时拷贝初始化,返回时s5也是拷贝初始化
string sa[2] = {"abc", "def"}; //拷贝初始化
vector<string> va;
va.push_back(s); //拷贝初始化


直接初始化根据参数的不同调用各个构造函数初始化,通常使用()直接调用构造函数。拷贝初始化只使用拷贝构造函数,通常表现为使用"="号,除此之外还有:1.调用函数时值传递对象;2.函数返回值对象;3.列表初始化数组或聚合类;4.标准库容器的insert和push行为(相对的,emplace调用是直接初始化)

4.拷贝赋值操作符:拷贝赋值操作符属于操作符重载的范畴。因为赋值操作符修改了类对象本身,所以一般将重载的赋值操作符定义为类内部的成员函数。拷贝赋值操作符特指参数为本类型的引用(通常为const)的赋值操作符重载版本,返回调用它的类对象本身的引用。

class Foo {
public:
....
Foo &operator=(const &Foo); //拷贝赋值操作符
};
同样,当类没有定义一个拷贝赋值操作符时,编译器会提供一个合成拷贝赋值操作符,它的行为与合成拷贝构造函数类似,对非static数据成员进行逐一赋值。拷贝赋值操作符与拷贝构造函数的区别在于:在复制之外,还要处理掉旧值;所以要正确处理拷贝自身的情况(this和参数的地址相同则不处理)。

5.析构函数:析构函数以~标识,没有参数,没有返回值,负责释放对象所占用的系统资源。
class Foo {
public:
....
~Foo(){....} //析构函数
};

类对象的析构行为包含两步:1.调用类提供的析构函数;2.隐式逆序销毁各个非static的数据成员(注意:这一步骤不会释放指针数据成员所指向的内容,所以必须在第一步显示的delete)。当类没有定义析构函数时,编译器将提供一个合成析构函数完成上述步骤2。

6.移动(move)语义:在之前的c++当中,对象值传递时只能使用拷贝构造函数或者拷贝构造操作符来完成。当一个对象持有的内存非常庞大的时候,这种行为会造成极大的浪费。通常使用指针来避免这种浪费,但是又带来了复杂的指针管理。在c++11中,新增了移动语义,支持将一个对象持有的内存转移给另外一个对象,既避免了浪费,又避免了指针的复杂性。

7.左值与右值:左值与右值来自赋值运操作符“=”的两端,左值放在左侧,右值放在右侧。左值的含义是“地址”(典型的例子是变量),右值的含义是“值”(典型的例子是算数表达式)。所以左值既可以被赋值,又可以转化成右值(取出地址中保存的值);而右值只是一个值。左值的状态具有持久性(它是地址),而右值要么是字面常量,要么就是表达式生成的临时对象(所以它是值)。

8.右值引用:为了支持移动语义,c++11新增了右值引用,使用&&表示。右值引用只能绑定到右值,而通常的引用只能绑定到左值。所以这就决定了右值引用是短暂的(确保了后面不使用),其中的内容是可以被安全移动的。

int i = 42;
int &r = i; //ok,左值引用
int &&rr = i; //错误,右值引用无法引用左值
int &r2 = i * 42; //错误,左值引用无法引用右值
const int &rc = i * 42; //ok,const可以引用右值
int &&rr1 = i * 42; //ok,右值引用
int &&rr2 = i++; //ok
int &&rr3 = i > 42; //ok
auto f = ()[]->string{return "";}
string &&rr4 = f(); //ok
返回临时对象的函数,算数、关系、位运算以及后置++/--返回的都是右值,都可以被右值引用所引用;反过来,返回左值引用的函数,赋值、下标、解引用和前置++/--返回的都是左值,只能用于左值引用。我们可以使用std::move将一个内置类型或者具备了移动构造函数(自定义或者合成)的类类型的变量,从左值转化为右值。

9.移动构造函数:与拷贝构造函数类似,参数为本类型的右值引用(&&),因为操作了参数的内容,所以不能声明为const &&。它将参数对象的内容移动到自身,同时保障参数对象是可以被正常析构的。

class Foo {
public:
....
Foo(Foo &&) noexecpt; //移动构造函数
};


10.noexcept:c++11中新增了noexpect关键字,放在参数列表之后(在构造函数中需要放在初始化列表的:号之前),用来提醒编译器此函数不会抛出异常,不需要在编译时做额外的操作。

11.移动赋值操作符:与拷贝赋值操作符类似,也是定义在类内部的操作符重载,以右值引用(&&)作为参数。实现时要注意:1.移动自身的情况;2.要保障参数对象可以被正常析构
class Foo {
public:
....
Foo &operator=(Foo &&); //移动赋值操作符
};
12.编译器同样会在类没有定义移动操作的时候合成移动构造函数和移动赋值操作符,来移动数据成员。但是,与拷贝构造函数和拷贝赋值操作符不同,只有当类没有定义拷贝控制成员(析构、拷贝、拷贝赋值、移动、移动赋值),并且类内所有成员都可以移动时(比如说聚合类),编译器才会合成移动构造函数和移动复制操作符。

13.移动迭代器:为了让移动语义与标准库算法一起工作,c++11还提供了一个make_move_iterator函数,可以将一个普通迭代器转换为移动迭代器。使用移动迭代器赋值时,使用移动语义,即:将一个容器中的内容移动到另外一个容器中去。
vector<string> va = {"123","456","789"};
vector<string> vb;
vb.reserve(3);
std::copy(make_move_iterator(va.begin()), make_move_iterator(va.end()), vb.begin());
14.从“三位一体”到“五虎上将”:在之前的c++中,best practice是如果你定义了析构函数、拷贝构造函数和拷贝赋值操作符中的一个,那么你就需要定义另外的两个。在c++11中,拷贝控制的成员又增加了移动构造函数和移动赋值操作符。所以,best
practice变成,如果你定义了这五个当中的一个那么你最好把剩下的四个也定义了。
15.实质上,拷贝构造函数和移动构造函数是重载函数,拷贝赋值操作符和移动赋值操作符也是重载函数,编译器会根据传入的参数是左值还是右值来正确的匹配到对应的构造函数或者赋值操作符完成对象的复制。

16.显式要求编译器生成合成版本的拷贝控制成员:可以使用c++11新增的"=default"来显式的要求编译器生成合成版本的拷贝控制成员。

<pre name="code" class="cpp">class Foo {
public:
~Foo() = default; //合成析构函数
Foo(const Foo &) = default; //合成拷贝构造函数
Foo &operator=(const Foo &) = default<span style="font-family: Arial, Helvetica, sans-serif;">; //合成拷贝赋值操作符</span>
Foo(Foo &&) = default; //合成移动构造函数
Foo &operator=(Foo &&) = default; //合成移动赋值操作符
};



注意:显式要求合成有可能恰得其反,如果一个类不能够合成要求类型的拷贝控制成员时,这个合成成员函数就变成删除函数了。例如:如果一个类的数据成员并非全部可以移动,那么"=default"会把移动构造函数和移动赋值操作符变成删除函数;如果一个类的析构函数是删除函数或者private的,那么"=default"会把拷贝构造函数和拷贝赋值操作符变成删除函数。

17.禁止拷贝与删除函数:在之前版本的c++中,如果需要禁止拷贝行为,可以通过把拷贝构造函数、拷贝赋值操作符或者析构函数声明为private的,并且只声明不定义。不过,在c++11中提供了更加方便的方法来达到这一目标,那就是在它们后面添加"=delete"来标记成删除函数。删除函数只有声明,不能被使用。

class Foo {
public:
~Foo() = delete; //删除析构函数
Foo(const Foo &) = delete; //删除拷贝构造函数
Foo &operator=(const Foo &) = delete; //删除拷贝赋值操作符
Foo(Foo &&) = delete; //删除移动构造函数
Foo &operator=(Foo &&) = delete; //删除移动赋值操作符
};
注意:如果一个类的析构函数是删除函数,那么你就不能delete它的指针。

18.引用限定符:c++11新增了引用限定符,可以在成员函数的参数列表后面,加上&表示只能由左值引用调用,加上&&表示只能由右值引用调用。当这个成员函数同时又是const的时候,&和&&要放在const之后。

class Foo {
public:
Foo sorted() const &;
Foo sorted() &&;
};
Foo i;
i.sorted(); //调用左值引用版本
Foo &&j = std::move(i);
j.sorted(); //调用右值引用版本
引用限定符可以计入函数重载,但是要注意的是如果一个成员函数使用了引用限定符,那么具有相同参数列表的重载版本都必须加上引用限定符。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息