Post

Cpp体系架构

Cpp体系架构

前言

本文旨在帮助读者快速复习Cpp,建立起一个完整的Cpp知识体系架构。

alt text

Cpp 新特性

掌握 auto、decltype

auto:变量类型推断;decltype:表达式类型推断

类型推断可以在编译器就推导出变量或者表达式的类型,方便开发者编码简化代码。

  • decltype:decltype(expression) var 将 var 的类型定义为 expression 的类型。
    • decltype 只会返回表达式的类型,不会对表达式进行求值。
    • 如果表达式是一个变量,decltype 返回该变量的类型;如果表达式是一个函数调用,decltype 返回函数的返回类型。

for range

for range:for(auto& var : container)

function & bind & lambda 函数绑定

function 对象通常使用bind和lambda函数绑定。

  • bind: std::function<int(int, int)> func = std::bind(&A::print, &a, std::placeholders::_1, std::placeholders::_2);

std::placeholders::_n 表示占位符,表示func调用中的第n个参数。

  • lambda
    1
    2
    
     std::function<void(int)> func = [&a](int x) { a.print(x); };
     std::function<void(int)> func = [](int x) -> int { return x; };
    

捕获:

  • [&]:捕获所有外部变量
  • [=]:捕获所有外部变量的值
  • [a]:捕获 a 变量
  • [&a]:捕获 a 变量的引用
  • [&, a]:捕获所有外部变量,但 a 除外
  • [=, &a]:捕获所有外部变量的值,但 a 除外
  • []

smart pointer

智能指针:std::shared_ptr, std::unique_ptr, std::weak_ptr

  • unique_ptr / 独占指针:std::unique_ptr<int> p(new int(10)); / std::unique_ptr<int> p = std::make_unique<int>(10);

独占指针拥有持有资源的所有权,资源不能拷贝,只能移动所有权

  • shared_ptr / 共享指针

相比于独占指针,共享指针持有的资源可以在多个共享指针中共享,每多一个共享指针,资源的引用计数加一。当共享指针析构时,引用计数减一,当判断到引用计数为0时,资源被释放。

1
2
std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2 = p1;
  • weak_ptr / 弱指针

弱指针的存在是为了解决share_ptr的引用循环的问题。弱指针不会增加资源的引用计数,当资源被释放后,弱指针不会自动释放。什么是引用循环的问题呢?两个对象互相引用,导致资源无法释放。

1
std::weak_ptr<int> p3 = p1;

explicit default delete

  • explicit:显示构造函数,禁止隐式构造。
  • default:声明默认构造函数,就不用显式定义函数体了
  • delete:禁用函数,比如禁用拷贝构造函数和拷贝赋值运算符,在unique_ptr中就需要使用delete禁用拷贝构造函数和拷贝赋值运算符。 default和delete搭配使用可以让对象只能显式构造生成。

右值引用与移动构造函数

如何产生右值引用?使用std::move(a)函数可以将左值a强制转换为右值引用。可以减少对象拷贝。

1
2
std::string str = "hello";
std::string&& str2 = std::move(str);

右值引用的本质

完美转发

完美转发允许函数模板将其参数完美地转发给另一个函数,无论参数是左值还是右值。完美转发通常使用 std::forward 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <utility>

void process(int& x) {
    std::cout << "Lvalue reference" << std::endl;
}

void process(int&& x) {
    std::cout << "Rvalue reference" << std::endl;
}

template <typename T>
void forwarder(T&& arg) {
    process(std::forward<T>(arg));
}

int main() {
    int a = 10;
    forwarder(a);        // 调用 process(int& x)
    forwarder(20);       // 调用 process(int&& x)
    forwarder(std::move(a)); // 调用 process(int&& x)

    return 0;
}

委托构造与继承构造

委托构造:在一个类中有多个构造函数时,构造函数可以调用其他构造函数,减少代码冗余。

1
2
3
4
5
6
7
8
class A {
public:
    A(int a, int b) : _a(a), _b(b) {}
    A(int a) : A(a, 0) {}
private:
    int _a;
    int _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
#include <iostream>
#include <string>

// 父类
class Base {
public:
    int baseVar;

    // 父类构造函数
    Base(int var) : baseVar(var) {
        std::cout << "Base class constructor called with baseVar = " << baseVar << std::endl;
    }
};

// 子类
class Derived : public Base {
public:
    std::string derivedVar;

    // 继承父类构造函数
    using Base::Base;

