C++11新特性

核心语言功能特性

C++11 是 C++ 的第二个主要版本(前一个是 C++98 而后一个是 C++17),并且是从 C++98 起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对 C++ 程序员可用的抽象。

在它最终由 ISO 在 2011 年 8 月 12 日承认前,人们曾使用名称“C++0x”,因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故而这是迄今为止最长的版本间隔。从那时起,C++ 规则地每 3 年更新一次。

C++博大精深,笔者水平有限,这里参考几个网站(见文章最后),整理一下C++11的新特性,新特性包含很多的知识点,笔者仅整理常用的,更多的学习请参考相关书籍/网站,

1 auto推导

在之前的 C++ 版本中,auto 关键字用来指明变量的存储类型,它和 static 关键字是相对的。auto 表示变量是自动存储的,这也是编译器的默认规则,所以写不写都一样,一般我们也不写,这使得 auto 关键字的存在变得非常鸡肋。

C++11 赋予 auto 关键字新的含义,使用它来做自动类型推导。也就是说,使用了 auto 关键字以后,编译器会在编译期间自动推导出变量的类型,这样我们就不用手动指明变量的数据类型了。

1.1 auto初识

auto:

(1)对于变量,指定要从它的初始化器自动推导出它的类型;

(2)对于函数,指定要从它的 return 语句推导出它的返回类型。(C++14 起)

(3)对于非类型模板形参,指定要从实参推导出它的类型。(C++17起)

后面两种情况先不讨论。

auto 关键字基本的使用语法如下:

auto name = value;

name 是变量的名字,value 是变量的初始值。

注意:auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。或者说,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。

auto 类型推导的简单例子:

auto n = 10;
auto f = 12.8;
auto p = &n;
auto url = "http://c.biancheng.net/cplus/";

下面解释一下:

  • 第 1 行中,10 是一个整数,默认是 int 类型,所以推导出变量 n 的类型是 int。
  • 第 2 行中,12.8 是一个小数,默认是 double 类型,所以推导出变量 f 的类型是 double。
  • 第 3 行中,&n 的结果是一个 int* 类型的指针,所以推导出变量 p 的类型是 int*。
  • 第 4 行中,由双引号""包围起来的字符串是 const char* 类型,所以推导出变量 url 的类型是 const char*,也即一个常量指针。

有一个值得注意的地方是:使用 auto 类型推导的变量必须马上初始化,这个很容易理解,因为 auto 在 C++11 中只是“占位符”,并非如 int 一样的真正的类型声明。

1.2 auto的限制

(1) auto 不能在函数的参数中使用。

这个应该很容易理解,我们在定义函数的时候只是对参数进行了声明,指明了参数的类型,但并没有给它赋值,只有在实际调用函数的时候才会给参数赋值;而 auto 要求必须对变量进行初始化,所以这是矛盾的。

(2) auto 不能作用于类的非静态成员变量(也就是没有 static 关键字修饰的成员变量)中。

(3) auto 关键字不能定义数组,比如下面的例子就是错误的

char url[] = "http://c.biancheng.net/";
auto  str[] = url;  // arr 为数组,所以不能使用 auto

(4) auto 不能作用于模板参数,请看下面的例子:

template <typename T>
class A{
    // TODO:
};

int  main(){
    A<int> C1;
    A<auto> C2 = C1;  // 错误
    return 0;
}

1.3 auto的应用

auto最常见的应用是定义迭代器定义lambda表达式

(1)定义迭代器

使用 stl 容器的时候,需要使用迭代器来遍历容器里面的元素;不同容器的迭代器有不同的类型,在定义迭代器时必须指明。而迭代器的类型有时候比较复杂,书写起来很麻烦,请看下面的例子:

#include <vector>
using namespace std;
int main(){
    vector< vector<int> > v;
    vector< vector<int> >::iterator i = v.begin();
    auto i = v.begin();  // 使用 auto 代替具体的类型
    return 0;
}

(2)定义lambda表达式(泛型编程也可)

int main(int argc, char **argv)
{
    // 简单的求和案例
    auto func = [](int a, int b)->int { std::cout <<  a + b << std::endl; };
    func(10, 20);
    
    system("pause");
    return 0;
}

更多详见:占位类型说明符 (C++11 起) - cppreference.com

2 decltype推导

decltype 是 C++11 新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推导。

2.1 decltype初识

既然已经有了 auto 关键字,为什么还需要 decltype 关键字呢?因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。

auto 和 decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的:

auto varname = value;
decltype(exp) varname = value;

其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。

auto 根据=右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。

另外,auto 要求变量必须初始化,而 decltype 不要求。这很容易理解,auto 是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。decltype 可以写成下面的形式:

decltype(exp) varname;

C++ decltype 用法举例:

