Cpp体系架构
前言
本文旨在帮助读者快速复习Cpp,建立起一个完整的Cpp知识体系架构。
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;
share_ptr和weak_ptr的底层实现
共享指针和弱指针的实现比较类似,都是指向数据的指针和指向控制块的指针。使用共享指针创建出来的弱指针内部的控制块共享。
控制块的结构如下:
1
2
3
4
5
6
struct ControlBlock {
std::atomic<size_t> share_count;
std::atomic<size_t> weak_count;
void* ptr;
Deleter deleter; // 用于释放ptr所指的资源
};
所指资源由共享指针的析构负责释放;控制块由弱指针的析构负责释放。
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);
右值引用的本质
右值引用是对右值的绑定,可以延长右值的生命周期至当前作用域;
移动语义
移动语义表明资源可以在对象之间转移,通常通过移动构造和移动赋值中体现。都是将使用的对象的资源移动到新创建的对象上,原来的对象的资源被置空(初始化为零值);
右值是可以被移动的,被编译器所认可的。一个左值对象是可移动的通常需要被标记使其被编译器认可,这个标记的过程就是调用move,move本质上没有发生任何事情,仅仅是给这个对象打上一个标记(右值类型),使其移动的行为被编译器认可,触发移动构造和赋值;
注意,移动只在自定义的类型中才会发生作用。
完美转发
完美转发允许函数模板将其参数完美地转发给另一个函数,无论参数是左值还是右值。完美转发通常使用 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;
}
完美转发通常在函数模板中使用,函数模板参数是万能引用,传入左值被解析为左值引用,右值被解析为右值引用;在通过forward函数转发给内部函数。内部函数如果有左值引用和右值引用的重载,就可以被正确区分;
委托构造与继承构造
委托构造:在一个类中有多个构造函数时,构造函数可以调用其他构造函数,减少代码冗余。
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
多继承与内存布局
简单非多态
虚函数 + 静态数据成员
单继承对象
多继承对象 + 虚函数
避免多继承变量歧义的机制
作用域解析运算符:
使用作用域解析运算符 :: 明确指定要访问的基类成员。 虚继承:
使用虚继承(virtual inheritance)来确保只有一个基类子对象被共享,从而避免重复继承带来的歧义。
虚继承内存布局
虚函数相关
实现多态的机制–虚函数表
基类指针通过指向不同的类对象,调用相同声明的不同实现的虚函数的过程是多态。其实现机理是通过虚函数表。当子类继承父类,父类中含有虚函数时,子类构造时会在data区创建一张虚函数表,并在子类的内存中开辟4字节的空间存放虚函数指针vptr指向这张虚表。
调用基类指针的虚函数时,会在执行期间使用所指对象的虚函数指针查找所指对象的虚函数表中虚函数的实现。这就是多态的实现机制。
虚析构
解决使用基类指针释放子类对象内存时,调用子类对象的析构函数,而不是父类自己的。防止内存泄漏。
运算符重载
如果类对象也要使用类似基本运算符操作,就需要进行类的运算符重载。除了以下运算符不能重载:
- 成员访问运算符:.(点运算符)
- 成员指针访问运算符:.* 和 ->*
- 作用域解析运算符:::
- 条件运算符:?:(三元运算符)
- sizeof 运算符
- 类型信息运算符:typeid
- 静态成员选择运算符:::
- 对齐运算符:alignof
- lambda 表达式运算符:[]
单目运算符与双目运算符
- 双目运算符重载为类的成员函数时,函数只显式声明一个参数,形参为该运算符的右操作数。比如你重载+,写在类外面可以写两个参数,但写在类里面是一个参数,因为该函数调用的时候会自动传入一个this指针,就是对象本身。
- 前置单目运算符(前置++):没有参数,返回值为引用。
- 后置单目运算符(后置++):多一个int参数,返回值为对象。(不是引用) / 待形参只是为了区分前置和后置,实际上不会用到这个参数。
注意后置:
1
2
3
4
5
Counter operator++(int) {
Counter temp = *this; // 创建当前对象的副本
++value; // 增加当前对象的值
return temp; // 返回增加前的副本
}
友元运算符 « » 运算符重载为友元函数,因为左操作数是cout,右操作数是对象,不是类的成员函数,而是标准头文件的类的函数。声明为友元函数后,那个类就可以访问你的私有成员了。
友元
在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;
}
访问控制
访问控制:
继承控制注意:默认是private继承,所以通常都要指定public继承。
template 模板 / 泛型编程
为什么模板不能份文件实现
函数模板
通过建立一个通用函数,其返回值类型和形参类型可以不具体制定,用一个虚拟的类型来表示
1
2
3
4
template <typename T, typename U, ...>
void func(T a, U b, ...) {
}
注意:模板函数在发生自动类型推导的时候不会进行隐式类型转换。只用显示制定类型才会触发隐式类型转换。
类模板
类模板的定义
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;
}
可变参数
基本概念:
- 参数包:在函数原型的声明中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
基本操作:
迭代器:
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
unordered_map
unordered_map是基于哈希表实现的,查找、插入、删除的时间复杂度是O(1)。unordered_map中的元素是无序的。
基本操作可以上网查
unordered_map 与 map的区别
为什么容器操作中的emplace的执行效率要更高?
如果要将一个结构体类型的实例,放入到容器中,一般有两个步骤:
- 构造这个实例
- 将这个实例copy到容器中
而这个copy的过程可以使用两个函数,一是拷贝构造函数,二是移动构造函数。push_back()和insert()函数就是按照这个步骤来的。
但是对于emplace_back() 和 emplace()函数,它们是直接在容器中的指定直接构造这个实例,而不是先构造再拷贝。所以效率更高。这就是区别。只有一个步骤
为什么map需要比vector多一次移动构造,应该是在构造pair的时候多出来的。