第3章 通用性【上】

1. 继承构造函数

一旦使用了继承构造函数,那么编译器就不会为派生类生成默认构造函数了。

struct A { A(int){} };
struct B: A { using A::A };
B b; // error , 没有默认无参构造函数

继承构造函数的出现是为了解决:基类有多个构造函数(不同参数)时,派生类仅仅要增加一个参数,却要“透传”拷贝多个基类构造函数,示例如下:

struct A { 
    A(int i) {}
    A(double d, int i) {}
    A(float f, int i, const char* c) {}
    // ...
};
struct B : A { 
    B(int i): A(i) {} 
    B(double d, int i) : A(d, i) {}
    B(float f, int i, const char* c) : A(f, i, c){}
    // ...
    virtual void ExtraInterface(){}
};

使用using 继承构造函数,则可以改造上诉代码

struct A { 
    A(int i) {}
    A(double d, int i) {}
    A(float f, int i, const char* c) {}
    // ...
};
struct B : A {
    using A::A;     // 继承构造函数
    // ...
    virtual void ExtraInterface(){}
};

C++11 标准继承构造函数被设计为跟派生类中的默认函数(默认构造,析构,拷贝构造等)一样,是隐式声明的。这意味着如果一个继承构造函数不被相关代码使用,编译器不会为其产生真正的函数代码。

不过继承构造只会初始化基类的成员变量,对于派生类中的成员变量则无能为力。

struct A {
    A(int i) {}
    A(double d, int i) {}
    A(float f, int i, const char* c) {}
    // ...
};
struct B : A { 
    using A::A;
    int d {0};
};
int main() {
    B b(356);   // b.d被初始化为0
}

有时,基类构造函数参数会有默认值,对于继承构造函数来讲,参数的默认值是不会被继承的。事实上,默认值会导致基类产生多个构造函数的版本,这些函数版本都会被派生类继承。

struct A {
    A(int a = 3, double = 2.4) {}
};
struct B : A {
    using A::A;
};

事实上,A会产生如下构造函数:

  • A(int a = 3, double = 2.4)
  • A(int a = 3)
  • A(const A&) 默认的复制构造
  • A()

相应的,B中的构造函数也会包括如下:

  • B(int , double) 继承构造
  • B(int) 继承构造
  • B(const B&) 复制构造,不是继承来的
  • B() 默认构造

2. 委派构造函数

委派构造函数:委派函数将构造的任务给了目标构造函数来完成类的构造的方式。
解决的问题:有多个参数表不同但是逻辑相近(或者有公共部分)的构造函数的时候,一个逻辑写好几遍造成代码重复
注意:委派构造函数不能有初始化列表,只能将成员函数放在结构体内赋值。不能既委派又初始化列表同时使用。
代码冗余的示例:

class Info{
public:
  Info():type(1),name('a'){InitRest();}
  Info(int i):type(i),name('a'){InitRest();}
  Info(char e):type(1),name(e){InitRest();}
  private:
  void InitRest(){/*其他初始化*/}
  int type;
  char name;
  //...
};

优化后如下:

class Info{
public:
  Info():Info(1){}//委派构造函数
  Info(int i):Info(i,'a'){}//既是目标构造函数,也是委派构造函数
  Info(char e):Info(1,e){}
private:
  Info(int i,char e):type(i),name(e){/*其他初始化*/}//目标构造函数
  int type;
  char name;
  //...
};

需要注意,在链式委托构造函数中,不能出现委托环,如下是编译不过的

struct Rule2{
  int i,c;
  Rule2():Rule2(2){}
  Rule2(int i):Rule2('c'){}
  Rule2(char c):Rule2(2){}
};

3. 右值引用

3.1 指针成员与拷贝构造

浅拷贝示例:

class HasPtrMem{
public:
    HasPtrMem() : d(new int(0)){}
    ~HasPtrMem(){
        delete d;
        d = nullptr;
    }
    int* d;
};
int main(){
    HasPtrMem a;
    HasPtrMem b(a);
    cout << *a.d << endl; // 0
    cout << *b.d << endl; // 0
} // 异常析构

