第三章 转向现代C++

七 在创建对象时注意区分()和{}

C++11中多了一种初始化的方式,就是通过{}来进行初始化,例如初始化一个int类型

int x(0);
int y = 0;
int z{0};
int z = {0};

使用大括号初始化容器非常方便:

vector<int> vec{1,2,3};

大括号同样可以用来初始化类内非静态成员变量,当然也可以用=初始化,但不可以用()

class A
{
private:
    int x{0};
    int y = 10;
    //int z(2); //不可以这样初始化
};

大括号是通用的,可以用在很多场合,所以尽量使用大括号进行初始化。

大括号的特性之一是禁止进行内建型别之间的向下类型转换,如果大括号内部的表达式无法保证能采用进行初始化的对象来表达,则代码不能通过编译。但是使用用()是可以初始化的。

double x = 1.0, y = 1.0, z = 1.0;
int sum1{x + y + z};
int sum2(x + y + z);    //double被向下转换为int

VS2017下无法通过编译:

1>error C2397: 从“double”转换到“int”需要收缩转换

小括号进行类对象的初始化时,如果没有参数传入是不能写括号的(写括号就变成声明一个函数了),但是此时可以写大括号进行初始化。

A a1(3);
A a2();
A a3{};
cout << typeid(a1).name() << endl;  //class A
cout << typeid(a2).name() << endl;  //class A __cdecl(void)
cout << typeid(a3).name() << endl;  //class A

输出已用注释方式写出。很明显这里用大括号进行初始化是正确无误的。

大括号如果初始化auto对象的话,auto的类型会被推导为std::initializer_list

std::initializer_list提供的操作如下:

扫描二维码关注公众号,回复: 2862679 查看本文章
initializer_list<T> lst; 
//默认初始化;T类型元素的空列表
initializer_list<T> lst{a,b,c...};//initializer_list对象中的元素是常量值,无法被更改    。
lst2(lst)   
lst2=lst  
//拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本元素共享
lst.size()  //列表中的元素数量
lst.begin()  //返回指向lst中首元素的指针
lst.end()   //返回指向lst中尾元素下一位置的指针
//也可以用迭代器来访问lst

类的构造函数可以声明具备initializer_list<T>型别的形参,那么采用大括号初始化语法的调用语句会优先调用这个重载版本。编译器只要有任何可能把一个采用了大括号初始化语法的调用语句解读为带有initializer_list<T>型别形参的构造函数,则编译器就是采用这种解释。

class A
{
public:
    A(int _x, bool _y) :x(_x), y(_y) { cout << "int and bool" << endl; }
    A(int _x, double _z) :x(_x), z(_z) { cout << "int and double" << endl; }
    //A(initializer_list<long double> ld) { cout << "initializer_list" << endl; }
private:
    int x{0};
    bool y = false;
    double z = 1.0;
};
int main()
{
    A a1(3, true);
    A a2{ 3, true };
    A a3(3, 1.5);
    A a4{ 3, 1.5 };

    return 0;
}

在没有initializer_list<T>型别形参的构造函数之前,输出如下:

int and bool
int and bool
int and double
int and double

将注释去掉,输出就变成了:

int and bool
initializer_list //发生了向上的类型转换
int and double
initializer_list //发生了向上的类型转换

只有找不到任何办法把大括号初始化物中的实参转换成initializer_list<T>模板中的型别的时候,编译器才会退而去检查普通的重载决议。

class A
{
public:
    A(int _x, bool _y) :x(_x), y(_y) { cout << "int and bool" << endl; }
    A(int _x, double _z) :x(_x), z(_z) { cout << "int and double" << endl; }
    //现在参数是string了
    A(initializer_list<string> ld) { cout << "initializer_list" << endl; }
private:
    int x{0};
    bool y = false;
    double z = 1.0;
};
int main()
{
    A a1(3, true);
    A a2{ 3, true };
    A a3(3, 1.5);
    A a4{ 3, 1.5 };

    return 0;
}

这时int,bool都无法转换为string类型,所以最终输出为:

int and bool
int and bool
int and double
int and double

空的大括号代表的意思是没有实参,而不是空的initializer_list,所以当一个类同时拥有默认构造函数和initializer_list<T>构造函数的时候,传入空的大括号A a{};调用的是默认构造函数。(不能写成A a();,这样是声明函数)。如果这时候想调用initializer_list<T>构造函数并且传入空的initializer_list的时候,则可以再大括号外面套一层小括号或者大括号:

A a({});    //调用initializer_list<T>构造函数并且传入空的initializer_list
A a{{}};    //同上

补充一个vector的对象定义,使用大括号和小括号定义会有区别:

vector<int> v(10, 20);  //创建一个包含10个20的vector
vector<int> v{10, 20};  //创建一个包含2个变量,分别为10,20的vector

如果想用任意数量的实参来创建一个任意型别的对象,那么最好使用可变参数模板。

可变参数模板

变长参数的模板声明:

template<typename... E> class tuple;

标识符E前面的省略号表示了该参数是变长的,C++11中,E被称作是一个模板参数包,有了这个参数包,就能接受任意多个参数作为模板参数。

同时模板参数包也可以不是模板类型:

template<int... A> 
class As{};
//定义对象
As<1, 0, 2> as;

模板参数包再模板推导的时候会被认为是模板的单个参数(虽然实际上是任意个数量的实参的集合),为了使用模板参数包要对其进行解包,C++11中通常使用一个名为包扩展的表达式完成。

包扩展就是把把类型后面加一个省略号,例子如下:

template<typename T1, typename T2>
class B 
{
public:
    B(int _x, int _y) : x(_x), y(_y) { cout << x << ' ' << y << endl; }
    virtual ~B() {}
private:
    int x;
    int y;
};
template<typename... T>
class A : private B<T...>   //<T...>是包扩展
{
public:
    A(int a = 0, int b = 1):B<T...>(a, b){ }
};

int main()
{
    A<int, int> a;

    return 0;
}

这里A将模板参数包解包并传递给私有基类B。

但是这里如果A后面跟着多个类型,就无法通过编译,因为这里的B仅仅要求两个类型,如何实现任意参数类型的模板参数包呢?可以使用递归+特化版本的方式。

template<typename... T>
class Mytuple{ };

template<typename H, typename... T>
class Mytuple<H, T...> : private Mytuple<T...>
{
public:
    Mytuple() { cout << typeid(head).name() << endl; }
private:
    H head;
};
template<>
class Mytuple<> {}; //特化版本,边界条件

int main()
{
    Mytuple<int, double, char, float> mt;

    return 0;
}

