2.C++11 构造函数相关

1. 继承构造函数

派生类如果要使用基类的成员函数,可以通过using声明来完成。

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class Base {
 5 public:
 6     void func(double f) { cout << "Base: " << f << endl; }
 7 };
 8 
 9 class Derived: Base {
10 public:
11     using Base::func;
12     void func(int i) { cout << "Derived: " << i << endl; }
13 };
14 
15 int main()
16 {
17     Base b;
18     b.func(4.5);   // Base: 4.5
19     Derived d; 
20     d.func(4.5);   // Base: 4.5
21     d.func(3);     // Derived: 3
22 }

如上代码,使用了using声明,声明派生类Derived也使用基类版本的函数func。在C++11中,这个想法被扩展到构造函数上。

class A {
public:
    A(int i) {}
    A(double d, int i) {}
    A(float f, int i, const char* c) {}
};

class B: A {
public:
    using A::A;  // 继承构造函数
    virtual void ExtraInterface() {}
    int d=3;     // 默认值
};

int main()
{
    //B b;                  // 编译报错,不会生成默认的构造函数 B()
    B b1(3);
    B b2(2.3, 3);
    B b3(2.3, 3, "abc");
}

这样使用using A::A 来声明把基类中的构造函数悉数继承到派生类B中。using继承有以下规则:

  • 继承构造函数只会初始化基类中的成员变量
  • 不能继承基类中的私有构造函数以及私有成员函数
  • 派生类如果是从基类中虚继承的,那么不能在派生类中声明继承构造函数
  • 一旦使用了继承构造函数,编译器就不会为派生类生成默认构造函数

2. 委派构造函数

如上例,我们在 Info(int) 和 Info(char)的初始化列表的位置调用了“基准版本”的构造函数Info() 。在初始化列表中调用“基准版本”的构造函数为委派构造函数,而被调用的“基准版本”则为目标构造函数。

所谓委派构造,就是指委派函数将构造的任务委派给了目标构造函数来完成的一种类构造方式。委派构造函数不能有初始化列表造成的,即构造函数不能同时“委派”和使用初始化列表,如果委派构造函数需要给成员变量赋初值,只能放在函数体中。

由于初始化列表的初始化方式总是先于构造函数完成的,这样上述初始化显得不够满意,可以改一下使得委派构造函数依然可以在初始化列表中初始化所有成员。

 2.1 委派构造函数的应用

应用1:使用构造模板函数产生目标构造函数。

#include <list>
#include <vector>
#include <deque>
using namespace std;

class TDConstructed {
    template<class T> TDConstructed(T first, T last) : l(first, last) {}
    list<int> l;

public:
    TDConstructed(vector<short> &v) : TDConstructed(v.begin(), v.end()) {}
    TDConstructed(deque<int> &d) : TDConstructed(d.begin(), d.end()) {}
};

如上例子,定义了一个构造函数模板。而通过两个委派构造函数的委托,构造函数模板会被实例化。这样TDConstructed类可以很容易的接受多种容器对其进行初始化。委托构造使得构造函数的泛型编程也成为了一种可能。

扫描二维码关注公众号,回复: 6061845 查看本文章

应用2:异常处理,如果在委派构造函数中使用try的话,那么从目标构造函数产生的异常,都可以在委派构造函数中被捕捉到。

class DCExcept {
public:
    DCExcept(double d) try : DCExcept(1, d) {
        cout << "Run the body." << endl;
        // 其他初始化
    }
    catch (...) {
        cout << "caught exception." << endl;
    }

private:
    DCExcept(int i, int d) {
        cout << "going to throw!" << endl;
        throw 0;
    }

    int type;
    double data;
};

int main()
{
    DCExcept a(1.2);
}

上述代码中,目标构造函数DCExcept(int i, int d)抛出了一个异常,并在委派构造函数DCExcept(double d)进行捕捉。

 3. 右值引用

一个左值表达式代表的是对象本身,而右值表达式代表的是对象的值;变量也是左值。可以这么理解:对于一个表达式,凡是对其取地址(&)操作可以成功的都是左值,否则就是右值。

为了支持移动操作(包括移动构造函数和移动赋值函数),C++才引入了一种新的引用类型——右值引用。右值引用的形式为: 类型 && a= 被引用的对象 ,右值引用只能绑定到右值

3.1 C++拷贝构造函数问题

拷贝构造函数中为指针成员分配新的内存再进行内容拷贝的做法在C++编程中几乎被视为不可违背的。不过这样能带来一些问题。

从运行结果来看,这个示例中调用了两次拷贝构造函数构造了两个临时对象,一次是在instance函数返回时,一次在对t的初始化。其实这两个临时对象并没有什么意义,构造完了马上就析构了。如果拷贝的内存非常大,就影响了效率,为了解决这个问题,C++11引用了右值引用这个类型。

3.2 移动构造函数

有没有可能将 在工厂函数当中所构造对象的成员变量(m_p)所指向的那块内存“偷”过来,而不是重新开辟一块内存将之前的内容复制过来呢? 这就是移动构造函数设计的思想。所谓移动构造函数,大家从名字上应该可以猜到:它应该就是一种构造函数,只不过它接受的参数是一个本类对象的右值引用,对于本例,移动构造函数的定义如下:
Test(Test &&t): m_p(t.m_p)
{
    cout << "move constructor" << endl;
    t.m_p = nullptr;
}

 

3.3 std::move

现在我们来看另外一种场景,在下面的情况下,我们知道在Test t2(t1)处会调用拷贝构造函数(t1是左值,因此不会调用移动构造函数),那么有没有一种办法在此处也调用移动构造函数而不是拷贝构造函数呢?

int main() 
{
    Test t1;
    // ......
    Test t2(t1);// ......
}
答案是肯定的,C++11标准中给我们提供了 std::move来解决这个问题,如下,只需将Test t2(t1)换成下面的语句即可: Test t2(std::move(t1))
这个 std::move的作用就是将左值转换为右值,以便调用移动构造函数。这里有一点特别需要注意的是, 在Test t2(std::move(t1))语句后,不能再对t1进行操作了,因为在移动构造函数中,已经将t1的成员变量m_p置为nullptr了。

猜你喜欢

转载自www.cnblogs.com/anlyse/p/10724827.html