int a = 0;
decltype(a) b = 1;       // b被推导成了 int
decltype(10.8) x = 5.5;  // x被推导成了 double
decltype(x + 100) y;     // y被推导成了 double

2.2 decltype推导规则

当程序员使用 decltype(exp) 获取类型时,编译器将根据以下三条规则得出结果:

  • 如果 exp 是一个不被括号( )包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么 decltype(exp) 的类型就和 exp 一致,这是最普遍最常见的情况。
  • 如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致。
  • 如果 exp 是一个左值,或者被括号( )包围,那么 decltype(exp) 的类型就是 exp 的引用;假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&。

更多详见:decltype 说明符 - cppreference.com

3 defaulted

C++11引入default特性,多数时候用于声明构造函数为默认构造函数,如果类中有了自定义的构造函数,编译器就不会隐式生成默认构造函数,如下代码:

namespace Test06 {
    struct Test
    {
        int _a;
        Test(int a) {this->_a = a; }
    };

    void test()
    {
        Test a;
    }
}

int main(int argc, char **argv)
{
    Test06::test();
    
    system("pause");
    return 0;
}

结果

上面代码编译出错,因为没有匹配的构造函数,因为编译器没有生成默认构造函数,而通过default,程序员只需在函数声明后加上“ =default;”,就可将该函数声明为defaulted 函数,编译器将为显式声明的defaulted函数自动生成函数体,如下:

如果对构造函数进行了重载,则编译器不会隐式的生成一个默认的构造函数,此时如果调用了默认构造函数会在编译时报错,为了避免这种情况,一般会选择重写默认构造函数,且函数体为空。关键字 =default 优化了这种行为,用该关键字标记重写的默认拷贝构造函数,编译器会隐式生成一个版本,在代码更加简洁的同时,编译器隐式生成的版本的执行效率更高

namespace Test06 {
    struct Test
    {
        int _a;
        Test(int a) {this->_a = a; }
        Test() = default;             // 加了默认构造函数
    };

    void test()
    {
        Test a;
    }
}



int main(int argc, char **argv)
{
    Test06::test();
    
    system("pause");
    return 0;
}

更多详见:Function declaration - cppreference.com

4 deleted functions

如果不是函数体,而是特殊语法= delete;时,该函数被定义为已删除。任何对已删除函数的使用都是错误的(程序将无法编译)。这包括显式调用(使用函数调用操作符)和隐式调用(调用已删除的重载操作符、特殊成员函数、分配函数等),构造指向已删除函数的指针或指向成员的指针,甚至在不可能求值的表达式中使用已删除的函数。但是,允许隐式地使用碰巧被删除的非纯虚成员函数。

有时候想禁止对象的拷贝与赋值,可以使用delete修饰,如下:

namespace Test06 {
    struct Test
    {
        int _a;
        Test() = default;
        Test(const Test&) = delete;                  // 禁止拷贝构造函数
        Test& operator = (const Test&) = delete;     // 禁止拷贝赋值函数
        Test(int a) { this->_a = a; }
    };

    void test()
    {
        Test a1;
        Test a2 = a1;      // 错误,拷贝构造函数被禁止
        Test a3;
        a3 = a1;           // 错误,拷贝赋值函数被禁止
    }
}

int main(int argc, char **argv)
{
    Test06::test();
    
    system("pause");
    return 0;
}

结果

delele函数在c++11中很常用,std::unique_ptr就是通过delete修饰来禁止对象的拷贝的。

更多详见:Function declaration - cppreference.com

5 final and override

final:指定不能在派生类中重写虚函数,或者不能从该类派生虚函数。

当应用于成员函数时,标识符final立即出现在成员函数声明的语法或类定义中的成员函数定义的声明符之后。当应用于类时,标识符final出现在类定义的开头,紧接在类名之后。

override:指定虚函数覆盖另一个虚函数。

如果使用了标识符重写,则在成员函数声明的语法或类定义中的成员函数定义的语法中立即出现在声明符之后。

用final修饰的类(虚函数),不能被子类继承(重写);而override则是一定要重写父类虚函数,加上防止书写时产生手误;

test1

namespace Test07
{
    struct Base final
    {
        virtual void func()
        {
            std::cout << "base" << std::endl;
        }
    };
    // 编译失败,final修饰的类不可以被继承
    struct Derived : public Base
    {
        void func() override
        {
            std::cout << "derived" << std::endl;
        }
    };
}

int main(int argc, char **argv)
{

    system("pause");
    return 0;
}

结果

test2

namespace Test07
{
    struct Base
    {
        // 加在虚函数处
        virtual void func() final
        {
            std::cout << "base" << std::endl;
        }
    };
    // 编译失败,final修饰的虚函数不可以被重写
    struct Derived : public Base
    {
        void func() override
        {
            std::cout << "derived" << std::endl;
        }
    };
}

