C++11新特性

nullptr

nullptr 出现的目的是为了替代 NULL。传统 C++ 会把 NULL, 0 视为同一种东西,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为 0。C++ 不允许直接将 void 隐式转换到其他类型,但如果 NULL 被定义为 **((void)0),那么当编译 char ch = NULL;* 时,NULL 只好被定义为 0。而这依然会产生问题,将导致了 C++ 中重载特性会发生混乱,考虑:

1
2
void foo(char *);
void foo(int);

对于这两个函数来说,如果 NULL 又被定义为了 0 那么 foo(NULL); 这个语句将会去调用 foo(int),从而导致代码违反直观。为了解决这个问题,C++11 引入了 nullptr 关键字,专门用来区分空指针、0。nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。

类型推导

C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导。

  1. auto: 使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器,可以使代码变得十分简洁。但需要注意的是 auto 不能用于函数传参,不能用于推导数组类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 迭代器
    for (vector<int>::const_iterator itr = v.cbegin(); itr != v.cend(); ++itr)
    // auto 类型推导
    for (auto itr = v.cbegin(); itr != v.cend(); ++itr)
    // 不能用于函数传参
    //! int add(auto x, auto y);
    // auto 不能用于推导数组
    //! int a = {1, 2, 3};
    //! auto b = a;
  2. decltype: decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷。它的用法和 sizeof 很相似: decltype(表达式) 在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值

    1
    2
    3
    auto x = 1;
    auto y = 2;
    decltype(x + y) z;
  3. 拖尾返回类型
    1
    2
    template<typename R, typename T, typename U>
    R add(T x, U y) { return x + y; }
    很多时候我们并不知道 add() 这个函数会做什么样的操作,获得一个什么样的返回类型。如果用 decltype(x + y) add(T x, U y); 来进行类型推导的话,并不能通过编译。
    这是因为在编译器读到 decltype(x + y) 时, x 和 y 尚未被定义。为了解决这个问题,C++11 还引入了一个叫做 拖尾返回类型,利用 auto 关键字将返回类型后置:
    1
    2
    3
    4
    template<typename T, typename U>
    auto add(T x, U y) -> decltype(x + y) {
    return x + y;
    }
    而从 C++14 开始,普通函数具备返回值推导的能力:
    1
    2
    template<typename T, typename U>
    auto add(T x, U y) { return x + y; }

模板增强

  1. 外部模板:传统 C++ 中,模板只有在使用时才会被编译器实例化。只要在每个编译单元(文件)中编译的代码中遇到了被完整定义的模板,都会实例化。这就产生了重复实例化而导致的编译时间的增加。并且,我们没有办法通知编译器不要触发模板实例化。C++11 引入了外部模板,扩充了原来的强制编译器在特定位置实例化模板的语法,使得能够显式的告诉编译器何时进行模板的实例化:

    1
    2
    3
    4
    5
    6
    7
        template class std::vector<bool>;   // 强行实例化
    extern template class std::vector<double>; // 不在该编译文件中实例化模板
    ```
    2. 尖括号 '>':在传统 C++ 的编译器中,>> 一律被当做右移运算符来进行处理。因此对于嵌套模板来说是不能够被编译的,而 C++11 开始,连续的右尖括号将变得合法,并且能够顺利通过编译。

    ```cpp
    std::vector<std::vector<int>> v;
  2. 类型别名模板:在传统 C++中,typedef 可以为类型定义一个新的名称,但却没有办法为模板定义一个新的名称,因为 模板不是类型。于是 C++11 引入了 using,并且同时支持对传统 typedef 相同的作用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    template<typename T, typename U, int value>
    class SuckType {
    public:
    T a;
    U b;
    SuckType(): a(value), b(value){}
    };
    // 不合法
    template<typename U>
    typedef SuckType<std::vector<int>, U, 1> NewType;
    // 合法
    template<typename T>
    using NewType = SuckType<int, T, 1>;
  3. 默认模板参数:在 C++11 中提供了一种便利,可以指定模板的默认参数。

    1
    2
    3
    4
    template<typename T = int, typename U = int>
    auto add(T x, U y) -> decltype(x + y) {
    return x + y;
    }
  4. 变长参数模板:在 C++11 之前,无论是类模板还是函数模板,都只能按其指定的样子,接受一组固定数量的模板参数;而 C++11 加入了新的表示方法,允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定。

    1
    2
    3
    template<typename... Ts> class Magic;
    // 可以指定模板参数以要求模板参数的个数至少为1
    template<typename Require, typename... Args> class Magic;

    构造函数

  5. 委托构造:C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的。

    1
    2
    3
    4
    5
    6
    7
    class Base {
    public:
    int v1;
    int v2;
    Base() { v1 = 1; }
    Base(int v) : Base() { v2 = 2; } // 委托 Base() 构造函数
    }
  6. 继承构造:在继承体系中,如果派生类想要使用基类的构造函数,需要在构造函数中显式声明。假若基类拥有为数众多的不同版本的构造函数,这样,在派生类中得写很多对应的“透传”构造函数。在 C++11 中,只要一句话搞定。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct B:A{
    // 太麻烦
    B(int i):A(i){}
    B(double d, int i){}
    ....
    }

    struct B:A {
    // 一句话搞定
    using A::A;
    }

    如果一个继承构造函数不被相关的代码使用,编译器就不会为之产生真正的函数代码,这样比透传基类各种构造函数更加节省目标代码空间。