就是a.d和b.d都指向了同一块堆内存。因此在main作用域结束的时候,对象b和对象a的析构函数会分别依次被调用。当其中之一完成析构之后(比如对象b先析构,b.d先被delete),那么a.d就成了一个“悬挂指针”(dangling pointer),因为其不再指向有效的内存了。

class HasPtrMem{
public:
    HasPtrMem() : d(new int(0)){}
    HasPtrMem(const HasPtrMem& h) : d(new int(*h.d)) { } // 拷贝构造函数,从堆中分配内存,并用*h.d初始化
    ~HasPtrMem(){
        delete d;
        d = nullptr;
    }
    int* d;
};

为类HasPtrMem添加了一个拷贝构造函数。拷贝构造函数从堆中分配新内存,将该分配来的内存的指针交还给d,又使用*(h.d)对*d进行了初始化。通过这样的方法,就避免了悬挂指针的困扰。

3.2 移动语义

先说点背景知识,调用复制构造函数的三种情况:
  1.当用类一个对象去初始化另一个对象时。
  2.如果函数形参是类对象。
  3.如果函数返回值是类对象,函数执行完成返回调用时。
拷贝构造函数又称复制构造函数。

拷贝赋值:将一个类对象一模一样的复制给另一个类对象。注意此时发生在对象到对象之间,两个对象已经创建。
拷贝构造:发生在对象构造期间,即是在对象创建时发生的动作。

String s1(“hello”);
String s2;
s2 = s1;//调用拷贝赋值
#include <iostream>
using namespace std;

class HasPtrMem{
  public:

  HasPtrMem():d(new int(0)){
    cout<<"Construct:  "<<++n_cstr<<endl;
  }

  HasPtrMem(const HasPtrMem &h):d(new int(*h.d)){
    cout<<"Copy construct:  "<<++n_cptr<<endl;
  }

  ~HasPtrMem(){
    cout<<"Destruct:  "<<++n_dstr<<endl;
  }

  int*d;
  static int n_cstr;
  static int n_dstr;
  static int n_cptr;

};
int HasPtrMem::n_cstr=0;
int HasPtrMem::n_dstr=0;
int HasPtrMem::n_cptr=0;
HasPtrMem GetTemp(){
	cout<<"Enter GetTemp"<<endl;
    return HasPtrMem();
}
int main(){
  HasPtrMem a = GetTemp();
}

如果默认开启了返回值优化(ROV - Return Value Optimization)则返回

Construct:  1
Destruct:  1

去掉返回值优化,需要编译的时候加上选项 -fno-elide-constructors

Enter GetTemp
Construct:  1			// HasPtrMem() 调用构造函数生成对象
Copy construct:  1		// 调用拷贝构造函数 将1步生成的对象拷贝生成临时对象
Destruct:  1			// 析构第一步生成的对象
Copy construct:  2		// 将拷贝生成的临时对象 拷贝到main函数中局部对象
Destruct:  2			// 析构第二步生成的对象
Destruct:  3			// 析构main函数中对象

如果构造一个移动构造函数,则上诉代码会调用移动构造函数替代 拷贝构造函数

HasPtrMem(HasPtrMem&&h):d(h.d){//移动构造函数
    h.d=nullptr;//将临时值的指针成员置空
    cout<<"Move construct:"<<++n_mvtr<<endl;
}

拷贝构造和移动构造
在C++11中,这样的“偷走”临时变量中资源的构造函数,就被称为“移动构造函数”。而这样的“偷”的行为,则称之为“移动语义”(move semantics)。当然,换成白话的中文,可以理解为“移为己用

3.3 左值、右值、左值引用和右值引用

左值是:locator value(可寻址的数据)

++x;
y*=33;

右值是:read value(不可寻址的数据或用来读的数据)