int main(int argc, char **argv)
{

    system("pause");
    return 0;
}

结果

test3

namespace Test07
{
    struct Base
    {
        virtual void func()
        {
            std::cout << "base" << std::endl;
        }
    };

    struct Derived : public Base
    { // 需要重写父类中的func函数,在子类中加上override,防止书写错误
        void func() override
        {
            std::cout << "derived" << std::endl;
        }

        // error,基类没有fu(),不可以被重写
        void fu() override
        { }
    };
}

int main(int argc, char **argv)
{

    system("pause");
    return 0;
}

结果

更多详见:final specifier (since C++11) - cppreference.com

更多详见:override specifier (since C++11) - cppreference.com

6 trailing return type

尾随返回类型,如果返回类型依赖于参数名,例如template<class T, class U> auto add(class T, class U) -> decltype(T + U);或者比较复杂,比如auto fpif(int)->int(*)(int)

如果使用模版,传入两个任意参数,求和:

template <typename T, typename Y>
XXX add(T v1, Y v2) 
{
    return v1 + v2;
}

那么,这里的返回值应当是什么类型呢?如果传入的两个参数类型不同,那么情况也是不同的:如果一个是int,一个是double,那么返回值就是double;如果一个是int,一个是short,那么返回值就是int;如果一个是char,一个是short,返回值则是int。可见,返回值类型是与v1+v2这—结果的类型相关的。

那么,可否直接把返回值也写成模板呢?如下所示:

template <typename R, typename T, typename Y>
R add(T v1, Y v2) 
{
    return v1 + v2;
}

编译没问题。

考虑使用decltype

namespace Test08
{
    template <typename T, typename Y>
    decltype(v1 + v2) add(T v1, Y v2)
    {
        return v1 + v2;
    }

}

int main(int argc, char **argv)
{
    Test08::add<int, double>(1, 1.1);
    system("pause");
    return 0;
}

这里的函数是返回值前置类型,而在推导decltype(v1+v2)时v1和v2还未定义,因此就会报错"未使模板专有化”。

 最后直接这样用:

template <typename T, typename Y>
auto add(T v1, Y v2) -> decltype(v1 + v2)
{
    return v1 + v2;
}

这样,就可以根据传入的参数类型来推导返回值类型了。

参考:模板函数——后置返回值类型(trailing return type)_模板函数返回值_HerofH_的博客-CSDN博客

更多详见:Function declaration - cppreference.com

7 rvalue references

7.1 左值与右值

在 C++ 或者 C 语言中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式。确切的说 C++ 中左值和右值的概念是从 C 语言继承过来的。

值得一提的是,左值的英文简写为“lvalue”,右值的英文简写为“rvalue”。很多人认为它们分别是"left value"、"right value" 的缩写,其实不然。lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 "read value",指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。

通常情况下,判断某个表达式是左值还是右值,最常用的有以下 2 种方法。

(1) 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。

int a = 5;
5 = a;          // 错误,5 不能为左值
int b = 10;     // b 是一个左值
a = b;          // a、b 都是左值,只不过将 b 可以当做右值使用

(2) 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。

以上面定义的变量 a、b 为例,a 和 b 是变量名,且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;反之,字面量 5、10,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此 5、10 都是右值。 

7.2 C++右值引用

前面提到,其实 C++98/03 标准中就有引用,使用 "&" 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++ 中的左值,无法对右值添加引用。举个例子:

int num = 10;
int &b = num;  // 正确
int &c = 10;   // 错误

为此,C++11标准中引入了另一种引用方式,成为右值引用,用"&&"表示。需要注意的,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:

int num = 10;
//int && a = num;  // 右值引用不能初始化为左值
int && a = 10;

和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:

int && a = 10;
a = 100;
cout << a << endl;

结果

表  C++左值引用和右值引用
        引用类型                                可以引用的值类型                使用场景
非常量左值常量左值非常量右值常量右值
非常量左值引用YNNN
常量左值引用YYYY常用于类中构建拷贝构造函数
非常量右值引用NNYN移动语义、完美转发
常量右值引用NNYY无实际用途

更多详见:Reference declaration - cppreference.com

8 move constructors and move assignment operators

8.1 移动构造函数

所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。

以String类为例:

class String {
public:
    String();                                   // 空构造函数
    String(const char* str);                    // 普通构造函数
    String(const String& other);                // 拷贝构造函数
    String& operator=(const String& other);     // 拷贝赋值函数
    ~String();                                  // 析构函数

    String(String&& other) noexcept;            // 移动构造函数
    String& operator=(String&& other) noexcept; // 移动赋值函数

    char& operator[](size_t index);             // 重载[]操作符
    bool operator==(const String& other);       // 重载==运算符
    String operator+(const String& other);      // 重载+运算符

