Skip to content

第三章-通用为本专用为末

回到目录

3.1 继承构造函数

class Base {
public:
    Base() { std::cout << "Base()" << std::endl; }
    Base(int a) { std::cout << "Base(int a)" << std::endl; }
    Base(int a, int b) { std::cout << "Base(int a, int b)" << std::endl; }
};

class ImpBase : public Base{
public:
    using Base::Base;
    ImpBase (int a) {
        std::cout << "ImpBase (int a)" << std::endl; 
    }
};

int main (int argc, char *argv[]) {
    ImpBase impBase;
    std::cout << "--------" << std::endl; 
    ImpBase impBase2(10);
    std::cout << "--------" << std::endl; 
    ImpBase impBase3(10, 20);

    return 0;
}

out

    Base()
    --------
    Base()
    ImpBase (int a)
    --------
    Base(int a, int b)

参数的默认值是不会被继承的。事实上,默认值会导致基类产生多个构造函数的版本

3.2 委派构造函数

TODO,对象的初始化顺序,父类,子类,子对象,初始化列表,就地初始化,就是初始化成员类

TODO,委派构造函数

3.3 移动语义和完美转发

  • 3.3.2 移动语义

从浅拷贝存在的问题谈起

class Base {
public:
    Base() : data(new int(20)) { std::cout << "Base()" << std::endl; }
    Base(const Base &b) :data(new int(*b.data)) { std::cout << "Base(const Base &b)" << std::endl; }
    ~Base() { std::cout << "~Base()" << std::endl; }

    int *data;
};

Base get_base() {
    return Base();
}

int main (int argc, char *argv[]) {
    Base b = get_base();

    return 0;
}

/*
g++ test.cpp -fno-elide-construcrors
out Base()
    Base(const Base &b)
    ~Base()
    Base(const Base &b)
    ~Base()
    ~Base()
不加-fno-elide-construcrors这个开关会直接优化掉
out Base()
    ~Base()
*/

一旦我们用到的是个临时变量,那么移动构造语义就可以得到执行

  • 3.3.3 左值、右值与右值引用

一个最为典型的判别方法就是,在赋值表达式中,出现在等号左边的就是“左值”,而在等号右边的,则称为“右值”。不过C++中还有一个被广泛认同的说法,那就是可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。那么这个加法赋值表达式中,&a是允许的操作,但&(b +c)这样的操作则不会通过编译。因此a是一个左值,(b + c)是一个右值。

更为细致地,在C++11中,右值是由两个概念构成的,一个是将亡值(xvalue,eXpiring Value),另一个则是纯右值(prvalue,Pure Rvalue)其中纯右值就是C++98标准中右值的概念,讲的是用于辨识临时变量和一些不跟对象关联的值。而将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。

在常量左值引用在C++98标准中开始就是个“万能”的引用类型,就是我们常说的常引用。

判断一个类型是否是引用类型,标准库<type_traits>头文件中定义的三个模板类。is_rvalue_referenceis_lvalue_referenceis_reference。eg.cout << is_rvalue_reference<string &&> << endl;

  • 3.3.4 std::move强制转化为右值

在C++11中,标准库在中提供了一个有用的函数std::move,它唯一的功能是将一个左值强制转化为右值引用,继而我们可以通过右值引用使用该值,以用于移动语义。 我们需要转换成为右值引用的一般是一个确实生命期即将结束的对象。move之后的左值不会立即析构,还活的好好地。

事实上,为了保证移动语义的传递,程序员在编写移动构造函数的时候,应该总是记得使用std::move转换拥有形如堆内存、文件句柄等资源的成员为右值,这样一来,如果成员支持移动构造的话,就可以实现其移动语义。而即使成员没有移动构造函数,那么接受常量左值的构造函数版本也会轻松地实现拷贝构造,因此也不会引起大的问题。

  • 3.3.5 移动语义的一些其他问题

Moveable(const Moveable &&m)会使得的临时变量常量化,成为一个常量右值,那么临时变量的引用也就无法修改,从而导致无法实现移动语义。程序员在实现移动语义一定要注意排除不必要的const关键字。

只有移动语义构造的类型往往都是“资源型”的类型,比如说智能指针,文件流等,都可以视为“资源型”的类型。

在标准库的头文件里,我们还可以通过一些辅助的模板类来判断一个类型是否是可以移动的。比如is_move_constructible、is_trivially_move_constructible、is_nothrow_move_constructible,使用方法仍然是使用其成员value。比如:cout << is_move_constructible<Type>::value

移动语义可以实现高性能的置换函数,swap,如下例子,加入T可移动

template<class T>
void swap(T &a, T &b) {
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

尽量编写不抛出异常的移动构造函数,添加一个noexcept关键字,

move_if_noexcept的用法

编译器中被称为RVO/NRVO的优化(RVO, Return Value Optimization,返回值优化,或者NRVO,Named Return Valueoptimization)

  • 3.3.6 完美转发

所谓完美转发(perfect forwarding),是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

std::forward

TODO ???????

3.4 显式转换操作符

explicit修饰构造函数

3.5 列表初始化

  • 列表初始化

  • 示例

    int b[] {2, 4, 6};
    vector<int> c{1, 3, 5};
    map<int, float> d = {{1, 1.0f}, {3, 9.9f}};
    

  • 这样一来,自动变量和全局变量的初始化在C++11中被丰富了。程序员可以使用以下几种形式完成初始化的工作: 而后两种形式也可以用于获取堆内存new操作符中,

    • 等号“=”加上赋值表达式(assignment-expression),比如int a = 3 + 4。
    • 等号“=”加上花括号式的初始化列表,比如int a = {3 + 4}。
    • 圆括号式的表达式列表(expression-list),比如int a (3 + 4)。
    • 花括号式的初始化列表,比如int a {3 + 4}
  • 只要#include了头文件,并且声明一个以initializer_list模板类为参数的构造函数,同样可以使得自定义的类使用列表初始化,例如:

    #include <iostream>
    #include <vector>
    
    enum Gender {boy, girl};
    
    class Test{
    public:
        Test(std::initializer_list<std::pair<int, Gender>> t) {
            for (auto &i : t) {
                t_.push_back(i);
            }
        }
        std::vector<std::pair<int, Gender>> t_;
    };
    int main() {
        Test t({{1, boy}, {2, girl}});
        return 0;
    }
    

  • 防止类型收窄

列表初始化会检查是否发生类型收窄的,比如const int a = 10245;char b = {a};,会编译不过(但是不加const可以编译过去)

在C++11中,列表初始化是唯一一种可以防止类型收窄的初始化方式。

3.6 POD类型

POD是英文中Plain Old Data的缩写。POD可以使用memcpy进行复制,使用memset进行初始化。

C++11将POD划分为两个基本概念的合集,即:平凡的(trivial)和标准布局的(standardlayout)

  • 平凡的(trivial) 使用默认的构造函数,析构函数,复制构造函数,拷贝构造函数,移动构造函数,不能包含虚函数以及虚基类

  • 标准布局的(standardlayout)