右值引用

新增容器

  1. std::array:保存在栈内存中,相比堆内存中的 std::vector,性能更高。

    1
    2
    3
    int len = 4;
    std::array<int, 4> a = {1, 2, 3, 4};
    std::array<int, len> a = {1, 2, 3, 4}; // 非法,数组大小必为常量表达式
  2. std::forward_list 是一个列表容器,使用方法和 std::list 基本类似。和 std::list 的双向链表的实现不同,std::forward_list 使用单向链表进 行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问(这也是链表的 特点),也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向 迭代时,具有比 std::list 更高的空间利用率。
  3. 无序容器:C++11 引入了两组无序容器: std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的 平均复杂度为 O(constant)。
  4. 元组:std::tuple 元组的使用有三个核心的函数:
    • std::make_tuple: 构造元组
    • std::get: 获得元组某个位置的值
    • std::tie: 元组拆包

explicit 关键字

explicit主要是用来修饰类的构造函数,从而使被构造的类只能发生显示转换,而不能进行隐式转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;
class Test1{
public:
Test1(int n){ // 隐式构造函数
num = n;
}
private:
int num;
};
class Test2{
public:
explicit Test2(int n){ //explicit(显式)构造函数
num = n;
}
private:
int num;
};
int main(){
Test1 t1 = 10; // 隐式转化
//等同于 Test1 temp(10); Test1 t1 = temp;

Test1 t2 = 'c'; // 'c'被转化为ascii码,然后同上

Test2 t3 = 12; // 编译错误,不能隐式调用其构造函数

Test2 t4 = 'c'; // 编译错误,不能隐式调用其构造函数

Test2 t5(10); // 正常的显式转化
return 0;
}

explicit关键字只用于类的单参数构造函数,对于无参数和多参数的构造函数总是显示调用,因此使用explicit没有意义。通常情况下,我们约定对于单参数构造函数必须使用explicit关键字,避免产生意外的类型转化,拷贝构造函数除外。

constexpr关键字

constexpr 关键字是 C++11 新增的关键字,其语义是“常量表达式”,也就是在编译期可求值的表达式。

对于constexpr修饰的函数:

  1. 函数体一般只包含一个return语句;
  2. 函数体可以包含其他语句,但是不能是运行期语句,只能是编译期语句;
1
2
3
4
5
6
7
constexpr int Inc(int i) {
return i + 1;
}

constexpr int a = Inc(1); // ok
constexpr int b = Inc(cin.get()); // !error
constexpr int c = a * 2 + 1; // ok