    const char* c_str() const;                  // 转换成c风格的字符串
    size_t length();                            // 获得字符串长度

    friend std::ostream& operator << (std::ostream& out, const String& str);        // 友远重载<<运算符
    friend std::istream& operator >> (std::istream& in, String& str);               // 友元重载>>运算符
private:
    char* m_data;                        // 数组首地址
    size_t m_size;                       // 数组长度

};

String类的移动构造函数

// 移动构造函数
/**
 *  所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。
 *  简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。
 */
String::String(String&& other) noexcept {
    std::cout<<"移动构造函数的调用"<<std::endl;
    // 先清理原来的内存
    if (this->m_size != 0 && this->m_data != nullptr) {
        delete [] this->m_data;
        this->m_data = nullptr;
    }
    this->m_size = 0;

    // 再调用移动
    this->m_data = nullptr;                    // 将原data置空
    this->m_data = other.m_data;
    this->m_size = other.m_size;
    
    other.m_data = nullptr;                    // 将other的data置空
    other.m_size = 0;                          // 将other的size置0
}

8.2 移动赋值构造函数

移动赋值运算符执行与析构函数和移动构造函数相同的工作。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值。

String类的移动赋值函数

// 移动赋值函数
String& String::operator=(String&& other) noexcept {
    std::cout<<"移动赋值函数的调用"<<std::endl;
    // 如果不是自身
    if (&other != this) {
        // 先清理原来的内存
        if (this->m_size != 0 && this->m_data != nullptr) {
            delete [] this->m_data;
            this->m_data = nullptr;
    }   
        this->m_size = 0;
        // 再调用移动
        this->m_data = nullptr;                    // 将原data置空
        this->m_data = other.m_data;
        this->m_size = other.m_size;
    
        other.m_data = nullptr;                    // 将other的data置空
        other.m_size = 0;                          // 将other的size置0
    }
    return *this;
}

更多详见:Move constructors - cppreference.com

更多详见:Move assignment operator - cppreference.com

9 scoped enums

目前没用过,学习了一下。

不带作用于的枚举,即之前常规的枚举

namespace Test11
{
    enum AColor
    {
        kRed,
        kGreen,
        kBlue
    };
    enum BColor
    {
        kwhite,
        kBlack,
        kYellow
    };
    void test()
    {
        if (kRed == kwhite)
        {
            std::cout << "red == white" << std::endl;
        }
    }

}

int main(int argc, char **argv)
{
    Test11::test();
    system("pause");
    return 0;
}

结果会输出red == white

如上代码,不带作用域的枚举类型可以自动转换成整形,且不同的枚举可以相互比较,代码中的红色居然可以和白色比较,这都是潜在的难以调试的bug,而这种完全可以通过有作用域的枚举来规避。
有作用域的枚举代码:

namespace Test11
{
    enum class AColor
    {
        kRed,
        kGreen,
        kBlue
    };
    enum class BColor
    {
        kwhite,
        kBlack,
        kYellow
    };
    void test()
    {
        if (AColor::kRed == BColor::kwhite)
        {
            std::cout << "red == white" << std::endl;
        }
    }

}

int main(int argc, char **argv)
{
    Test11::test();
    system("pause");
    return 0;
}

使用带有作用域的枚举类型后,对不同的枚举进行比较会导致编译失败,消除潜在bug,同时带作用域的枚举类型可以选择底层类型,默认是int,可以改成char等别的类型。

平时编程过程中使用枚举,一定要使用有作用域的枚举取代传统的枚举。

更多详见:枚举声明 - cppreference.com

10 constexpr and literal types

10.1 constexpr

constexpr——指定变量或函数的值可以出现在常量表达式中

constexpr说明符声明可以在编译时计算函数或变量的值。这样的变量和函数可以在只允许编译时常量表达式的地方使用(前提是给出了适当的函数参数)。

对象声明或非静态成员函数(c++ 14之前)中使用的constexpr说明符暗示const。在函数或静态数据成员(c++ 17起)声明中使用的constexpr说明符意味着内联。如果函数或函数模板的任何声明都有constexpr说明符,则每个声明都必须包含该说明符。

C++ 程序的执行过程大致要经历编译、链接、运行这 3 个阶段。值得一提的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果;而常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。

constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。

10.2 constexpr修饰普通变量

C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。值得一提的是,使用 constexpr 修改普通变量时,变量必须经过初始化且初始值必须是一个常量表达式。举个例子:

namespace Test09
{
    void test()
    {
        constexpr int num = 1 + 2 + 3;
        int url[num] = {1, 2, 3, 4, 5, 6};
        std::cout << url[1] << std::endl;       // 输出2
    }
}

int main(int argc, char **argv)
{
    Test09::test();
    system("pause");
    return 0;
}