上述代码中,先是定义了一个只有一个模板参数包的模板类Mytuple,紧接着定义了偏特化版本,包含一个模板参数和一个模板参数包的模板类,这个类以template<typename... T> class Mytuple为私有基类。这样,当我们定义Mytuple<int, double, char, float> mt;时,将会引起递归构造,由于特化版本和最优匹配版本具有最高优先权,所以首先会匹配template<typename H, typename... T> class Mytuple,在这里会递归构造基类并传入类型包Mytuple<T...>,这里的类型包已经不包括int类型,因为int类型已经给了head变量,然后是一样的偏特化匹配,递归直到最后匹配template<> class Mytuple<>,再逐渐弹栈进行构造。所以构造的顺序是Mytuple<>->Mytuple<float>->Mytuple<char, float>->Mytuple<double, char, float>->Mytuple<int, double, char, float>。所以最终输出为:

float
char
double
int

同样的,可以使用可变模板参数进行递归计算,这样可以把运行期的计算转移到编译期来。(这里就是非类型的模板参数包)

#include <array>    //用来检测是否为编译期的常量值
template<long long... nums> //声明这个类是一个可变参数模板类
struct Multiply;    
template <long first, long... last> //偏特化版本
struct Multiply<first, last...>
{
    static const long long val = first * Multiply<last...>::val;    //递归求乘数值
};
template <> //特化版本
struct Multiply<>
{
    static const long long val = 1; //初始值为1
};
int main()
{
    cout << Multiply<2, 3, 4, 5>::val << endl;  //编译期就能确定的常量值
    array<int, Multiply<2, 3, 4, 5>::val> a;    //std::array传入的长度值必须是编译期确定的常量

    return 0;
}

补充一个使用逗号表达式展开变长参数模板的例子(我对其不是非常理解)

#include <iostream>
using namespace std;
template<typename F, typename ...Args>
void expand(const F &f, Args&&... args)
{
    //这里采用了逗号表达式+大括号初始化列表,完美转发一个变长参数模板,展开的同时,通过逗号表达式将0赋值到initializer_list中
    initializer_list<int>{ (f(forward<Args>(args)), 0)... };
}

int main()
{
    expand([](auto i) { cout << i << endl; }, 1,2,3,"test");

    return 0;
}

除了变长模板类,C++11还可以声明变长模板函数,C++11额外的要求是模板函数的模板参数包必须唯一,并且是函数的最后一个参数。

