1. 继承构造函数

C++98中,如果派生类需要使用基类的构造函数,需要在构造函数中显式声明:

  1. struct A{A(int i){}};
  2. struct B:A{
  3. B(int i):A(i),d(i){};
  4. int d;
  5. }

:::info 但是,倘若基类中有大量构造函数,而派生类只有一些成员函数,其构造就等同于构造基类,但我们还需要写很多“透传”的构造函数 ::: :::warning 在C++中,如果派生类想要使用基类的函数的话,可以使用using声明来完成,在C++11中,对于构造函数也可这样做 :::

  1. struct A{
  2. A(int i){}
  3. A(double d,int i){}
  4. A(float f,int i, const char* c){}
  5. };
  6. struct B:A{
  7. using A::A;
  8. //...
  9. virtual void ExtraInterface(){};
  10. }

通过使用using的方式,基类的构造函数被继承到B中,这样就不需要透传构造函数了。
此外,继承构造函数并不会继承参数的默认值。

2. 委派构造函数

和继承构造函数类似,委派构造函数也是C++11中的构造函数的一种改进,为了减少构造函数书写的时间。具体来讲继承构造函数可能是针对基类衍生出的派生类的,而委派构造函数时针对同一个类中的多种构造函数的
具体思想是提供一个构造函数作为基准版本,而其他构造函数通过基准版本来进行初始化。 :::warning C++11中委派构造函数是在构造函数的初始化列表进行构造、委派的 :::

  1. class Info{
  2. public:
  3. Info() {InitRest();}
  4. Info(int i):Info(){type =i;}
  5. Info(char e): Info(){ name=e;}
  6. private:
  7. void InitRest(){};
  8. int type{1};
  9. char name{'a'};
  10. }

值得注意的是委派构造函数中成员变量赋初值只能在函数体中进行,因为委派构造函数不能有初始化列表,即构造函数不能同时委派和使用初始化构造列表。 :::info 委派构造函数的一个实际的应用就是用构造模板函数产生目标构造函数 :::

  1. #include<list>
  2. #include<vector>
  3. #include<deque>
  4. using namespace std;
  5. class TDConstructed{
  6. template<class T> TDConstructed(T first, T last):
  7. l(first,last){}
  8. list<int> l;
  9. public:
  10. TDConstructed(vector<short>& v):
  11. TDConstructed(v.begin(),v.end()){}
  12. TDConstructed(deque<int>& d):
  13. TDConstructed(d.begin(),d.end()){}
  14. };

如上述代码,TDConstructed类可以很容易地接受多种容器进行初始化。

3.右值引用

拷贝构造和移动构造

如果类中包含一个指针成员(或者说类中包含一个堆上的成员的话),就需要小心拷贝构造函数的编写,因为可能发生内存泄漏的情况。(即存在深拷贝和浅拷贝问题)
但是拷贝构造函数在拷贝的过程中会给指针成员分配新的内存空间,然后在进行拷贝,有时候我们并不需要多次分配新的内存。

  1. #include<iostream>
  2. using namespace std;
  3. class HasPtrMem{
  4. public:
  5. HasPtrMem():d(new int(0)){
  6. cout<<"Construct:"<<++n_cstr<<endl;
  7. }
  8. HasPtrMem(const HasPtrMem& h):d(new int(*h.d)){
  9. cout<<"Copy construct:"<< ++n_cptr<<endl;
  10. }
  11. ~HasPtrMem(){
  12. cout<<"Destruct:"<<++n_dstr<<endl;
  13. }
  14. int *d;
  15. static int n_cstr;
  16. static int n_dstr;
  17. static int n_cptr;
  18. };
  19. int HasPtrMem::n_cstr=0;
  20. int HasPtrMem::n_dstr=0;
  21. int HasPtrMem::n_cptr=0;
  22. HasPtrMem GetTemp(){
  23. return HasPtrMem();
  24. }
  25. int main(){
  26. HasPtrMem a=GetTemp();
  27. return 0;
  28. }

g++ test.cpp -fno-elide-constructors
注意:指定这个参数(-fno-elide-constructors)是为了关闭编译器的优化,强制 g++ 在所有情况下都会调用拷贝构造函数。

  1. Construct:1 //GetTemp内构造了一个匿名对象
  2. Copy construct:1 //匿名对象拷贝给一个临时对象,该临时对象是函数的返回值
  3. Destruct:1 //匿名对象析构
  4. Copy construct:2 //函数返回值(临时对象)拷贝给a
  5. Destruct:2 //临时对象析构
  6. Destruct:3 //a析构