将 constexpr 删除,运行也正常

10.3 constexpr修饰函数

constexpr 还可以用于修饰函数的返回值,这样的函数又称为“常量表达式函数”。
注意,constexpr 并非可以修改任意函数的返回值。换句话说,一个函数要想成为常量表达式函数,必须满足如下 4 个条件。

(1) 整个函数的函数体中,除了可以包含 using 指令、typedef 语句以及 static_assert 断言外,只能包含一条 return 返回语句。

(2) 该函数必须有返回值,即函数的返回值类型不能是 void。

(3) 函数在使用之前,必须有对应的定义语句。我们知道,函数的使用分为“声明”和“定义”两部分,普通的函数调用只需要提前写好该函数的声明部分即可(函数的定义部分可以放在调用位置之后甚至其它文件中),但常量表达式函数在使用前,必须要有该函数的定义。

(4) return 返回的表达式必须是常量表达式

10.4 constexpr修饰类的构造函数

对于 C++ 内置类型的数据,可以直接用 constexpr 修饰,但如果是自定义的数据类型(用 struct 或者 class 实现),直接用 constexpr 修饰是不行的。

namespace Test10
{
    // 自定义类型的定义
    constexpr struct myType
    {
        const char *name;
        int age;
        // 其它结构体成员
    };
    void test()
    {
        constexpr struct myType mt
        {
            "zhangsan", 10
        };
        std::cout << mt.name << " " << mt.age << std::endl;
    }
}

int main(int argc, char **argv)
{
    Test10::test();
    system("pause");
    return 0;
}

想自定义一个可产生常量的类型时,正确的做法是在该类型的内部添加一个常量构造函数。例如,修改上面的错误示例如下:

namespace Test10
{
    // 自定义类型的定义
    struct myType
    {
        const char *name;
        int age;
        // 其它结构体成员
        constexpr myType(char *name,int age):name(name),age(age) {};
    };
    void test()
    {
        constexpr struct myType mt
        {
            "zhangsan", 10
        };
        std::cout << mt.name << " " << mt.age << std::endl;
    }
}

int main(int argc, char **argv)
{
    Test10::test();
    system("pause");
    return 0;
}

结果

更多详见:constexpr specifier (since C++11) - cppreference.com

11 list initialization

在 C++11 中,初始化列表的适用性被大大增加了。它现在可以用于任何类型对象的初始化,请看下面的代码。

namespace Test12
{
    class Foo
    {
    public:
        Foo(int) {}

    private:
        Foo(const Foo &);
    };

    void test()
    {
        Foo a1(123);
        // Foo a2 = 123; // error: 'Foo::Foo(const Foo &)' is private
        Foo a3 = {123};
        Foo a4{123};
        int a5 = {3};
        int a6{3};
    }
}

在上例中,a3、a4 使用了新的初始化方式来初始化对象,效果如同 a1 的直接初始化。
a5、a6 则是基本数据类型的列表初始化方式。可以看到,它们的形式都是统一的。
这里需要注意的是,a3 虽然使用了等于号,但它仍然是列表初始化,因此,私有的拷贝构造并不会影响到它。a4 和 a6 的写法,是 C++98/03 所不具备的。在 C++11 中,可以直接在变量名后面跟上初始化列表,来进行对象的初始化。

更多详见:List-initialization (since C++11) - cppreference.com

12 delegating and inherited constructors

12.1 委托构造函数

委托构造函数允许在同一个类中一个构造函数调用另外一个构造函数,可以在变量初始化时简化操作,通过代码来感受下委托构造函数的妙处吧:

不使用委托构造函数:

namespace Test13
{
    struct A
    {
        A() {}
        A(int a) { a_ = a; }
        A(int a, int b)
        {
            // 好麻烦
            a_ = a;
            b_ = b;
        }
        A(int a, int b, int c)
        { // 好麻烦
            a_ = a;
            b_ = b;
            c_ = c;
        }
        int a_;
        int b_;
        int c_;
    };
}

使用委托构造函数:

namespace Test13
{
    struct A
    {
        A() {}
        A(int a) { a_ = a; }
        // 使用委托构造函数
        A(int a, int b) : A(a)
        {
            b_ = b;
        }
        // 使用委托构造函数
        A(int a, int b, int c) : A(a, b)
        {
            c_ = c;
        }
        int a_;
        int b_;
        int c_;
    };
}

12.2 继承构造函数

继承构造函数可以让派生类直接使用基类的构造函数,如果有一个派生类,我们希望派生类采用和基类一样的构造方式,可以直接使用基类的构造函数,而不是再重新写一遍构造函数。

不使用继承构造函数

