编程语言
首页 > 编程语言> > 10C++11通用为本,专用为末

10C++11通用为本,专用为末

作者:互联网

1. 继承构造函数
struct A
{
    A(int i){}
    A(double d, int i){}
    A(float f, int i, const char* c){}
};

struct B : public A
{
	using A::A;   //继承构造函数
	virtual void extraInterface(){}
}

​ 这里我们通过 using A::A 的声明,把基类中的构造函数悉数集成到派生类 B 中。这样就不需要要再为派生类定义多个构造函数了。

2. 委派构造函数(主要用于多个重载构造函数中)
class Info
{
public:
    Info(){initRest()};
    Info(int i):type(i) {initRest();}
    Info()(char e):name(e) {initRest();}
    
private:
    void initRest(){}
    int type {1};
    char name {'a'};
}

​ 上例中,每个构造函数都需要调用 initRest 函数进行初始化。而现实编程中,构造函数中的代码还会更长,比如可能还需要一些基类的构造函数等。那能不能在一些构造函数中连 initRest 都不用调用呢?

​ 在 c++11 中,我们可以使用委派构造函数来达到预期的效果。

class Info
{
public:
    Info(){initRest()};
    Info(int i):Info() { type = i;}
    Info()(char e):Info() {name = e;};
    
 private:
    void initRest(){}
    int type {1};
    char name {'a'};
}

​ 上例中,我们在 Info(int) 和 Info(char) 的初始化列表的位置,调用了“基准版本”的构造函数 Info()。这里我们为了区分被调用者和调用者,称在初始化列表中调用“基准版本”的构造函数为 委派构造函数(delegating construnctor),而被调用的“基准版本”则为目标构造函数(target constructor)。

​ 在 C++11 中,所谓委派构造,就是指委派函数将构造的任务委派给了目标构造函数来完成这样一种类构造的方式。

​ 注意:在 C++ 中,构造函数不能同时 “委派”和使用初始化列表。

3. 移动语义

​ 拷贝构造函数中未指针成员分配新的内存再进行内容拷贝的做法在 c++ 编程中几乎被视为不可违背的。不过有些时候,我们确实不需要这样的拷贝构造语义,这时候就可以使用 C++ 提供的移动语义。

#include <iostream>
using namespace std;

class HasPtrMem
{
 public:
 	HasPtrMem() : d(new int (3)) 
    {
        cout << "Construct:" << ++ n_cstr << endl;
    }
    HasPtrMem(const HasPtrMem& h): d(new int(*h.d))
    {
        cout << "Copy construct:" << ++n_cptr <<endl;
    }
    //移动构造函数   
    HasPtrMem(HasPtrMem && h): d(h.d)
    {
        h.d = nullptr;			//将临时值的指针成员置空
         cout << "Move construct:" << ++n_mvtr <<endl;
    }
    ~HasPtrMem()
    {
        delete d;
        cout << "Destruct:" << ++n_dstr <<endl;
    }
    
