C++11新关键字

alignas:指定对齐大小

alignof:获取对齐大小

decltype

auto(重新定义):可作为返回值类型后置时的占位符

static_assert:静态断言

using(重新定义):类型别名或者模板别名

noexcept:声明函数不可以抛出任何异常

export(弃用,不过未来可能留作他用)

nullptr

constexpr:可在在编译期确认的常量表达式

thread_local:等价于TLS

快速初始化成员变量

C++11中支持使用等号 = 或者花括号 {} 进行就地的(也就是在声明的时候)非静态成员变量初始化。例如:

struct init{
    int a = 1;
    double b {1.2};
}

在C++11标准支持了就地初始化非静态成员的同时,初始化列表这个手段也被保留下来了。只不过初始化列表总是看起来“后作用于”非静态成员。

final/override控制

C++11提供关键字final,作用是使派生类不可覆盖它所修饰的虚函数。final关键字也可 用于基类中,但是这样定义的虚函数就没有意义了。final通常就是在继承关系的“中途”终止派生类的重载。

在C++中对于基类声明为virtual的函数,之后的重载版本都不需要再声明该重载函数为virtual。即使在派生类中声明了virtual,该关键字也是编译器可以忽略的。另外,有的虚函数会“跨层”,没有在父类中声明的接口有可能是祖先的虚函数接口。所以C\++11引入了虚函数描述符override,来帮助程序员写继承结构复杂的类型。如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数,否则代码将无法通过编译。

继承构造函数

C++11提供了继承构造函数,在派生类中使用using声明,就可以把基类中的构造函数继承到派生类中。 但其实质是编译器自动生成代码,通过调用父类构造函数来实现,不是真正意义上的“继承”,仅仅是为了减少代码书写量。

class A {
    A(int i) {}
    A(double d, int i) {}
    A(char *c , double d, int i) {}
    //... 更多构造函数
};

class B : A {
    using A::A; //继承构造函数
    //...
}

委派构造函数

C++11提供委派构造函数,可以简化多构造函数的类的编写。如果我们能将一个构造函数设定为“基准版本”,则其他构造函数可以通过委派“基准版本”来进行初始化。我们将这个“基准版本”称为目标构造函数。

class Info {
public:
    Info() { InitRest(); }//目标构造函数
    Info(int i) : Info() { type = i; }//委派构造函数
    Info(char c) : Info() { name = c; }//委派构造函数
    
private:
    void InitRest(); { /* 其他初始化 */}
    int type {1};
    char name {'a'};
    //...
}

注意:委派构造函数不能有初始化列表。如果委派构造函数要给变量赋初值,初始化代码必须放在函数体中。

在使用委派构造函数时,建议程序员抽象出最为“通用”的行为作为目标构造函数。

移动语义

拷贝构造与移动构造
在C++11中,这样的“偷走”临时变量中资源的构造函数,被称为移动构造函数。而这样的“偷”的行为,则称之为“移动语义”。

class HasPtrMem(){
public:
    HasPtrMem() : d(new int(3)) {}
    HasPtrMem(const HasPtrMem & h) : d(new int(*h.d)) {}// 拷贝构造函数
    HasPtrMem(HasPtrMem && h) : d(h.d) {h.d = nullptr;}// 移动构造函数,需将临时值的指针成员置空
    ~HasPtrMem() {delete d;}
private:
    int *d;
}

左值,右值,右值引用

关于左值,右值很难去做一个非常明确的定义,但是在C++中有一个被广泛认同的说法,那就是可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。

无论是声明一个左值引用还是右值引用,都必须立即进行初始化。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。

标准库在<type_traits>头文件中提供了3个模板类:is_rvalue_reference、is_lvalue_reference、is_reference,可供我们进行判断引用类型是左值引用还是右值引用。

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

C++11中提供了std::move这个函数使我们能将一个左值强制转化为右值引用,继而我们可以通过右值引用使用该值,以用于移动语义。

// 使用上面例子中的HasPtrMem
HasPtrMem a;
HasPtrMem b(std::move(a));// 调用移动构造函数

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

显示转换操作符

在C++中,有一个非常好也非常坏的特性,就是隐式类型转换。隐式类型转换的“自动性”可以让程序员免于层层构造类型。但也是由于它的自动性,会在一些程序员意想不到的地方出现严重的但不易被发现的错误。

关键字explicit主要用于修饰的构造函数,其作用主要就是阻止构造函数的隐式转换。

C\++11中将explicit的使用范围扩展到了自定义的类型转换操作符上,以支持所谓的“显示类型转换”。

列表初始化

在C\++11中可以使用花括号“{}”来进行初始化,这种初始化方式被称为列表初始化,已经称为C++语言的一个基本功能。我们甚至可以使用列表初始化的方式对vector、map等非内置的复杂的数据类型进行初始化。

