# C++基础17-C++11新标准

本文是《C++ Primer Plus》的笔记,本文中的案例均自己实践过,如需转发请在转发开头贴上原文地址,谢谢!

# 复习前面的C++11新标准特性

# 新类型

C++11新增了类型long long和unsigned long long,以支持64位(或更宽)的整形;新增了类型char16_t和char32_t,以支持16位和32位的字符表示;新增了"原始"字符串。


int main()
{
    wchar_t wchar = L'世';
    //const char c1 = u8'你';
    //UTF-8字符文本值不能占用多个代码单元,在c++14中deprecated,在c++17中removed
    char16_t char16 = u'你';
    char32_t char32 = U'好';
    long long longValue = 10;
    unsigned long long longlongValue = 20;
    string path = R"(C:\windows)";
    string path2 = R"+!(this is a self defined string)+!";
    cout << path << endl << path2 << endl;
    //setlocale(LC_ALL, "");
    //wcout.imbue(locale("chs"));
    setlocale(LC_ALL, "chs");
    wcout.put(wchar);
    wcout << char16 << endl;
    wcout.put(char16);
    wcout.put(char32);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 统一初始化

C++11扩大了大括号括起的列表(初始化列表)的使用范围,使其可用于所有内置类型和用户定义类型(类对象)。使用初始化列表时,可添加等号,也可不添加。

# 缩窄

初始化列表可防止缩窄,即禁止将数值赋给无法存储它的数值数量。常规初始化允许程序员执行可能没有意义的操作。

int main()
{
    char c1 = 1.57e25;
    char c2{7373737373};
}
上面会提示error C2397: 从“__int64”转换到“char”需要收缩转换。
1
2
3
4
5
6

# initializer_list

可将其用作构造函数的参数,如果类有接受initializer_list作为参数的构造函数,则初始化列表语法只能用于该构造函数。列表中的元素必须是同一种类型或可转换为同一种类型。

# 声明

# auto

之前关键字是一个存储类型说明符,C++11将其用于实现自动类型推断。但是它要求显示初始化,让编译器能够将变量的类型设置为初始值的类型。另外auto还能简化模板声明。

# decltype

关键字decltype将变量类型声明为表达式指定的类型。指定的类型可以为引用和const。

# 返回类型后置

C++11新增一种函数声明语法:在函数名和参数列表后面指定返回类型。它能够使用decltype来指定模板函数的返回类型。

template<typename T,Typename U>
auto calc(T t,U u)->decltype(T*U){

}
1
2
3
4

# 模板别名

using=和之前的typedef类似,他们的差别在于新语法可用于模板部分具体化,但typedef不能。


template<typename T>
    using arrInt = array<T, 12>;
//template<typename T>
//typedef std::array<T,12>::iterator itType;
//上面定义错误
int main()
{
    arrInt<double> a1;
}

1
2
3
4
5
6
7
8
9
10
11

# nullptr

空指针是不会指向有效数据的指针,以前C++在源代码中使用0来表示这种指针,这使得0既表示指针常量,又表示整形常量,这使得语义可能会不清楚。C++11增加了关键字nullptr,用于表示空指针,它是指针类型,不能转换为整形类型。为向后兼容,C++11仍允许使用0来表示空指针,因此表达式nullptr==0为true,但使用nullptr而不是0提供了更高的类型安全。比如把0传递给接受int参数的函数,但如果试图将nullptr传递给这样的函数,编译器会将此视为错误,所以出于清晰和安全,请使用nullptr吧。

# 智能指针

在程序中如果使用new从堆(自由存储区)分配内存,等到不再需要时,应使用delete自动将其释放。这便是智能指针的概念,C++引入了auto_ptr,后基于程序员编程体验和BOOST库提供的解决方案,C++11摒弃了auto_ptr,新增了三种智能指针,unique_ptr,shared_ptr和weak_ptr,所有新增的智能指针都能与STL容器和移动语义协同工作。

# 异常规范

C++提供了一种语法,用于指出函数可能引发哪些异常

void f1(int) throw(exception1);
void f2(long) throw();
void f3(double) noexcept;
1
2
3

C++编程社区集体经验表明,异常规范效果没有预期好,因此C++将摒弃该异常规范,然而标准委员会认为,指出函数不会引发异常有一定的价值,为此添加了关键字noexcept。

# 作用域内枚举

传统C++枚举提供了一种创建名称常量的方式,但类型检查相当低级。枚举名的作用域为枚举定义所属的作用域,这意味着如果同一个作用域内定义两个枚举,他们的枚举成员不能同名,这可能导致枚举不是可完全移植的。为解决这个问题,C++提供了一种作用域枚举,用class或struct定义:

enum OldVersion {yes,no};
enum class Color {white,black,green};
enum struct Color2 {white,purple};
1
2
3

新枚举要求使用显式限定,引用特定枚举时,需要使用Color::white,Color2::white。

# 对类的修改

为简化和扩展类设计,C++11做了多项改进。这包括允许构造函数被继承和彼此调用,更佳的方法访问控制方式以及移动构造函数和移动赋值运算符。

# 显式转换运算符


class Number {

public:
    Number() {
        cout << "generate a number." << endl;
    }
    Number(int a) {
        cout << "int:" << a << endl;
    };
    explicit Number(double a){
        cout << "double:" << a << endl;
    };
    operator int() {
        cout << "convert to Int" << endl;
        return 0;
    }
    explicit operator double() {
        cout << "convert to double" << endl;
        return 0;
    }
    ~Number() {};
};

int main()
{
    Number num1, num2;
    num1 = 1;
    num2 = Number(5.2);
    int c = num1;
    double d = double(num2);
}

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

# 类内成员初始化

在类定义中初始化成员,可避免在构造函数中编写重复的代码,从而使代码结构更清晰。

# 模板和STL方面的修改

为改善模板和标准模板库的可用性,C++11做了多个改进,有些是库本身,有些与易用性有关。

# 基于范围的for循环

对于内置数组以及包含方法begin()和end()的类和STL容器,基于范围的for循环可简化为它们编写循环的工作。

# 新的STL容器

C++11新增了forward_list,unordered_map,unordered_multimap,unordered_set,unordered_multiset。容器forward_list是一种单向链表,只能沿一个方向遍历;与双向链接的list容器相比,它更简单,占用存储空间方面更经济,其他四种容器都是使用哈希表实现。

C++11还新增了模板array,要实例化这种模板,可指定元素类型和固定元素数。它没有满足常规模板需求,但是由于长度固定,您不能使用修改容器大小的方法,但它确实有begin()和end(),让您能够对array对象使用众多基于范围的STL算法。

# 新的STL方法

C++11新增了STL方法cbegin()和cend()。同begin()和end()一样,这些新方法也返回一个迭代器,指向容器的第一个元素和最后一个元素的后面,但这些新方法将元素视为const。类似的,crbegin()和crend()是rbegin()和rend()的const版本。

# valarray升级

模板valarray独立于STL开发,其最初的设计导致无法将基于范围的STL算法用于valarray对象。C++11添加了两个函数(begin()和end()),它们都接受valarray作为参数,并返回迭代器,这些迭代器分别指向valarray对象的第一个元素和最后一个元素后面,这让您能够将基于范围的STL算法用于valarray。

# 摒弃export

C++98新增的export旨在提供一种方法,让程序员能够将模板定义放在接口文件和实现文件中,前者包含原型和模板声明,后者包含模板函数和方法的定义。实践证明这不现实,因此被启用。

# 尖括号

为了避免与运算符>>混淆,C++要求在声明嵌套模板时使用空格将尖括号分开,C++11不在这样要求。

vector<list<int>> v1;
1

# 右值引用

传统C++的引用(现称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式,如变量名或解除引用的指针,程序可获取其地址。最初,左值可出现在赋值语句的左边,但修饰符const的出现使得可以声明这样的标识符,不能给它赋值,但可获取其地址。

C++新增了右值引用,这是使用&&表示的。右值引用可关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。右值包括字面常量(C-风格字符串除外,它表示地址),诸如x+y等表达式以及返回值的函数(该函数返回的不是引用)。


double func(double r) {
    return r * 2;
}
int main()
{
    int x = 1, y = 2;
    double && d1 = 1;
    cout << d1 << endl;
    double && d2 = x + y;
    cout << d2 << endl;
    double && d3 = func(2);
    cout << d3 << endl;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 移动语义和右值引用

# 移动语义的应用场景

#include <ctime>
#include <random>
#include <windows.h>
#pragma comment( lib,"winmm.lib" )


class recordTime {

    clock_t beginClock;
    clock_t endClock;
    DWORD useClock;

    time_t beginSecond;
    time_t endSecond;
    time_t useSecond;

    DWORD beginMilliSecond;
    DWORD endMilliSecond;
    DWORD useMilliSecond;

public:
    recordTime() {};
    ~recordTime() {};
    void StartClock() {
        beginClock = clock();
    }
    void EndClock() {
        endClock = clock();
        useClock = double(endClock-beginClock) / CLOCKS_PER_SEC * 1000;//单位是ms
        cout << "beginClock:" << beginClock << endl;
        cout << "endClock:" << endClock << endl;
        cout << "use Time:" << useClock << endl;
    }
    void StartSecond() {
        beginSecond = time(NULL);
    }
    void EndSecond() {
        endSecond = time(NULL);
        useSecond = endSecond - beginSecond;
        cout << "use Time:" << useSecond << endl;
    }
    void StartMilliSecond() {
        beginMilliSecond = timeGetTime();
    }
    void EndMilliSecond() {
        endMilliSecond = timeGetTime();
        useMilliSecond = endMilliSecond - beginMilliSecond;
        cout << "use milliseconds:" << useMilliSecond << endl;
    }

};

void generateStr(int strNum,string & targetStr) {
    srand(time(0));
    char baseChar = 'A';
    if (rand() % 2 == 0) {
        baseChar = 'a';
    }
    for (int i = 0; i < strNum; i++) {
        targetStr += baseChar + rand() % 26;
    }
}

vector<string> allString(const vector<string> & vecstr) {
    vector<string> temp(vecstr);
    return temp;
}

int main()
{
    recordTime t1;
    vector<string> sourceStr;
    for (int i = 0; i < 20000; i++) {
        string tempStr;
        generateStr(1000, tempStr);
        sourceStr.push_back(tempStr);
    }
    t1.StartMilliSecond();
    vector<string> targetStr(sourceStr);
    t1.EndMilliSecond();

    t1.StartMilliSecond();
    vector<string> targetStr2(allString(sourceStr));
    t1.EndMilliSecond();

}

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

上面的类recordTime用来计算一段代码运行时间,generateStr用来生成一个固定长度的字符串,vector和string都使用动态内存分配,当用sourceStr来初始化targetStr时,需要将这些字符串都复制过去,这个工作量很大,但妥当就好。但是从targetStr2来看可能就要曲折一点了,先需要将这些字符复制到临时对象temp中,然后再将temp复制给targetStr2,再删除temp,这个过程中做了大量的无用功,那么如何提高效率呢?如果不是复制temp中的字符到targetStr2,而只是将数据的所有权转移给targetStr2,这样就避免了很多无用操作,就好比计算机的文件系统移动文件,实际文件还留在原来的地方,而只是修改记录。这种方法被称为移动语义(move semantics)。

要实现移动语义,就需要采取某种方式,让编译器知道什么时候需要复制,什么时候不需要。这时就是右值引用发挥作用了,可定义两个构造函数,一个是常规复制构造函数,使用const左值引用作为参数,这个引用关联到左值实参,另一个是移动构造函数,它使用右值引用作为参数,将引用关联到右值实参,复制构造函数可执行深复制,而移动构造函数只调整记录。在将所有权转移给新对象的过程中,移动构造函数可能修改其实参,这意味着右值引用参数不应是const。

# 移动语义示例


class MyString {

private:
char * str;
int len;
int id;

public:
    MyString(int tlen) {
        str = new char[tlen + 1];
        ShowObject();
        len = tlen;
        cout << "apply for the memory and create a empty object!" << endl;
    }
    MyString(const char * ch) {
        len = std::strlen(ch);
        str = new char[len + 1];
        strcpy_s(str,len+1,ch);
        ShowObject();
    };
    MyString(const MyString & tstr):MyString(tstr.str) {
        cout << "call the copy constructor!" << endl;
    }
    MyString(MyString && tstr) {
        cout << "call the move constructor!" << endl;
        str = tstr.str;
        len = tstr.len;
        tstr.str = nullptr;
        ShowObject();
    }
    MyString operator+(const MyString & tstr) {
        MyString temp = MyString(len+tstr.len);
        strcpy_s(temp.str, len + 1, str);
        strcpy_s(temp.str + len, tstr.len + 1, tstr.str);
        cout <<"-----------new string content:"<< temp.str <<"---------operator + end----------" << endl;
        return temp;
    };
    ~MyString() {
        cout << "address:" << (void *)str << "  is destroyed!" << endl;
    };
    void ShowObject() {
        if (len != 0) {
            cout << str ;
        }
        else {
            cout << "MyString is empty!" << endl;
        }
        cout << "  Data address:" << (void *)str << endl;
    }
};

int main()
{
    MyString str1("hello");
    MyString str2("world");
    MyString str3(str1);
    cout << "-----------operator + begin---------------" << endl;
    MyString str4(str1 + str2);
    cout << "----------show all MyString--------------" << endl;
    cout << "str1 show:" << endl;
    str1.ShowObject();
    cout << "str2 show:" << endl;
    str2.ShowObject();
    cout << "str3 show:" << endl;
    str3.ShowObject();
    cout << "str4 show:" << endl;
    str4.ShowObject();
}

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

从上面的输出可以看到,str4的地址和operator+()中的temp一样,这是因为str4对象是由移动复制构造函数创建的,注意在创建str4后,为临时对象调用了析构函数,它的地址是0。

# 移动构造函数解析

虽然使用右值引用可支持移动语义,但是它需要两个关键步骤,编写移动构造函数和使用右值引用让编译器使用移动构造函数。总之,通过提供一个左值引用的构造函数和一个使用右值引用的构造函数将初始化分成了两组,使用左值对象初始化对象时,将使用复制构造函数,使用右值对象初始化对象时,使用移动构造函数。我们可根据需要赋予这些构造函数的不同行为。

注意在引入右值引用前,如果要执行到str4时,会调用复制构造函数。但是左值引用不能指向右值,那么tstr将指向一个临时变量。

# 赋值

移动语义也适用于赋值运算符,移动赋值运算符删除目标对象中的原始数据,并将源对象的所有权转让给目标,同移动构造函数一样,移动赋值运算符的参数也不能是const引用。

# 强制移动

移动构造函数和移动赋值运算符使用右值,但要强制让它们使用左值怎么办?比如程序分析一个包含候选对象的数组,选其中一个对象供以后使用,并丢弃数组,如果可以使用移动构造函数和移动赋值运算符来保留选定对象,也是一种不错的解决方案。为此,可使用运算符static_cast<>将对象类型强制转换成MyString &&,C++11提供了一种更简单的方式,使用头文件utility中的函数move,下面基于之前的例子演示下:

    MyString str6(MyString && str1);
    MyString str5(move(str1));
1
2

对大多数程序员来说,右值引用带来的好处并非是让他们能够编写使用右值引用的代码,而是能够利用右值引用实现移动语义的库代码。现在STL类都有复制构造函数,移动构造函数,复制赋值运算符和移动赋值运算符。

# 类新功能

# 特殊的成员函数

在原本4个特殊成员函数(默认构造函数,复制构造函数,复制赋值运算符和析构函数)的基础上,C++11新增了两个:移动构造函数和移动赋值运算符。在没有提供任何参数的情况下,将调用默认构造函数,此时如果没有提供任何构造函数,编译器将提供一个默认构造函数,对于使用内置类型成员,默认构造函数不对其进行初始化,对于属于类对象的成员,将调用其默认构造函数。

注意如果提供了复制构造函数或复制赋值运算符,编译器将不会自动提供默认的构造函数,移动构造函数和移动赋值运算符,如果提供了移动构造函数或移动赋值运算符,编译器将也不会自动提供默认的构造函数,复制构造函数和复制赋值运算符。如果把上例中的移动构造函数注释掉,移动操作会使用复制构造函数, 如果只把复制构造函数注释掉,则复制构造操作显示引用删除的函数,因为有移动构造函数不会提供默认的赋值构造函数,而复制构造操作不会使用移动构造函数。

# 默认的方法和禁用的方法

在上面的情况中,提供了移动构造函数,就不会自动创建默认的构造函数,复制构造函数和复制赋值构造函数,在这种情况下可使用default来显式声明这些方法的默认版本。如果要禁止使用什么方法,比如复制赋值运算符,可以使用delete关键字,当然如果把要禁用的方法放到private里面也可以实现禁用,但是使用delete更明确。

class MyString{
public:
    MyString(MyString &&);
    MyString() = default;
    MyString(const MyString &)=default;
    MyString & operator=(const MyString &)=delete;
}
1
2
3
4
5
6
7

default只能用于6个特殊成员函数,但delete可用于任何成员函数。delete的一种用法是禁用转换。比如:

class Number{
public:
    void GetValue(double);
    void GetValue(int)=delete;
}
1
2
3
4
5

在没有void GetValue(int)=delete声明时,调用GetValue(5),会被提升到GetValue(5.0)。但是有声明时,则会视为编译错误,而不会把5提升到5.0。

# 委托构造函数

有些构造函数可能只是调用下其他构造函数,C++11允许在一个构造函数的定义中使用另一个构造函数,这被称为委托,因为构造函数暂时将创建对象的工作委托给另一个构造函数。

class Number{
    int intValue;
    double doubleValue;
public:
    Number(int it){
        intValue=it;
    };
    Number(int it,double dt):Number(it),doubleValue(dt){
    };
}
1
2
3
4
5
6
7
8
9
10

# 继承构造函数

C++11提供了一种让派生类能够继承基类构造函数的机制。C++98提供了一种让名称空间中函数可用的方法:

namespace MYQF {
    void f1() {
        cout << "This is in MYQF";
    }
}

int main()
{
    using MYQF::f1;
    f1();
}
1
2
3
4
5
6
7
8
9
10
11

C++11可使用这种方法让基类的所有非特殊成员函数对派生类可用。

class Number{

public:
    Number(){
        cout << "Number created!" << endl;
    };
    Number(int a){
        cout << "Number created with a:" << a << endl;
    };
    Number(int a, int b) {
        cout << "Number created with a:" << a << " b:" << b << endl;
    };

    ~Number() {
    };
};
class DoubleNumber:public Number {

public:
    using Number::Number;
    DoubleNumber() {
        cout << "DoubleNumber created!" << endl;
    }

    DoubleNumber(int a){
        cout << "DoubleNumber created with a:" << a << endl;
    };

    ~DoubleNumber() {
    };
};

int main()
{
    DoubleNumber d1;        //使用DoubleNumber()
    DoubleNumber d2(10);    //使用DoubleNumber(int)
    DoubleNumber d3(10, 12);//使用Number(int,int)
}

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

# 管理虚方法

class Number {
public :
    Number() {};
    ~Number() {};
    virtual void calc() {
        cout << "This is  a number calc!" << endl;
    }
    virtual void calc(int a) final {
        cout << "Number calc with a:" << a << endl;
    }
    virtual void calc(int a, int b) {
        cout << "Number calc with a:" << a << " with b:" << b << endl;
    }

};

class IntNumber : public Number {
public:
    virtual void calc() override {
        cout << "This is a IntNumber calc!" << endl;
    }

//virtual void calc(int a) {}
//因为基类方法中该特征标的函数被标识为final,所以无法重写或覆盖

    //virtual void calc(int a, int b) {
    //cout << "DoubleNumber calc with a:" << a << " with b:" << b << endl;
    //}
};

int main()
{
    Number n1;
    IntNumber n2;
    Number * nptr1 = & n1;
    Number * nptr2 = & n2;
    nptr1->calc();
    nptr2->calc();
    nptr2->calc(2);
    nptr2->calc(2, 10);
    //n2.calc(10,11);//找不到相应的方法

}

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
41
42
43
44

假设基类声明了一个虚方法,而你决定在派生类中提供一个不一样的版本,这将覆盖旧版本,但是如果特征标不匹配,将隐藏而不是覆盖旧版本。此时在派生类中的虚函数声明时可使用override,那么该函数必须重载其基类中的同名函数,否则代码无法编译通过。

当想让基类中的某个虚函数禁止被派生类重写时,可在基类中的方法后面添加final。说明符override和final并非关键字,而是具有特殊含义的标识符,在某些上下文中可用作常规标识符,如变量名或枚举。

# Lambda函数

Lambda表达式提供了一种有用的服务,对使用函数谓词的STL来说尤其有用。

# 应用场景

有三种方法能给STL算法传递信息:函数指针,函数符和Lambda。现假设我们要生成一个随机整数列表,并判断其中多少个整数可被3整除,多个整数可被13整除。

#include <random>

bool f3(int x) {
    return x % 3 == 0;
}
bool f13(int x) {
    return x % 13 == 0;
}

int main()
{

    vector<int> numbers(100);
    srand(time(0));
    generate(numbers.begin(),numbers.end(),rand);
    //use function pointers
    int count3 = count_if(numbers.begin(), numbers.end(), f3);
    cout << "Count of numbers divisible by 3 (function pointer) : " << count3 << endl;
    int count13 = count_if(numbers.begin(), numbers.end(), f13);
    cout << "Count of nubmers divisible by 13 (function pointer) : " << count13 << endl;

    //use a functor
    class fdivide
    {
    public:
        int divisor;
        fdivide(int d = 1) :divisor(d) {

        }
        bool operator()(int x) {
            return x % divisor == 0;
        }
    };

    count3 = count_if(numbers.begin(), numbers.end(), fdivide(3));
    cout << "Count of numbers divisible by 3 (functor) : " << count3 << endl;
    count13 = count_if(numbers.begin(), numbers.end(), fdivide(13));
    cout << "Count of nubmers divisible by 13 (functor) : " << count13 << endl;

    //use lambda
    count3 = count_if(numbers.begin(), numbers.end(), [](int x) {return x % 3 == 0; });
    cout << "Count of numbers divisible by 3 (lambda) : " << count3 << endl;
    count13 = count_if(numbers.begin(), numbers.end(), [](int x) {return x % 13 == 0; });
    cout << "Count of nubmers divisible by 13 (lambda) : " << count13 << endl;

}

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
41
42
43
44
45
46
47

这三种方法,函数指针阻止了内联,因为编译器传统上不会内联其地址被获取的函数,函数地址的概念意味着非内联函数。而函数符和lambda通常不会阻止内联。

# 注意事项

  1. lambda名称来自lambda calculus,一种定义和应用函数的数学系统,这个系统能够让你使用匿名函数(无需给函数命名),在C++11中,对于接受函数指针或函数符的函数,可使用lambda函数作为其参数。对比lambda和函数发现差别有两个:一是使用[]替代了函数名(匿名函数的由来);另一个是没有声明返回类型,返回类型相当于使用decltype根据返回值推断,如果lambda不包含语句,推断出的返回类型将为void。注意,仅当lambda表达式有返回语句时,自动类型推断才管用,否则需要使用新增的返回类型后置语法。

    [](int x){return x % 3 == 0;}
    bool f(int x){return x % 3 == 0;}
    [](double x)->double{int y = x;return x - y;}
    
    1
    2
    3
  2. lambda的好处是让定义位于使用的地方附近,这样无需翻阅多页的源代码,如果修改代码,涉及的内容都将在附近。相对比来说,函数是糟糕的选择,不能在函数内部定义其他函数,因此函数的定义可能离使用它的地方很远。

  3. 注意如果要使用同一个lambda两次,可以给lambda指定一个名称,并使用该名称两次:

    auto mod3 = [](int x) {
        return x % 3 == 0;
    }
    count1 = count_if(n1.beigin(), n1.end(), mod3);
    count2 = count_if(n2.beigin(), n2.end(), mod3);
    bool result = mod3(10);
    
    
    1
    2
    3
    4
    5
    6
    7
  4. lambda有一些额外的功能,比如它可访问作用域内的任何动态变量;要使用参数变量,可将其名称放在中括号内。直接使用中括号的变量名,按值访问变量;在中括号变量名前加上&,将按引用访问变量。[&]能够按引用访问所有动态变量,[=]让您能够按值访问所有动态变量。它们还可以混合使用,比如[&,ted]能够按引用访问所有动态变量和按值访问ted。看下面实例:

    int main()
    {
    vector<int> numbers(100);
    srand(time(0));
    generate(numbers.begin(),numbers.end(),rand);
    int count3=0, count13=0;
    
    //use lambda
    int countAll=0;
    int outputValue = 120;
    count_if(numbers.begin(), numbers.end(), [&,outputValue](int x) { count3 += (x % 3 == 0); count13 += x % 13 == 0; countAll++; if(x%13==0)cout << outputValue << endl; return true; });
    cout << "Count of numbers divisible by 3 (lambda) : " << count3 << endl;
    cout << "Count of nubmers divisible by 13 (lambda) : " << count13 << endl;
    cout << "all count of numbers :" << countAll << endl;
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

# 包装器

C++提供了多个包装器(wrapper,也叫适配器[adapter])。这些对象用于给其他编程接口提供更一致或更合适的接口。比如之前说的bind1st和bind2nd,它们让接受两个参数的函数能够与只接受一个参数的函数作为参数。C++11还提供了其他的包装器,包括模板bind,mem_fn和reference_wrapper以及包装器function。其中bind可替代bind1st和bind2nd,但更灵活;模板mem_fn让您能够将成员函数作为常规函数进行传递;模板reference_wrapper让您能够创建行为像引用但可被复制的对象;包装器function让您能够以统一的方式处理多种类似于函数的形式。

# 使用场景

template<typename T,typename F>
T use_f(T v, F f) {
    static int count = 0;
    count++;
    std::cout << "use_f count = " << count << ",&count = " << &count << std::endl;
    return f(v);
}

class F1 {
private:
    double value_;
public:
    F1(double v = 1) :value_(v) {

    }
    double operator()(double factor) {
        return value_ * factor;
    }
};

class F2 {
private:
    double value_;
public:
    F2(double v = 2) :value_(v) {

    }
    double operator()(double factor) {
        return value_ * factor;
    }

};


double F3(double x) {
    return 3 * x;
}
double F4(double x) {
    return 4 * x;
}

int main()
{
    double base = 10;
    cout << "Function pointer F3:" << endl;
    cout << use_f(base, F3) << endl;
    cout << "Function pointer F4:" << endl;
    cout << use_f(base, F4) << endl;
    cout << "Function pointer F1:" << endl;
    cout << use_f(base, F1()) << endl;
    cout << "Function pointer F1 with value 10:" << endl;
    cout << use_f(base, F1(10)) << endl;
    cout << "Function pointer F2:" << endl;
    cout << use_f(base, F2()) << endl;
    cout << "Function pointer F2 with value 10:" << endl;
    cout << use_f(base, F2(10)) << endl;
    cout << "lambda expression 5:" << endl;
    cout << use_f(base, [](double u) {return 5 * u; }) << endl;
    cout << "lambda expression 6:" << endl;
    cout << use_f(base, [](double u) {return 6 * u; }) << endl;
}

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

如果有一行代码

result = ct(parameters);
1

ct 可以是函数名,函数指针,函数对象或有名称的lambda表达式,这些都是可调用类型(callable type)。因为调用的类型比较多,可能导致模板的效率低。就拿上面的实例来说,我们每次实例化模板F都是接受一个函数,该函数接受一个double值并返会一个double值,因此我们觉得F的类型都相同,只会实例化模板一次,但实际却不是这样的。实际它有五个不同的实例化,比如第一次调用F3,F3是一个函数名,它是一个指针,模板参数F的类型为double(*)(double),第二次F4依旧是函数名,因此实例化同F3相同,下面的F1,F2都是两个类,同一个类的不同实例化都归为这个类类型,因此他们调用use_f只实例化了两次,最后两个lambda函数实例化了两次。

我们最初只想让use_f实例化1次而不是5次,因为函数指针,函数对象和lambda表达式有一个相同的地方,他们都接受一个double参数并返回一个double,也就是说它们的调用特征标(call signature)相同。调用特征标是有返回类型以及用括号括起并用逗号分隔的参数类型列表定义的,这六个实例的调用特征标都是double(double)。

模板function是在头文件functional中声明的,它从调用特征标的角度定义了一个对象,可用于包装调用特征标相同的函数指针,函数对象或lambda表达式。将上面的实例改成如下:

#include <functional>

typedef function<double(double)> fdd;

int main()
{
    double base = 10;
    cout << "Function pointer F3:" << endl;
    fdd ff3 = F3;
    fdd ff4 = F4;
    fdd ff1 = F1();
    fdd ff2 = F2();
    fdd ff5 = [](double u) {return 5 * u; };
    fdd ff6 = [](double u) {return 6 * u; };
    cout << use_f(base, ff3) << endl;
    cout << "Function pointer F4:" << endl;
    cout << use_f(base, ff4) << endl;
    cout << "Function pointer F1:" << endl;
    cout << use_f(base, ff1) << endl;
    cout << "Function pointer F2:" << endl;
    cout << use_f(base, ff2) << endl;
    cout << "lambda expression 5:" << endl;
    cout << use_f(base, ff5) << endl;
    cout << "lambda expression 6:" << endl;
    cout << use_f(base, ff6) << endl;
}

可以把上面简化为:

typedef function<double(double)> fdd;

int main()
{
    double base = 10;
    cout << "Function pointer F3:" << endl;
    cout << use_f(base, fdd(F3)) << endl;
    cout << "Function pointer F4:" << endl;
    cout << use_f(base, fdd(F4)) << endl;
    cout << "Function pointer F1:" << endl;
    cout << use_f(base, fdd(F1())) << endl;
    cout << "Function pointer F2:" << endl;
    cout << use_f(base, fdd(F2())) << endl;
    cout << "lambda expression 5:" << endl;
    cout << use_f(base, fdd([](double u) {return 5 * u; })) << endl;
    cout << "lambda expression 6:" << endl;
    cout << use_f(base, fdd([](double u) {return 6 * u; })) << endl;
}

或者再进一步,把use_f改为:

template<typename T>
T use_f(T v, function<T(T)> f) {
    static int count = 0;
    count++;
    std::cout << "use_f count = " << count << ",&count = " << &count << std::endl;
    return f(v);
}

main函数中的调用改为:

int main()
{
    double base = 10;
    cout << "Function pointer F3:" << endl;
    cout << use_f<double>(base, F3) << endl;
    cout << "Function pointer F4:" << endl;
    cout << use_f<double>(base, F4) << endl;
    cout << "Function pointer F1:" << endl;
    cout << use_f<double>(base, F1()) << endl;
    cout << "Function pointer F1 with value 10:" << endl;
    cout << use_f<double>(base, F1(10)) << endl;
    cout << "Function pointer F2:" << endl;
    cout << use_f<double>(base, F2()) << endl;
    cout << "Function pointer F2 with value 10:" << endl;
    cout << use_f<double>(base, F2(10)) << endl;
    cout << "lambda expression 5:" << endl;
    cout << use_f<double>(base, [](double u) {return 5 * u; }) << endl;
    cout << "lambda expression 6:" << endl;
    cout << use_f<double>(base, [](double u) {return 6 * u; }) << endl;
}

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

# 可变参数模板

可变参数模板(variadic template)可以创建接受任意数量任意类型的模板函数和模板类。 来看下面实例:

template<typename T>
void showList(T oneparameter) {
    cout << oneparameter << endl;
}

template<typename First,typename... Others>
void showList(First firstValue,Others... argsList) {
    cout << firstValue << ",";
    showList(argsList...);
}

template<typename... Args>
void showSize(Args... args) {
    cout << sizeof...(args) << endl;
}

template<typename... Args>
void showList2(Args... args) {
    int arr[] = {(showList(args),0)...};
    //使用数组目的纯粹是为了在数组构造过程中展开参数包,最终会创建一个数组int arr[sizeof...(args)],它的元素全为0。
}


template<typename T>
void showList3(T oneparameter) {
    cout << oneparameter << endl;
}

template<typename First,typename... Others>
void showList3(First& firstValue,Others&... argsList) {
    cout << firstValue++ << ",";
    showList3(argsList...);
}

int main()
{
    showList(2,3.0,7.45,'x',"hello world!");
    showSize(2,3.0,7.45,'x',"hello world!");
    cout << "使用逗号表达式展开" << endl;
    showList2(2,2,3.0,7.45,'x',"hello world!");
    int a = 1;
    int b = 2;
    showList3(a, b);
    showList3(a, b);
}

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
41
42
43
44
45
46

上面的Others和Args是模板参数包,args和argsList都是函数参数包。Others&... 是以引用的方式使用各个参数。要查看函数参数包的个数使用sizeof...(args)。上面用两种方式展开了参数模板包,一种是用递归的方式,这种方式需要定义一个只有一个参数或没有参数的函数;另一种方式便是使用逗号表达式和初始化列表。

# C++11新增的其他功能

# 并行编程

现在,为了提高计算性能,增加处理器数量比提高处理器速度更容器,装备了多核处理器的计算机很常见,这让计算机能够同时执行多个线程,其中一个处理器可能处理视频下载,另一个处理器处理电子表格。

有些操作能受益于多线程,有些却不能,但多线程确实带来了问题,如果一个线程挂起或两个线程试图访问同一项数据,会引起资源的并发问题,C++定义了一个支持线程化执行的内存模型,添加了关键字thread_local,该关键字将变量声明为静态存储,其持续性和特定线程相关,当定义这种变量的线程过期时,变量也将过期。库支持由原子操作(atomic operation)库和线程支持库组成,其中原子操作库提供了头文件atomic,而线程支持库提供了头文件thread,mutex,condition_variable和future。

# 新增库

C++11添加了多个专用库。头文件random支持的可扩展随机数库提供了大量比rand()复杂的随机数工具。比如可选择随机数生成器和分布状态,分布状态包括均匀分布(类似于rand()),二项分布和正态分布。

头文件chrono提供了处理时间间隔的途径。

头文件tuple支持模板,tuple对象时广义的pair对象,pair对象可存储两个类型不同的值,而tuple对象可存储任意多个类型不同的值。

头文件ratio支持的编译阶段有理数算术库让您能够准确地表示任何有理数,其分子和分母可用最宽的整型表示,它还支持对这些有理数进行算术运算。

头文件regex支持正则表达式,可用于与文本字符串的内容匹配。有点特殊的是C++中斜杠有特殊含义,对于模式\d\t\w\d(一位数字,制表符,单词和一位数字)必须写成字面量"\d\t\w\d",这也是引入原始字符串的原因之一,可写成R"\d\t\w\d"。ed,grep和awk等UNIX工具都使用正则表达式,而解释型语言Perl扩展了正则表达式的功能,C++正则表达式库则让您能够选择多种形式的正则表达式。

# 低级编程

低级编程中的"低级"指的是抽象程度,而不是编程质量。低级意味着接近于计算机硬件和机器语言使用的比特和字节。对于嵌入式编程而言低级编程很重要。

C++11变化之一是放松了POD(Plain Old Data)的要求。C++98中,POD是标量类型(单值类型,如int或没有构造函数,基类,私有数据,虚函数等老式结构。)以前的理念是POD是可安全地逐字节复制的东西。在C++11中,这种理念没有变,但是更进一步,C++11认为在移除C++98的某些约束的情况下仍是合法的POD。这对低级编程很有用,因为一些低级操作,比如为了逐字节复制或二进制IO使用C函数需要POD。

另一项修改是允许共用体的成员有构造函数和析构函数,这让共用体更灵活,但保留了一些其他限制,比如成员不能有虚函数。

C++11解决了内存对齐问题,计算机系统可能对数据在内存中的存储方式有一定的限制。例如,一个系统可能要求double值得内存地址为偶数,而另一个系统可能要求其初始位置为8的整数倍。要获悉有关了类型或对象的对齐要求,可使用运算符alignof()。要控制对齐方式可使用说明符alignas。

constexpr机制让编译器能够在编译阶段计算机结果为常量的表达式,让const变量可存储在只读内存中,这对嵌入式编程很有用。

# 杂项

C99引入了依赖于实现的扩展整型,C++11继承了这种传统。在使用128位整数的系统中可使用这样的类型。在C语言中,扩展类型由头文件stdint.h支持,在C++11中,为头文件cstdint。

C++11提供了一种创建用户自定义字面量的机制:字面量运算符(literal operator)。使用这种机制可定义二进制字面量,如1001001b,相应的字面量运算符把它转换成整数值。

C++提供了调试工具assert。这是一个宏,它在运行阶段对断言进行检查,如果为true则显示一条消息,否则调用abort()。断言通常是程序员认为在程序的某个阶段应为true的东西,C++11新增了关键字static_assert,可用于在编译阶段对断言进行测试。这样做的主要目的在于对于在编译阶段(而不是运行阶段)实例化的模板,调试起来更加方便。

C++11加强了对元编程(metaprogramming)的支持。元编程指的是创建或修改其他程序,甚至自身。在C++11中,可使用模板在编译阶段完成这种工作。

# 语言变化

C++的使用范围足够广后,需要国际标准,并将其控制权交给标准委员会:最初是ANSI委员会,随后是ISO/ANSI联合委员会,当前是ISO/IEC JTC1/SC22/WG21(C++标准委员会),ISO是国际标准组织,IEC是国际电子技术委员会,JTC1是前两家组织组建的联合技术委员会1,SC22是JTC1下属的编程语言委员会,而WG21是SC22下属的C++工作小组。

委员会考虑缺陷报告和有关语言修改和扩展的提议,并试图达成一致。这个过程既繁琐又漫长,《The Design and Evolution of C++》(Stroustrup,Addison-Wesley 1994)介绍了这方面的一些情况。寻求一致的委员会沉闷争议不断,这可能不是鼓励创新的好方式,也不是标准委员会应扮演的角色。

就C++而言,还有一种变更的途径,就是依靠C++编程社区的直接行动。程序员无法不受羁绊地改进语言,但可创建有用的库。设计良好的库可改善语言的用途和功能,提高可靠性,让编程更容易,更有乐趣。库是现有语言功能的基础上创建的,不需要额外的编译器支持。如果库是通过模板实现的,可以用头文件的方式分发。

一项变革是STL,它在编程社区获得了巨大的成功,成为第一个ANSI/ISO标准的候选内容。

# Boost项目

该项目发起于1998年,当时的C++库工作小组主席Beman Dawes召集其他几位小组成员制定了一项计划,准备在标准委员会的框架之外创建新库。该计划的理念是创建一个开放论坛的网站,让人发布免费的C++库。这个项目提供有关许可和编程实践的指南,并要求对提议的库进行同行审阅。最后,产出一系列得到高度赞扬和广泛使用的库。这个项目提供了一个环境,让编程社区能够检验和评估编程理念以及提供反馈。

# TR1

TR1(Technical Report 1)是C++标准委员会的部分成员发起的一个项目,它是一个库扩展选集,这些扩展与C++98标准兼容,但不是必不可少。这些扩展是下一个C++标准的候选内容。TR1库让C++社区能够检验其组成部分的价值,当标准委员会TR1的大部分内容融入C+11时,面对的时众所周知经过实践检验的库。

在TR1中,Boost库占了很大一部分。这包括tuple和array,bind和function,只能指针,static_assert,regex和random库。另外Boost社区和TR1用户的经验也导致了实际语言的变更,如异常规范的摒弃和可变参数模板的添加,其中可变参数让tuple模板类和function模板的实现更好了。

# 接下来的任务

本书主要是介绍C++11,如果想进一步深入了解C++17的话可参考《C++高级编程》(Marc Gregoire著)。当然后面读者如果想了解面向对象的话可以参考《大象:Thinking in UML》(第二版)。这都是不同的方向,可自行选择。