x++;
y+3;
123//y+3这个值会被计算,但是没有被承接到,
//后来即便我们再用y+3去获得这个临时的值,大小是一样的,
//但不是我上次计算的那个值,我又经过了以此计算的到的。

//x++;这个从底层去分析:
//x++会产生一个临时变量,用来保存x+1的值,
//等到语句结束,将x+1赋值给x.
//但是语句没结束时,这个临时变量时在寄存器中保存的,一个计算结果的临时变量,
//此时是不可寻址的!!即右值。

什么是引用?

引用表示为符号“&”;
引用就是用另外的名称来索引到该变量。

左值引用

左值引用得到的就是还是一个左值。

// 以下几个是对上面左值的左值引用
int& ra = a;
int*& rp = p;
int& r = *p;
const int& rb = b;

右值引用

右值引用操作符为 “&&”;
右值引用得到的是一个左值。
右值引用通常将一个临时变量拿过来用。
右值引用最主要的功能是解决的是自定义类重复构造冗余的问题。

右值引用就是把右值变成左值,通常实在C++返回值上,对于自定子类的重复拷贝做了重要改善,大大提高了C++的效率。右值引用的概念是C++中的重要概念!!!!

// 以下几个是对上面右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);

对比总结
左值引用总结

  1. 左值引用智能引用左值,不能引用右值
  2. const左值引用既可以引用左值,又可以引用右值
// 1.左值引用只能引用左值
int t = 8;
int& rt1 = t;
//int& rt2 = 8;  // 编译报错,因为10是右值,不能直接引用右值

// 2.但是const左值引用既可以引用左值
const int& rt3 = t;
const int& rt4 = 8;  // 也可以引用右值
const double& r1 = x + y;
const double& r2 = fmin(x, y);

问:为什么const左值引用也可以引用右值?
答:在 C++11标准产生之前,是没有右值引用这个概念的,当时如果想要一个类型既能接收左值也能接收右值的话,需要用const左值引用,比如标准容器的 push_back 接口:void push_back (const T& val)。
也就是说,如果const左值引用不能引用右值的话,有些接口就不好支持了。

下面就是 C++98标准中相关接口const左值引用引用右值的例子:

vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);

右值引用总结

  1. 右值引用智能引用右值,不能直接引用左值。
  2. 右值引用可以被 move左值

move,本文指std::move(C++11),作用是将一个左值强制转化为右值,以实现移动语义。
左值被 move 后变为右值,于是右值引用可以引用。

// 1.右值引用只能引用右值
int&& rr1 = 10;
double&& rr2 = x + y;
const double&& rr3 = x + y;
int t = 10;
//int&& rrt = t;  // 编译报错,不能直接引用左值

// 2.但是右值引用可以引用被move的左值
int&& rrt = std::move(t);
int*&& rr4 = std::move(p);
int&& rr5 = std::move(*p);
const int&& rr6 = std::move(b);

左值引用的使用场景和意义

// 1.左值引用做参数
void func1(string s){...}
void func2(const string& s){...}
int main(){
	string s1("Hello World!");
	func1(s1);  // 由于是传值传参且做的是深拷贝,代价较大
	func2(s1);  // 左值引用做参数减少了拷贝,提高了效率
	return 0;
}
// 2.左值引用做返回值(仅限于对象出了函数作用域以后还存在的情况)
string s2("hello");
// string operator+=(char ch)  传值返回存在拷贝且是深拷贝
// string& operator+=(char ch)  左值引用做返回值没有拷贝,提高了效率
s2 += '!';

意义:
传值传参和传值返回都会产生拷贝,有的甚至是深拷贝,代价很大。而左值引用的实际意义在于做参数和做返回值都可以减少拷贝,从而提高效率。
短板:
左值引用虽然较完美地解决了大部分问题,但对于有些问题仍然不能很好地解决。
当对象出了函数作用域以后仍然存在时,可以使用左值引用返回,这是没问题的。

string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}

但当对象(对象是函数内的局部对象)出了函数作用域以后不存在时,就不可以使用左值引用返回了。