fig0301.svg
可以看出有两次拷贝构造函数的调用,如果成员包含很大的数据的话,这会耗时很多,因此使用移动构造函数。
对于拷贝构造而言,内存的变化如下
fig0301.svg
对于移动构造而言,内存的变化如下:
fig0301.svg
在C++11中,这样”偷走“临时变量中的资源的构造函数就被称为”移动构造函数“,这样”偷“的行为则被称作移动语义。

  1. HasPtrMem::HasPtrMem(HasPtrMem &&h):d(h.d){
  2. h.d=nullptr;
  3. cout<<"Move construct:"<<++n_mvtr<<endl;
  4. }
  5. /*
  6. Construct: 1
  7. Move construct: 1
  8. Destruct: 1
  9. Move construct: 2
  10. Destruct: 2
  11. Destruct: 3
  12. */

移动构造函数接受的右值引用的参数,所谓的偷取内存,就是指将本对象d指向h.d所指的内存这一条语句,同时要将h的成员d置为空指针。
何时触发移动构造函数?遇到临时变量时

左值、右值、右值引用

所谓左值和右值的概念是源自C,在C++中还有一个广泛认可的说法,可以取地址的、有名字的就是左值,反之不能去地址的、没有名字的就是右值。 :::info 更细致地,在C++11中右值是由两个概念组成:将亡值(xvalue,eXpiring Value)和纯右值(prvalue,pure Rvalue)。

  • 纯右值是C++98的概念,表示临时变量和一些不跟对象关联的值,例如非引用返回的函数的返回值、运算表达式、不跟对象关联的字面值(2,'c',true),类型转换函数的返回值、lambda表达式也是
  • 将亡值是C++11新增的与右值引用相关的表达式,通常是将要被移动的对象,例如,返回右值引用T&&的函数返回值、std::move的返回值 :::

    注意事项

  1. 右值引用实际上就是对于一个右值进行引用的类型,实际上由于 右值常常不具备名字,我们只能通过引用的方式找到他的存在。e.g., T&& a=ReturnRvalue()
  2. 此外,无论是声明一个左值还是右值,都是需要在声明的同时立刻进行初始化。
  3. 右值引用可以延长右值的生命周期,让其和右值引用类型的变量的存活时间一样。
  4. 通常情况下右值引用无法绑定一个左值,但是左值引用可以绑定右值对象,前提是左值引用是const修饰的,即常量左值引用,不允许修改引用对象。
  5. 为了语义的完整,C++11还允许常量右值引用,但是没有什么用。。。。
  6. 通常情况下如果需要移动语义,程序员需要自己定义移动构造函数,并且类似之前的拷贝构造函数,一旦声明了移动构造函数,C++便不会生成默认的copy 、move construct

    确定引用类型

  7. 有时我们不知道引用是啥类型,标准库在<type_traits>中提供了三个模板类:is_rvalue_reference is_lvalue_reference is_reference,可以用来判断。

  8. 此外,可以通过一些辅助的模板类来判断一个类型是否可以移动:is_move_constructible is_trivially_move_constructible is_nothrow_move_constructible

用法:is_move_constructible<UnknownType>::value

std::move 强制转换右值

在C++11标准库<utility>中提供了函数std::move,他的功能是将一个左值强制转换为一个右值引用。
基本上等价于:static_cast<T&&>(lvalue);
需要注意的是转化后,左值的生命周期并未随着转换而变化,即左值并不会立刻被析构。因此被转换后不应该再使用。

  1. #include <iostream>
  2. #include <utility>
  3. using namespace std;
  4. class HugeMem{
  5. public:
  6. HugeMem(int size):sz(size>0?size:1){
  7. c=new int [sz];
  8. }
  9. ~HugeMem(){ delete []c ;}
  10. HugeMem(HugeMem&& hm):sz(hm.sz),c(hm.c){
  11. hm.c=nullptr;
  12. }
  13. int *c;
  14. int sz;
  15. };
  16. class Moveable{
  17. public:
  18. Moveable():i(new int(3)),h(1024){}
  19. ~Moveable(){ delete i; }
  20. Moveable(Moveable&& m):i(m.i),h(move(m.h)){
  21. //m.h被强制转换为右值,可以用移动构造函数了
  22. //如果不改右值的话需要写个深拷贝版本的的HugeMem的拷贝构造
  23. //然后这里会调他的拷贝构造在新建一个然后赋值,开销巨大
  24. m.i=nullptr;
  25. }
  26. int * i;
  27. HugeMem h;
  28. };
  29. Moveable GetTemp(){
  30. Moveable temp=Moveable();
  31. cout<<hex<<"Huge Mem from "<<__func__<<
  32. " @ "<<temp.h.c<<endl;
  33. return temp;
  34. }
  35. int main()
  36. {
  37. Moveable a = GetTemp();
  38. cout<<hex<<"Huge Mem from "<<__func__<<
  39. " @ "<<a.h.c<<endl;
  40. return 0;
  41. }
  1. Huge Mem from GetTemp @ 0x1e3ed0
  2. Huge Mem from main @ 0x1e3ed0 //发现是同一个内存空间的