namespace Test13
{
    struct Base
    {
        Base() {}
        Base(int a) { a_ = a; }
        // 使用委托构造函数
        Base(int a, int b) : Base(a)
        {
            b_ = b;
        }
        // 使用委托构造函数
        Base(int a, int b, int c) : Base(a, b)
        {
            c_ = c;
        }
        int a_;
        int b_;
        int c_;
    };

    struct Derived : Base {
        Derived() {};
        Derived(int a) : Base(a) {};
        Derived(int a, int b) : Base(a, b) {};
        Derived(int a, int b, int c) : Base(a, b, c) {};
    };

    void test()
    {
        Derived d(1, 2, 3);
    }

}

使用继承构造函数

namespace Test13
{
    struct Base
    {
        Base() {}
        Base(int a) { a_ = a; }
        // 使用委托构造函数
        Base(int a, int b) : Base(a)
        {
            b_ = b;
        }
        // 使用委托构造函数
        Base(int a, int b, int c) : Base(a, b)
        {
            c_ = c;
        }
        int a_;
        int b_;
        int c_;
    };

    struct Derived : Base {
        using Base::Base;           // 确实简单
    };

    void test()
    {
        Derived d(1, 2, 3);
    }

}

只需要使用using Base::Base继承构造函数,就免去了很多重写代码的麻烦。

更多详见:Constructors and member initializer lists - cppreference.com

更多详见:Using-declaration - cppreference.com

13 brace-or-equal initializers

花括号或等号初始化器

变量的初始化会在构造时提供变量的初始值。

初始值可以由声明符或new表达式的初始化器部分提供。在函数调用时也会发生:函数形参及函数返回值也会被初始化。

对于每个声明符,初始化器必须是下列之一:

(表达式列表)
=表达式
{初始化器列表}

更多详见:Initialization - cppreference.com

14 nullptr

参考:C++中NULL和nullptr的区别_StudyWinter的博客-CSDN博客

更多详见:nullptr, the pointer literal (since C++11) - cppreference.com

15 long long

目标类型的宽度至少为64位。

如同 long 类型整数需明确标注 "L" 或者 "l" 后缀一样,要使用 long long 类型的整数,也必须标注对应的后缀:

  • 对于有符号 long long 整形,后缀用 "LL" 或者 "ll" 标识。例如,"10LL" 就表示有符号超长整数 10;
  • 对于无符号 long long 整形,后缀用 "ULL"、"ull"、"Ull" 或者 "uLL" 标识。例如,"10ULL" 就表示无符号超长整数 10;

对于任意一种数据类型,读者可能更关心的是此类型的取值范围。对于 long long 类型来说,如果想了解当前平台上 long long 整形的取值范围,可以使用<climits>头文件中与 long long 整形相关的 3 个宏,分别为 LLONG_MIN、LLONG_MAX 和 ULLONG_MIN:

  1. LLONG_MIN:代表当前平台上最小的 long long 类型整数;
  2. LLONG_MAX:代表当前平台上最大的 long long 类型整数;
  3. ULLONG_MIN:代表当前平台上最大的 unsigned long long 类型整数(无符号超长整型的最小值为 0);
namespace Test14
{
    void test()
    {
        std::cout << "long long max:" << LLONG_MIN << " " << LLONG_MIN << "\n";
        std::cout << "long long min:" << LLONG_MAX << " " << LLONG_MAX << "\n";
        std::cout << "unsigned long long max:" << ULLONG_MAX << " " << ULLONG_MAX << "\n";
    }
}

结果

 更多详见:Fundamental types - cppreference.com

16 char16_t and char32_t

char16_t -用于UTF-16字符表示的类型,需要足够大以表示任何UTF-16代码单元(16位)。它具有与std::uint_least16_t相同的大小、符号和对齐方式,但是是一个不同的类型。

char32_t -用于UTF-32字符表示的类型,需要足够大以表示任何UTF-32代码单元(32位)。它具有与std::uint_least32_t相同的大小、符号和对齐方式,但是是不同的类型。

更多详见: Fundamental types - cppreference.com

17 type aliases

类型别名是引用先前定义的类型的名称(类似于typedef)。

别名模板是一个引用类型族的名称。

别名声明是具有以下语法的声明:

(1)using identifier attr (optional) = type-id ;  

(2)template < template-parameter-list >

using identifier attr (optional) = type-id ;

例如:

// type alias, identical to
// typedef std::ios_base::fmtflags flags;
using flags = std::ios_base::fmtflags;
// the name 'flags' now denotes a type:
flags fl = std::ios_base::dec;

就是和typedef一样起别名。

更多详见:Type alias, alias template (since C++11) - cppreference.com

18 variadic templates