string operator+(const string& s, char ch)
{
	string ret(s);
	ret.push_back(ch);
	return ret;
}
// 拿现在这个函数来举例:ret是函数内的局部对象,出了函数作用域后会被析构,即被销毁了
// 若此时再返回它的别名(左值引用),也就是再拿这个对象来用,就会出问题

右值引用的使用场景和意义
为了解决上述传值返回的拷贝问题,C++11标准就增加了右值引用和移动语义。移动语义:将一个对象中的资源移动到另一个对象(资源控制权的转移)。

  1. C++11标准的STL 容器的相关接口函数也增加了右值引用版本
    在这里插入图片描述
    在这里插入图片描述

3.4 std::move:强制转化为右值

功能:将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存搬迁或者内存拷贝。
特点:

  1. 可以非常简单的方式将一个左值强制转化为右值引用
  2. 避免不必要的拷贝操作,提高操作性能

引用移除remove_reference 具体实现:

//原始的,最通用的版本
template <typename T> struct remove_reference{
    typedef T type;  //定义 T 的类型别名为 type
};
//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }
 
template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }   
  
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a;             //使用原版本,
remove_refrence<decltype(i)>::type  b;             //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type  b;  //右值引用特例版本 

move函数原型:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type &&>(t);
}

std::move实现,首先,通过右值引用传递模板实现,利用引用折叠原理将右值经过T&&传递类型保持不变还是右值,而左值经过T&&变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。然后我们通过static_cast<>进行强制类型转换返回T&&右值引用,而static_cast之所以能使用类型转换,是通过remove_refrence::type模板移除T&&,T&的引用,获取具体类型T。关于引用折叠如下:

引用折叠
公式一)T& &、T&& &、T& &&都折叠成T&,用于处理左值
公式二)T&& &&折叠为T&&,用于处理右值

举例:

int var = 10;
转化过程:

  1. std::move(var) => std::move(int&& &) => 折叠后 std::move(int&)
  2. 此时:T 的类型为 int&,typename remove_reference::type 为 int,这里使用 remove_reference 的左值引用的特例化版本
  3. 通过 static_cast 将 int& 强制转换为 int&&
    整个std::move被实例化如下
    int&& move(int& t)
    {
    return static_cast<int&&>(t);
    }

3.5 完美转发

完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。C++11标准为 C++ 引入了右值引用和移动语义,因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)

函数原型

template <typename T>
T&& forward(typename std::remove_reference<T>::type& param) //重载接收左值
{
    return static_cast<T&&>(param); // 强制类型转换
}
template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param) //重载接收右值
{
    return static_cast<T&&>(param); // 强制类型转换
}

forward有两个重载模板函数,一个接收左值,一个接收右值;

可以看到,std::forward模板函数对传入的参数进行强制类型转换,转换的目标类型符合引用折叠规则,因此左值参数最终转换后仍为左值,右值参数最终转成右值。由于forward只用了std::remove_reference,所以,它不仅可以保持左值或者右值不变,同时还可以保持const、Lreference、Rreference、validate等属性不变;

#include <iostream>
using namespace std;
template<typename T>
void myprint(T& t) {
    cout <<"left"<< endl;
}
template<typename T>
void myprint(T&& t) {
    std::cout << "right" << std::endl;
}
template<typename T>
void wrapper(T&& v) {
    myprint(v);
    myprint(std::forward<T>(v));
    myprint(std::move(v));
}
int main(int argc, char * argv[]) {
    wrapper(1); //int&& 右值经过参数传递,被分配了内存
    std::cout << "--------------" << std::endl;
    int x = 1;
    wrapper(x); //int&
    return 0;
}

打印结果如下

left	// 传入值是右值,但是经过参数传递变成了左值(有内存分配)
right   // 使用了forward 则保留右值属性
right   // 使用了move,一定是右值
--------------
left	// x变量是左值
left	// 使用了forward 则保留左值属性
right 	// 使用了move,一定是右值
  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值