# c 和 cpp 的差异

C/C++ 从 1999 年 C99 标准开始开始分道扬镳,C++ 不再兼容 C。

1. c++ 不支持 VLA,c99 支持 VLA

C++ 定义 array,array 的长度必须是常量表达式。

2. const 的实现不一样

常量最大的特点是不可更改,编译时就知道其具体的值。

C 语言中const就是一个值不能改变的变量,就是个受限制的变量,实际上是不允许作为左值, 但是,我们虽然我们不能通过a修改那块内存的值,但是我们可以通过指针间接去修改(const 局部变量)。

C++ 中的const之所以能够看成常量,就是因为 C++ 在读取const的时候,实际上读取的是代码段的字面常量。 而不是数据区(对于全局变量来说是静态区,对于局部变量来说是栈区)的内存中的数值。

但是不当的使用下, C++ 的 const 也可能退化为 C 语言中的 const。

总结:

C语言中const就是一个受限制的变量,读取const时是从数据区的内存读取的(全局从静态区,局部从栈区) ,可以用指针间接修改其标识的数据区的内存区域,在读取const的值时,也是读取它标识的数据区的内存中 的值。

在C++中,const大多数情况下可以当成常量来使用,这是因为,虽然C++也会为const在数据区开辟内存 (C++尽量不这样做),我们也可以通过指针(或者非常量的引用)来简介修改其标识的数据区的内存区域的 值,但是,在读取const时,不是读取数据区的内存区域中的值(也很有可能根本就没有分配内存),而是读取 代码段的字面常量。所以可以达到常量的效果。

最后要警惕C++中 const退化成C语言中的const, 有两种情况,

c++中是否要为const全局变量分配内存空间,取决于这个const变量的用途, 如果是充当着一个值替换(即就是将一个变量名替换为一个值),那么就不分配内存空间, 不过当对这个const全局变量取地址或者使用extern时,会分配内存,存储在只读数据段。也是不能修改的。

  • 初始化的const的时候是用变量初始化的,而不是字面常量(或常量表达式)。
  • const修饰类中成员变量的时候。

给一个建议:如果我们想用const定义常量,那么我们就要用字面常量或常量表达式初始化const, 而且要将const用static修饰(注意在C++中如果定义的是全局的const,默认为static修饰, 但是在类中并不是这样的,这也是为什么我们在类中定义常量要用static修饰)。在类中定义常量的时候要 同时用cosnt和static修饰,而且尽量在类的内部进行初始化。

C++ 中,const 默认是内部链接的(static)。

在C语言中:

  • const修饰的变量是只读,本质上还是一个变量,不是真正的常量
  • const修饰的局部变量在栈上分配存储空间
  • const修饰的全局变量在只读存储区中分配存储空间
  • const在只在编译期有用,在运行的时候没有用

在C++中:

c++里的const要分情况,编译期能确定值的,就是编译期常量,否则也是只读变量,

const修饰的变量是常量放在符号表中,在编译的过程中,如果发现常量,则直接以符号表中的值进行替换。

在编译的过程中,如果发现以下情况,则为常量分配存储空间:

  • 对const常量使用了extern;
  • 对const常量使用了取地址符&。
  • 自定义数据类型。

在这两种情况下,虽然会为常量分配存储空间,但是却不会使用该存储空间中的值。

c++11的常量表达式constexpr就保证是编译期常量。

  1. 类中的const成员会被分配空间
  2. 类中的const成员本质是只读变量
  3. 类中的const成员只能在初始化列表中指定初始值。
  4. 编译器无法直接得到const成员初始值,因此无法进入符号表成为真正意义上的常量
  5. 类中的const成员可以使用const_cast去除只读属性后通过指针修改值
  6. 初始化是对正在创建的对象进行初值设置,赋值是对已经存在的对象进行值设置

C++中的局部 const 变量

c++中对于局部的const变量要区别对待:

对于基础数据类型,也就是const int a = 10这种,编译器会把它放到符号表中,不分配内存,当对其取地址时,会分配内存。

对于基础数据类型,如果用一个变量初始化const变量,如果 const int a = b,那么也是会给 a 分配内存。

对于自定数据类型,比如类对象,那么也会分配内存。

c中 const 默认为外部连接,c++中 const 全局变量默认为内部链接(static)。

3. C++ 的 goto 语句

在goto语句,与goto目标点之间,不能定义新的变量,如果要定义, 就在goto语句前定义好。

C 中没有这个限制。

4. 更为严格的数据类型检查

比如:

char *p;
int *q;
p = q;
1
2
3

在 C 中可以通过编译但有警告。

C++ 中,无法通过编译。

5. 结构体

空结构体

C++ 中,空类型的实例中不包含任何信息,本来求空结构体的 sizeof 应该是 0,但是当我们声明该类型的实例的时候, 它必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存,由编译器决定。

C++ 中每个空类型的实例一般占用1字节的空间。

而 C 中空结构体占用 0 字节。

因为 C++ 标准禁止对象大小为 0,规定对象的大小为正数,因为两个不同的对象需要不同的地址表示。

静态变量

C 中结构体内不能定义静态变量。

6. 全局变量的初始化

C ++ 全局变量的初始化可以通过函数初始化。

C 不行。

void 指针

C 允许从 void* 隐式转换到其它的指针类型,但C++不允许。

# extern "C"

extern "C" 只影响链接期,它决定符号(变量/函数)以什么样的命名规则导出。extern "C" 告诉 编译器,这段代码使用 C 语言符号导出的命名标准。

extern "C" 包含的代码不可以函数重载,变量类型、函数参数类型里不能有 C++ 特有类型,否则无法 被 C 语言调用。

但是函数的内部实现可以用,因为影响的只是对应的符号链接。

# const 和 constexpr

constexpr 用来修饰常量表达式。

# 使用引用作为函数参数的优点

使用引用传递函数的参数,在内存中没有产生实参的副本,当参数传递的数据较大时,使用引用传递参数 比用一般变量传递参数的效率和所占空间都好,如果传递的是对象,还将调用拷贝构造函数。

引用也可以作为函数的返回值,这样函数就可作为左值。

int& func(void)
{
    static int a = 0;
    return a;
}