模板形参包是一个接受零个或多个模板实参(非类型、类型或模板)的模板形参。函数参数包是接受零个或多个函数实参的函数参数。至少有一个参数包的模板称为可变模板。

  • 可变参数模板(variadic template)为一个接受可变数目参数的模板函数或模板类
  • 参数包(parameter packet)可变数目的参数。
  • 模板参数包(template parameter packet)表示零个或多个模板参数
  • 函数参数包(function parameter packet)表示零个或多个函数参数

用省略号指出一个模板参数或函数参数表示一个包。

  • 用class...或 typename. ..指出接下来的参数表示零个或多个类型的列表。
  • 一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表(可以是一个函数的实参列表).

案例:

namespace Test16 {
    template<typename T, typename ... Args>
    void foo(const T& t, const Args&... rest) {
        std::cout << sizeof...(Args) << "------";
        std::cout << sizeof...(rest) << std::endl;
    }

    void test()
    {
        int i = 0;
        double d = 3.14;
        std::string s = "haha";
        foo(i, s, 42, d);     // 3 3
        foo(s, 42, "hi");
        foo(d, s);
        foo("hihihi");
        foo(d, "asdf", "asdfasdf");
    }

}
int main(int argc, char **argv)
{
    Test16::test();
    system("pause");
    return 0;
}

结果

 这里会把参数分成1个确定的和后面n-1个不确定的,因此

foo(i, s, 42, d);             // 1个确定的 和 后面3个
foo(s, 42, "hi");             // 1个确定的 和 后面2个
foo(d, s);                    // 1个确定的 和 后面1个
foo("hihihi");                // 1个确定的 和 后面0个
foo(d, "asdf", "asdfasdf");   // 1个确定的 和 后面2个

这里也可以看侯捷老师的视频。

更多详见:Parameter pack(since C++11) - cppreference.com

19 generalized (non-trivial) unions

C++11之前union中数据成员的类型不允许有非POD类型,而这个限制在c++11被取消,允许数据成员类型有非POD类型,看代码:

struct A {
    int a;
    int* b;
};

union U {
    A a;            // 非POD类型,c++11之前不可以这样定义结构体
    int c;
};

POD:什么是 POD 数据类型?_pod数据类型_致守的博客-CSDN博客

更多详见:Union declaration - cppreference.com

20 generalized PODs (trivial types and standard-layout types)

指定类型为POD (Plain Old Data)类型。这意味着该类型与C编程语言中使用的类型兼容,也就是说,可以直接以二进制形式与C库交换。

注意:标准没有用这个名字定义命名需求。这是由核心语言定义的类型类别。这里将其作为命名需求包含,只是为了保持一致性。

这种类型要求在c++标准中已被弃用。它的所有用途都已被更精细的类型需求所取代,例如TrivialType、ScalarType或StandardLayoutType。

更多详见:C++ named requirements: PODType (deprecated in C++20) - cppreference.com

21 Unicode string literals

更多详见:String literal - cppreference.com

22 user-defined literals

更多详见:User-defined literals (since C++11) - cppreference.com

23 attributes

更多详见:Attribute specifier sequence(since C++11) - cppreference.com

24 lambda expressions

参考:C++中的Lambda表达式详解_[&]() c++ 用法_StudyWinter的博客-CSDN博客

更多详见:Lambda expressions (since C++11) - cppreference.com

25 noexcept specifier and noexcept operator

noexcept-specification不是函数类型的一部分(就像动态异常规范),只能表现为说明符的一部分或一个顶级函数说明符声明函数时,变量,非静态数据成员类型的函数,函数指针,引用函数,或指向成员函数的指针,当声明的参数或返回类型的声明之一,又恰好是一个指针或引用函数。它不能出现在typedef或类型别名声明中。

比如下面就是std::initializer_list的默认构造函数,其中使用了noexcept。

constexpr initializer_list() noexcept
    : _M_array(0), _M_len(0) { }

该关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化。
如果在运行时,noexecpt函数向外抛出了异常(如果函数内部捕捉了异常并完成处理,这种情况不算抛出异常),程序会直接终止,调用std::terminate()函数,该函数内部会调用std::abort()终止程序。

使用noexcept表明函数或操作不会发生异常,会给编译器更大的优化空间。然而,并不是加上noexcept就能提高效率,步子迈大了也容易扯着蛋。
以下情形鼓励使用noexcept:

  • 移动构造函数(move constructor)
  • 移动分配函数(move assignment)
  • 析构函数(destructor)。这里提一句,在新版本的编译器中,析构函数是默认加上关键字noexcept的。下面代码可以检测编译器是否给析构函数加上关键字noexcept。

最后强调一句,在不是以上情况或者没把握的情况下,不要轻易使用noexception。

参考:C++11 带来的新特性 (3)—— 关键字noexcept - 翻书 - 博客园 (cnblogs.com)

更多详见:noexcept specifier (since C++11) - cppreference.com

更多详见:noexcept operator (since C++11) - cppreference.com