    // 子类构造函数
    Derived(int baseVar, const std::string& var) : Base(baseVar), derivedVar(var) {
        std::cout << "Derived class constructor called with derivedVar = " << derivedVar << std::endl;
    }
};

int main() {
    // 使用继承的父类构造函数创建子类对象
    Derived obj1(10);
    std::cout << "Base class variable: " << obj1.baseVar << std::endl;

    // 使用子类自己的构造函数创建子类对象
    Derived obj2(20, "Hello");
    std::cout << "Base class variable: " << obj2.baseVar << std::endl;
    std::cout << "Derived class variable: " << obj2.derivedVar << std::endl;

    return 0;
}

random 随机值的获取

random库,组件分为两种,一种是随机数引擎类、另一种是随机数分布类

  • 随机数引擎类

  • 随机数分布类

to_string()

  • to_string():将数字转换为字符串
1
2
int a = 10; // long, long long, unsigned, unsigned long, unsigned long long, float, double, long double
std::string str = std::to_string(a);

面向对象

new 和 malloc的区别

  • 最大的区别:new在申请内存空间的时候会调用构造函数,malloc不会。

  • 申请失败返回:new失败会返回错误码bad_alloc,malloc失败会返回NULL。

  • 属性上:new是Cpp关键字,是操作符,需要编译器的支持;malloc是库函数,需要头文件支持。

  • 参数上:new不需要传参,编译器会计算类型大小;malloc需要传入参数显式指定申请内存的大小,且大小不同调用的底层函数也不同。这里扩展一下,malloc申请的内存大小超过128K时,会调用mmap函数,否则调用brk函数。

  • 返回值:new返回的是对象的指针,malloc返回的是void*,需要我们自己去强制类型转换。

delete和delete[]的区别

  • delete:释放new申请的内存空间,释放对象的内存空间,调用析构函数。
  • delete[]:释放new[]申请的内存空间,释放数组的内存空间,调用析构函数。delete[]释放数组的时候,会调用数组中每个元素的析构函数。

volatile 和 mutable

alt text

多继承与内存布局

简单非多态

alt text

虚函数 + 静态数据成员

alt text

单继承对象

alt text

多继承对象 + 虚函数

避免多继承变量歧义的机制

  • 作用域解析运算符:

  • 使用作用域解析运算符 :: 明确指定要访问的基类成员。 虚继承:

使用虚继承(virtual inheritance)来确保只有一个基类子对象被共享,从而避免重复继承带来的歧义。

alt text

虚继承内存布局

alt text

虚函数相关

运算符重载

如果类对象也要使用类似基本运算符操作,就需要进行类的运算符重载。除了以下运算符不能重载:

  • 成员访问运算符:.(点运算符)
  • 成员指针访问运算符:.* 和 ->*
  • 作用域解析运算符:::
  • 条件运算符:?:(三元运算符)
  • sizeof 运算符
  • 类型信息运算符:typeid
  • 静态成员选择运算符:::
  • 对齐运算符:alignof
  • lambda 表达式运算符:[]

单目运算符与双目运算符

  • 双目运算符重载为类的成员函数时,函数只显式声明一个参数,形参为该运算符的右操作数。比如你重载+,写在类外面可以写两个参数,但写在类里面是一个参数,因为该函数调用的时候会自动传入一个this指针,就是对象本身。
  • 前置单目运算符(前置++):没有参数,返回值为引用。
  • 后置单目运算符(后置++):多一个int参数,返回值为对象。(不是引用) / 待形参只是为了区分前置和后置,实际上不会用到这个参数。

注意后置:

1
2
3
4
5
Counter operator++(int) {
    Counter temp = *this; // 创建当前对象的副本
    ++value;              // 增加当前对象的值
    return temp;          // 返回增加前的副本
}

友元运算符 « » 运算符重载为友元函数,因为左操作数是cout,右操作数是对象,不是类的成员函数,而是标准头文件的类的函数。声明为友元函数后,那个类就可以访问你的私有成员了。

alt text

友元

在C++中,友元(friend)是一种允许一个函数或另一个类访问某个类的私有成员和保护成员的机制。友元可以是函数、类或成员函数。友元关系是单向的和非传递的,即被声明为友元的函数或类可以访问该类的私有和保护成员,但反过来不行。

友元函数

友元函数是一个可以访问类的私有和保护成员的非成员函数。它在类的定义中使用 friend 关键字声明。

示例:

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

class MyClass {
private:
    int data;
public:
    MyClass(int value) : data(value) {}

    // 声明友元函数
    friend void display(const MyClass& obj);
};

// 友元函数定义
void display(const MyClass& obj) {
    std::cout << "Data: " << obj.data << std::endl;
}

int main() {
    MyClass obj(42);
    display(obj); // 友元函数可以访问私有成员
    return 0;
}

友元类

友元类是一个可以访问另一个类的私有和保护成员的类。它在类的定义中使用 friend 关键字声明。

示例:

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
#include <iostream>

class MyClass {
private:
    int data;
public:
    MyClass(int value) : data(value) {}