变长模板函数的两个例子:

  1. 变长模板函数实现printf打印:

    
    #include <exception>
    
    
    #include <string>
    
    void myprint(const char *s)
    {
    while (*s) {    //遍历直到'\0'
        if (*s == '%' && *++s != '%') {
            throw runtime_error("invalid format string: missing arguments");
        }
        cout << *s++;
    }
    }
    template <typename T, typename... Args>
    void myprint(const char *s, T value, Args... args)
    {
    while (*s) {    //遍历直到'\0'
        if (*s == '%' && *++s != '%') {
            cout << value;
            return myprint(++s, args...);
        }
        cout << *s++;
    }
    throw runtime_error("extra argmuments provided to myprint");
    }
    int main()
    {
    myprint("hello %s%s%s%s\n", string("world"), string("world"), string("world"), string("world"));
    
    return 0;
    }
  2. 变长参数模板实现完美转发:

    template<typename T,     //创建对象的型别
         typename... Ts>    //一系列实参的型别
    void func(Ts&&... params)
    {
    //利用params构造局部对象T
    //T localObject(forward<Ts>(params)...);    //构造1020
    T localObject{forward<Ts>(params)...};      //构造两个数,1020
    for (auto it : localObject) cout << it << ' ';
    cout << endl;
    }
    //这里有一个decltype(auto)的使用,如果想返回T的话,可以使用型别推导,使用decltype(auto)
    /*
    template<typename T,     //创建对象的型别
    typename... Ts> //一系列实参的型别
    decltype(auto) func(Ts&&... params)
    {
    //利用params构造局部对象T
    //T localObject(forward<Ts>(params)...);
    T localObject{ forward<Ts>(params)... };
    return localObject;
    }
    */
    int main()
    {
    vector<int> v;
    func<vector<int>>(10, 20);  
       /*函数模板可以只写一部分,或者不写模板参数列表,没写的参数模板可以通
       过推导得到(前提是这个模板类型声明的形参在参数列表中,要是不在参数列
       表,就要手动写模板参数列表,否则编译期无法推导)*/
    return 0;
    

P.s. 上述的模板函数推导,可以推导的样例如下:

template<typename T1, typename T2>
void func2(T1 a, T2 b)
{
  cout << a << ' ' << b << endl;
}
//以下三种方式均可,T1,T2类型的参数都在形参列表中
func2(10, 20);
func2<int>(10, 20);
func2<int, int>(10, 20);

不可以推导的样例如下:

template<typename T1, typename T2, typename T3>
void func2(T1 a, T2 b)
{
  cout << a << ' ' << b << endl;
  T3 aaa = a + b;
}
//不可以,T3不在形参列表中,要显式指出
func2(10, 20);

八 优先选用nullptr,而非0或NULL

最根本的原因是0和NULL其实都是int类型的0,这样在重载函数,进行选择的时候,可能会匹配错误。

//写代码时,应避免在指针型别和整型之间重载
void f(void *);
void f(int);
void f(bool);
f(0);       //调用void f(int)
f(NULL);    //可能通不过编译,但是一般调用void f(int),不会调用f(void *)

所以在表示空指针的时候最好使用nullptr。nullptr的优点在于它不具备整型型别,事实上它也不具备指针型别,但可以把其当作一种任意型别的指针。

nullptr的类型是std::nullptr_t,这个型别可以隐式转换到所有的裸指针类型。

f(nullptr); //调用f(void *)版本

还有一种情况是当在模板参数推导的时候,如果传入的是0或者NULl将会被推导为int类型,而传入nullptr将会隐式推导为需要的裸指针类型。

九 优先选用别名声明,而非typedef

C++11中添加了别名声明,使用using可以代替typedef的功能,并且功能更加强大。

旧式:

typedef unique_ptr<unordered_map<string, string>> UptrMapSS;
typedef void (*fp)(int, const string &);

新式:

using UptrMapSS = unique_ptr<unordered_map<string, string>>;
using fp = void (*)(int, const string &);

当然最重要的是,别名生命可以模板化,在这种情况下,被称作是别名模板

例子:要定义一个同义词表达一个链表,这个链表使用了自定义分配器MyAlloc。

新式:

template<typename T>
using MyAllocList = list<T, MyAlloc<T>>;
//调用
MyAllocList<Widget> lw;

旧式:

template<typename T>
struct MyAllocLst{
    typedef list<T, Myalloc<T>> type;
};
//调用
MyAllocList<Widget>::type lw;

更加麻烦的情况是,如果在模板内用typedef创建链表的话,如果容纳的对象型别由模板形参指定的话,那就要加上typename以表明这是一个型别。因为这里的type是取决于类型T的。

template<typename T>
class A{
private:
    typename MyAllocList<T>::type list;     //A<T>包含一个MyAllocList<T>的成员
};

MyAllocList<T>::type称为带依赖类型,但如果使用using别名模板的话,就不需要使用typename和type了。

template<typename T>
using MyAllocList = list<T, MyAlloc<T>>;
template<typename T>
class A{
private:
    MyAllocList<T> list;
};

当编译器遇到MyAllocList<T>时,编译器知道这是一个型别的名字,因为MyAllocList是一个别名模板,它命名了一个型别,综上,MyAllocList<T>是一个非依赖型别,所以不需要typename。

在模板元编程中,需要经常进行类型的转换,比如去掉volatile,const属性,或者增加这些属性或者引用属性。

C++11中以型别特征的形式给了程序员一以执行此类变换的工具,型别特征是在头文件<type_traits>给出的一整套模板,该头文件中有几十个型别特征。对待给定型别T,结果型别为std::transformation<T>::type,例如:

remove_const<T>::type           //const T->T
remove_reference<T>::type       //T&或者T&&->T
add_lvalue_reference<T>::type   //T->T&

C++11中的型别特征是嵌套在模板化的struct里面的typedef实现的。

在C++14中,都有对应的别名模板版本,名称为在后面加上“_t”:

remove_const_t<T>           //const T->T
remove_reference_t<T>       //T&或者T&&->T
add_lvalue_reference_t<T>   //T->T&

typedef不支持模板化,但别名声明支持

别名模板可以让人免写::type后缀,并且在模版内,对于内嵌typedef的引用经常要求加上typename前缀。

十 优先选用限定作用域的枚举性别,而非不限作用域的枚举型别

C++的枚举量的内部名字的可见性不会被限定在括号括起来的范围内,这些枚举量的名字属于包含着这个枚举型别的作用域,这就意味着在这个作用域内不能有其他实体取相同的名字。

#include <iostream>
using namespace std;
int main()
{

    enum Color { black, white, red };
    int black = 10; //无法通过编译,error C2365: “main::black”: 重定义;以前的定义是“枚举数”
    return 0;
}

这个枚举型别是不限范围的枚举型别。它们在C++11中的对等物,限定作用域的枚举型别,不会以刚才的方式泄露名字。

#include <iostream>
using namespace std;
int main()
{

    enum class Color { black, white, red };
    int black = 10;
    //Color c = white;  //error C2065: “white”: 未声明的标识符
    Color c2 = Color::white;
    auto c3 = Color::white;
    return 0;
}

由于限定作用域的枚举型别是通过“enum class”声明的,所以也被称作是枚举类。

限定作用域的枚举型别除了可以降低命名空间的名称污染,还有一个有点:它的枚举量是更强型别的。

对于不限定作用域的enum来说,会隐式转换到整数型别。这样的语法可以通过编译,但是却不合乎常识。

#include <iostream>
#include <vector>
using namespace std;
vector<size_t> f(size_t x)
{
    return vector<size_t>{x + 1};
}
int main()
{
    enum Color { black, white, red };
    Color c = red;
    if (c < 14.5)
        auto r = f(c);  //计算一个Color型别的质因数

    return 0;
}

但是限定作用域的枚举型别不会产生隐式转换:

int main()
{
    enum class Color { black, white, red };
    Color c = Color::red;
    if (c < 14.5)   //error C2678: 二进制“<”: 没有找到接受“main::Color”类型的左操作数的运算符(或没有可接受的转换)
        auto r = f(c);  //error C2664: “std::vector<::size_t,std::allocator<_Ty>> f(::size_t)”: 无法将参数 1 从“main::Color”转换为“::size_t”

    return 0;
}

这里如果要转换类型,就需要static_cast进行强制转换。

在C++11中,不限作用域的枚举量可以进行前置声明,但必须完成一些额外工作,那就是限定底层的型别类型。原因是不限定作用域的枚举量的底层型别是不定的(没有默认底层型别),编译器可能会选择具备最小可容尺寸的型别。

上述枚举量

enum Color { black, white, red };

只有三个量0,1,2,所以只需要用char做底层型别就行,但是如果是以下的enum的话,就需要用int来作为底层型别:

enum status{ good = 0, failed = 1, incomplate = 100, corrupt = 200, indeterminate = 0xFFFFFFF };

前置声明的弊端就是增加了编译的依赖性。

C++11中的限定作用域的枚举型别的底层型别是已知的,而对于不限范围的枚举型别,你可以指定这个底层型别。

默认的限定作用域的枚举类型的底层型别是int。

enum class Status;              //底层型别是int
enum class Status : uint32_t;    //底层型别是uint32_t
enum Color : uint8_t;           //底层型别是uin8_t

枚举类型和tuple搭配使用,效果很好,因为tuple取用域成员的时候,用的是数字,往往不知道取用的是什么,但是和不限定范围的枚举类搭配,使用不限定范围的枚举类的隐式转换的特性,就可以标志取用的是什么域,限定范围的枚举类型也可以,但是由于要显示转换,代码就比较啰嗦。例子如下:

#include <iostream>
#include <vector>
#include <tuple>
using namespace std;
int main()
{
    using UserInfo = tuple<string, string, size_t>;
    UserInfo info = make_tuple("fry", "[email protected]", 25);
    auto val = get<1>(info);    //不清楚取得的是什么域
    enum class UserInfoField { name, mail, age };   //限定范围的作用域
    enum UserInfoField2 { name, mail, age };
    auto val2 = get<mail>(info);    //取得的是邮件域
    auto val3 = get<static_cast<size_t>(UserInfoField::mail)>(info);    //取得的是邮件域(比较啰嗦)
    return 0;
}

要想不那么啰嗦,就要写一个函数,以枚举量为形参并返回其对应的size_t型别的值。std::tuple::get是一个模板,而传入的是一个模板形参(用的是尖括号),所以这个枚举量变换成size_t类型的函数,必须在编译期知道其结果。所以这里要用到constexpr函数。

constexpr变量必须在编译时进行初始化,如果在编译期无法确定返回值,那么即便用constexpr修饰,其函数作用也和普通函数没有区别,例子如下:

#include <iostream>
#include <array>
using namespace std;
constexpr int f(int i)
{
    return 2 * i;
}
int main()
{
    array<int, f(10)> a1;   //正确
    int i = 10;
    //array<int, f(i)> a2;  //无法通过编译,error C2975: “_Size”:“std::array”的模板参数无效,应为编译时常量表达式
    return 0;
}

但是constecpr要配合不同的枚举类型(enum底层的型别),所以要选用函数模板,并且返回值也要是底层模板的类型,enum底层型别的类型可以通过std::underlying_type获得,并且这个执行类型转换的函数模板是不会发生异常的,所以我们可以对其声明为noexcept。例子如下:

//C++11写法
template<typename T>
constexpr typename underlying_type<T>::type //返回类型
toUType(T enumerator) noexcept
{
    return static_cast<typename underlying_type<T>::type>(enumerator);
}

这个函数非常值得思考,首先是返回值typename underlying_type<T>::type,之所以要用到typename的原因是:1.类中使用的是typedef而不是别名声明using来声明type;2.这里的type类型和传入的模板T类型有关,所以要使用typename。其次是由于不会产生异常而加上的noexcept。而在C++14中,我们可以使用别名声明underlying_type_t<T>来代替typename underlying_type<T>::type,或者直接使用auto来表示返回值。

//C++14写法
template<typename T>
constexpr underlying_type_t<T>  //返回类型,也可以写auto
toUType(T enumerator) noexcept
{
    return static_cast<underlying_type_t<T>>(enumerator);
}

十一 优先选用删除函数,而非private未定义函数

C++会在需要的时候,自动生成某些成员函数,在C++98中,如果想阻止这些函数被使用,具体的方法是声明其为private,并且不去定义它们,声明为private可以阻止客户代码调用这些函数,而不定义,就会使友元函数或者类的其他成员函数在调用它们的时候,链接时,缺少函数定义而失败

C++11中,不再使用这种方法,而是使用删除函数“ = delete”。如果不想使用复制构造函数,和复制赋值运算符,就将其声明为” = delete“。例子如下:

basic_ios(const basic_ios& ) = delete;
basic_ios &operator=(const basic_ios& ) = delete;

删除函数无法通过任何方法使用,所以即使成员和友元函数中的代码也会因为调用这些函数而无法工作,这样在编译的时候就知道函数是无法调用的,而不用等到链接时的报错。习惯上会将删除函数声明为public的,因为C++会先校验可访问性,后校验删除状态,这样如果将删除函数声明为private,报错就比较难以追踪真正的原因。

删除函数还有一个优点,那就是任何函数都能成为删除函数,但只有成员函数能声明成private,当重载函数中,我们不希望产生某种隐式类型转换的时候,我们就可以将那种类型的重载声明为删除函数。

#include <iostream>
using namespace std;
bool isFive(int k)
{
    return k == 5 ? true : false;
}
//删除函数删除不需要的重载类型
bool isFive(char) = delete;
bool isFive(bool) = delete;
bool isFive(double) = delete;   //无需声明float,因为float会自动调用double类型的函数而被拒绝调用
int main()
{
    cout << isFive(1) << endl;
    //cout << isFive('a') << endl;  //error C2280: “bool isFive(char)”: 尝试引用已删除的函数
    //cout << isFive(true) << endl; //同上错误
    //cout << isFive(1.11) << endl; //同上错误

    return 0;
}

删除函数还有一个有点就是阻止那些不应该进行的模板具现。

如果我们有一个模板函数需要传入一个指针,但是不希望传入void 和char 的指针,那么我们只需要使用删除函数即可。(无法用private做到,因为模板特化只可以在命名空间内撰写,不可以在类内撰写)

template <typename T>
void fp(T *p)
{ }
//阻止不必要的模板特化
template <>
void fp<void>(void *) = delete;
template <>
void fp<char>(char *) = delete;

int main()
{
    int a = 3;
    int *pa = &a;
    void *pv = &a;
    char *pc = reinterpret_cast<char *>(&a);
    fp(pa);
    //fp(pv);   //error C2280: “void fp<void>(void *)”: 尝试引用已删除的函数
    //fp(pc);   //同上错误

    return 0;
}

十二 为意在改写的函数添加override声明

C++中的多态来源就是虚函数,在派生类中重写(override)基类的虚函数,重写要满足以下要求:

  • 基类中的函数必须是虚函数
  • 基类和派生类中的函数名字必须完全相同(析构函数除外)
  • 基类和派生类中的函数形参型别必须完全相同
  • 基类和派生类的中的函数常量性必须完全相同
  • 基类和派生类中的函数返回值和异常规格必须兼容
  • (C++11新增)基类和派生类的引用饰词必须完全相同

派生类重写基类某些虚函数的时候,可能会手误写错,比如说少写了const,形参类型不完全相同,或者是引用饰词不同,又或者基类中的某个函数未被声明为虚函数,并且有重载版本,这样在派生类中重写改函数将会掩盖基类中所有的重载版本。这些问题只会产生警告,而不是错误。

C++11中提供了“override声明”来显式的表明派生类中的函数是为了改写其基类版本。那么在派生类中只要在函数末尾加上override,这样刚才手误的版本产生的就是错误了,将会无法通过编译

class Base
{
public:
    virtual void fc() const { cout << "this is const function." << endl; }
    virtual void doWork() & { cout << "lvalue *this" << endl; }
    virtual void fi(int i) { cout << "this is " << i << endl; }
    void fv() { cout << "this is virtual" << endl; }
};
class Derived : public Base
{
public:
    virtual void fc() const override { cout << "this is derived const function." << endl; }
    //以下三个方法皆无法通过编译,报错为:error C3668: “Derived::doWork”: 包含重写说明符“override”的方法没有重写任何基类方法
    //virtual void doWork() && override { cout << "lvalue *this" << endl; }
    //virtual void fi(unsigned int i) override  { cout << "this is " << i << endl; }
    //void fv() override  { cout << "this is virtual" << endl; }
};

C++11添加了两个语境关键字:override和final。它们的特色是,语言保留这两个关键字,但尽在特定语境下保留,对于override的情况而言,它仅出现在成员函数声明的末尾才有意义。这样就说明如果旧式代码中有函数名字为override,将不需要改名。将final用于虚函数,会阻止它在派生类中被改写,final也可以被用于一个类,在这种情况下,该类被禁止用作基类。

引用饰词

成员函数引用饰词是C++11中的一个语言特性,是为了限制成员函数仅用于左值还是右值,带有引用饰词的成员函数,不必是虚函数。如果基类中的虚函数带有引用饰词,则派生类要对改函数进行改写版本必须也带有完全相同的引用饰词,如若不然,那么这些声明了的函数在派生类中依然存在,只是它们不会改写基类中的任何函数。

例子如下:

#include <iostream>
using namespace std;
class Base
{
public:
    //重载函数
    void doWork() & { cout << "lvalue *this" << endl; }
    void doWork() && { cout << "rvalue *this" << endl; }
};
int main()
{
    Base b;
    b.doWork();         //输出lvalue *this
    Base().doWork();    //输出rvalue *this

    return 0;
}

引用饰词的作用就是针对发起成员函数调用的对象,即*this,加一些区分度,这和在成员函数声明末尾加一个const的情形一摸一样:后者表明发起成员函数调用的对象,即*this,应该是const类型。

对于带有引用饰词的成员函数用处不多,比如说当一个类某个成员函数是返回类内部的私有成员变量的话,如果这个私有的成员变量比较大(例如vector数组),那么当*this是右值的时候,我们使用移动而不是复制,就会更快。因为对一个右值执行复制完全是浪费效率的做法,这样我们就可以通过引用饰词来区分使用复制还是移动。具体的代码就不写了。例子主要部分如下:

class Base
{
public:
    using DataType = vector<double>;
    //重载函数
    DataType data() & { return values; }
    DataType data() && { return move(values); }
private:
    DataType values;
};
auto r1 = b.data();         //将调用左值重载版本,采用复制构造完成初始化
auto r2 = Base().data();    //将调用右值重载版本,采用移动构造完成初始化

十三 优先选用const_iterator,而非iterator

当迭代器指涉到容器的内容但是没有修改必要的时候,就应该使用const_iterator。

C++98中,从非const迭代器到const迭代器,只能static_cast进行强制类型转换,并且插入和删除等操作还不能通过const_iterator来实现(这点很奇怪,因为const_iterator应该实现的操作是不能修改迭代器指向的内容)。

C++11中,这些现象彻底改变了,获取和使用const_iterator变得更加容易,容器对应begin()和end()函数有cbegin()和cend(),都返回const_iterator,甚至对于非const容器也是如此,例子如下:

int main()
{
    vector<int> val{1,2,3,4,5,6,7,8,9};
    auto it = find(val.cbegin(), val.cend(), 9);
    val.insert(it, 10);
    for (auto it : val) cout << it << ' ';//1 2 3 4 5 6 7 8 10 9
    return 0;
}

C++11中对const_iterator的实现还不是非常充分,因为某些情况下,某些容器或者数据结构会使用非成员函数的方法提供begin和end(还有cbegin、cend和rbegin等),而不是以成员函数方式。但是C++11中仅仅提供了std::begin()std::end()方法,这样非const属的容器不能返回const属性的迭代器(带有const属性的容器通过begin返回的是const属性的迭代器),不过在C++14中这种情况得到了扩充,添加了std::cbegin()std::cend()std::rbegin()std::rend()std::crbegin()std::crend(),所以在C++14中,我们可以对任意属性的容器返回带有const属性的迭代器和不带const属性的迭代器。我们使用非成员函数返回容器的begin和end的时候,可以如下书写:

//C++14
template<typename C, typename V>
void FindAndInsert(C &container, const V& targetval, const V& insertval)
{
    auto it = find(cbegin(container), cend(container), targetval);
    container.insert(it, insertval);
}
int main()
{
    vector<int> val{ 1,2,3,4,5,6,7,8,9 };
    FindAndInsert(val, 9, 10);
    for (auto it : val) cout << it << ' ';  //1 2 3 4 5 6 7 8 10 9

    return 0;
}

但是在C++11中如何自己实现C++14的这些额外功能呢,方法非常简单,如之前所说,不同属性的容器返回的迭代器属性不同。只需要调用相应属性(带有const,或者不带)的容器,返回对应的迭代器,这些迭代器就自动具备了这些属性。

template<typename C>
auto mycbegin(const C& container)->decltype(begin(container))
{
    return begin(container);//begin(const C&)返回的是const_iterator
}
int main()
{
    vector<int> val{ 1,2,3,4,5,6,7,8,9 };
    auto it = mycbegin(val);
    cout << typeid(it).name();  //class std::Vector_const_iterator<class std::Vector_val<struct std::_Simple_types<int> > >
    return 0;
}

这样通过begin(const C&)返回的是const_iterator这个行为,我们就可以轻松得到这个属性的迭代器。

P.s. 使用非成员函数的begin()和end(),而不是成员函数,可以增加代码的通用性。

十四 只要函数不会发射异常,就为其加上noexcept声明

C++11中增加了noexcept关键字。这个关键字是为了不会发射异常的函数准备的。

函数是否要加上如此声明,事关接口设计,函数是否会发射异常这一行为,是客户方面关注的核心。调用方可以查询函数的noexcept状态,而查询结构可能会影响调用代码的异常安全性和运行效率。(事实上noexcept关键字和const关键字的重要程度一致)

//C++98
int f(int x) throw();
//C++11
int f(int x) noexcept;

如果在运行期,一个异常发生在f的作用域内,C++98中,函数调用栈会开解至调用f的函数内部,执行一些操作并中止程序,C++11中,栈只是可能会开解。这样代码编译的时候,优化器不需要在异常传出函数之前,将执行期栈保持在可开解状态,也不需要在异常溢出函数的前提下,保证其中的对象以其被构造的顺序的逆序完成析构。

异常全保证见Effective C++ Item29

STL中有std::swap函数,作用是交换两个传入的模板参数,代码很简单:

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

一个简单的swap例子如下:

vector<int> vec1 = { 1,2,3,4,5 };
vector<int> vec2 = { 5,4,3,2,1,0 };
swap(vec1, vec2);   //交换vec1和vec2的内容

在vector中也有一个成员函数swap,这个swap可以交换两个vector的所有内容,但是速度很快,这是因为swap交换的是两个vector的数据结构,而不是具体内容。

vector的内存是只增不减的,这样就会导致当使用erase或者clear等成员函数进行元素删除的时候,会造成空间的浪费(例如原来有10000个元素,删除了9999个,就剩1个元素,但是仍然占据10000个元素的空间),这个时候我们就可以使用右值左值交换空间的把戏,通过swap函数来进行巧妙的内存缩减。

一般,我们都会通过vector中成员函数clear进行一些清除操作,但它清除的是所有的元素,使vector的大小减少至0,却不能减小vector占用的内存。要避免vector持有它不再需要的内存,这就需要一种方法来使得它从曾经的容量减少至它现在需要的容量,这样减少容量的方法被称为“收缩到合适(shrink to fit)” 。

​ ——《Effecitve STL》

具体的使用样例如下:

vector<int> vec(10000, 10);
vec.erase(vec.begin() + 2, vec.end());
//以下方法二选一
//vector<int>().swap(vec);  //直接删除所有内容,交换后vec占用的内存和元素都为空
vector<int>(10).swap(vec);  //保留vec原有的元素

C++11中增加了成员函数vector::shrink_to_fit()来释放多余的内存,使得capacity和size对应上。具体的使用方法很简单

vector<int> vec1;
for(int i = 1; i < 10000; ++i)
    vec1.push_back(i);
cout << "size1: " << vec1.size() << " capacity1: " << vec1.capacity() << endl;  //9999,14053(vector的素数上限)
vec1.erase(vec1.begin() + 10, vec1.end());
cout << "size1: " << vec1.size() << " capacity1: " << vec1.capacity() << endl;  //10,14053
vec1.shrink_to_fit();
cout << "size1: " << vec1.size() << " capacity1: " << vec1.capacity() << endl;  //10,10(收缩成功)

shrink方法就是对内存的重新分配和拷贝,源代码如下:

    void shrink_to_fit()
        {   // reduce capacity to size, provide strong guarantee
        if (_Has_unused_capacity()) //有没用的空间(last和end指针计算得出)
            {   // something to do
            if (empty())    //是不是空的(last和efirst指针计算得出)
                {
                _Tidy();
                }
            else
                {
                _Reallocate_exactly(size());    //重新分配内存
                }
            }
        }

在以下几种情况,shrink_to_fit会放弃重分配:

  • 元素类型不支持无异常移动。使用拷贝完成内存重分配期间某个元素构造抛出异常。此时函数回滚容器到调用前状态,错误不返回
  • 重分配新位置需要的内存失败
  • capacity()和size()相等或者vector为空

STL中的vector有一个成员函数:push_back(),该函数在C++99中是强异常安全保证的,因为使用的是复制进行传值,当申请空间的构造函数抛出异常或者复制的时候产生异常,原类型会不变,编译器会回退到未做这个动作的时候。但是在C++11中,许多复制操作被移动操作所代替,也就是说原来是强异常安全保证的,现在无法保证了,毕竟移动的时候抛出异常是无法恢复的。所以这个时候push_back()不一定是移动操作,只有当其确认其移动的类型是noexcept的,这样才会进行移动操作,其余的进行复制操作,用以维持强异常安全保证。

同样的,标准库中的swap是否带有noexcept声明,取决于用户定义的swap是否带有noexcept声明。

这里要介绍一个noexcept()运算符,noexcept 运算符进行编译时检查,若表达式声明为不抛出任何异常则返回 true 。

标准库中为pair准备的swap函数如下:

template<class T, size_t N>
void swap(T (&a)[N], T (&b)[N]) noexcept(noexcept(swap(*a, *b)));
template<class T1, class T2>
struct pair{
    void swap(pair &p) noexcept(noexcept(swap(first, p.first)) && noexcept(swap(second, p.second)));
};

从这段代码可以看出,swap是不是noexcept的,完全决定于其交换的具体类型是不是noexcept。

noexcept声明对以下几个函数(移动函数和swap函数,内存释放函数和所有的析构函数)最有价值。

事实上,大多数函数都是异常中立的,也就是说,其自身不会抛出异常,而其调用的函数却可能抛出异常。所以在写代码的时候,如果确定函数不会发出异常(尤其是移动函数和swap函数),那么就应该为其加入noexcept声明。相比于不带有noexcept声明的函数,带有此声明的函数更容易被优化。

一般而言,内存释放函数和所有的析构函数都隐式的具备noexcept性质,这样依赖就无需为其加入noexcept声明,析构函数不具备noexcept性质的唯一可能是类中所有的数据成员(包括继承而来的数据成员、数据成员中包含的数据成员)的型别显式地将其析构函数声明为可能发射异常(即加上noexcept(false)声明)。

十五 只要有可能使用constexpr,就使用它

constexpr既可以修饰对象,也可以修饰函数。

constexpr修饰对象

constexpr修饰对象的时候,这些对象:

  • 具备const属性
  • 编译期可知

编译期可知就可以用来表示数组的长度,整型模板的规则,枚举量的值,对齐的规格等。

int main()
{
    int sz;
    cin >> sz;
    //constexpr auto a = sz;    //error C2131: 表达式的计算结果不是常数
    //array<int, a> arr1;   //error C2971: “std::array”: 模板参数“_Size”:“a”: 包含非静态存储持续时间的变量不能用作非类型参数
    constexpr auto b = 10;
    array<int, b> arr2;

    return 0;
}

const并未提供和constexpr一样的保证,这样也就是说constexpr auto a = sz;改为const auto a = sz;是没问题的,但是也一样无法用作array的模板参数。

也就是说,所有的constexpr对象都是const对象,但是并非所有的const对象都是constexpr对象。

constexpr修饰函数

constexpr修饰的函数接受的参数如果传入的是编译期常量,则返回的是编译期常量,如果传入的是运行期才知道的值,那么就返回运行期值(constexpr修饰的函数内部不能有IO语句)。具体来说就是:

  • constexpr函数可以用在要求编译期常量的语境中,若传给其的实参值在编译期可知,则结果也会在编译期被计算出来,如果任意一个实参值在编译期未知,那么代码将无法通过编译。
  • 在调用constexpr函数的时候,若传入的值有一个或多个在编译期未知,则运作方式和普通函数没有区别,也就是说结果是在运行期被计算出来的。
constexpr int getX(int x)
{
    return x * 2;
}
//array<int, getX(test)> t1;    //error C2975: “_Size”:“std::array”的模板参数无效,应为编译时常量表达式
array<int, getX(10)> t1;    //正确

如果我们需要实现一个运行期可以返回结果的pow函数的话,需要如下书写:

constexpr int constexprpow(int base, int exp) noexcept
{
    return exp == 0 ? 1 : base * constexprpow(base, exp - 1);
}
int main()
{
    array<int, constexprpow(3, 2)> test;    //array只接受编译期常量作为参数
    cout << test.size() << endl;

    return 0;
}

C++11中,constexpr只可以拥有一条运行语句,就如同上述代码中的实现。而在C++14中,这个限制条件被放宽了,所以可以如下实现:

constexpr int constexprpow(int base, int exp) noexcept
{
    auto ret = 1;
    for(int i = 0; i < exp; ++i) ret *= base;
    return ret;
}

constexpr仅限于传入和返回字面型别,意思就是这样的型别能够有编译期可以决定的值。C++11中所有的内建型别(除了void)都满足这个条件,但是用户自定义型别同样也可能是字面型别,因为它的构造函数和其他成员函数可能也是constexpr函数。

样例如下:

//A的所有成员函数都是constexpr函数,所以A也可以被声明为constexpr类型
class A
{
public:
    constexpr A(int _a1, int _a2) noexcept : a1(_a1), a2(_a2){ }
    constexpr int getA1() const noexcept { return a1; } //这里必须要是const,否则avg返回的变量是const类型,无法调用此函数
    constexpr int getA2() const noexcept  { return a2; }
    void setA1(int a) noexcept { a1 = a; }
    void setA2(int a) noexcept { a2 = a; }
    //C++14中才可使用
    //constexpr void setA1(int a) noexcept { a1 = a; }
    //constexpr void setA2(int a) noexcept { a2 = a; }
private:
    int a1;
    int a2;
};
//同样是一个可以在编译期运行的函数,在传入的两个变量都是运行期函数的前提下
constexpr A avg(A a1, A a2)
{
    return { (a1.getA1() + a2.getA1()) / 2,
             (a1.getA2() + a2.getA2()) / 2};
}
int main()
{
    constexpr A a1(1, 2);
    constexpr A a2(3, 4);
    constexpr auto m = avg(a1, a2);
    array<int, m.getA1()> test; //编译期变量可以拿来做模板参数
    cout << test.size() << endl;

    return 0;
}

C++11中,上述代码中的setA1()和setA2()两个函数,无法被声明为constexpr类型,因为这个函数修改了操作对象,但是constexpr类型的变量都是const类型的,这样这个函数无法修改成员变量(除非用mutable修饰),并且返回值是void。

而在C++14中可用,这样就可以在代码中直接修改对象的值了,如下述函数:

constexpr A reflection(A a1)
{
    A ret;
    ret.setA1(-a.getA1());
    ret.setA2(-a.getA2());
    return ret;
}

十六 保证const成员函数的线程安全性

虽然带有const关键字的类成员函数是无法修改类内的成员变量的,但是如果成员变量带有mutable的话,还是允许被修改的,这就意味着在多线程环境下,const成员函数也可能存在一定的“数据竞险”问题。

#include <iostream>
#include <thread>
using namespace std;
class A
{
public:
    void testconstf() const { 
        int i = 0;
        while (i++ < 100) {
            a++;
            cout << a << endl;
        }
    }
private:
    mutable int a{0};
};

int main()
{
    A a;
    thread t1(&A::testconstf, ref(a));
    thread t2(&A::testconstf, ref(a));

    t1.join();
    t2.join();

    return 0;
}

这里的两个线程处理的是同一个类的对象,虽然这个函数是const函数,但是这个函数仍然改变了类内的对象,最终a的值也可能不是为200。

解决方法是使用mutex或者是atomic原子变量。

对于mutex来说一般可以用std::lock_guard<std::mutex>来获取,这样就不需要释放的过程了。lock_guard 对象通常用于管理某个锁(Lock)对象,因此与 Mutex RAII 相关,方便线程对互斥量上锁,即在某个 lock_guard 对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 lock_guard 的生命周期结束之后,它所管理的锁对象会被解锁,样例如下:

//此时只要对类内添加mutex对象,并在成员函数中获取此对象即可实现互斥操作
class A
{
public:
    void testconstf() const { 
        lock_guard<mutex> lg(mt);   //用lock_guard来持有锁
        int i = 0;
        while (i++ < 100) {
            a++;
            cout << a << endl;
        }
    }
private:
    mutable mutex mt;   //加入此对象,且声明为mutable(否则成员函数无法更改此对象状态)
    mutable int a{0};
};

这样就可以完美实现互斥增加a的值。最终输出的结果为200。

但是很明显,用了互斥量的情况下,程序的运行效率会受影响,这样我们就可以使用原子变量,在更快的速度下实现互斥更新。但是原子变量仅仅适合对单个变量或内存区域的操作。

class A
{
public:
    void testconstf() const {
        int i = 0;
        while (i++ < 100) {
            a++;
            cout << a << endl;
        }
    }
private:
    mutable atomic<int> a{ 0 }; //非常简单的原子变量
};

这里简单介绍一个C++11的时间库:std::chrono

std::chrono

chrono库实现了三种类型:时间间隔chrono::duration,时钟chrono::system_clock,具体时间chrono::time_point

chrono::duration

duration实现的是一段时间,任意一段时间间隔都可以来表示。原型如下:

template<class Rep, class Period = std::ratio<1>> class duration;

这里的Rep是一个数值类型(如long long),表示时钟个数,第二个模板参数也是一个模板参数std::ratio。原型如下:

template<std::intmax_t Num, std::intmax_t Denom = 1> class ratio;

它表示每个时钟周期的秒数,其中第一个模板参数Num代表分子,Denom代表分母,分母默认为1,ratio代表的是一个分子除以分母的分数值。

通过增加分子,就可以表示1分钟(ratio<60>),1小时(ratio<60*60>),1天(ratio<60*60*24>),通过增加分母就能表示1毫秒等等,chrono命名空间已经为我们定义了这些时间常用时间段:

typedef duration <Rep, ratio<3600,1>> hours;
typedef duration <Rep, ratio<60,1>> minutes;
typedef duration <Rep, ratio<1,1>> seconds;
typedef duration <Rep, ratio<1,1000>> milliseconds;
typedef duration <Rep, ratio<1,1000000>> microseconds;
typedef duration <Rep, ratio<1,1000000000>> nanoseconds;

同时duration库提供了duration_cast函数来实现不同类型的强制转换。

这些可以在线程内部使用,让线程休眠一段时间。

this_thread::sleep_for(chrono::seconds(5)); //当前线程休眠5s

duration还有一个成员函数count()返回Rep类型的Period数量。

chrono::time_point

time_point表示一个时间点,用来获取1970.1.1以来的秒数和当前的时间, 可以做一些时间的比较和算术运算,可以和ctime库结合起来显示时间。time_point必须要clock来计时,time_point有一个函数time_from_eproch()用来获得1970年1月1日到time_point时间经过的duration。

#include <iostream>
#include <ratio>
#include <chrono>
using namespace std;
using namespace chrono;
int main()
{
    using days =  duration<int, std::ratio<60 * 60 * 24>>;  //别名声明
    time_point<system_clock, days> today = time_point_cast<days>(system_clock::now());
    duration<int, ratio<60 * 60 * 24, 1>> d = today.time_since_epoch();
    std::cout << d.count() << " days since epoch" << std::endl;
    return 0;
}

chrono::system_clocks

表示当前的系统时钟,内部有time_point, duration, Rep, Period等信息,它主要用来获取当前时间,以及实现time_t和time_point的相互转换。Clocks包含三种时钟:

  • system_clock:当前系统范围(即对各进程都一致)的一个实时的日历时钟(wallclock)
  • steady_clock:当前系统实现的一个维定时钟,该时钟的每个时间嘀嗒单位是均匀的(即长度相等)
  • high_resolution_clock:当前系统实现的一个高分辨率时钟 ,实际上是system_clock或者steady_clock的别名

这个类型拥有以下几个成员函数:

  • now() 当前时间time_point
  • to_time_t() time_point转换成time_t秒
  • from_time_t() 从time_t转换成time_point

可以通过now()来获取当前时间点:

#include <iostream>
#include <chrono>
using namespace std;
using namespace chrono;
int main()
{
    steady_clock::time_point t1 = steady_clock::now();
    cout << "Hello World\n";
    steady_clock::time_point t2 = steady_clock::now();
    cout << (t2 - t1).count() << " tick count" << endl;
}

十七 理解特种成员函数的生成机制

特种成员函数是指C++会自行生成的成员函数,C++98中有4种成员函数:默认构造函数,析构函数,复制构造函数,以及复制赋值运算符。

仅当一个类没有声明任何的构造函数,才会生成默认构造函数,只要制定了一个要求传参的构造函数,就会阻止编译器生成默认构造函数。生成的特种成员函数都是inline的,具有public访问权限并且是非虚的。

C++11中,特种成员函数增加了两个新成员:移动构造函数和移动赋值运算符。这两个成员函数是以右值为形参进行构造,不再进行复制,而是以速度较快的移动语意代替。

class A
{
public:
    A(A &&a);   //移动构造函数
    A &operator=(A &&a);    //移动赋值运算符
};

这两个函数的生成规则和运行表现都和之前的复制函数一致,也就是说,仅在需要的时候才生成,而一旦生成,执行的也是作用域非静态成员的“按成员移动”操作。意思是,移动构造函数将依照其形参的各个非静态成员对于本类的对应成员执行移动构造,而移动赋值则依照形参的各个非静态成员对本类的对应成员进行移动赋值。移动构造函数/移动赋值同时还会移动构造/移动赋值基类部分(如果存在)。

但是事实上,虽然有移动操作,但是不代表真的会进行移动,也就是说,按成员移动其实是一个按成员的移动请求,因为那些不可移动的型别(即并未定义移动语意的型别)将通过复制来实现“移动”。也就是说按成员的移动操作,其实是通过std::move()将原对象转换为右值再进行移动操作,最终是否为移动操作还是看函数重载的决定。所以,按成员移动是两部分组成的:一部分是支持移动操作的成员执行的移动操作,另一部分是不支持移动操作的成员的复制操作。

对于复制构造和复制赋值符,定义其中一个并不影响编译器自动生成另外一个。也就是说,当实现复制构造成员函数的时候,如果出现了对象的复制赋值,编译器会自动生成此函数。

但是对于移动构造和移动赋值符,定义其中一个,编译器就不会生成另外一个。原因是因为,移动构造如果被产生,则说明移动构造是需要特殊去定义的,而自动生成的移动构造可能不符合这个特殊的规则。所以声明移动构造函数会阻止编译器生成移动赋值符,反过来也一样。

并且当生成了复制操作,移动操作也不会再生成了,理由同上述,编译器同样认为重新定义了复制构造函数的类将不适合进行移动构造。反之亦然,当声明了移动操作,编译器就会废除复制操作(使用delete关键字删除函数)。

有一条规则如下:

如果声明了复制构造函数,复制赋值运算符或者析构函数中的任意一个,将需要同时声明这三个函数。理由很简单,当需要特殊的方式来进行复制构造的时候,这种特殊的方式肯定也要用在复制赋值符中。同时也说明了在析构中也需要特殊的对内存进行处理。

根据这条规则,在C++11中,如果用户声明了析构函数,则编译器不会生成移动操作。

也就是说,移动构造的隐式生成条件有:

  1. 该类未声明任何复制操作
  2. 该类未声明任何移动操作
  3. 该类未声明任何析构函数

并且现今如果已经存在复制操作和析构操作,则仍然生成复制操作已经是被废弃的行为。

但是如果编译器生成的特种函数具有正确的行为的话,将不需要去重新再写一遍,只需要使用关键字“default”即可,该关键字的使用方法和“delete”关键字一致,

class A
{
public:
    ~A();
    A(const A &) = default; //使用编译器的默认行为
    A &operator(const A &) = default;
};

这种手法对多态基类是非常有用的。因为多态的基类要声明为虚函数以进行内存的释放,防止多态情况下的内存泄漏,这样通常还要写一个析构函数的定义,而往往基类的析构函数使用编译器自动生成的析构就足够了。

所以在C++11中,可以在虚析构函数后直接定义“= default”,不过这个时候由于析构函数的定义,移动操作就不会再生成,所以如果可以支持移动操作的话,直接将移动函数显式声明并在之后直接加上“= default”。

class Base
{
public:
    virtual ~Base() = default; //虚析构函数
    Base(Base &&) = default;    //提供移动操作
    Base &operator=(Base &&) = default;
    Base(Base &) = default;     //提供复制操作
    Base &operator=(Base &) = default;
};

总而言之,特种成员的规则如下:

  • 默认构造函数:当类中不包括用户声明的构造函数时自动生成
  • 析构函数:当没有析构函数的时候自动生成,并且是默认为noexcept的,仅当基类的析构函数是虚函数,派生类的析构函数才会是虚函数
  • 复制构造函数:按成员进行非静态数据成员的复制构造。仅当类中不包括用户自己声明的复制构造函数才生成,如果该类声明了移动操作,则复制构造函数被删除。在已经拥有复制构造函数或析构函数的条件下,仍然生成复制赋值运算符已经是被废弃的行为。
  • 复制赋值运算符:按成员进行非静态数据的复制赋值。仅当类中不包括用户自己声明的复制赋值运算符才生成,如果该类声明了移动操作,则复制构造函数被删除。在已经拥有复制构造函数或析构函数的条件下,仍然生成复制赋值运算符已经是被废弃的行为。
  • 移动构造函数和移动赋值运算符:都按成员进行非静态数据的移动操作。仅当类中不包含用户声明的复制操作、移动操作和析构操作才生成。

成员函数模板在任何情况下都不会抑制特种成员函数的生成。

猜你喜欢

转载自blog.csdn.net/u012630961/article/details/80917246