需要注意的是,用move时应该是在将要转换的对象即将消亡的时候。
典型应用2:高性能swap函数

  1. template<class T>
  2. void swap(T& a,T& b){
  3. T tmp(move(a));
  4. a=move(b);
  5. b=move(tmp);
  6. }

抛出异常

对于移动语义,抛出异常是十分危险的。因此可以用std::move_if_noexcept模板函数来替代move
该函数在类的移动构造函数没有noexcept修饰时,返回左值引用,可以实现拷贝语义,反之返回右值引用。

完美转发

:::info 完美转发指的是,在函数模板中,完全依靠模板的参数类型,将参数传递给函数模板中调用的另一个函数,要求不产生额外的开销,就好像转发者不存在一样。 :::

  1. template<typename T>
  2. void IamForwording(T t){ IrunCodeActually(t); }

因为要求不需要额外开销,所以通常都是用引用来完成,左值引用和右值引用都应该支持。但是因为如之前所说,右值引用不能引用左值,因此存在问题。
C++11引入了一条所谓的“引用折叠(reference collapsing)”规则,并且结合新的模板推导规则来完成完美转发。所谓的引用折叠是指将复杂的未知表达式折叠成已知的简单表达式,其大致规则是一旦定义中出现了左值引用,引用折叠总是优先将其折叠为左值。

  1. typedef T& TR;
  2. TR& v=xx;
TR类型 声明v的类型 v的实际类型
T& TR A&
T& TR& A&
T& TR&& A&
T&& TR A&&
T&& TR& A&
T&& TR&& A&&

因此完美转发可以用如下形式:

  1. template<typename T>
  2. void IamForwording(T && t){ IrunCodeActually(static_cast<T&&>(t)); }

例如传入一个X类型的左值引用:

  1. void IamForwording(X& && t){ IrunCodeActually(static_cast<X& &&>(t)); }
  2. //根据折叠的规则:
  3. void IamForwording(X& t){ IrunCodeActually(static_cast<X&>(t)); }

如此左值和右值都没有问题了。

列表初始化

C++98中允许用花括号来对数组进行统一的初始化,C++11扩大了这一范围,这种初始化方法被称作列表初始化。

  1. int a=3+4;//=
  2. int a={3+4};//花括号+-
  3. int a(3+4);//圆括号
  4. int a{3+4};//花括号
  5. double* d=new double{1.2f};

此外还可以对自定义的类进行列表初始化,需要包含头文件。

  1. #include<vector>
  2. #include<string>
  3. using namespace std;
  4. enum Gender{M,F};
  5. class People{
  6. People(initializer_list<pair<string,Gender>> l){
  7. auto i=l.begin();
  8. for(;i!=l.end();i++){
  9. data.push_back(*i);
  10. }
  11. }
  12. private:
  13. vector<pair<string,Gender>> date;
  14. };
  15. People ship2012={{"Garfield",M},{"HelloKitty",F}};

此外还能对函数参数列表进行列表初始化

  1. #include<initializer_list>
  2. using namespace std;
  3. void Func(initializer_list<int> iv){};
  4. int main(){
  5. Func({1,2});
  6. Func({});
  7. }

模板的别名

C++中使用typedef定义类型的别名:typedef int myint
对于比较长的名字具有较大的优势:typedef std::vector<std::string> strvec;
C++11中定义别名不在是typedef的专属,using也可以达到同样甚至更好的效果

  1. template <typename T> using MapString =std::map<T,char*>;
  2. MapString <int> numberedString;