    int * d;
    static int n_cstr;
    static int n_dstr;
    static int n_cptr;
    static int n_mvtr;
    
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;

HasPtrMem getTemp()
{
    HasPtrMem h;
    cout << "Resource from " << __func__ << ": " << hex << h.d << endl;
    return h;
}

int main()
{
    HasPtrMem a = getTemp();
    cout << "Resource from " << __func__ << ": " << hex << a.d << endl;
}

​ 上例中,HasPtrMem( HasPtrMem &&) 就是所谓的移动构造函数。与拷贝构造函数不同的是,移动构造函数接受一个所谓的 “右值引用” 的参数。可以看到,移动构造函数使用了参数 h 的成员 d 初始化了本对象的成员 d (而不是像拷贝构造函数一样需要分配内存,然后将内容依次拷贝到新分配的内存中),而 h 的成员 d 随后被置为指针空值 nullptr。这就完成了移动构造的全过程。

​ 这里所谓的 “偷” 堆内存,就是指将本对象 d 指向 h.d 所指的内存这一条语句,相应地,我们还将 h 的成员 d 置为指针空值。这其实也是我们 “偷” 内存时必须做的。这是因为在移动构造完成之后,临时对象会立即被析构。如果不改变 h.d (临时对象的指针成员)的话,则临时对象会析构掉本是我们 “偷” 来的堆内存。这样一来,本对象中的 d 指针也就成了一个悬挂指针,如果我们对指针进行解引用,就会发生严重的运行时错误。

​ 那么还有一个最为关键的问题没有解决,那就是移动构造函数何时会被触发。之前我们只是提到了临时对象,一旦我们用到的是个临时变量,那么移动构造函数就可以得到执行。

​ 那么,在 C++ 中如何判断产生了临时对象?如何将其用于移动构造函数?是否只有临时变量可以用于移动构造?

4. 左值、右值与右值引用
4.1 左值与右值

​ 左值、右值的最为典型的判断方法就是,在赋值表达式中,出现在等号左边的就是 “左值”,而在等号右边的,则称为 “右值”。

​ 另一个被广泛认同的说法,那就是可以取地址的、有名字的就是左值,反之,不能取地址的,没有名字的就是右值。

​ 在 c++11 中,右值是由两个概念构成的,一个是将亡值(xvalue),另一个则是纯右值(prvalue)。

​ 其中纯右值就是 c++98 标准中右值的概念。比如非引用返回的函数返回的临时变量值就是一个纯右值。一些运算表达式,比如 1+3 产生的临时变量值,也是纯右值。而不跟对象关联的字面量值,比如 2、'c'、true 也是纯右值。此外,类型转换函数的返回值、lambda 表达式等,也都是右值。

​ 而将亡值则是 c++11 新增的跟右值引用相关的表达式,这种表达式通常是将要被移动的对象(移为他用),比如返回右值引用 T&& 的函数返回值、std::move 的返回值,或者转换为 T&& 的类型转换函数的返回值。

​ 而剩余的,可以标识函数、对象的值都属于左值。 在 c++ 程序中,所有的值必属于左值、将亡值、纯右值三者之一。

4.2 右值引用

​ 在 c++11 中,右值引用就是对一个右值进行引用的类型。事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。通常情况下,我们只能是从右值表达式获得其引用。比如:

T && a = returnRvalue();
//这个表达式中,假设 returnRvalue 返回一个右值,我们就声明可一个名为 a 的右值引用,其值等于 returnRvalue 函数返回的临时变量的值。

​ 为了区别与 c++98 中的引用类型,我们称 c++98 中的引用为 “左值引用”。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是改对象的一个别名。最值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。

​ 在上面的例子中,returnRvalue(); 函数返回的右值在表达式语句结束后,其生命也就终结了,而通过右值又 “重获新生”,其生命期将与右值引用类型比变量 a 的生命期一样。只要 a 还 “活着”, 该右值临时量将会一直 “存活”下去。

T b = returnRvalue();

​ 相比以上语句的声明方式,我们刚才的右值引用变量声明,就会少一次对象的析构及一次对象的构造。因为 a 是右值引用,直接绑定了 ReturnRvalue() 返回的临时量。而 b 只是由临时值构造而成的,临时值在表达式结束后会析构,因此就会多一次析构和构造的开销。

4.3 常量左值引用

​ 相对的,在 c++98 标准中就已经出现的左值引用是否可以绑定到右值(由右值进行初始化)呢?例如:

T & e = returnRvalue();			//编译出错
const T & f = returnRvalue();	//通过编译
//出现这样的状况的原因是,常量左值引用就是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化
//而且在使用右值对其进行初始化的时候,常量左值引用还可以像右值引用一样将右值的生命期延长。
//不过相比于右值引用所引用的右值,常量左值所引用的右值在它的“余生”中只能是只读的。
//相对地,非常量左值只能接受非常量左值对其进行初始化。

​ 在 c++11 之前,左值、右值对程序员来说,一致呈透明状态。不知道什么是左值、右值,并不影响写出正确的 c++ 代码。引用的是左值和右值通常也并不重要。

​ 为了语义的完整, c++11 中还存在着常量右值引用,比如我们通过以下代码声明一个常量右值引用、

const T & crvalueref = returnRvalue();
//但是,一来右值引用主要就是为了移动语义,而移动语义需要右值是可以被修改的,那么常量右值引用在移动语义中就没有用武之处了。
//而来如果要引用右值且让常量右值引用不可以更改,常量左值引用往往就足够了。因此,目前我们还没有看到常量右值引用有何用处。
4.4 小结

image

​ 有时候,我们可能不知道一个类型是否是引用类型,以及是左值引用还是右值引用(这在模板中比较常见),标准库在 <type_traits> 头文件中提供了 3 个模板类: is_rvalue_reference、is_lvalue_reference、is_reference,可供我们进行判断。

cout << is_rvalue_reference<string &&>::value;

​ 我们通过模板类的成员 value 就可以打印出 string&& 是否是一个右值引用了。配合第四章中的类型推导操作符 decltype, 我们甚至可以对变量的类型进行判断。

标签:11,10,右值,左值,c++,为本,引用,Info,构造函数
来源: https://www.cnblogs.com/rock-cc/p/13336811.html