而且,C++11中,标准总是倾向于使用更为通用的方式来支持新的特性。标准模板库中容器对初始化列表的支持源自<initilizer_list>这个头文件中的initilizer_list类模板的支持。我们可以通过声明已initilizer_list<T>模板类为参数的构造函数,来使得自定义的类使用列表初始化。

enum Gender {boy, girl};
class People{
public:
    People(initilizer_list<pair<string, Gender>> l) {//initilizer_list的构造函数
        auto i = l.begin();
        for (; i != l.end(); ++i)
            data.pushback(*i);
    }
private:
    vector<pair<string, Gender>> data;
};

People ship2012 = {{"Garfield", boy}, {"HelloKitty", girl}};

另外,使用列表初始化可以有效的防止类型收窄。类型收窄一般是指一些可以使得数据变化或者精度丢失的隐式类型转换。

auto类型推导

auto声明的变量必须被初始化,以使编译器能够从其初始化表达式中推导出其类型。

auto使用时需注意:

(1)、可以使用const、volatile、pointer(*)、reference(&)、rvalue reference(&&)等说明符和声明符来修饰auto关键字;

(2)、用auto声明的变量必须初始化;

(3)、auto不能与其它任何类型说明符一起使用;

(4)、方法、参数或模板参数不能被声明为auto;

(5)、定义在堆上的变量,使用了auto的表达式必须被初始化;

(6)、auto是一个占位符,不是类型,不能用于类型转换或其它一些操作,如sizeof、typeid;

(7)、auto关键字内声明的声明符列表的所有符号必须解析为同一类型;

(8)、auto不能自动推导成CV-qualifiers(constant& volatile qualifiers),除非被声明为引用类型;

(9)、auto会退化成指向数组的指针,除非被声明为引用;

(10)、auto不能作为函数的返回型,在C++14中是可以的。

decltype

decltype的类型推导并不是像auto一样是从变量声明的初始化表达式获得变量的类型,decltype总是以一个普通的表达式为参数,返回该表达式的类型。而与auto相同的是,作为一个类型指示符,decltype可以将获得的类型来定义另外一个变量。与auto相同,decltype类型推导也是在编译时进行的。

decltype只能接受表达式做参数,像函数名做参数的表达式decltype(hash)这种是无法通过编译的。

追踪返回类型

利用auto和decltype以及返回类型后置的语法就能实现追踪返回类型。比如,我们想写一个泛型的加法函数时可能直观的写下如下代码。

template <typename T1, typename T2>
decltype(t1 + t2) sum(const T1& t1, const T2& t2) {
   return t1 + t2;
}

但是,这样写是编译不过的,因为编译器只会从左往右地读入符号,这里的t1和t2在编译器看来都是未声明的。正确的写法是这样的。

template <typename T1, typename T2>
auto sum(const T1& t1, const T2 & t2) -> decltype(t1 + t2) {
    return t1 + t2;
}

基于范围的for循环

语法很简单,不赘述。主要使用时需要注意两点。一是使用基于范围的for循环需要for循环迭代的范围是可确定的。二是,基于范围的for循环要求迭代对象实现++和==等操作符,这点标准库中的容器不会有问题,但用户自定义的类需要自己实现。

强类型枚举

原来的枚举类型有非强类型作用域,允许隐式转换为整型,占用存储控件及符号性不确定的缺点。C++11引入了强类型枚举来解决问题。

声明强类型枚举只需要在enum后加上关键字class。比如:

enum class Type { General, Light, Medium, Heavy};

强类型枚举具有一下几点优势:

  • 强作用域,强类型枚举成员的名称不会输出到其父作用域空间。
  • 转换限制,强类型枚举成员的值不可以与整型隐式地相互转换。
  • 可以指定底层类型。强类型枚举默认的底层类型为int,但也可以显式地指定底层类型,具体方法为在枚举名称后面加上":type",其中type可以是除了wchar_t以外的任何整型。比如:

enum class Type: char { General, Light, Medium, Heavy};

由于enum class是强类型作用域的,故匿名的enum class很可能什么都做不了。

常量表达式函数

常量表达式函数需要满足几个条件,否则不能用constexpr关键字进行修饰:

  • 函数只能包含return语句。
  • 函数必须有返回值。
  • 在使用前必须已经定义。
  • return返回语句中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式。

constexpr int GetConst() { return 1; }

常量表达式值

const int i = 0;

constexpr int j = 0;

constexpr表示的就是编译期常量,const表示的是运行期常量。

大部分情况下这两个定义是没有区别的。不过i只要在全局范围内声明,编译器一定会为它产生数据;而对于j,如果没有地方调用它,编译器可以选择不为它生成数据。

并且,默认只有内置类型才能修饰为常量表达式值,自定义类型如果要成为常量表达式值,必须定义一个constexpr修饰的构造函数。

tuple元组