constexpr还能用于修饰类的构造函数,即保证如果提供给该构造函数的参数都是constexpr,那么产生的对象中的所有成员都会是constexpr,该对象也就是constexpr对象了,可用于各种只能使用constexpr的场合。注意,constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。

1
2
3
4
5
6
7
struct A {
constexpr A(int xx, int yy): x(xx), y(yy) {}
int x, y;
};

constexpr A a(1, 2);
enum {SIZE_X = a.x, SIZE_Y = a.y};

好处:

  1. 是一种很强的约束,更好地保证程序的正确语义不被破坏。
  2. 编译器可以在编译期对constexpr的代码进行非常大的优化,比如将用到的constexpr表达式都直接替换成最终结果等。
  3. 相比宏来说,没有额外的开销,但更安全可靠。

区别

const 和 constexpr 变量之间的主要区别在于:const 变量的初始化可以延迟到运行时,而 constexpr 变量必须在编译时进行初始化。所有 constexpr 变量均为常量,因此必须使用常量表达式初始化。

修饰指针

在使用const时,如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针本身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

与const不同,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
using namespace std;

int g_tempA = 4;
const int g_conTempA = 4;
constexpr int g_conexprTempA = 4;

int main(void)
{
int tempA = 4;
const int conTempA = 4;
constexpr int conexprTempA = 4;

/*1.正常运行,编译通过*/
const int *conptrA = &tempA;
const int *conptrB = &conTempA;
const int *conptrC = &conexprTempA;

/*2.局部变量的地址要运行时才能确认,故不能在编译期决定,编译不过*/
constexpr int *conexprPtrA = &tempA;
constexpr int *conexprPtrB = &conTempA;
constexpr int *conexprPtrC = &conexprTempA;

/*3.第一个通过,后面两个不过,因为constexpr int *所限定的是指针是常量,故不能将常量的地址赋给顶层const*/
constexpr int *conexprPtrD = &g_tempA;
constexpr int *conexprPtrE = &g_conTempA;
constexpr int *conexprPtrF = &g_conexprTempA;

/*4.局部变量的地址要运行时才能确认,故不能在编译期决定,编译不过*/
constexpr const int *conexprConPtrA = &tempA;
constexpr const int *conexprConPtrB = &conTempA;
constexpr const int *conexprConPtrC = &conexprTempA;

/*5.正常运行,编译通过*/
constexpr const int *conexprConPtrD = &g_tempA;
constexpr const int *conexprConPtrE = &g_conTempA;
constexpr const int *conexprConPtrF = &g_conexprTempA;

return 0;
}

修饰引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;

int g_tempA = 4;
const int g_conTempA = 4;
constexpr int g_conexprTempA = 4;

int main(void)
{
int tempA = 4;
const int conTempA = 4;
constexpr int conexprTempA = 4;
/*1.正常运行,编译通过*/
const int &conptrA = tempA;
const int &conptrB = conTempA;
const int &conptrC = conexprTempA;

/*2.有两个问题:一是引用到局部变量,不能在编译器确定;二是conexprPtrB和conexprPtrC应该为constexpr const类型,编译不过*/
constexpr int &conexprPtrA = tempA;
constexpr int &conexprPtrB = conTempA;
constexpr int &conexprPtrC = conexprTempA;

/*3.第一个编译通过,后两个不通过,原因是因为conexprPtrE和conexprPtrF应该为constexpr const类型*/
constexpr int &conexprPtrD = g_tempA;
constexpr int &conexprPtrE = g_conTempA;
constexpr int &conexprPtrF = g_conexprTempA;

/*4.正常运行,编译通过*/
constexpr const int &conexprConPtrD = g_tempA;
constexpr const int &conexprConPtrE = g_conTempA;
constexpr const int &conexprConPtrF = g_conexprTempA;

return 0;
}

简单的说 constexpr 所引用的对象必须在编译期就决定地址。可以通过上例 conexprPtrD 来修改 g_tempA 的值,也就是说 constexpr 修饰的引用不是常量,如果要确保常量引用需要 constexpr const 来修饰。

作者

Benboby

发布于

2020-05-01

更新于

2021-02-01

许可协议

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×