26 alignof and alignas

26.1 alignof

查询类型的对齐要求。

返回type-id表示的类型的任何实例所需的字节对齐方式。type-id可以是完整的对象类型、元素类型完整的数组类型或指向其中某个类型的引用类型。

如果类型是引用类型,该操作符返回引用类型的对齐;□如果是数组类型,则返回元素类型的对齐要求;

没用过,后续用到再学习。

更多详见:alignof operator (since C++11) - cppreference.com

更多详见:alignas specifier (since C++11) - cppreference.com

27 multithreaded memory model

更多详见:Memory model - cppreference.com

28 thread-local storage

C++11引入thread_local,用thread_local修饰的变量具有thread周期,每一个线程都拥有并只拥有一个该变量的独立实例,一般用于需要保证线程安全的函数中。

我的环境是vs code + minGw,mingw没有thread,解决方案:C++11 thread类在windows上无法使用。std 没有成员 thread、thread not member of std_Cris6866的博客-CSDN博客

#include <thread>
#include<mingw.thread.h>
// 加上这两个头文件

namespace Test18 {
    class A {
    public:
        A() {}
        virtual ~A() {};

        void foo(const std::string& name) {
            thread_local int count = 0;
            count++;
            std::cout << name << ": " << count << std::endl;
        }
    };

    void func(const std::string& name) {
        A a1;
        a1.foo(name);
        a1.foo(name);
        A a2;
        a2.foo(name);
        a2.foo(name);
    }

    void test() {
        std::thread(func, "thread1").join();
        std::thread(func, "thread2").join();
    }

}

int main(int argc, char **argv)
{
    Test18::test();
    system("pause");
    return 0;
}

结果

更多详见:Storage class specifiers - cppreference.com

29 GC interface (removed in C++23)

C++23已经移除

更多详见:Dynamic memory management - cppreference.com

30 range-for (based on a Boost library)

C++ 11 标准中,除了可以沿用前面介绍的用法外,还为 for 循环添加了一种全新的语法格式,如下所示:

for (declaration : expression){
    // 循环体
}

其中,两个参数各自的含义如下:

  • declaration:表示此处要定义一个变量,该变量的类型为要遍历序列中存储元素的类型。需要注意的是,C++ 11 标准中,declaration参数处定义的变量类型可以用 auto 关键字表示,该关键字可以使编译器自行推导该变量的数据类型。
  • expression:表示要遍历的序列,常见的可以为事先定义好的普通数组或者容器,还可以是用 {} 大括号初始化的序列。

案例

namespace Test19
{
    void test()
    {
        char arc[] = "http://123456789/cplus/11/";
        // for循环遍历普通数组
        for (char ch : arc)
        {
            std::cout << ch;
        }
        std::cout << '!' << std::endl;
        std::vector<char> myvector(arc, arc + 23);
        // for循环遍历 vector 容器
        for (auto ch : myvector)
        {
            std::cout << ch;
        }
        std::cout << '!';
    }

}
int main(int argc, char **argv)
{
    Test19::test();
    system("pause");
    return 0;
}

更多详见:Range-based for loop (since C++11) - cppreference.com

31 static_assert (based on a Boost library)

更多详见:static_assert declaration (since C++11) - cppreference.com

C++11引入了三种智能旨针:

  • std:shared_ptr
  • std:weak_ptr
  • std:unique_ptr

库功能特性

标头

库功能特性

参考文献

[1]C++11 - cppreference.com

[2]c++11新特性,所有知识点都在这了! - 知乎 (zhihu.com)

[3]C++ 11是什么,C++ 11标准的由来 (biancheng.net)

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++11引入了许多新特性,其中包括线程池的实现。在引用中的代码中,ZERO_ThreadPool类封装了线程池的功能。线程池的原理是通过维护一个线程队列和一个任务队列来实现的。 在初始化阶段,通过调用init函数来初始化线程池。该函数会创建指定数量的线程,并将其保存在threads_队列中。如果线程池已经被初始化过,则直接返回false。 在启动线程池后,调用start函数。该函数会循环创建指定数量的线程,并将它们放入threads_队列中。每个线程都会调用run函数来执行任务。 当调用exec函数时,会将任务添加到tasks_队列中。其中,std::bind用于绑定一个成员函数和其参数,以及占位符std::placeholders::_1表示传入的参数。 在waitForAllDone函数中,会判断atomic_是否为0且tasks_是否为空。如果是,则表示所有任务已经执行完毕,线程池可以退出。 线程池的stop函数用于停止线程池的运行。它会遍历threads_队列,并调用每个线程的join函数,等待线程执行完毕后再返回。 以上就是C++11新特性线程池的基本原理。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [基于C++11新特性手写线程池实现](https://blog.csdn.net/m0_70418130/article/details/126805390)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值