当我们希望将一些数据组合成单一对象,但又不想麻烦地定义一个新的数据结构来表示这些数据时,tuple是非常有用的。一般,tuple可以用于函数返回多个返回值。

tuple容器, 可以使用直接初始化, 和"make_tuple()"初始化, 访问元素使用"get<>()"方法, 注意get里面的位置信息, 必须是常量表达式(const expression)。

可以通过"std::tuple_size<decltype(t)>::value"获取元素数量; "std::tuple_element<0, decltype(t)>::type"获取元素类型。

如果tuple类型进行比较, 则需要保持元素数量相同, 类型可以比较。

#include <iostream>  
#include <vector>  
#include <string>  
#include <tuple>  
  
using namespace std;  
  
std::tuple<std::string, int>  
giveName(void)  
{  
    std::string cw("Caroline");  
    int a(2013);  
    std::tuple<std::string, int> t = std::make_tuple(cw, a);  
    return t;  
}  
  
int main()  
{  
    std::tuple<int, double, std::string> t(64, 128.0, "Caroline");  
    std::tuple<std::string, std::string, int> t2 =  
            std::make_tuple("Caroline", "Wendy", 1992);  
  
    //返回元素个数  
    size_t num = std::tuple_size<decltype(t)>::value;  
    std::cout << "num = " << num << std::endl;  
  
    //获取第1个值的元素类型  
    std::tuple_element<1, decltype(t)>::type cnt = std::get<1>(t);  
    std::cout << "cnt = " << cnt << std::endl;  
  
    //比较  
    std::tuple<int, int> ti(24, 48);  
    std::tuple<double, double> td(28.0, 56.0);  
    bool b = (ti < td);  
    std::cout << "b = " << b << std::endl;  
  
    //tuple作为返回值  
    auto a = giveName();  
    std::cout << "name: " << get<0>(a)  
            << " years: " << get<1>(a) << std::endl;  
  
    return 0;  
}  

nullptr

在C++11中,nullptr是一个所谓“指针空值类型”的编译期常量。指针空值类型被命名为nullptr_t。

需要注意的是,nullptr是C++11中的关键字,它是有类型的,且仅可以被隐式转化为指针类型,其类型定义是:

typedef decltype(nullptr) nullptr_t;

lambda函数

lambda表达式的语法定义如下:

\[capture] (parameters) mutable ->return-type {statement};

  • \[capture]:捕捉列表。捕捉列表总是出现在lambda函数的开始处。实质上,[]是lambda引出符(即独特的标志符),编译器根据该引出符判断接下来的代码是否是lambda函数。捕捉列表能够捕捉上下文中的变量以供lambda函数使用。捕捉列表由一个或多个捕捉项组成,并以逗号分隔,捕捉列表一般有以下几种形式:

    • [var] 表示值传递方式捕捉变量var。
    • [=] 表示值传递方式捕捉所有父作用域的变量(包括this指针)。
    • [&var] 表示引用传递捕捉变量var。
    • [&] 表示引用传递捕捉所有父作用域的变量(包括this指针)。
    • [this]表示值传递方式捕捉当前的this指针。
    • [=,&a,&b]表示以引用传递的方式捕捉变量 a 和 b,而以值传递方式捕捉其他所有的变量。
    • [&,a,this]表示以值传递的方式捕捉 a 和 this,而以引用传递方式捕捉其他所有变量。
备注:父作用域是指包含lambda函数的语句块。
  • (parameters):参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号()一起省略。
  • mutable :mutable修饰符。默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。
  • ->return-type :返回类型。用追踪返回类型形式声明函数的返回类型。出于方便,不需要返回值的时候也可以连同符号->一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
  • {statement} :函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
// 简单示例
int a = 20, b = 10;

auto totalAB = [] (int x, int y)->int{ return x + y; };
int aAddb = totalAB(a, b);
cout << "aAddb :" << aAddb << endl;

// lambda与STL
vector<int> v{ 1, 2, 3, 4, 5 }; 
  
for_each( v.begin(), v.end(), [] (int val)  
{  
    cout << val;  
} );  

在现阶段,通常编译器都会把lambda函数转化为一个仿函数对象。这是编译器实现lambda函数的一种方式。因此,C++11中,lambda函数可以视为仿函数的一种等价形式。

原生字符串字面量

原生字符串字面量的意思就是所见即所得,在代码中的字符串常量是怎么样的,我们得到的就是怎么样的,不需要转义字符来控制特定的字符。

在C++11中程序员只需要在字符串前面加入前缀字母R,并在引号中使用括号左右标识即可将该字符串声明为原生字符串了。

// 输出带引号的字符串"你好"
int main()
{
    std::string str = "\"你好\"";
    std::cout << str << std::endl;

    const char* str1 = R"("你好")";
    std::cout << str1 << std::endl;

    return 0;
}

吴尼玛
32 声望11 粉丝

记问之学