int main(void)
{
    int &temp = func();
    cout <<"temp" << temp<< endl;
    func() = 10;
    cout <<"temp" << temp<< endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

引用的本质:指针常量

int a = 10;
int &b = a; // int * const b = &a;
b = 20; // *b = 20;
1
2
3

# new 和 delete

new 和 delete 是操作符而不是关键字。

sizeof 不是函数而是关键字。

new/delete 和 malloc/free 的区别

  • malloc和free是函数,new和delete是操作符
  • malloc申请的空间不会初始化,new可以初始化
  • malloc申请空间时需要手动计算空间大小并传递,new不需要
  • malloc的返回值为void*,使用时必须强转来接收,new不需要
  • malloc申请失败时返回NULL,new申请失败会抛异常
  • 申请自定义类型的对象时,malloc/free不会调用构造函数和析构函数,而new会申请空间后调用构造函数,delete会调用析构函数后再释放空间。

由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于 malloc/free。 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符 delete。

delete 和 delete[] 的区别

针对简单类型 使用 new 分配后的不管是数组还是非数组形式内存空间用两种方式均可。

分配简单类型内存时,内存大小已经确定,系统可以记忆并且进行管理,在析构时,系统并不会调用析构函数, 它直接通过指针可以获取实际分配的内存空间,哪怕是一个数组内存空间。

delete 只会调用一次析构函数,有一块区域记录了动态内存的大小。

delete[] 会对所有对象逐一调用析构。

int/char/long/int*/struct 等简单数据类型,由于对象没有 destructor,所以用 delete 和 delete[] 是一样的。

# define

C++ 中,尽量使用 constenum classinline 替代 #define

C++ 中不主张使用全局变量。

# 枚举和 constexpr 的区别

constexpr 用于声明可以在编译期进行计算的常量表达式

枚举用于定义一组命名常量,在编译时被赋予相应的整数值,提供了类型安全值。

# 前置声明

前向引用也适用于结构体、共用体和枚举类型,C 语言也适用。

  1. 前向声明的类不能定义对象。
  2. 可以用于定义指向这个类型的指针和引用。
  3. 用于声明使用该类型作为形参或返回类型的函数

前置声明 (opens new window)

# thread

thread (opens new window)

# delete 和 override

delete 和 override (opens new window)

# 结构体和类

结构体默认权限是 public,类默认权限 private。

# explicit

尽量使用 explicit,它让单参数构造函数不能被隐式调用。

#include <iostream>
using namespace std;

class Point {
public:
    int x, y;
    Point(int x = 0, int y = 0)
        : x(x), y(y) {}
};

void displayPoint(const Point& p)
{
    cout << "(" << p.x << ","
         << p.y << ")" << endl;
}

int main()
{
    displayPoint(1);
    Point p = 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

我们定义了一个再简单不过的Point类, 它的构造函数使用了默认参数. 这时主函数里的两句话都会触发该构造函数的隐式调用. (如果构造函数不使用默认参数, 会在编译时报错)

显然, 函数displayPoint需要的是Point类型的参数, 而我们传入的是一个int, 这个程序却能成功运行, 就是因为这隐式调用. 另外说一句, 在对象刚刚定义时, 即使你使用的是赋值操作符=, 也是会调用构造函数, 而不是重载的operator=运算符.

这样悄悄发生的事情, 有时可以带来便利, 而有时却会带来意想不到的后果. explicit关键字用来避免这样的情况发生.

# 浅拷贝和深拷贝

浅拷贝只是简单的赋值,可能导致析构函数出错。

深拷贝把堆空间的数据一起拷贝。

# c++11

现代 C++ 教程 (opens new window)

# data() 和 c_str()

string 类本来的设计中没有要求内部字符串的形式是 C 风格字符串。即,不必以 \0 作为字符串的结尾。

由于这个原因,在 C++ 11 标准之前,只有调用 string::c_str() 才能得到以 \0 结尾的字符串。

C++11 标准做出了修改,要求 string 内部必须以 C 风格字符串的形式储存。

c_str() and data() perform the same function. (since C++ 11)

所以在这之后两个函数效果相同。

# nothrow

#include <new>
int* p = new(std::nothrow) int[100000000ul]; // non-throwing overload
if (p == nullptr)
{
    std::cout << "Allocation returned nullptr\n";
    break;
}int *p = new (std::nothrow) int[10];
if (p == nullptr) {

}
1
2
3
4
5
6
7
8
9
10

# 枚举类

优势:

  1. 强作用域,其作用域限制在枚举类中。
  2. 转换限制,枚举类对象不可以与整型隐式地互相转换。
  3. 可以指定底层类型。

enum 的名字作用域是全局的,enum class 的作用域是处于类的作用域内。

实际上默认的底层类型是int类型,就像在简单的枚举类型中定义的所有枚举常量实际上都是整数类型一样。如果不定义‘底层类型’它默认的枚举值都是int类型。

可以指定类型

enum class Type { General, Light, Medium, Heavy};//所有枚举常量都是int类型
enum class Type: char { General, Light, Medium, Heavy};//所有枚举常量都是字符类型
enum class Category { General=1, Pistol, MachineGun, Cannon};//后面的枚举常量值依次增加
1
2
3
enum Alert{
    red
};
enum class Color{
    blue
};
Alert a1 = red; //ok
Alert a2 = Alert::red; //error in C++98; ok in C++11
int red = 0; // error, red is redefined

Color c1 = Color::blue; //ok
Color c2 = blue; // error
int blue = 0; //ok, blue is in class scope
1
2
3
4
5
6
7
8
9
10
11
12
13

保证了类型安全,不能隐式转换

限定范围的枚举类,可以降低命名空间的污染,避免发生隐式转换。

enum class Color{
    black,
    white,
    red
};

Color c = Color::white;

if (c < 14.5) { // 不会隐式转换,枚举类不能和普通类型比较

}

enum class Color; // 可以做前置声明
1
2
3
4
5
6
7
8
9
10
11
12
13

匿名枚举

class test{
    enum {ONE = 1};
}

test::ONE;
1
2
3
4
5

枚举是一个常量,在编译的时候已经被放入了常量区,调用的时候因此不需要该枚举的变量也可以调用。

# 初始化列表

在C++11中可以直接在变量名后面加上初始化列表来进行对象的初始化。

C++11列表初始化(统一了初始化方式)。

//初始化列表
int i_arr[3] = { 1, 2, 3 };  //普通数组
struct A
{
    int x;
    struct B
    {
        int i;
        int j;
    } b;
} a = { 1, { 2, 3 } };  //POD类型,POD 类型即 plain old data 类型,简单来说,是可以直接使用 memcpy 复制的对象。
//拷贝初始化(copy-initialization)
int i = 0;
class Foo
{
    public:
    Foo(int) {}
} foo = 123;  //需要拷贝构造函数
//直接初始化(direct-initialization)
int j(0);
Foo bar(123);



// 使用统一初始化后:

int i_arr[3] = { 1, 2, 3 };
long l_arr[] = { 1, 3, 2, 4 };
struct A
{
    int x;
    int y;
} a = { 1, 2 };

class Foo
{
public:
    Foo(int) {}
private:
    Foo(const Foo &);
};
int main(void)
{
    Foo a1(123);
    Foo a2 = 123;  //error: 'Foo::Foo(const Foo &)' is private
    Foo a3 = { 123 };
    Foo a4 { 123 }; // only c++11
    int a5 = { 3 };
    int a6 { 3 };// only c++11
    return 0;
}
int i_arr[3] { 1, 2, 3 };  //普通数组
struct A
{
    int x;
    struct B
    {
        int i;
        int j;
    } b;
} a { 1, { 2, 3 } };  //POD类型

// 也可以用在函数的返回值上
std::vector<int> func() {
    return {};
}
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

std::initializer_list

std::initializer_list 可以当作参数来一次性传递同类型的多个数据。

#include <initializer_list>

using namespace std;

class Test_Class_A {
public:
    //virtual ~Test_Class_A() {};
    Test_Class_A(std::initializer_list<int> l): data(l) {}
    std::vector<int> data;
};

int main() {
    Test_Class_A tca = { 1, 2, 3};
    return 0;
}

//======================================
std::initializer_list<int> func(void)
{
    int a = 1, b = 2;
    return { a, b }; // a、 b 在返回时并没有被拷贝
}
std::vector<int> func(void)
{
    int a = 1, b = 2;
    return { a, b };
}
// 使用真正的容器,或具有转移 / 拷贝语义的物件来替代 std::initializer_list 返回需要的结果。
// 我们应当总是把 std::initializer_list 看做保存对象的引用,并在它持有对象的生存期结束之前完成传递。
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

列表初始化的应用:return {};,如 return {"dora"}return {"str", 3}, 不过需要对应构造函数不是 explicit 的。

# auto type

std::vector<int> vec = {1, 2, 3, 4};
for (auto it = vec.begin(); it != vec.end(); it++) {
    std::cout << std::endl;
}

auto a = 3 + 2;
auto s = "123";
1
2
3
4
5
6
7

不允许使用auto的四个场景

  1. 不能作为函数参数使用,因为只有在函数调用的时候才会给函数参数传递实参,auto要求必须要给修饰的变量赋值,因此二者矛盾。
  2. 不能用于类的非静态常量成员的初始化。
  3. 不能使用auto关键字定义数组。auto t3[] = { 1,2,3,4,5 };//error,auto无法定义数组
  4. 无法使用auto推导出模板参数。Test<auto> t1 = t;//error,无法推导模板类型

# foreach

std::vector<int> = {1, 2, 3, 4};

// copy
for (auto i : vec) {
    std::cout << i << std::endl;
}

// readonly
for (const auto& i : vec) {
    std::cout << i << std::endl;
}

// 引用
for (auto& i : vec) {

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

# default 编译器自动生成构造函数

class Point2D{
    public:
        Point2D() = default; // 告诉编译器强制生成
};
1
2
3
4

# tuple 元组

std::pair<T,Y> 的扩展,可以当作一个通用的结构体使用。

std::tuple<int, std::string, char> t(2, "foo", 'a');
auto t1 = std::make_tuple(0, "dog", 'b');
std::cout << std::get<0>(t) << std::endl;
std::cout << std::get<1>(t) << std::endl;
std::cout << std::get<2>(t) << std::endl;

1
2
3
4
5
6

# nullptr 和 NULL

nullptrNULL是两个表示空指针的关键字,在C++中有一些区别:

  1. 类型安全性:

    • nullptr是C++11引入的新关键字,它是一个空指针字面量,具有明确的类型nullptr_t。因此,使用nullptr可以提供更好的类型安全性,避免了一些指针混淆的问题。
    • NULL是C++早期用于表示空指针的宏,通常被定义为整数0。因为它是一个整数,所以在某些情况下可能会导致类型转换问题,可能导致意外的行为。
  2. 重载决议:

    • nullptr可以被重载,这意味着可以根据不同的重载函数进行不同的操作。
    • NULL作为一个宏,没有类型信息,不能被重载,它只能代表空指针。
  3. 使用范围:

    • nullptr推荐在C++11及以后的版本中使用,以提高代码的可读性和类型安全性。
    • NULL仍然可以在旧的C++代码或兼容性考虑的情况下使用,但在新的C++代码中,建议使用nullptr

示例使用nullptr

void func(int* ptr) {
    if (ptr == nullptr) {
        // 处理空指针的情况
    } else {
        // 处理非空指针的情况
    }
}

int main() {
    int* ptr = nullptr; // 定义一个空指针
    func(ptr);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

示例使用NULL(不推荐):

#include <iostream>

void func(int* ptr) {
    if (ptr == NULL) { // 可能引起类型转换问题
        // 处理空指针的情况
    } else {
        // 处理非空指针的情况
    }
}

int main() {
    int* ptr = NULL; // 定义一个空指针
    func(ptr);
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

总结来说,nullptr是C++11引入的更加现代化和类型安全的表示空指针的关键字,而NULL是旧的宏定义,不推荐在新的C++代码中使用。

# override final

override 和 final 是基于声明的关键字。

final 可以修饰类、虚函数,表明此类不能被继承,不能再被重写。

不能在类定义之外放置 virt 说明符( override 和 final )。只需将该说明符放在类定义中的函数声明上。这同样适用于,例如,explicit , static , virtual , ...

# lambda 表达式

lambda 表达式实际上是函数对象。

完整声明格式如下:

[capture list] (params list) mutable exception-> return type { function body }

  • capture list:捕获外部变量列表
  • params list:形参列表
  • mutable指示符:用来说用是否可以修改捕获的变量
  • exception:异常设定
  • return type:返回类型,一般可以省略掉,由编译器来推导。
  • function body:函数体

Lambda表达式可以使用其可见范围内的外部变量,但必须明确声明(明确声明哪些外部变量可以被该Lambda表达式使用)。

Lambda表达式通过在最前面的方括号 [] 来明确指明其内部可以访问的外部变量,这一过程也称过Lambda表达式“捕获”了外部变量。

如:

#include <iostream>
using namespace std;

int main()
{
    auto plus = [] (int v1, int v2) -> int { return v1 + v2; }
    int sum = plus(1, 2);
    int a = 123;
    auto f = [a] (){ cout << a << endl; };
    f(); // 输出:123

    //或通过“函数体”后面的‘()’传入参数
    auto x = [](int a) -> void {cout << a << endl;}(123);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面这个例子先声明了一个整型变量a,然后再创建Lambda表达式,该表达式“捕获”了a变量,这样在Lambda表达式函数体中就可以获得该变量的值。

类似参数传递方式(值传递、引用传递、指针传递),在Lambda表达式中,外部变量的捕获方式也有值捕获、引用捕获、隐式捕获。

  1. 值捕获和参数传递中的值传递类似,被捕获的变量的值在Lambda表达式创建时通过值拷贝的方式传入,因此随后对该变量的修改不会影响影响Lambda表达式中的值。
  2. 使用引用捕获一个外部变量,只需要在捕获列表变量前面加上一个引用说明符&。如 [&a]{}
  3. 隐式捕获有两种方式,分别是[=][&][=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。

也可以进行混合捕获,C++11中的Lambda表达式捕获外部变量主要有以下形式:

  1. [],不捕获任何外部变量。
  2. [变量名,...],默认以值的形式捕获指定的外部变量。
  3. [this],以值的形式捕获 this 指针。
  4. [=],以值的形式捕获所有外部变量。
  5. [&],以引用的形式捕获所有外部变量。
  6. [=,&x],变量 x 以引用形式捕获,其余变量以传值形式捕获。
  7. [&,x],变量 x 以值的形式捕获,其余变量以引用形式捕获。

函数参数的限制:

  1. 参数列表不能有默认参数。
  2. 不支持可变参数。
  3. 所有参数都必须有参数名。

lambda 表达式无法显式捕获类的成员变量,只能捕获 this。

引用捕获例子:

int x = 1; int y = 2;
auto plus = [&] (int a, int b) -> int { x++; return x + y + a + b;};
int c = plus(1, 2);

// 编译器翻译结果为:
class LambdaClass
{
public:
    LambdaClass(int& xx, int& yy)
    : x(xx), y(yy) {}

    int operator () (int a, int b)
    {
        x++;
        return x + y + a + b;
    }

private:
    int &x;
    int &y;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# function

是个函数包装器,能包装任何类型的可调用实体,如普通函数、函数对象、lambda 表达式等。

功能类似 C 中的函数指针,可以使用仿函数。

// 包装函数指针
int (*funcptr)(int, int);
function<int(int, int)> fun_ptr {funcptr};

#include <functional>
void print1(){
    std::cout << "hello, print1" << std::endl;
}

void print2(){
    std::cout << "hello, print2" << std::endl;
}

int main(int argc, char *argv[])
{
    std::function<void()> func(&print1);
    func();

    func = &print2;
    func();

    return 0;
}

// 实例2
#include "CMakeProject1.h"
#include <functional>

using namespace std;

class Compare {
public:
    Compare() = default;
    ~Compare() = default;
    bool operator ()(int , int);

};

void swap(int& a, int& b) {
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
}

void bubble_sort(int arr[], int n, function<bool(int, int)> compare) {
    for (size_t i = 0; i < n - 1; ++i) {
        for (size_t j(0); j < n - 1 - i; ++j) {
            if (compare(arr[j], arr[j+1])) {
                swap(arr[j], arr[j + 1]);
            }
        }
    }
}

int main()
{
    int arr[]{ 49,38,65,97,76,13,27,49 };
    auto arr2 = new int[] {49, 38, 65, 97, 76, 13, 27, 49};
    function<bool(int, int)> compare{[](int a, int b) {
        return a > b;
    } };


    // 使用函数
    bubble_sort(arr, 8, compare);
    // 使用仿函数
    bubble_sort(arr2, 8, Compare());


    for (const auto& num : arr) {
        cout << num << " ";
    }
    cout << endl;
    for (int i = 0; i < 8; ++i) {
        cout << arr2[i] << " ";
    }

    delete[] arr2;
    cout << endl;
}

bool Compare::operator()(int a, int b)
{
    return 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
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

# 智能指针

智能指针-Microsoft Learn (opens new window)

C++ 智能指针最佳实践&源码分析 (opens new window)

C++ 智能指针 (opens new window)

C++中的智能指针是一种用于管理动态分配的对象的智能工具,可以帮助您避免内存泄漏和悬空指针等问题。C++11引入了三种主要的智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr。每种智能指针都有其特定的用途和行为,下面分别介绍它们的使用方法:

优先使用make_xxxx 来构造智能指针,因为比较高效。

C++ 11标准库中默认实现了make_shared,但是没有给出一个 make_unique 的实现。

智能指针相比裸指针不会带来任何额外的开销,shared_ptr 除外。

智能指针有一个通用的规则,就是->表示用于调用指针原有的方法,而.则表示调用智能指针本身的方法。

函数参数尽量使用裸指针和引用。

智能指针错误用法

  1. 使用智能指针托管的对象后,继续使用原生指针。
  2. 把一个原生指针交给多个智能指针管理。
  3. 随意使用 get()获取原生指针。
  4. 将 this 指针直接用智能指针托管,要使用 shared_form_this()
  5. 用智能指针管理栈上对象。

在函数中作为参数传递时,尽量避免使用智能指针,使用 *或者引用,跟以前一样

尽量使用unique_ptr,他比shared_ptr更light

尽量使用makeshared/ make_unique 来代替new

不出现 new 和 delete

//初始化方式1
std::shared_ptr<int> sp1(new int(123));

//初始化方式2
std::shared_ptr<int> sp2;
sp2.reset(new int(123));

//初始化方式3
std::shared_ptr<int> sp3;
sp3 = std::make_shared<int>(123);
1
2
3
4
5
6
7
8
9
10
  1. std::unique_ptr:独占式智能指针,它表示一个独占所有权的对象指针。当std::unique_ptr销毁或重置时,它会自动删除所拥有的对象。

unique_ptr经常作为返回值使用,设计模式中经常用到,异常安全。

unique_ptr本身拥有的方法主要包括:

  1. get() 获取其保存的原生指针,尽量不要使用

  2. operator bool() 判断是否拥有指针

  3. release() 释放所管理指针的所有权,返回原生指针。但并不销毁原生指针。

  4. reset() 释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针

std::unique_ptr 有两个版本:

1. 管理单个对象(例如以 new 分配) 2. 管理动态分配的对象数组(例如以 new[] 分配)

使用场景:

  1. 对象内部使用。
  2. 方法内部使用。
#include <memory>

int main() {
    // 使用make_unique创建unique_ptr,避免显式调用delete
    // c++ 14
    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    // c++11
    std::unique_ptr<int> ptr(new int(5));

    // 完美转发,隐式调用构造函数
    auto ptr = std::make_unique<std::pair<string, int>>("type1", 3);


    // 判断智能指针是否持有指针
    if (ptr)
    // 使用智能指针操作对象
    *ptr = 24;

    unique_ptr<T> ptr2 = std::move(ptr); //转移所有权
    assert(ptr2 == nullptr);

    // 不再需要时,unique_ptr会自动释放内存
    // 也可以使用reset方法来显式释放对象并置空指针
    ptr2.reset();

    return 0; // unique_ptr在此处销毁,对象也会被自动删除
}
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
 unique_ptr<Temp[]> b(new Temp[2]{{1,2} ,{2,3}});
 auto c = make_unique<Temp[]>(2);
1
2

作为函数参数的情况:

#include<iostream>
#include<memory>
void test(int *p)
{
    *p = 10;
}
int main()
{
    std::unique_ptr<int> up(new int(42));
    test(up.get());//传入裸指针作为参数
    std::cout<<*up<<std::endl;//输出10
    return 0;
}
// 使用引用作为参数
#include<iostream>
#include<memory>
void test(std::unique_ptr<int> &p)
{
    *p = 10;
}
int main()
{
    std::unique_ptr<int> up(new int(42));
    test(up);
    std::cout<<*up<<std::endl;//输出10
    return 0;
}
// 转义所有权
#include<iostream>
#include<memory>
void test(std::unique_ptr<int> p)
{
    *p = 10;
}
int main()
{
    std::unique_ptr<int> up(new int(42));
    test(std::unique_ptr<int>(up.release()));
    //test(std::move(up));//这种方式也可以
    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
35
36
37
38
39
40
41
42
// 返回 unique_ptr
class PizzaFactory {
public:
	enum PizzaType {
		HamMushroom,
		Deluxe,
		Hawaiian
	};

	static unique_ptr<Pizza> createPizza(PizzaType pizzaType) {
		switch (pizzaType) {
		case HamMushroom: return make_unique<HamAndMushroomPizza>();
		case Deluxe:      return make_unique<DeluxePizza>();
		case Hawaiian:    return make_unique<HawaiianPizza>();
		}
		throw "invalid pizza type.";
	}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. std::shared_ptr:共享式智能指针,多个std::shared_ptr可以共享拥有一个对象,直到最后一个shared_ptr销毁或重置时,对象才会被删除。

通过 shared_from_this() 返回 this 指针,不要将 this 指针作为 shared_ptr 返回出来,可能会导致重复析构。

不要用同一个原始指针初始化多个 shared_ptr,会造成二次销毁。

shared_ptr本身拥有的方法主要包括:

1、get() 获取其保存的原生指针,尽量不要使用

2、bool() 判断是否拥有指针

3、reset() 释放并销毁原生指针。如果参数为一个新指针,将管理这个新指针

4、unique() 如果引用计数为 1,则返回 true,否则返回 false

5、use_count() 返回引用计数的大小

#include <memory>

int main() {
    // 使用make_shared创建shared_ptr,避免显式调用delete
    std::shared_ptr<int> ptr1 = std::make_shared<int>(42);

    // 多个shared_ptr可以共享拥有同一个对象
    std::shared_ptr<int> ptr2 = ptr1;

    // 使用智能指针操作对象
    *ptr1 = 24;

    // 不再需要时,智能指针会自动释放内存
    // 也可以使用reset方法来显式释放对象并置空指针
    ptr1.reset();
    ptr2.reset(); // 此时对象才会被删除

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

使用 shared_from_this():

  1. 继承 enable_shared_from_this
struct EnableSharedPtr : enable_shared_from_this<EnableSharedPtr> {
public:
    shared_ptr<EnableSharedPtr> getptr() {
        return shared_from_this();
    }
    ~EnableSharedPtr() {
        cout << "~EnableSharedPtr() called" << endl;
    }
};

int main()
{
    shared_ptr<EnableSharedPtr> gp1(new EnableSharedPtr());
    // 注意不能使用raw 指针
    // EnableSharedPtr* gp1 = new EnableSharedPtr();
    shared_ptr<EnableSharedPtr> gp2 = gp1->getptr();
    cout << "gp1.use_count() = " << gp1.use_count() << endl;
    cout << "gp2.use_count() = " << gp2.use_count() << endl;
    return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. std::weak_ptr:弱引用智能指针,它可以观测一个由std::shared_ptr管理的对象,但不会增加对象的引用计数。主要用于解决std::shared_ptr的循环引用问题。

它本身是不能直接调用原生指针的方法的,如果想要使用原生指针的方法,需要将其先转换为一个shared_ptr。

weak_ptr本身拥有的方法主要包括:

1、expired() 判断所指向的原生指针是否被释放,如果被释放了返回 true,否则返回 false

2、use_count() 返回原生指针的引用计数

3、lock() 返回 shared_ptr,如果原生指针没有被释放,则返回一个非空的 shared_ptr,否则返回一个空的 shared_ptr

4、reset() 将本身置空

#include <memory>

int main() {
    // 使用make_shared创建shared_ptr,避免显式调用delete
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
    std::weak_ptr<int> weakPtr = sharedPtr;

    // 使用weak_ptr.lock()来获取一个可用的shared_ptr,用于访问对象
    if (std::shared_ptr<int> sharedPtr2 = weakPtr.lock()) {
        *sharedPtr2 = 24;
    } else {
        // 对象已经被销毁
    }

    // shared_ptr被销毁,但由于weak_ptr不增加引用计数,对象不会被删除

    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

通过使用这些智能指针,可以有效地管理资源和避免内存泄漏。在编写现代C++代码时,推荐使用智能指针而不是裸指针来处理动态分配的资源。

# 类型转换

在扩大转换中,较小的变量中的值将赋给较大的变量,同时不会丢失数据。 由于扩大转换始终是安全的,编译器将在不提示的情况下执行它们且不会发出警告。

编译器隐式执行收缩转换,但会发出有关数据丢失可能的警告。 请重视这些警告。 如果您确定数据丢失不会发生(因为较大的变量中的值始终适合较小的变量),则添加显式强制转换,使编译器不再发出警告。 如果不确定转换是否安全,请为代码添加某种运行时检查以处理可能出现的数据丢失,从而确保转换不会导致程序生成错误的结果。

C++11引入了四种新的强制类型转换操作符,它们分别是:static_castdynamic_castreinterpret_castconst_cast。这些操作符提供了不同类型转换的方式,适用于不同的情况。 `

  1. static_cast,用于仅在编译时检查的强制转换。

不能用在不同类型的指针间相互转换。如 int* -> char *

static_cast用于基本类型之间的转换。另外,如果对象所属的类重载了强制类型转换运算符 T(如 T 是 int、int* 或其他类型名),则 static_cast 也能用来进行对象到 T 类型的转换。

示例:

int x = 10;
double y = static_cast<double>(x); // 基本类型转换
1
2
#include <iostream>
using namespace std;
class A
{
public:
// 重载强制类型转换符
    operator int() { return 1; }
    operator char*() { return NULL; }
};
int main()
{
    A a;
    int n;
    char* p = "New Dragon Inn";
    n = static_cast <int> (3.14);  // n 的值变为 3
    n = static_cast <int> (a);  //调用 a.operator int,n 的值变为 1
    p = static_cast <char*> (a);  //调用 a.operator char*,p 的值变为 NULL
    n = static_cast <int> (p);  //编译错误,static_cast不能将指针转换成整型
    p = static_cast <char*> (n);  //编译错误,static_cast 不能将整型转换成指针
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. dynamic_cast,用于从指向基对象的指针到指向派生对象的指针的、安全且经过运行时检查的强制转换。

    dynamic_cast用于在含有虚函数的类层次结构中进行安全的向下转型。它会在运行时检查是否能够安全地转换指针或引用的类型。如果转换是不安全的,它将返回一个空指针(对于指针)或抛出std::bad_cast异常(对于引用)。

    dynamic_cast 在向下转换方面比 static_cast 更安全,但运行时检查会产生一些开销。

    示例:

    class Base {
        virtual void foo() {}
    };
    
    class Derived : public Base {
        // ...
    };
    
    Base* basePtr = new Derived;
    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 安全的向下转型
    if (derivedPtr) {
        // 转型成功
    } else {
        // 转型失败
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  2. reinterpret_cast:用于无关类型(如指针类型和 int)之间的强制转换。

不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换,转换时,执行的是逐个比特重新解释的操作,可进行非多态类的转换、可以用于将父类指针或引用转换为派生类指针或引用(在不涉及多态性的情况下)。

reinterpret_cast用于进行低级别的转换,它将指针或引用重新解释为不同的类型。这种转换的结果往往是与原始类型无关的,并且编译器对转换的结果不会进行任何检查。使用reinterpret_cast需要非常小心,因为它可能导致未定义行为或潜在的错误。

此强制转换运算符不像其他运算符一样常用,并且不能保证可将其移植到其它编译器。

示例:

int num = 42;
char* charPtr = reinterpret_cast<char*>(&num); // 将int指针重新解释为char指针

   class Base {
    // ...
};

class Derived : public Base {
    // ...
};

Base* basePtr = new Derived;
Derived* derivedPtr = reinterpret_cast<Derived*>(basePtr); // 父类指针向子类指针转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. const_cast:用于转换掉变量的 const 性,或者将非 const 变量转换为 const。 const_cast用于添加或删除指针或引用的常量性。它主要用于使得函数能够接受常量参数或去除函数返回值的常量性。需要注意的是,使用const_cast去除一个本来就是常量的变量的常量性,或者对一个本来不是常量的变量添加常量性是非常危险的,因为这样可能会导致未定义行为。

    示例:

    const int x = 10;
    int* ptr = const_cast<int*>(&x); // 去除指针常量性
    
    const int* constPtr = &x;
    int* nonConstPtr = const_cast<int*>(constPtr); // 去除指针常量性
    
    int y = 20;
    const int* constPtr2 = &y;
    int* ptr2 = const_cast<int*>(constPtr2); // 添加指针常量性
    
    void Func(double& d) { ... }
     void ConstCast()
     {
        const double pi = 3.14;
        Func(const_cast<double&>(pi)); //No error.
     }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

请注意,尽管强制类型转换在某些情况下是必要的,但过度和不正确使用强制类型转换可能导致代码难以维护和理解。在使用强制类型转换时,请确保对可能的副作用和潜在错误有清楚的理解,并尽量避免使用reinterpret_cast,因为它是最不安全的转换方式。

# RAII

RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,他说:使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入;

# C++ 说明符与限定符

auto,在C++11中不再是说明符。在C++11前,auto指出变量为自动变量,但在C++11后,auto用于自动类型推导。

constexpr static inline friend virtual explicit void func() const volatile & noexcept final override try {}

  • static 这个修饰符用于声明静态成员函数或静态局部变量。静态成员函数属于类,而不是类的实例,因此它没有隐含的this指针。
  • constexpr这个修饰符用于声明函数或变量为编译期常量表达式。在C++11之前,constexpr只能用于变量,C++11引入了对constexpr函数的支持。
  • friend 这个修饰符用于声明一个非成员函数或外部类可以访问当前类的私有成员。
  • inline 修饰符,定义函数为内联函数,仅向编译器申请。在类内定义的成员函数默认内联。
  • virtual 说明符,声明函数是虚函数。
  • explicit 这个修饰符用于声明单参数构造函数为显式构造函数,禁止隐式类型转换。
  • const 限定符,声明和定义函数是常函数,const 对象只能调用常函数,非const对象可以调用常函数,也能调用非常函数。但是常对象只能调用常函数,不能调用非常函数(常对象也包括常指针和常引用)。
  • volatile 类似于const修饰的函数,表示对象状态可能随时会改变;const修饰的函数内只能调用自身的const成员方法,同理volatile函数内也只能调用自身volatile成员函数。
  • & C++11引入的功能,左值引用限定符,指示函数只能被左值对象调用。
  • &&,C++11引入,右值引用限定符,指示函数只能被右值调用。如果函数没有引用限定符修饰,左值和右值均可调用。
  • override说明符,C++11引入的功能,声明成员函数覆盖父类的虚函数。声明为override后,子类声明时可不写virtual。
  • final说明符,C++11引入,用于防止类的继承或虚函数的重写。在类声明或函数声明后加上final关键字,将阻止其他类继承该类或派生类重写虚函数。
  • noexpect说明符,C++11引入,告诉编译器该函数不会抛出异常,以便编译器做优化。
  • try,指示函数抛出异常及类型,C++11起被废弃。

在C++11中,根据关键字的位置和语义,可以将关键字分为基于声明的关键字(declaration-based keywords)和基于实现的关键字(implementation-based keywords)。

基于声明的关键字是指必须写在函数或类的声明(头文件)中的关键字,用于指定函数或类的属性和特性。

  1. 基于声明的关键字包括:
    • virtual: 用于声明虚函数,在类的声明中指定该函数可被子类覆盖。
    • explicit: 用于声明构造函数为显式构造函数,防止隐式类型转换。
    • override: 用于在派生类中声明函数为覆盖基类的虚函数。
    • final: 用于阻止类被继承或虚函数被重写。
    • constexpr: 用于在类或函数声明中指定常量表达式。

基于实现的关键字是指必须写在函数或类的实现(源文件)中的关键字,用于实现函数或类的具体逻辑。

  1. 基于实现的关键字包括:
    • const: 用于修饰成员函数的声明和实现,指定该函数不会修改类对象的状态,本质是修饰 this 指针,所以不能修饰静态成员函数。
    • inline: 用于指定函数为内联函数,建议编译器将函数代码插入到调用处,以减少函数调用开销。
    • static: 用于声明和定义静态成员变量或函数,静态成员变量在类的所有实例中共享,静态函数不与特定实例关联。

需要注意的是,有些关键字可能同时在声明和实现中出现,例如virtualconst。它们的意义可能有所不同,具体取决于它们在类的声明还是实现中出现。

如果变量被 mutable 修饰,即使常函数也可以更改变量值。

# 参数入栈顺序和参数计算顺序

c/c++中规定了函数参数的压栈顺序是从右至左,函数调用协议会影响函数参数的入栈方式、栈内数据的清除方式、编译器函数名的修饰规则等。

每个参数都有自己的地址,但不定长参数无法确认地址,并且函数参数的个数也不确定,C/C++中规定了函数参数的压栈顺序是从右至左,对于含有不定参数的printf函数,其原型是printf(const char* format,…);其中format确定了printf的参数(通过format的%个数判断)。

假设是从左至右压栈,那么先入栈的是format,然后依次入栈未知参数,此时想要知道参数个数,就必须找到format,而要找到format,就必须知道参数个数,这样就会陷入一个死胡同里面了。

而c/c++中规定参数压栈为从右至左的顺序,这种方式对于不定参数,最后入栈的是参数个数,只需要取栈顶就可以得到。

函数参数的计算顺序依照编译器的实现,所以在编码中避免编写诸如 fun(++x, x+y)这种的程序,其在不同的平台得到的结果可能不一样

# 多态和引用

引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。

Class A;
Class B : Class A{
    ...
};

B b;

A& ref = b;
1
2
3
4
5
6
7
8

# 链式操作

链式操作是利用运算符进行的连续运算(操作),它的特点是在一条语句中出现两个或者两个以上相同的操作符,如连续的赋值操作、连续的输入操作、连续的输出操作、连续的相加操作等都是链式操作的例子。

#include <iostream>

class MyClass {
public:
    MyClass& operation1() {
        // 执行操作1
        std::cout << "Operation 1\n";
        return *this; // 返回自身的引用
    }

    MyClass& operation2() {
        // 执行操作2
        std::cout << "Operation 2\n";
        return *this; // 返回自身的引用
    }

    MyClass& operation3() {
        // 执行操作3
        std::cout << "Operation 3\n";
        return *this; // 返回自身的引用
    }
};

int main() {
    MyClass obj;
    obj.operation1().operation2().operation3(); // 类似链式调用的效果
    return 0;
}#include <iostream>
#include <vector>
using namespace std;
class Person{
public:
    int age;
    Person& AddAge(int page){
        age+=page;
        return *this;
    }
    Person(){
        cout<<"构造函数执行"<<endl;
    }
    Person(const Person & p){
        age=p.age;
        cout<<"拷贝构造函数执行"<<endl;
    }
    ~Person(){
        cout<<"析构函数执行"<<endl;
    }
    Person GetPerson(Person & p){
        Person ptmp;
        ptmp.age=p.age+2;
        return ptmp;
    }
};



int main(int argc, const char * argv[]) {
    Person p;
    p.age=20;
    p.AddAge(1).AddAge(2).AddAge(3); //对自己连续进行链式编程。
    cout<<p.age<<endl;
    cout << "Hello, World!\n";
    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
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

# 类的 8 种默认函数

1、默认构造函数;

2、默认拷贝构造函数,当用已有对象创建新对象时,给函数传递值类型参数时,函数返回对象的时候会调用拷贝构造函数。

3、默认析构函数;

4、默认重载赋值运算符函数;

5、默认重载取址运算符函数;

6、默认重载取址运算符const函数;

7、默认移动构造函数(C++11);

8、默认重载移动赋值操作符函数(C++11)。

只是声明一个空类,不做任何事情的话,编译器会自动为你生成一个默认构造函数、一个默认拷贝构造函数、 一个默认重载赋值操作符函数和一个默认析构函数。这些函数只有在第一次被调用时,才会被编译器创建, 当然这几个生成的默认函数的实现就是什么都不做。所有这些函数都是inline和public的。

class A
{
public:

    // 默认构造函数;
    A() = default;

    // 默认拷贝构造函数
    A(const A&);

    // 默认析构函数
    ~A();

    // 默认重载赋值运算符函数
    A& operator = (const A&);

    // 默认重载取址运算符函数
    A* operator & ();

    // 默认重载取址运算符const函数
    const A* operator & () const;

    // 默认移动构造函数
    A(A&&);

    // 默认重载移动赋值操作符
    A& operator = (const A&&);

};
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

# 禁用默认函数

禁用默认函数的方法有两种:

// C++11引入了删除函数的特性,通过在函数声明后加上= delete来禁用该函数的生成。
class MyClass {
public:
    // 禁用默认构造函数
    MyClass() = delete;

    // 禁用拷贝构造函数
    MyClass(const MyClass&) = delete;

    // 禁用移动构造函数
    MyClass(MyClass&&) = delete;

    // 禁用拷贝赋值运算符
    MyClass& operator=(const MyClass&) = delete;

    // 禁用移动赋值运算符
    MyClass& operator=(MyClass&&) = delete;
};
// 私有化函数:将默认生成的函数声明为私有,并不提供其实现,从而禁止外部访问。
class MyClass {
public:
    // 公有函数...

private:
    // 禁用默认构造函数
    MyClass();

    // 禁用拷贝构造函数
    MyClass(const MyClass&);

    // 禁用移动构造函数
    MyClass(MyClass&&);

    // 禁用拷贝赋值运算符
    MyClass& operator=(const MyClass&);

    // 禁用移动赋值运算符
    MyClass& operator=(MyClass&&);
};

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

# 构造函数(Constructor)

  1. 构造函数作用是对对象进行初始化,在堆上new一个对象或在栈上定义一个临时对象时,会自动调用对象的构造函数。有初始化列表和构造函数体内赋值两种方式, 初始化列表在初始化对象时更高效(每个成员在初始化列表中只能出现一次),减少了一次赋值操作,推荐此方法; 以下成员变量必须在初始化列表中初始化:常量成员变量、引用类型成员变量、没有缺省构造函数的成员变量(如果构造函数的参数列表中有一个类的对象,并且该对象的类里没有缺省参数的构造函数时,要是不使用初始化列表,参数中会调用无参或者全缺省的构造函数,而那个类中又没有);
  2. 函数名与类名相同,可以重载,不能为虚函数,不能有返回值,连void也不行;
  3. 如果没有显式定义,编译器会自动生成一个默认的构造函数,默认的构造函什么都不会做;
  4. 无参构造函数和带有缺省值的构造函数(全缺省)都认为是缺省的构造函数,并且缺省的构造函数只能有一个;
  5. 函数体内可以使用this指针,但不可以用于初始化列表。 因为构造函数只是初始化对象,初始化之前此对象已经存在了,所以可以有this,函数体里面是进行赋值, 初始化列表是对类中的各个成员变量进行初始化,初始化的位置对象不完整,所以不能使用this用于初始化列表;
  6. 对于出现单参数的构造函数需要注意,C++会默认将参数对应的类型转换为该类类型,有时候这种隐式的转换是我们不想要的,需要使用explicit关键字来限制这种转换;
  7. 构造顺序:虚拟基类的构造函数(如果有多个虚拟基类,按照它们被继承的顺序构造,而不是它们在成员初始化列表中的顺序); 非虚拟基类的构造函数(如果有多个非虚拟基类,按照它们被继承的顺序构造,而不是它们在成员初始化列表中的顺序); 成员对象的构造函数(如果有多个成员类对象,按照它们声明的顺序调用,而不是它们在成员初始化列表中的顺序); 本类构造函数。构造的过程是递归的。

# 拷贝构造函数(Copy Constructor)

  1. 拷贝构造函数实际上是构造函数的重载,具有一般构造函数的所有特性,用此类已有的对象创建一个新的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。用类的一个已知的对象去初始化该类的另一个对象时,会自动调用对象的拷贝构造函数;
  2. 函数名与类名相同,第一个参数是对某个同类对象的引用,且没有其他参数或其他参数都有默认值,返回值是类对象的引用,通过返回引用值可以实现连续构造,即类似A(B(C))这样;
  3. 如果没有显式定义,编译器会自动生成一个默认的拷贝构造函数,默认的拷贝构造函数会依次拷贝类的数据成员完成初始化;
  4. 浅拷贝和深拷贝:编译器创建的默认拷贝构造函数只会执行"浅拷贝",也就是通过赋值完成,如果该类的数据成员中有指针成员,也只是地址的拷贝,会使得新的对象与拷贝对象该指针成员指向的地址相同,delete该指针时则会导致两次重复delete而出错,如果指针成员是new出来就是“深拷贝”。

# 析构函数(Destructor)

  1. 析构函数作用是做一些清理工作,delete一个对象或对象生命周期结束时,会自动调用对象的析构函数;
  2. 函数名在类名前加上字符~,没有参数(可以有void类型的参数),也没有返回值,可以为虚函数(通过基类的指针去析构子类对象时候),不能重载,故析构函数只有一个;
  3. 如果没有显式定义,编译器会自动生成一个默认的析构函数,默认的析构函什么都不会做;
  4. 析构顺序:和构造函数顺序相反。析构的过程也是递归的。

# 重载赋值运算符函数(Copy Assignment operator)

  1. 它是两个已有对象,一个给另一个赋值的过程。当两个对象之间进行赋值时,会自动调用重载赋值运算符函数,它不同于拷贝构造函数,拷贝构造函数是用已有对象给新生成的对象赋初值的过程;
  2. 赋值运算符重载函数参数中const和&没有强制要求,返回值是类对象的引用,通过返回引用值可以实现连续赋值,即类似a=b=c这样,返回值类型也不是强制的,可以返回void,使用时就不能连续赋值;
  3. 赋值运算符重载函只能定义为类的成员函数,不能是静态成员函数,也不能是友元函数,赋值运算符重载函数不能被继承,要避免自赋值;
  4. 如果没有显式定义,编译器会自动生成一个默认的赋值运算符重载函数,默认的赋值运算符重载函数实现将数据成员逐一赋值的一种浅拷贝,会导致指针悬挂问题。

# 重载取址运算符(const)函数

  1. 重载取址运算符函数没有参数;
  2. 如果没有显式定义,编译器会自动生成默认的重载取址运算符函数,函数内部直接return this,一般使用默认即可。

# 移动构造函数和重载移动赋值操作符函数

  1. C++11 新增move语义:源对象资源的控制权全部交给目标对象,可以将原对象移动到新对象, 用于a初始化b后,就将a析构的情况;
  2. 移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用;
  3. 临时对象即将消亡,并且它里面的资源是需要被再利用的,这个时候就可以使用移动构造。移动构造可以减少不必要的复制,带来性能上的提升。

# 使用实例

#define _CRT_SECURE_NO_WARNINGS

#include <cstdio>
#include <cstdlib>
#include <cstring>

#include <iostream>
#include <string>

class MyClass
{
public:
    explicit MyClass(const char * str = nullptr);  // 默认带参构造函数 // 默认构造函数指不带参数或者所有参数都有缺省值的构造函数
    ~MyClass(void);  // 默认析构函数
    MyClass(const MyClass &);  // 默认拷贝构造函数
    MyClass & operator =(const MyClass &);  // 默认重载赋值运算符函数
    MyClass * operator &();  // 默认重载取址运算符函数
    MyClass const * operator &() const;  // 默认重载取址运算符const函数
    MyClass(MyClass &&);  // 默认移动构造函数
    MyClass & operator =(MyClass &&);  // 默认重载移动赋值操作符函数

private:
    char *m_pData;
};

// 默认带参构造函数
MyClass::MyClass(const char * str)
{
    if (!str)
    {
        m_pData = nullptr;
    }
    else
    {
        this->m_pData = new char[strlen(str) + 1];
        strcpy(this->m_pData, str);
    }
    std::cout << "默认带参构造函数" << " this addr: " << this << std::endl;
}

 // 默认析构函数
MyClass::~MyClass(void)
{
    if (this->m_pData)
    {
        delete[] this->m_pData;
        this->m_pData = nullptr;
    }
    std::cout << "默认析构函数" << " this addr: " << this << std::endl;
}

// 默认拷贝构造函数
MyClass::MyClass(const MyClass &m)
{
    if (!m.m_pData)
    {
        this->m_pData = nullptr;
    }
    else
    {
        this->m_pData = new char[strlen(m.m_pData) + 1];
        strcpy(this->m_pData, m.m_pData);
    }
    std::cout << "默认拷贝构造函数" << " this addr: " << this << std::endl;
}

// 默认重载赋值运算符函数
MyClass & MyClass::operator =(const MyClass &m)
{
    if ( this == &m ) {
        return *this;
    }

    delete[] this->m_pData;
    if (!m.m_pData)
    {
        this->m_pData = nullptr;
    }
    else
    {
        this->m_pData = new char[strlen(m.m_pData) + 1];
        strcpy(this->m_pData, m.m_pData);
    }

    std::cout << "默认重载赋值运算符函数" << " this addr: " << this << std::endl;
    return *this;
}

// 默认重载取址运算符函数
MyClass * MyClass::operator &()
{
    std::cout << "默认重载取址运算符函数" << " this addr: " << this << std::endl;
    return this;
}

// 默认重载取址运算符const函数
MyClass const * MyClass::operator &() const
{
    std::cout << "默认重载取址运算符const函数" << " this addr: " << this << std::endl;
    return this;
}

// 默认移动构造函数
MyClass::MyClass(MyClass && m):
    m_pData(std::move(m.m_pData))
{
    std::cout << "默认移动构造函数" << std::endl;
    m.m_pData = nullptr;
}

// 默认重载移动赋值操作符函数
MyClass & MyClass::operator =(MyClass && m)
{
    if ( this == &m ) {
        return *this;
    }

    this->m_pData = nullptr;
    this->m_pData = std::move(m.m_pData);
    m.m_pData = nullptr;
    std::cout << "默认重载移动赋值操作符函数" << " this addr: " << this << std::endl;
    return *this;
}

void funA(MyClass a)
{
    std::cout << "调用funA函数" << " param addr: " << &a << std::endl;
}

void mytest1(void)
{
    std::cout << "mytest1 >>>>" << std::endl;
    MyClass myclass1; // 等价于 MyClass myclass1 = MyClass(); // 调用默认带参构造函数
    myclass1 = MyClass(); // MyClass()为右值,需要右值引用 // 先调用默认带参构造函数,然后调用默认重载取址运算符函数,最后调用默认重载移动赋值操作符函数
    std::cout << "<<<<< mytest1" << std::endl;
    // 析构两次 1: myclass1 = MyClass()中的MyClass() 2: MyClass myclass1
}

void mytest2(void)
{
    std::cout << "mytest2 >>>>" << std::endl;
    MyClass myclass1; // 等价于 MyClass myclass1 = MyClass(); // 调用默认带参构造函数
    MyClass myclass2(myclass1);  // 调用默认拷贝构造函数
    myclass2 = myclass1; // myclass2为左值,所以此操作为赋值操作,会调用默认重载取址运算符const函数,然后调用默认重载赋值运算符函数
    funA(myclass1); // 参数传值会导致赋值操作,会调用默认拷贝构造函数,然后funA函数调用默认重载取址运算符函数取得参数
    funA(std::move(myclass1)); // funA函数的参数现为右值,会调用默认移动构造函数,然后funA函数调用默认重载取址运算符函数取得参数
    // 在移动构造函数中对于基本类型所谓移动只是把其值拷贝,对于如string这类类成员来说才会真正的所谓资源移动
    std::cout << "<<<<< mytest2" << std::endl;
}

void mytest3(void)
{
    std::cout << "mytest3 >>>>" << std::endl;
    funA(MyClass()); // 会调用默认带参构造函数,生成该类的对象,然后funA函数调用默认重载取址运算符函数取得参数
    std::cout << "<<<<< mytest3" << std::endl;
    // 析构一次 1: funA(MyClass())中的MyClass()形成的对象,是在funA函数结束调用的时候,调用默认析构函数
}

void mytest(void)
{
    std::cout << "<<<<<<<<<<<<<<<<<<<<<<<<<" << std::endl;

    mytest1();
    mytest2();
    mytest3();

    std::cout << "<<<<<<<<<<<<<<<<<<<<<<<<<" << std::endl;
}

int main(int argc, char * argv[], char * envp[])
{
    mytest();

    system("pause");
    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
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176

# 运算符重载

对操作符重载,我们需要坚持四项基本原则:

  • 不可臆造运算符。
  • 运算符原有操作数的个数、优先级和结合性不能改变。
  • 操作数中至少一个是自定义类型。
  • 保持重载运算符的自然含义。

在类内部重载运算符,默认左操作数就是这个类的对象,参数需要给出右操作数。

在类外部重载,参数中需要给出两个操作数,运算符重载在类外,一般要声明为类的友元,因为有可能牵扯到访问类的私有成员。

在类内部可以访问此类的私有成员,外部只能访问public成员。

一般来说,对于双目运算符,应当将其重载为非成员函数(友元函数),而对于单目运算符,则应将其重载为成员函数(存在例外)。

双目运算符中,=[]->()是必须重载为成员函数的。

<< 应该重载为全局函数。

赋值操作符的返回值必须是一个左值,以便可以被继续赋值。

注意:自增运算符++、自减运算符--都可以被重载,但是它们有前置、后置之分。 C++ 规定,在重载++或--时,允许写一个增加了无用 int 类型形参的版本,编译器处理++或--前置的表达式时,调用参数个数正常的重载函数;处理后置表达式时,调用多出一个参数的重载函数

// 重载为成员函数
Demo demo;
demo<<cout; // demo.operator<<(cout);
// 重载为友元全局函数
cout << demo; // operator <<(cout, demo)
1
2
3
4
5

例子:

friend ostream& operator <<(ostream &out, complex &A);
ostream & operator<<(ostream &out, complex &A){
    out << A.m_real <<" + "<< A.m_imag <<" i ";
    return out;
}
1
2
3
4
5

不能重载的运算符:?:、.、::、sizeof、#和.*、->*

# .* 和 ->*

# 继承

# 权限

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

# 继承类型

当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。

我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员函数来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。

派生类要重写的虚函数,需要显式声明。

子类只负责对新增的成员进行初始化和扫尾编写构造和析构函数,父类成员的初始化和扫尾工作由父类的构造函数和析构函数完成。

所以在进行列表初始化时,不能在子类的列表中初始化父类的成员变量。

派生类必须实现基类的纯虚函数,抽象类不能实例化,抽象类的派生类也可能是抽象类。

# 动态联编

静态成员函数不能成为虚函数。

动态联编是指编译程序在编译阶段并不能确切地知道将要调用的函数,只有在程序执行时才能确定将要调用 的函数,为此要确切地知道将要调用的函数,要求联编工作在程序运行时进行,这种在程序运行时进行的联编 工作被称为动态联编。C++规定:动态联编是在虚函数的支持下实现的。

# 虚函数表

类中的函数是所有该类对象通用的方法,不算作对象的成员,因此也不算在对象的存储空间内。

如果类中有虚函数的话,则类中会有一个指向虚函数表的指针 vptr,所以大小为 8。

# 虚析构函数

有时会让一个基类指针指向用 new 运算符动态生成的派生类对象;同时,用 new 运算符动态生成的对象 都是通过 delete 指向它的指针来释放的。如果一个基类指针指向用 new 运算符动态生成的派生类对象, 而释放该对象时是通过释放该基类指针来完成的,就可能导致程序不正确。

虚析构函数是为了避免内存泄露,而且是当子类中会有指针成员变量时才会使用得到的。也就说虚析构函数 使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露.

如果你的类需要支持多态,那你的析构函数就要是 virtual 的,如果你不希望能直接 delete 基类, 那就把析构函数声明成 protected 的。

# 必须使用指针的场景

  1. 多态
  2. 函数传参,减少对象拷贝。
  3. 前向引用,提高编译速度,在 C 中,前向引用也适用于结构体、共用体和枚举类型。
  4. Lazy initialization,成员变量是指针,延迟初始化。

# std::nothrow

在C++中new在申请内存失败时默认会抛出一个 std::bad_alloc 异常。

所以,按照C++标准,如果想检查new是否成功,则应该通过 try catch 捕捉异常。

异常机制需要堆栈的支持,这样消耗大量空间,在嵌入式等平台下,使用 std:nothrow 方式。

在内存不足时,new (std::nothrow) 并不抛出异常,而是将指针置 NULL。若不使用 std::nothrow,则分配失败时程序直接抛出异常。

分配失败是非常普通的,它们通常在植入性和不支持异常的可移动的器件中发生更频繁。因此,应用程序开发者在这个环境中使用nothrow new来替代普通的new是非常安全的。

#include <new>
#include <iostream> // for std::cerr
#include <cstdlib> // for std::exit()
Task * ptask = new (std::nothrow) Task;
if (!ptask)
{
    std::cerr<<"allocation failure!";
    std::exit(1);
}
//... allocation succeeded; continue normally
1
2
3
4
5
6
7
8
9
10

# static、const、static const 修饰变量

# static

经过 static 修饰的变量会作为类的属性而不是实体属性存在。 static 修饰的变量作为程序运行时的静态变量,存在于内存的静态区,静态区的数据初始化工作由操作系统 在加载完程序后执行 main 函数前进行。操作系统在加载完程序后,将常量区中存放的初值复制给静态变量,完成其初始化。

static修饰的变量通过 int ClassName::value = 1这种方式在类外进行初始化。

static 成员变量不占用对象的内存,而是存储在静态/全局存储区。

静态成员变量可以在类外单独分配空间,因此可以定义为类自身类型对象,如:

class A {
    static A a;
};

A::A a;

1
2
3
4
5
6

# const

const 修饰成员变量,就会退化成 C 中的 const。

const修饰的属性仍然属于对象属性,所以其初始化工作,需要由构造函数的初始化列表中完成

而且也只能在构造函数的初始化列表中初始化,运行期间将不能再对const属性进行修改。

值得注意的是,虽然经过const修饰,但是因为变量属于实体属性,而实体对象存在于动态区,

所以const属性也属于动态区,所以可以通过取址直接操作指向的内存的值,以绕过编译器对其只读的限制检查。

# static const

禁止定义静态储存周期非POD变量(static local 除外,构造顺序是明确的,当且仅当程序执行路径首次达到局部静态变量的定义处才构造。),禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植。

static const 不修饰成员变量时,相当于 C 中的 #define,但是需要编译器进行类型检查,保证了程序的健壮性, 因为是存储在常量区,常量区是在程序真正执行代码前进行初始化,所以 static const 修饰的必须是基本数据类型。 不可取地址。

static const 修饰成员变量,作为类的常量属性,其也必须是基本数据类型,在类中只有一次初始化,且该变量不可修改。 建议在声明时直接初始化。

c++ 中定义全局字符串常量建议定义成 static const char [] ,指针变量本身额外占8个字节(64bit),访问字符串需要先读取指针的值,性能比数组形式稍差。

字符串常量建议都定义成数组形式。

只允许 POD 类型的静态变量,即完全禁用 vector (使用 C 数组替代) 和 string (使用 const char [])。

如果您确实需要一个 class 类型的静态或全局变量,可以考虑在 main() 函数或 pthread_once() 内初始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。

全局变量出问题的根源在于全局变量的初始化在 main 函数之前,顺序不可控,是随机的,如果出现依赖,则会导致问题。

析构发生在主函数后,那么析构顺序也是随机的,可能出问题,因此,全局变量、静态变量之间不能出现依赖关系,否则,由于其构造、析构顺序不可控,因此可能会出现问题。

不允许用函数返回值来初始化 POD 全局变量,除非该函数(比如 getenv() 或 getpid() )不涉及任何全局变量,函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。

Google C++ 开源风格指南 (opens new window)

判断是否是 POD (“POD类型”指的是“平凡 trivial ”+“标准内存布局”,):

bool is_test1_tri = std::is_trivial_v<Test1>; // true
bool is_test_std = td::is_standard_layout <Test1> // 来判断一个类型是否是标准内存布局的。
1
2

符合 POD 类型的基本就是基本数据类型,加上一个普通C语言的结构体。

# 左值引用

左值(lvalue, left value) 是能被取地址、不能被移动 的值。

左值引用(l-ref, lvalue reference) 用 &符号引用 左值(但不能引用右值)

可以将左值引用视为对象的另一名称。 左值引用声明由说明符的可选列表后跟一个引用声明符组成。 引用必须初始化且无法更改。

// reference_declarator.cpp
// compile with: /EHsc
// Demonstrates the reference declarator.
#include <iostream>
using namespace std;

struct Person
{
    char* Name;
    short Age;
};

int main()
{
   // Declare a Person object.
   Person myFriend;

   // Declare a reference to the Person object.
   Person& rFriend = myFriend;

   // Set the fields of the Person object.
   // Updating either variable changes the same object.
   myFriend.Name = "Bill";
   rFriend.Age = 40;

   // Print the fields of the Person object to the console.
   cout << rFriend.Name << " is " << myFriend.Age << 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

# 右值引用

C++11 之前,能同时引用左值和右值的只有 const T&,C++11 后,与之对应的是通用引用,也就是右值引用。

深入浅出 C++ 11 右值引用 (opens new window)

右值(rvalue, right value) 是表达式中间结果/函数返回值(可能拥有变量名,也可能没有)。

右值引用(r-ref, rvalue reference) 用 && 符号引用 右值(也可以移动左值)

TODO:

利用右值引用,可以将左值与右值区分开。 lvalue 引用和 rvalue 引用在语法和语义上相似,但它们遵循的规则稍有不同。

右值引用主要用于移动语义和完美转发。

和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化

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

移动语义和完美转发 (opens new window) 引用声明符&& (opens new window)

# 移动语义

# 完美转发

# RAII

RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的, 中文翻译为资源获取即初始化,他说:使用局部对象来管理资源的技术称为资源获取即初始化; 这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象, 它的生命周期是由操作系统来管理的,无需人工介入。

# assert

通 C,头文件 <cassert>

# 引用

声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。

注意:不能建立数组的引用。

# 返回值是引用

当c++成员函数返回值是成员变量的引用,可以同时实现get和set属性的获取与设置

// 以下是一段示例代码, 代码定义了一个图像的一个通道,float& p(int x, int y)函数
// 可以同时设置像素值和获取像素值, 设置像素值的的用法是plane.p(5, 5) = 0.5;,
// 获取像素值的用法是 cout << plane.p(5, 5) << endl;
#include <iostream>

using namespace std;

class Plane
{
public:
	Plane(int w, int h);
	~Plane();

	float& p(int x, int y);

private:
	int _width;
	int _height;
	float* _plane;
};

Plane::Plane(int w, int h)
{
	_width = w;
	_height = h;
	_plane = new float[w*h];
}

Plane::~Plane()
{
}

float& Plane::p(int x, int y) {
	return _plane[y * _width + x];
}

int& Plane::val()
{
	return _val;
}

int main()
{
	cout << "hello" << endl;
	Plane plane(10, 10);
	plane.p(5, 5) = 0.5;
	cout << plane.p(5, 5) << endl;
	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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 虚继承

以下是菱形继承的例子:

//间接基类A
class A{
protected:
    int m_a;
};

//直接基类B
class B: public A{
protected:
    int m_b;
};

//直接基类C
class C: public A{
protected:
    int m_c;
};

//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //命名冲突
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

int main(){
    D d;
    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

这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a, 结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。

为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。

//间接基类A
class A{
protected:
    int m_a;
};

//直接基类B
class B: virtual public A{  //虚继承
protected:
    int m_b;
};

//直接基类C
class C: virtual public A{  //虚继承
protected:
    int m_c;
};

//派生类D
class D: public B, public C{
public:
// 派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

int main(){
    D d;
    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

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为 虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下, 不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。

# 虚函数

1、纯虚函数一定要在子类中声明并定义,如果子类不定义,那么子类就也是纯虚函数,但是必须要有子类实现,不然编译器会报错。

2、虚函数在子类中可以不声明与定义,但是它在子类中一经声明,就必须要定义。

3、类的成员函数声明后允许不定义,前提是不调用该已声明但未定义的函数。

# C++ 函数中的 auto

C++14 auto 推导类型时,编译器必须需要知道函数的定义,也就是说,这种用法被限制在了 内联函数、函数模板以及lambda表达式中。对于一个在头文件中声明、在其他文件中实现的函数来说, auto这样的用法是不可行的。

int (*generatorArr(int i))[10086];  //返回一个指向大小为10086的int类型的数组的指针
auto generatorArr(int i) -> int (*)[10086];

// C++ 11 尾置返回类型
//等价于 std::string someFunc(int i, double j);
auto someFunc(int i, double j) -> std::string;

// C++ 14
// C++14中,编译器终于可以自己推导任何函数的返回类型了,无论多复杂的都可以。唯一的条件就是,
//在单一的返回语句中,返回的类型必须在编译期时确定的,其他的规则就和在变量中使用auto一模一样了。
auto someFunc(int i, double j) {
  //自动推断返回类型为std::string
  return std::to_string(i + j);
}
// 在很多返回类型要取决于参数类型的时候,比如在函数模板中,
// 上边这种写法就会有很大作用,因为你很有可能并不知道进行某种操作后自己会得到什么类型
// 函数模板中的写法
// 返回T类型变量和V类型变量的和,如果T和V分别是short和int,那么返回类型就会自动推断为int,
// 但是如果一个是double一个是int,那么返回类型就会是double。因此,返回值类型和两个模板类型都相关。
template<typename T, typename V>
 auto addWithTwoTypes(T t, V v) -> decltype(t + v) {
   return t + v;
 }

// 下面的例子会推导出 auto 为 Baby
// 需要在编译期确定
class JackRoseCreator {
 public:
   Jack giveMeJack();
   Rose giveMeRose();
 };
 ​
 Baby operator+(Jack const& jack, Rose const& rose);template<typename T>
 auto giveMeSomething(T const& t) -> decytype(t.giveMeJack() + t.giveMeRose()) {
   return t.giveMeJack() + t.giveMeRose();
 }

// C++14 自动推导 例子
// 只能用于 inline、函数模板、lambda 表达式
class Container {
   typedef std::vector<int> Container_t;
   Container_t vals;

 public:
   auto begin() const {
     return std::begin(vals);
   }

   auto at(Containter_t :: size_type id) const {
     return vals[id];
   }
   //...
 };
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

如果条件允许,尽可能的去使用返回类型推导吧,这样做会让你的变量类型上下一致性更高。

# using

// 导入命名空间
// 导入整个命名空间到当前作用域
using namespace std;

// 只导入某个变量到当前作用域
using std::cout;

// 指定别名
typedef int T; // 用 T 代替 int
using T = int; // 用 T 代替 int

// 在派生类中引用基类成员
class Base {
public:
    void showName() {}
protected:
    bool bValue;
};

class Derived : private Base {
public:
    using Base::bValue;     //< 基类成员变量
    using Base::ShowName;   //< 基类成员函数
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

尽管派生类 Derived 对 基类 Base 是私有继承,但通过 using 声明,派生类的对象就可以访问基类的 proteced 成员变量和 public 成员函数了。

注意:using只是引用,不参与形参的指定。

# 外围类和嵌套类

在C++中, 可以将类声明放在另一个类中. 在另一个类中声明的类被称为嵌套类(nested class), 它通过提供新的类型类作用域来避免名称混乱. 包含类的成原函数可以创建和使用被嵌套类的对象; 而仅当声明位于共有部分, 才能在包含类的外面使用嵌套类, 而且必须使用作用域解析运算符.

对类进行嵌套和包含并不同, 包含意味着将类对象作为另一个类的成员, 而对类进行嵌套不创建类成员, 而是定义了一种类型, 该类型仅在包含嵌套类声明的类中有效。

class Outer {
    // Outer 是外围类
    class Inner {
        // Inner 是嵌套类
    };
};

Outer::Inner inner;
1
2
3
4
5
6
7
8

嵌套类有这样一些性质:

  • 嵌套类的名字只在其外围类内可见,在类外使用时,需要加作用域;
  • 嵌套类可以直接引用外围类的静态成员、类名和枚举成员,不需要加作用域;
  • 继承一个嵌套类时,被继承的嵌套类类名需要加作用域。

嵌套类是外围类的友元类,外围类对嵌套类没有访问特权。

注意:嵌套类仅仅是友元而已,它不属于外围类,也就是说,在访问时必须给它传一个外围类的对象,外围类不能直接访问嵌套类

要想访问嵌套类的私有成员,嵌套类必须提供公有接口,外围类只能通过共有接口间接访问嵌套类私有成员。

嵌套类的可见性与在外围类中声明的位置有关,如果在public区域声明,这种情况下相当于在一个命名空间声明了一个类。

# typename 的使用

template<typename C> void print2nd(const C& container) {  
    C::const_iterator *x; // 二义性,const_iterator 是变量还是类型?是定义变量还是两个变量相乘?
    ...
    }
1
2
3
4

模板中依赖于模板参数的名称称为从属名称(dependent name), 当一个从属名称嵌套在一个类里面时,称为嵌套从属名称(nested dependent name)。嵌套从属名称是需要用typename声明的,其他的名称是不可以用typename声明的。

比如:

template<typename T>
void fun(const T& proto ,typename  T::const_iterator it);
1
2

对于用于模板定义的依赖于模板参数的名称,只有在实例化的参数中存在这个类型名,或者这个名称前使用了typename关键字来修饰,编译器才会将该名称当成是类型。除了以上这两种情况,绝不会被当成是类型。

因此,如果你想直接告诉编译器 T::iterator 是类型而不是变量,只需用typename修饰,这样编译器就可以确定 T::iterator 是一个类型,而不再需要等到实例化时期才能确定,因此消除了歧义。

模板中的嵌套从属名称是需要typename声明的,然而有一个例外情况: 在派生子类的基类列表中,以及构造函数的基类初始化列表中,不允许typename声明。 例如 Derived<T>继承自Base<T>::Nested

template<typename T>
class Derived: public Base<T>::Nested{  // 继承基类列表中不允许声明`typename`
public:
    explicit Derived(int x): Base<T>::Nested(x){    // 基类初始化列表不允许声明`typename`
        typename Base<T>::Nested tmp;   // 这里是要声明的
    }
};
1
2
3
4
5
6
7

# 杂项

  • lambda 函数,可以定义的同时调用。
  • 函数定义时可以不写形式参数,只写类型以用于重载。
  • 内置类型的默认初始值由定义的位置决定,定义在函数体之外的变量被静态初始化为 0,其他未定义。
  • 类类型的默认初始值由类自己决定,初始化列表优先于函数列表先执行,如果有初始化列表,默认初始值被忽略。
  • const 成员变量只能在构造函数初始化列表初始化,必须有构造函数,不同对象 const 数据成员值可以不同。
  • static constexpr 必须有类内初始值,不是整型也可在类内初始化(float、double 等)。
  • static const 建议在类外初始化,只有是整型才能在类内初始化。

# STL

# 模板

函数模板的返回值也可以定义为模板参数(template parameter), 但是由于无法推导(deduce), 需要显式(explicit)指定;

# 基本概念

标准模板库

广义上分为:container、alogorithm、iterator。

容器和算法之间通过迭代器进行连接

标准库STL的容器都是值语义的。即,无法将一个变量放到容器里。

实际上容器里并没有存储这个变量,而是拷贝了值给新的变量。

# 六大组件

  1. 容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
  2. 算法:各种常用的算法,如sort、find、copy、for_each等
  3. 迭代器:扮演了容器与算法之间的胶合剂。
  4. 仿函数:行为类似函数,可作为算法的某种策略。
  5. 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
  6. 空间配置器:负责空间的配置与管理

容器

Sequence containers(序列容器):

  • array Array class (class template)
  • vector Vector (class template)
  • deque Double ended queue (class template)
  • forward_list Forward list (class template)
  • list List (class template)

Container adaptors(容器适配器):

  • stack LIFO stack (class template)
  • queue FIFO queue (class template)
  • priority_queue Priority queue (class template)

Associative containers(关联容器):

  • set Set (class template)
  • multiset Multiple-key set (class template)
  • map Map (class template)
  • multimap Multiple-key map (class template)

Unordered associative containers(无序关联容器):

  • unordered_set Unordered Set (class template)
  • unordered_multiset Unordered Multiset (class template)
  • unordered_map Unordered Map (class template)
  • unordered_multimap

# iostream

cin

int num;
cin >> num;
cin.getline();
getline(cin, ch);
cin.ignore();
cin.get();
1
2
3
4
5
6

cout

cout << num << endl;
cout.cout.precision(3);
1
2

# vector

vector 数据存放在堆中,是顺序表结构,支持随机访问。

插入删除效率低。

适合使用数据量大,动态调整数组长度,高效率存储,需要随机访问,不关心插入、删除效率的场景。

#include <vector>
// 初始化
vector<int> vec;
// 初始化并指定大小,并初始化为 0。
vector<int> vec(10);
// 初始化
vector<int> vec{3};
// 使用数组初始化
int arr[] = {1, 2, 3, 5, 8};
vector<int> vec(arr, arr + 5);
vector<int> vec {1, 2, 3, 4}; // 列表初始化

// vector 大小
size_t s = vec.size();
// 设置大小
vec.resize();
// 判空
bool b = vec.empty();
// 添加元素
vec.push_back(3);
// 访问最新添加的元素
vec.back();
// 访问最先添加进的元素
vec.front();
// 访问指定位置的元素,at 越界会抛出异常,[] 访问会返回错误的引用。
vec.at(i);
// 删除指定位置的元素
auto iter = find(vec.begin(), vec.end(), val);
vec.erase(iter);
vec.erase(vec.begin(), vec.end()); //< 类似 vec.clear()
// 注意:erase 删除 iter 后 iter 变为野指针,返回删除元素的下一个元素
iter = vec.erase(iter);
// 向 vector 插入元素可能导致迭代器失效
vec.emplace_back(4);    // c++11
// 指定位置创建元素,效率比 insert 高,如果插入的元素有构造函数,需要传入构造函数需要的参数。
vec.emplace(vec.begin(), 3);
// 调用无参构造函数
vec.emplace(vrc.begin());
// 指定位置插入新元素
vec.insert(vec.begin(), 3);
vec.begin();
vec.end();
// 指向逆序 reverse (vector)的迭代器
vec.rbegin();
vec.rend();
// 指向常量content(vector)的迭代器
vec.cbegin();
vec.cend();
vec.assign();
vec.clear();
// Vector的容量之所以重要,有以下两个原因:
// 1. 容器的大小一旦超过capacity的大小,vector会重新配置内部的存储器,导致和vector元素相关的所有reference、pointers、iterator都会失效。
// 2.内存的重新配置会很耗时间。
// 重新分配内存后需要重新获取指针。
// capacity 的增长轨迹是 2 的幂次增长,是指数爆炸。
vec.capacity(); //< 容量
vec.reserve();  //< 创建容器后用于设置容量


// 访问
cout << vec[0];
// 遍历
for (const auto &num : vec) {
    cout << num;
}
// 下标遍历
for (size_t i = 0; i < arr.size(); ++i) {
    cout <<arr[i];
}
// 使用迭代器
vector<int>::iterator iter;
// const 迭代器
vector<int>::const_iterator const_iter = begin();
for(iter = vec.begin(); iter != vec.end(); ++iter) {
    cout << *iter;
}
// 使用算法库
class myprint {
public:
	myprint() = default;
	~myprint() = default;
	void operator()(int val) {
		cout << val << " ";
	}
};
for_each(vec.begin(), vec.end(), myprint());
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

# array

适合数据量小、长度固定的场景,对数组做了一些封装,提供了更好的封装和类型检查,不会退化成指针,避免指针操作失误。

C++11 建议尽量用 array 来替代数组。

#include <array>
std::array<int, 5> a0 = {0, 1, 2, 3, 4};          //正确
std::array<int, 5> a1 = a0;                       //正确
int m = 5;
int b[m];                                 //正确,内置数组
std::array<int, 5> a2;                    //正确
std::array<int, m> a3;                    //错误,array不可以用变量指定
std::array<int, 5> a4 = b;                //错误,array不可以用数组指定
// 访问元素
// 进行越界检查
arr.at(0);
arr[0];
arr.front();
arr.back();
// 返回数组第一个元素的指针
arr.data();
arr.empty();
arr.size();
arr.begin();
arr.end();
arr.swap();
// 用 val 填充容器
arr.fill(val);
begin();
end();
cbegin();
cend();
rbegin();
rend();
crbegin();
crend();
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

# string

string 与 vecotr<char> 几乎一样。

string增加了更多对字符串专门的操作,比如加法操作符,查找子串,截取子串等字符串独有的功能。

#include <string>
// 初始化
string s{"string"};
string s("string");

// 字符串大小,不包含 \0
s.size();
// 转成 c 风格的字符串
s.c_str();
// 遍历字符串的字符,和数组类似。
for (const auto& ch : s) {
    cout << ch << " ";
}
// 复制构造新字符串
string s1 = s;
// 字符串拼接
s += s1;
// 读取一行字符串
getline(cin, str);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# list

链表结构,内存中不连续,不支持随机访问。插入删除效率高。

适合有大量插入、删除操作、不关心随机访问的场景,如数据库增删改查。

std::list<int> l {7, 5, 16, 8};
// 不提供 at() 和 []
// 头
l.front();
// 尾
l.back();
l.emplace_front(25);
l.emplace_back(13);
l.push_front(25);
l.push_back(13);
l.pop_front();
l.pop_back();
l.clear();
l.insert(values.begin() , 3);
// 在容器的指定位置直接生成新的元素;
l.emplace(values.end(), 6);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# forward_list

如果是功能单一,不需要很复杂操作的,应该优先使用单链表,因为单链表耗用的内存空间更少,空间效率更高;但如果是需要向前或向后查找,则应该优先使用高效一些的双向循环链表。

常用 API

begin()		//返回一个前向迭代器,其指向容器中第一个元素的位置。
end()		//返回一个前向迭代器,其指向容器中最后一个元素之后的位置。
assign()	//用新元素替换容器中原有内容。
push_front()	//在容器头部插入一个元素。
pop_front()		//删除容器头部的一个元素。
swap()			//交换两个容器中的元素,必须保证这两个容器中存储的元素类型是相同的。
remove(val)		//删除容器中所有等于 val 的元素
sort()			//通过更改容器中元素的位置,将它们进行排序。
sort([](element){}) //  sort(greater<int>())
emplace_front()
before_begin();
cbdfore_begin();
empty()
insert_after()
emplace_after() // 构造一个元素放在一个元素后面
clear()
insert_after(iterator position, element)
insert_after(iterator position, n, element)
insert_after(iterator position, list)
erase_after()
emplace_front()
resize()
merge()
remove_if([](element)->bool{})
reverse()
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

# map/unordered_map/multi_map

std::pair

主要的作用是将两个数据组合成一个数据,两个数据可以是同一类型或者不同类型。

pair 实质上是一个结构体,其主要的两个成员变量是first和second,这两个变量可以直接使用。

map:不重复、有序

multi_map:重复、有序

对它的容器元素进行新增操作或者删除操作时,操作之前的所有迭代器,在操作完成之后依然有效。(vector 无效)

map 的排序和 set 类似。

#include <utility>

auto p = make_pair(1, 3);
p.first
p.second
1
2
3
4
5

std::map

映射,也称作字典,是一个关联容器。

multimap,key 可以重复。

map的元素是一个key和一个value共同构成的组合体,这个组合体是一个 pair<T,Y> 类型。

pair<T, Y> 类型有两个成员:first,second。

map<int, int> student_score{
        //key 不可以有重复
        //key 可以不按顺序创建
        //key 在创建完成之后会在内部自动排好顺序
        //key 排序按照大小顺序,int类型的数据就按数值大小排序
        {003, 90},
        {004, 88},
        {001, 88},//value可以相同
        {002, 78},
};
pair<string, int> item1{"Harvard University", 1};
pair<string, int> item2{"Stanford University", 2};

map<string, int> m { item1,item2 };

// 可以通过下标访问
cout << m["Havard University"];
// 通过 at 访问,更安全,但是要进行异常处理
m.at("Havard University");
// 但是如果 [] 内的是不存在的 key,就会插入一个新的 key,并初始化 value 为 0。
// 如果不存在的 key ,插入新的 key 需要调用默认构造函数。
// 插入新 pair
m["Massachusetts Institute of Technology"] = 3;
// 使用 insert 插入,与使用 [] 效果相同。
m.insert(pair<key, value>{"University of Cambridge", 4});
m.insert(make_pair<key, value>(111, {1, "ddd", {11,11}, {11,11}}));
// 遍历
for (const auto& item : m) {
    cout << item.first << " " << item.second << endl;
}

size(); // 元素个数
emplace();  // 插入
operator[]; // 插入、按下标访问
at();
find()// key
swap()
count()// key
empty()
size()
clear()
erase()
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

更改 map 排序方式(默认 std::less):

#include <QCoreApplication>

#include <map>
#include <algorithm>
#include <QDebug>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    //按QString逆序
    std::map<QString, int, std::greater<QString> > strIntMap;
    strIntMap.insert(std::pair<QString, int>(QString("aa"), 1));
    strIntMap.insert(std::pair<QString, int>(QString("bb"), 2));
    strIntMap.insert(std::pair<QString, int>(QString("cc"), 3));
    strIntMap.insert(std::pair<QString, int>(QString("dd"), 4));

    for (std::map<QString, int, std::greater<QString> >::iterator iter = strIntMap.begin();
         iter != strIntMap.end(); ++iter)
    {
        qDebug() << "(" << iter->first << "," << iter->second << ")";
    }

    //按int逆序
    std::map<int, QString, std::greater<int> > intStrMap;
    intStrMap.insert(std::pair<int, QString>(1, QString("aa")));
    intStrMap.insert(std::pair<int, QString>(2, QString("bb")));
    intStrMap.insert(std::pair<int, QString>(3, QString("cc")));
    intStrMap.insert(std::pair<int, QString>(4, QString("dd")));

    qDebug() << Qt::endl;

    for (std::map<int, QString, std::greater<int> >::iterator iter = intStrMap.begin();
         iter != intStrMap.end(); ++iter)
    {
        qDebug() << "(" << iter->first << "," << iter->second << ")";
    }
    return a.exec();
}
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

# set/multi_set/unordered_set

集合。

set 用来存储同一数据类型的数据类型,并且能从一个数据集合中取出数据,在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序。

不能直接更改 set 中元素的值。 必须先删除旧值,才能插入具有新值的元素。

set<int> s;

// 插入 3
s.insert(3);
// 判断 3 是否出现
s.count(3);
begin()        ,返回set容器的第一个元素
end()      ,返回set容器的最后一个元素
clear()          ,删除set容器中的所有的元素
empty()    ,判断set容器是否为空
max_size()   ,返回set容器可能包含的元素最大个数
size()      ,返回当前set容器中的元素个数
rbegin()    ,返回的值和end()相同
rend()     ,返回的值和rbegin()相同
insert()
erase()
emplace()
swap()
clear()
find()
count()
empty()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

set一般插入元素时,默认使用关键字类型的< 运算符来比较两个关键字,故一般插入后为升序

实现自定义排序的方法:

  1. 重载 < ,在自定义的数据结构中重载<即可。
  2. 重载 () 符。
#include<iostream>
#include<set>
using namespace std;

struct Students
{
    string id;
    int age,height;
    Students(string s,int a,int h):id(s),age(a),height(h){}
    Students() {}
};

class comp{
public:
    bool operator()(const Students &s1,const Students &s2){
        if(s1.id!=s2.id) return s1.id<s2.id;
        return s1.age<s2.age;
    }
};

int main(){
    set<Students,comp> se;
    // 自定义或者传入 STL 自身的模板类
    set<Students, greater<int>> s;
    se.insert(Students("zhou",12,134));
    se.insert(Students("wu",13,42));
    se.insert(Students("zheng",34,43));
    se.emplace("wang",13,43);
    se.emplace("zhou",23,43);
    for(auto it=se.begin();it!=se.end();it++){
        cout<<it->id<<" "<<it->age<<" "<<it->height<<endl;
    }
    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
35

# stack

push()  // 插入元素到 top
emplace() // 插入元素到 top
top()
empty()
pop()
swap()
1
2
3
4
5
6

# queue

queue为队列,它和stack堆栈的正好相反,栈是先进后出,而队列则是先进先出(FIFO)。看到这里是不是想起了我们前面学过的一个顺序性容器deque(双端队列),下面来区分一下他们之间的不同之处:

1、queue可以访问两端但是只能修改队头,而deque可以访问两端并且可以在队首和队尾删除和插入元素

2、deque可以从两端入队,但是queue只能从队尾入队,

3、对于弹出队内元素,deque拥有pop_front(删除队头元素)以及pop_back(删除队尾元素)

front()
back()
empty()
size()
push()
emplace()
pop()
swap()
1
2
3
4
5
6
7
8

# iteator

迭代器是专门用来遍历容器的对象(是容器类的内部类型)。

使用逆序迭代器:

vector<int> vec{3, 1, 3, 3, 56, 23 ,89};
vector<int>::reverse_iterator rit = vec.rbegin();

for (; rit != vec.rend(); ++iter) {
    cout << *rit;
}
1
2
3
4
5
6

# 算法

max/min

#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
	int arr[10] = { 5,8,2,4,6,9,7,0,1,3 };
	int m1=arr[0];
	int m2 = arr[0];
	for (int i = 0; i < 10; ++i) {
		m1 = max(m1, arr[i]);
		m2 = min(m2, arr[i]);
	}
	cout << "最大值为: " << m1
		 << "\n最小值为: " << m2 << endl;
}

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

find

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std

int main()
{
    vector<int> arr{ 1,2,3,4,5,3,9,3 };
    /*find 找到第一个指定值的元素,
      并返回这个元素对应的迭代器
    */
    auto itr = find(arr.begin(), arr.end(), 3);
    arr.erase(itr);// 删除第1个3

    for (auto item : arr)
    {
        cout << item << " ";
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

find_if

按指定规则查找元素

find_if(iterator beg,iterator end,_Pred);

beg起始迭代器

end结束迭代器

_Pred称为谓词(返回值是bool类型的函数或仿函数称为谓词),在这里用做查找规则。 谓词这个概念很重要,此后会经常见到 找到返回指定位置的迭代器,找不到返回结束迭代器

bool greaterFive(int val)//查找规则
{
	return val > 5;
}
int main()
{
	vector<int> v;
	for (int i = 0; i < 10; ++i) {
		v.push_back(i);
	}
	auto it = find_if(v.begin(), v.end(), greaterFive);
	if (it != v.end()) {
		cout << "找到大于5的元素:" << *it << endl;
	}
	else {
		cout << "未找到" << endl;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

binary_find

二分查找

查找速度比一般查找算法快的多,因为一般查找算法的时间复杂度为O(n),而二分查找算法的时间复杂度为O(log(n))

但二分查找也有局限性,它不能用于查找无序序列,也就是说只能利用二分查找查升序排序或降序排序序列中指定的元素

int main()
{
	vector<int> v;
	for (int i = 0; i < 10; ++i) {
		v.push_back(i);
	}
	bool flag = binary_search(v.begin(), v.end(), 5);
	if (flag == 1) {
		cout << "找到指定元素" << endl;
	}
	else {
		cout << "未找到" << endl;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

remove

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    vector<int> arr{ 1,2,3,4,5,3,9,3 };
    /*remove 把需要保留的数据紧凑的保留在数组的前面,
     并返回第一个不应该再属于数组的元素的迭代器,
     供后续按照范围删除
     remove 之后数组元素排列如下:
     1, 2, 4, 5, 9, #, #, #
    */
    auto itr = remove(arr.begin(), arr.end(), 3);
    arr.erase(itr, arr.end());// 删除 { # # # }

    //now print all int in arr : no 3 anymore.
    for (auto item : arr)
    {
        cout << item << " ";
    }
    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

for_each

// for_each example
#include <iostream>     // std::cout
#include <algorithm>    // std::for_each
#include <vector>       // std::vector

void myfunction (int i) {  // function:
  std::cout << ' ' << i;
}

struct myclass {           // function object type:
  void operator() (int i) {std::cout << ' ' << i;}
} myobject;

int main () {
  std::vector<int> myvector;
  myvector.push_back(10);
  myvector.push_back(20);
  myvector.push_back(30);

  std::cout << "myvector contains:";
  for_each (myvector.begin(), myvector.end(), myfunction);
  std::cout << '\n';

  // or:
  std::cout << "myvector contains:";
  for_each (myvector.begin(), myvector.end(), myobject);
  std::cout << '\n';

  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

sort

内部主要由快速排序+插入排序+堆排序实现的。

如果不提供比较用的方法需要重载 < 运算符。

#include <algorithm>
#include <functional>
#include <array>
#include <iostream>
#include <string_view>

int main()
{
    std::array<int, 10> s = {5, 7, 4, 2, 8, 6, 1, 9, 0, 3};

    auto print = [&s](std::string_view const rem)
    {
        for (auto a : s)
            std::cout << a << ' ';
        std::cout << ": " << rem << '\n';
    };

    std::sort(s.begin(), s.end());
    print("sorted with the default operator<");

    std::sort(s.begin(), s.end(), std::greater<int>());
    print("sorted with the standard library compare function object");

    struct
    {
        bool operator()(int a, int b) const { return a < b; }
    }
    customLess;

    std::sort(s.begin(), s.end(), customLess);
    print("sorted with a custom function object");

    std::sort(s.begin(), s.end(), [](int a, int b)
                                  {
                                      return a > b;
                                  });
    print("sorted with a lambda expression");
}
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

stable_sort

基于归并排序

//我们按对组的第一个元素大小对对组进行排序
bool cmp(pair<int, char> a, pair<int, char> b)
{
	return (a.first > b.first);//>表示从大到小,<表示从小到大(注意这里return 0是不移动元素,return 1是移动元素,
        //也就是说a.first==b.first时return 0不改变原来的顺序,vs中相等情况必须返回0)
}


int main()
{
	vector<pair<int, char>>v;
	v.push_back(make_pair(1, 'a'));
	v.push_back(make_pair(1, 'b'));
	v.push_back(make_pair(2, 'a'));
	v.push_back(make_pair(3, 'a'));

	stable_sort(v.begin(),v.end(),cmp);
	for (auto x : v)
	{
		cout << x.first << " " << x.second<<endl;
	}

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

count

功能: 统计相同元素出现次数

class Person
{
public:
	string m_name;
	int m_age;
	Person(string name,int age):m_name(name),m_age(age) {}
	bool operator==(const Person& p)//重载==,作为统计元素规则
	{
		if (m_age == p.m_age) {
			return true;//统计年龄相同的元素
		}
		return false;
	}
};
int main()
{
	vector<Person> v;
	v.push_back(Person("张三", 30));
	v.push_back(Person("李四", 25));
	v.push_back(Person("王五", 35));
	v.push_back(Person("赵六", 25));
	v.push_back(Person("熊大", 25));
	v.push_back(Person("熊二", 20));

	Person p("光头强", 25);
	int num = count(v.begin(), v.end(), p);
	cout << "年龄为25的生物的个数:" << num << 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

count_if

bool greater5(int val)
{
	return val > 5;
}
int main()
{
	vector<int> v;
	for (int i = 0; i < 10; ++i) {
		v.push_back(i);
	}
	int num = count_if(v.begin(), v.end(), greater5);
	cout << "大于5的元素的个数为:" << num << endl;
}

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

swap

交换两个容器内的元素,或者仅仅交换两个元素

#include <algorithm>
#include <iostream>

namespace Ns
{
    class A
    {
        int id{};

        friend void swap(A& lhs, A& rhs)
        {
            std::cout << "swap(" << lhs << ", " << rhs << ")\n";
            std::swap(lhs.id, rhs.id);
        }

        friend std::ostream& operator<< (std::ostream& os, A const& a)
        {
            return os << "A::id=" << a.id;
        }

    public:
        A(int i) : id{i} {}
        A(A const&) = delete;
        A& operator = (A const&) = delete;
    };
}

int main()
{
    int a = 5, b = 3;
    std::cout << a << ' ' << b << '\n';
    std::swap(a, b);
    std::cout << a << ' ' << b << '\n';

    Ns::A p{6}, q{9};
    std::cout << p << ' ' << q << '\n';
//  std::swap(p, q);  // error, type requirements are not satisfied
    swap(p, q);       // OK, ADL finds the appropriate friend `swap`
    std::cout << p << ' ' << q << '\n';
}
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

replace/replace_if

#include <algorithm>
#include <array>
#include <iostream>
#include <functional>

int main()
{
    std::array<int, 10> s{5, 7, 4, 2, 8, 6, 1, 9, 0, 3};

    std::replace(s.begin(), s.end(), 8, 88);

    for (int a : s)
        std::cout << a << " ";
    std::cout << '\n';

    std::replace_if(s.begin(), s.end(),
                    std::bind(std::less<int>(), std::placeholders::_1, 5), 55);

    for (int a : s)
        std::cout << a << " ";
    std::cout << '\n';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

accumulate

功能: 计算容器内元素累计总和

#include<iostream>
#include<vector>
#include<numeric>
using namespace std;
int main()
{
	vector<int> v;
	for (int i = 0; i <= 100; i++) {
		v.push_back(i);
	}
	int sum = accumulate(v.begin(), v.end(), 0);
	cout << "容器内元素和为:" << sum << endl;
}

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

fill

功能: 向容器中填充指定元素

#include<iostream>
#include<vector>
#include<numeric>
using namespace std;
int main()
{
	vector<int> v(10);
	fill(v.begin(), v.end(), 1);
	for (int i = 0; i < 10; ++i) {
		cout << v[i] << " ";
	}
}
1
2
3
4
5
6
7
8
9
10
11
12

# 输入输出流

iostream

cout.write("ddd", 3);;
int i = 33;
// 按照字节输入
cout.write(reinterpret_cast<char*>(&i), 4);
// 文本格式输入
cout << i<< endl;

1
2
3
4
5
6
7

ofstream

文件输出流

#include <fstream>

// ios::ate 文件打开后定位到文件尾,操作过程中可以移动指针,与 ios::out 配合将清空文件。
// ios::out 输出方式打开,覆盖。
// ios::in  输入方式打开
// ios::app 追加的方式打开,每次写操作都将指针置于文件尾
// ios::trunc 如果文件存在就将长度设置为 0
// ios::binary 在 window 上关掉 \r \n 之间的转换。
fstream fin("out.txt", ios::in | ios::out | ios::binary);
ofstream fout("out.txt", ios::app | ios::binary);
// 判断文件是否正确打开
fout.is_open();
assert(fout.is_open());
// text output
fout << "插入数据到流中,直接覆盖";

bad(); // 读写过程中出错
fail(); // 读写过程中出错
eof(); // 到达文件尾,返回 true。
good(); // 以上任意一个返回 true,就返回 false。


close(); // 关闭文件流

// 注意:operator << 运算符输出 formated text

// 输入一个字符
// 输入一个字符块
#include <fstream>
#include <iostream>
#include <string>

int main()
{
    std::string filename{"test.bin"};
    std::fstream s{filename, s.binary | s.trunc | s.in | s.out};

    if (!s.is_open())
        std::cout << "failed to open " << filename << '\n';
    else
    {
        // write
        double d{3.14};
        s.write(reinterpret_cast<char*>(&d), sizeof d); // binary output
        s << 123 << "abc";                              // text output

        // for fstream, this moves the file position pointer (both put and get)
        s.seekp(0);

        // read
        d = 2.71828;
        s.read(reinterpret_cast<char*>(&d), sizeof d); // binary input
        int n;
        std::string str;
        if (s >> n >> str)                             // text input
            std::cout << "read back from file: " << d << ' ' << n << ' ' << str << '\n';
    }
    s.close();
}
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

istringstream

istringstream是一个比较有用的c++的输入输出控制类。

C++引入了ostringstream、istringstream、stringstream这三个类,要使用他们创建对象就必须包含 <sstream> 这个头文件。

它的作用是从 string 对象 str 中读取字符。

// 将多个字符串放入 stringstream 中,实现字符串的拼接目的
sstream ss("xxx");
ss << "yyy";
ss.str(); // 转换成 string
ss.str(""); // 清空 sstream
ss.clear(); // 多次类型转换前使用 clear()


// 切割字符串

#include <sstream>
#include <iostream>

using namespace std;

int main()
{
    stringstream sstream;
    int first, second;

    // 插入字符串
    sstream << "456";
    // 转换为int类型
    sstream >> first;
    cout << first << endl;

    // 在进行多次类型转换前,必须先运行clear()
    sstream.clear();

    // 插入bool值
    sstream << true;
    // 转换为int类型
    sstream >> second;
    cout << second << endl;




    string s = "a/b/c/d";
    istringstream iss(s);
    string buffer;
    while(getline(iss, buffer, '/'))
    {
        cout<<buffer<<endl;
    }
    return 0;
}

#include <iostream>
#include <sstream> //istringstream
using namespace std;

int main()
{
	//字符串数据(注意字符串中间有一个空格):用来区分是两个数据一个是年龄,一个是姓名
	string data("23 Jay");

	//创建一个istringstream对象iss,用字符串数据初始化对象iss
	istringstream iss(data);

	int age;
	string name;

	//从字符串流中读取数据赋值给变量
	iss >> age >> name;

	//输出变量
	cout << "age=" << age << ", name=" << name << endl;
  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
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

# mt19937 随机数产生器

std::mt19937是伪随机数产生器,用于产生高性能的随机数。 C++11引入。

mt是因为这个伪随机数产生器基于Mersenne Twister算法。

19937是因为产生的随机数的周期长,可达到2^19937-1。

#include <random>
// std::mt19937接收一个unsigned int数作为种子。
// std::random_device本身是均匀分布整数随机数生成器,通常仅用于播种
std::mt19937 mt_rand(std::random_device{}());
std::mt19937 mt_rand(time(0));
std::mt19937 mt_rand(std::chrono::system_clock::now().time_since_epoch().count());

#include <iostream>
#include <random>

using namespace std;

int main()
{
    std::mt19937 rng(std::random_device{}());
    for (int i = 0; i < 5; i++) {
        cout << rng() << endl;
    }

    return 0;
}

// 产生正态分布的随机数的例子如下:
#include <iostream>
#include <random>

using namespace std;

int main()
{
    std::mt19937 rng(std::random_device{}());
    std::normal_distribution<double> nd(5, 2);
    for (int i = 0; i < 5; i++) {
        cout << nd(rng) << endl;
    }

    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
35
36
37
38
39

# std::mutex

mutex又称互斥量,用于提供对共享变量的互斥访问。C++11中mutex相关的类都在头文件中。

共四种互斥类:

序号 名称 用途
1 std::mutex 最基本也是最常用的互斥类
2 std::recursive_mutex 同一线程内可递归(重入)的互斥类
3 std::timed_mutex 除了具备 mutex 功能外,还提供了带时限请求锁定的能力
4 std::recursive_timed_mutex 同一线程可递归(重入)的 timed_mutex

mutex相关类不支持拷贝构造、不支持赋值。同时 mutex 类也不支持 move 语义(move构造、move赋值)。

// 锁住互斥量。
// 如果互斥量没有被锁住,则调用线程将该mutex锁住,直到调用线程调用unlock释放。
// 如果mutex已被其它线程lock,则调用线程将被阻塞,直到其它线程unlock该mutex。
// 如果当前mutex已经被调用者线程锁住,则std::mutex死锁,而recursive系列则成功返回。
void lock();
// 尝试锁住mutex
// 如果互斥量没有被锁住,则调用线程将该mutex锁住(返回true),直到调用线程调用unlock释放。
// 如果mutex已被其它线程lock,则调用线程将失败,并返回false。
// 如果当前mutex已经被调用者线程锁住,则std::mutex死锁,而recursive系列则成功返回true。
bool try_lock();
// 解锁mutex,释放对mutex的所有权。值得一提的时,对于recursive系列mutex,unlock次数需要与lock次数相同才可以完全解锁。
void unlock();
// 相对于手动lock和unlock,可以使用RAII(通过类的构造析构)来实现更好的编码方式。
// Lock 类(两种)
std::lock_guard // 与 Mutex RAII 相关,方便线程对互斥量上锁。
std::unique_lock // 与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。
// unclock()
// once_flag 非 pod 类。
template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );

/*
lock_guard:

std::lock_guard 在构造函数中进行加锁,析构函数中进行解锁。
锁在多线程编程中,使用较多,因此c++11提供了lock_guard模板类;在实际编程中,我们也可以根据自己的场景编写resource_guard RAII类,避免忘掉释放资源。

std::unique_lock:

1. unique_lock 是通用互斥包装器,允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与 条件变量一同使用。
2. unique_lock比lock_guard使用更加灵活,功能更加强大。
3. 使用unique_lock需要付出更多的时间、性能成本。

虽然递归锁能解决这种情况的死锁问题,但是尽量不要使用递归锁,主要原因如下:

1. 需要用到递归锁的多线程互斥处理本身就是可以简化的,允许递归很容易放纵复杂逻辑的产生,并 且产生晦涩,当要使用递归锁的时候应该重新审视自己的代码是否一定要使用递归锁。
2. 递归锁比起非递归锁,效率会低。
3. 递归锁虽然允许同一个线程多次获得同一个互斥量,但可重复获得的最大次数并未具体说明,一旦 超过一定的次数,再对lock进行调用就会抛出std::system错误。
*/

// 代码示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <stdexcept>

std::mutex mtx;

void print_event(int x)
{
        if(x%2==0)
                std::cout << x << " is even\n";
        else
                throw (std::logic_error("not even"));

}

void print_thread_id(int id)
{
        try{
                std::unique_lock<std::mutex> lck(mtx);
                print_event(id);
        }
        catch(std::logic_error&)
        {
                std::cout << "[exception caught]\n";
        }
}

int main(int argc,char **argv)
{
        std::thread threads[10];
        for(int i=0;i<10;i++)
        {
                threads[i]=std::thread(print_thread_id,i+1);
        }
        for (auto& th : threads) th.join();
        return 0;

}
// unique_lock 配合 条件变量:

#include <iostream>
#include <mutex>
#include <thread>
#include <deque>
#include <condition_variable>
#include <unistd.h>

std::deque<int> q;
std::mutex mtx;
std::condition_variable cond;
int count=0;

void func1()
{
        while(true)
        {
                // {
                std::unique_lock<std::mutex> locker(mtx);
                q.push_front(count++);
                locker.unlock();//如果是lock_guard,不支持手动解锁
                cond.notify_one();

                sleep(1);
        }
}

// 条件变量的目的就是为了,在没有获得某种提醒时长时间休眠; 如果正常情况下, 我们需要
// 一直循环 (+sleep), 这样的问题就是CPU消耗+时延问题,条件变量的意思是在cond.wait
// 这里一直休眠直到 cond.notify_one唤醒才开始执行下一句; 还有cond.notify_all()接口用于唤醒所有等待的线程。

void func2()
{
        while(true)
        {
                // 条件变量也是共享资源,需要上锁操作
                std::unique_lock<std::mutex> locker(mtx);
                // 条件变量在wait时会进行unlock再进入休眠, lock_guard并无该操作接口。
                cond.wait(locker,[](){return !q.empty();});
                auto data=q.back();
                q.pop_back();
                std::cout << "thread2 get value form thread1: " << data << std::endl;
        }
}
// wait: 如果线程被唤醒或者超时那么会先进行lock获取锁, 再判断条件(传入的参数)是否成立, 如果成立则 wait函数返回否则释放锁继续休眠。
// notify: 进行notify动作并不需要获取锁。
// 使用场景:需要结合notify+wait的场景使用unique_lock; 如果只是单纯的互斥使用lock_guard。

int main(int atgc,char ** argv)
{
        std::thread t1(func1);
        std::thread t2(func2);
        t1.join();
        t2.join();
        return 0;
}
//=============================================================================
// call_once 使用
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <iostream>
#include <mutex>
#include <type_traits>
#include <chrono>

using namespace std;

static int _p = 3;
// non-pod 类不存储在静态存储区
// 如果确实需要一个 class 类型的静态或全局变量,可以考虑在 main() 函数
// 或 pthread_once() 内初始化一个指针且永不回收。注意只能用 raw 指针,
// 别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。
static once_flag * _flag;
static mutex * _mtx;
static condition_variable *_g_cnd;
static int _g_tmp = 0;


static void _call_once(int *val)
{
    _mtx = new mutex;
    _g_cnd = new condition_variable;
    *val  = *val + 1;
}

int func()
{
    call_once(*_flag, _call_once, &_p);
    while (true) {
        //lock_guard<mutex> lck(*_mtx);
        unique_lock<mutex> lck(*_mtx);
        // 解锁,等待信号。
        // 收到信号,上锁。
        _g_cnd->wait(lck, []() -> bool {
            return _g_tmp >= 5;
        });
        cout << "tid: " << this_thread::get_id() << endl;
        _g_tmp = 0;
        cout << "tmp reset to 0" << endl;
        this_thread::sleep_for(chrono::seconds(1));
        // 离开作用域解锁。
    }
    return 0;
}

void func2()
{
    call_once(*_flag, _call_once, &_p);
    while (true) {
        //lock_guard<mutex> lck(*_mtx);
        unique_lock<mutex> lck(*_mtx);
        cout << "tid: " << this_thread::get_id() << endl;
        _g_tmp++;
        cout << "tmp: " << _g_tmp << endl;
        this_thread::sleep_for(chrono::seconds(1));

        if (_g_tmp >= 5) {
            // 解锁,发信号。
            lck.unlock();
            _g_cnd->notify_one();
        }
        // 离开作用域,lck 析构。
    }
}

int main(int argc, const char* argv[])
{
    _flag = new once_flag;
    // 等同 vector<thread> thrd_vec(2);
    vector<thread> thrd_vec;
    thrd_vec.reserve(2);

    thrd_vec.emplace_back(thread(func));
    thrd_vec.emplace_back(thread(func2));

    for (size_t i = 0; i < thrd_vec.size(); ++i) {
        thrd_vec[i].join();
    }

    cout << "p = " <<  _p << endl;
    cout << boolalpha;
    cout << "mutex is pod ?: " << is_pod<mutex>::value << endl;

    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
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228

# std::thread

C++ std::thread (opens new window)

linux 下的 thread,是基于 pthread 实现的。

  • 不可复制,只能移动。
  • 每个线程具有唯一标志,线程 id。
  • 创建线程。
// 默认构造
thread() noexcept;
// 移动构造
// 如果当前对象不可 joinable,需要传递一个右值引用(rhs)给 move 赋值操作;如果当前对象可被 joinable,则会调用 terminate() 报错。
// 用于给默认构造函数创建的线程重新赋值。
thread( thread&& other ) noexcept;
// 初始化构造函数
// 创建一个 std::thread 对象,该 std::thread 对象可被 joinable,非分离的,
// 新产生的线程会调用 f 函数,该函数的参数由 args 给出。
// 注意:可被 joinable 的 std::thread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached.
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
// 拷贝构造函数
thread( const thread& ) = delete;
// Join 线程,调用该函数会阻塞当前线程,直到由 *this 所标示的线程执行完毕 join 才返回。
// 放弃线程执行,回到就绪状态
void join();
// 设置线程为分离属性。
// Detach 线程。 将当前线程对象所代表的执行实例与该线程对象分离,使得线程的执行可以单独进行。一旦线程执行完毕,它所分配的资源将会被释放。
// 调用 detach 函数之后:

// *this 不再代表任何的线程执行实例。
// joinable() == false
// get_id() == std::thread::id()
// 分离线程陷阱:线程的生命周期比所使用的资源生命周期长,可能造成位置错误,如:线程分离后调用 cout、全局变量。
void detach();
// Swap 线程,交换两个线程对象所代表的底层句柄(underlying handles)。
void swap( std::thread& other ) noexcept;
// 检查线程是否可被 join。检查当前的线程对象是否表示了一个活动的执行线程,由默认构造函数创建的线程是不能被 join 的。
// 另外,如果某个线程 已经执行完任务,但是没有被 join 的话,该线程依然会被认为是一个活动的执行线程,因此也是可以被 join 的。
bool joinable() const noexcept;
// 休眠函数
template< class Rep, class Period >
void sleep_for( const std::chrono::duration<Rep, Period>& sleep_duration );
// 调用示例:
std::this_thread::sleep_for(std::chrono::seconds(5));

// 休眠线程直到时间点
template< class Clock, class Duration >
void sleep_until( const std::chrono::time_point<Clock,Duration>& sleep_time );
(since C++11)
// 获取线程号
std::thread::id this_thread::get_id() noexcept;
// 放弃当前线程占用时间片使CPU重新调度以便其它线程执行:
void this_thread::yield() noexcept;
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

# std::chrono

C++11 时间工具chrono (opens new window)

chrono是c++ 11中的时间库,提供计时,时钟等功能。

std::chrono::seconds(C++11)	duration type with Period std::ratio<1>
std::chrono::minutes(C++11)	duration type with Period std::ratio<60>
std::chrono::hours(C++11)	duration type with Period std::ratio<3600>
std::chrono::days(C++20)	duration type with Period std::ratio<86400>
std::chrono::weeks(C++20)	duration type with Period std::ratio<604800>
std::chrono::months(C++20)	duration type with Period std::ratio<2629746>
std::chrono::years(C++20)	duration type with Period std::ratio<31556952>

// 使用示例:
std::chrono::minutes t1( 10 );
std::chrono::seconds t2( 60 );
std::chrono::seconds t3 = t1 - t2;
std::cout << t3.count() << " second" << std::endl;
cout << chrono::duration_cast<chrono::minutes>( t3 ).count() << endl;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

<chrono> 中重载了一些字面量:

operator""h//C++14 chrono::duration 字面量,表示小时
operator""min// 表示分钟
operator""s // 表示秒钟
operator""ms // 表示毫秒
operator""us // 表示微秒
operator""ns // 表示纳秒
operator""d // 表示天 chrono::day
operator""y // 表示年 chrono::year
1
2
3
4
5
6
7
8

# condition_variable

头文件 <condition_variable>

condition_variable ​condition_variable_any

相同点:两者都能与std::mutex一起使用。

不同点:前者仅限于与 std::mutex 一起工作,而后者可以和任何满足最低标准的互斥量一

起工作,从而加上了_any的后缀。condition_variable_any会产生额外的开销。

一般只推荐使用condition_variable。除非对灵活性有硬性要求,才会考虑condition_variable_any。

void wait( std::unique_lock<std::mutex>& lock );
void wait( std::unique_lock<std::mutex>& lock, Predicate pred );
//以上二者都被notify_one())或notify_broadcast()唤醒,但是
//第二种方式是唤醒后也要满足Predicate的条件。
//如果不满足条件,继续解锁互斥量,然后让线程处于阻塞或等待状态。
//第二种等价于
while (!pred())
{
    wait(lock);
}
//  wait_for可以指定一个时间段,在当前线程收到通知或者指定的时间 rel_time 超时之前,
// 该线程都会处于阻塞状态。而一旦超时或者收到了其他线程的通知,wait_for返回
template <class Rep, class Period>
  cv_status wait_for (unique_lock<mutex>& lck,
                      const chrono::duration<Rep,Period>& rel_time);
// 通知或者超时都会解锁
wait_for(lck,std::chrono::seconds(1));
// wait_for 的重载版本的最后一个参数pred表示 wait_for的预测条件,
// 只有当 pred条件为false时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred为 true时才会被解除阻塞。
template <class Rep, class Period, class Predicate>
    bool wait_for (unique_lock<mutex>& lck,
         const chrono::duration<Rep,Period>& rel_time, Predicate pred);
//Predicate是lambda表达式。
template< class Predicate >
// 通知wait()取消对线程的阻塞有:
notify_one()
//通知第一个进入阻塞或者等待的线程。
void notify_all() noexcept
// 通知全部进入阻塞或者等待的线程
template< class Rep, class Period >
std::cv_status wait_for( std::unique_lock<std::mutex>& lock,
                         const std::chrono::duration<Rep, Period>& rel_time);
template< class Clock, class Duration >
std::cv_status
    wait_until( std::unique_lock<std::mutex>& lock,
                const std::chrono::time_point<Clock, Duration>& timeout_time );

// 例子:
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;

void worker_thread()
{
    // Wait until main() sends data
    std::unique_lock<std::mutex> lk(m);
    //子进程的中wait函数对互斥量进行解锁,同时线程进入阻塞或者等待状态。
    cv.wait(lk, []{return ready;});

    // after the wait, we own the lock.
    std::cout << "Worker thread is processing data\n";
    data += " after processing";

    // Send data back to main()
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";

    // Manual unlocking is done before notifying, to avoid waking up
    // the waiting thread only to block again (see notify_one for details)
    lk.unlock();
    cv.notify_one();
}

int main()
{
    std::thread worker(worker_thread);

    data = "Example data";
    // send data to the worker thread
    {
        //主线程堵塞在这里,等待子线程的wait()函数释放互斥量。
        std::lock_guard<std::mutex> lk(m);
        ready = true;
        std::cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();

    // wait for the worker
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return processed;});
    }
    std::cout << "Back in main(), data = " << data << '\n';

    worker.join();
}
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
88
89
90
91
92
93
94

# std::future

c++11引入的future是为了解决异步通信问题的。future可以看做是数据通道,可以获取到async的 线程函数的返回结果,也可以与promise连用,获取promise的数据。

举个例子来说明,在C++11之前我们在线程A中创建子线程B,为了拿到B中计算的数据,我们声明一个全局变量 保存B的计算结果数据,线程A等待,直到B计算完成后,读取全局变量,拿到计算结果数据。C++11之后, 我们可以声明future通过get方法获取异步返回值,很大程度的简化了操作步骤。

future 只支持移动语义,不支持拷贝构造语义. 只允许一个future对象有权限获取结果

T get();
(1)	(member only of generic future template)
(since C++11)
T& get();
(2)	(member only of future<T&> template specialization)
(since C++11)
void get();
(3)	(member only of future<void> template specialization)
(since C++11)


// future example
#include <iostream>       // std::cout
#include <future>         // std::async, std::future
#include <chrono>         // std::chrono::milliseconds

// a non-optimized way of checking for prime numbers:
bool is_prime (int x) {
  for (int i=2; i<x; ++i) if (x%i==0) return false;
  return true;
}

int main ()
{
  // call function asynchronously:
  std::future<bool> fut = std::async (is_prime,444444443);

  // do something while waiting for function to set future:
  std::cout << "checking, please wait";
  std::chrono::milliseconds span (100);
  while (fut.wait_for(span)==std::future_status::timeout)
    std::cout << '.' << std::flush;

  bool x = fut.get();     // retrieve return value

  std::cout << "\n444444443 " << (x?"is":"is not") << " prime.\n";

  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
35
36
37
38
39

# 函数对象

谓词概念

谓词概念:

  • 返回值是 bool 类型的函数或函数对象称为谓词
  • 如果operator()接收一个参数,就叫一元谓词
  • 如果operator()接收两个参数,就叫二元谓词

在C++ STL的内置算法中有很多函数都是有谓词这个参数的,比如大家常用的sort()排序算法,它的参数列表如下: void sort<_Ranlt>(const_Ranlt_First, const_Ranlt_Last, _Pr_Pred); 它的第三个参数_Pr_Pred就是谓词,这个参数可以不用传,默认升序排序,但如果你想要降序排序,这个谓词参数就必须要传了。

内建函数对象

C++STL中内建了一些函数对象:

算术仿函数

原型:

#include <functional>
template<class T> T plus<T> 加法仿函数
template<class T> T minus<T> 减法仿函数
template<class T> T multiplies<T> 乘法仿函数
template<class T> T divides<T> 除法仿函数
template<class T> T modulus<T> 取模仿函数
template<class T> T negate<T> 取反仿函数
1
2
3
4
5
6
7

功能: 实现四则运算

其中negate是一元运算,其他都是二元运算

关系仿函数

原型:

#include <functional>
template<class T> bool equal_to<T> 等于

template<class T> bool not_equal_to<T> 不等于

template<class T> bool greater<T> 大于

template<class T> bool greater_equal<T> 大于等于

template<class T> bool less<T> 小于

template<class T> bool less_equal<T> 小于等于
1
2
3
4
5
6
7
8
9
10
11
12

用途:

如果只是简单的比较两个数的大小,我们完全没必要用到关系仿函数,完全可以使用关系运算符>、=、<等 来判断。但关系仿函数可以充当C++STL算法中的谓词。

逻辑仿函数

原型:

template<class T> bool logical_and<T> 逻辑与
template<class T> bool logical_or<T> 逻辑或
template<class T> bool logical_not<T> 逻辑非
1
2
3

# VS 2022

CMake

project(HELLO_CPP)
include_directories(include)
aux_source_directory(./src SRC_LIST)
add_executable(hello_main ${SRC_LIST})
1
2
3
4
最后更新: 2023/11/7 08:36:05