    // 声明友元类
    friend class FriendClass;
};

class FriendClass {
public:
    void display(const MyClass& obj) {
        std::cout << "Data: " << obj.data << std::endl; // 友元类可以访问私有成员
    }
};

int main() {
    MyClass obj(42);
    FriendClass friendObj;
    friendObj.display(obj);
    return 0;
}

友元成员函数

友元成员函数是另一个类的成员函数,可以访问该类的私有和保护成员。它在类的定义中使用 friend 关键字声明。

示例:

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
#include <iostream>

class MyClass;

class AnotherClass {
public:
    void display(const MyClass& obj);
};

class MyClass {
private:
    int data;
public:
    MyClass(int value) : data(value) {}

    // 声明友元成员函数
    friend void AnotherClass::display(const MyClass& obj);
};

void AnotherClass::display(const MyClass& obj) {
    std::cout << "Data: " << obj.data << std::endl; // 友元成员函数可以访问私有成员
}

int main() {
    MyClass obj(42);
    AnotherClass anotherObj;
    anotherObj.display(obj);
    return 0;
}

访问控制

访问控制:

alt text

继承控制注意:默认是private继承,所以通常都要指定public继承。

template 模板 / 泛型编程

为什么模板不能份文件实现

函数模板

通过建立一个通用函数,其返回值类型和形参类型可以不具体制定,用一个虚拟的类型来表示

1
2
3
4
template <typename T, typename U, ...>
void func(T a, U b, ...) {

}

注意:模板函数在发生自动类型推导的时候不会进行隐式类型转换。只用显示制定类型才会触发隐式类型转换。

alt text

类模板

类模板的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>
class Myclass {
public:
    T data;
    void print() {
        std::cout << data << std::endl;
    }
};

int main() {
    Myclass<int> obj;  // 类模板需要显式指定类型
    obj.data = 10;
    obj.print();
    return 0;
}

alt text

可变参数

基本概念:

  • 参数包:在函数原型的声明中Args… args同理存在0个或者一个1以上的类型参数,C++中将“typename… Args”这样的语义,称为参数包 (parameter pack)。
  • 包展开:参数包只有在使用时就必須把它展开变成一个个的参数,概念上称为包展开(pack expansion),将参数包当作普通的参数一样放到被调用函数的参数列表的最后一个位置,并在后面加上 …

可变参数模板的本质是允许函数或类接受任意数量的参数,并通过递归或参数包展开的方式处理这些参数。在这个示例中,make_unique 函数模板通过展开参数包,将所有参数传递给 T 的构造函数,从而创建一个 T 类型的对象。

1
2
3
4
5
6
7
8
9
10
11
12
// 函数模板中的可变参数 / 递归使用
template <typename T, typename... Args> 
void func(T a, Args... args) {
    std::cout << a << std::endl;
    func(args...);
}

// 类模板中的可变参数 / 函数包展开使用
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique<T>(new T(std::forward<Args>(args)...));
}

STL

容器 / Container

  • 序列容器:array、vector、queue、deque、priority_queue、stack、list、forward_list
  • 关联容器:map、set multimap、multiset
  • 关联容器:unordered_map、unordered_set unordered_multimap、unordered_multiset

vector

基本操作:

alt text

迭代器:

alt text

map

map保存的是键值对,可以通过key来快速查找添加删除,但是还是O(logN)。底层使用红黑树。map中的元素是有序的。

基本操作:

  • 迭代器: ```cpp for(auto it = m.begin(); it != m.end(); ++it) { std::cout « it->first « ” “ « it->second « std::endl; }

for(auto& [key, value] : m) { std::cout « key « ” “ « value « std::endl; } ```

  • CURD

alt text

alt text

alt text

unordered_map

unordered_map是基于哈希表实现的,查找、插入、删除的时间复杂度是O(1)。unordered_map中的元素是无序的。

基本操作可以上网查

unordered_map 与 map的区别

alt text

为什么容器操作中的emplace的执行效率要更高?

如果要将一个结构体类型的实例,放入到容器中,一般有两个步骤:

  • 构造这个实例
  • 将这个实例copy到容器中

而这个copy的过程可以使用两个函数,一是拷贝构造函数,二是移动构造函数。push_back()和insert()函数就是按照这个步骤来的。

但是对于emplace_back() 和 emplace()函数,它们是直接在容器中的指定直接构造这个实例,而不是先构造再拷贝。所以效率更高。这就是区别。只有一个步骤

alt text

为什么map需要比vector多一次移动构造,应该是在构造pair的时候多出来的。

This post is licensed under CC BY 4.0 by the author.