C++ Notes
C++ 11/14/17/20 笔记
教程链接
https://changkun.de/modern-cpp/zh-cn/00-preface/
C++ 新特性总结
指针空值: 现在可以使用nullptr赋值给一个空指针,nullptr只可以被隐式转换为指针类型
1
2
3int *p = nullptr; //合法
int n1 = (int)nullptr; //不合法
int n2 = reinterpret_cast<int>(nullptr); //不合法nullptr_t 和 nullptr
nullptr是一个指针空值类型的常量,而nullptr_t是一个指针空值类型,是一个关键字,一个类型1
2
3
std::nullptr_t NullPtr;constexpr: 使用这个标志来修饰表达式,函数,可以告诉编译器这个函数最后会输出一个固定的数,从而让编译器在编译的时候就进行某些替换
1
2
3
4
5
6
7
8
9
10
11
12
13constexpr int return_const_value(){
return 1;
}
int [return_const_value()]; //现在是合法的,因为constexpr关键字会把函数名替换为1
// constexpr也可以用在if语句里面
template<typename T>
auto print_type_info(const T& t) {
if constexpr (std::is_integral<T>::value) { //constexpr可以用在if语句中
return t + 1;
} else {
return t + 0.1;
}
}POD类型(Plain Old Data),是一种特殊类型的结构体或类。POD类型具有以下特征:
- 所有非静态成员都是public的:POD类型的成员变量都是公开的,可以被外部访问。
- 没有用户自定义的构造函数、析构函数、拷贝构造函数和移动构造函数:POD类型不能包含任何用户自定义的构造函数、析构函数、拷贝构造函数和移动构造函数。它们必须使用默认的构造函数、析构函数和复制/移动操作。
- 没有用户自定义的拷贝赋值运算符和移动赋值运算符:POD类型不应该包含自定义的拷贝赋值运算符和移动赋值运算符。它们应该使用默认的复制/移动操作。
- 没有虚函数:POD类型不能包含任何虚函数,因为虚函数会引入虚函数表指针,破坏了POD的简单性和内存布局的确定性。
POD类型通常用于需要直接内存布局控制和与C语言交互的情况,比如与底层硬件交互或进行内存操作时。在C++11之后,POD类型的概念被扩展为标准布局类型(Standard Layout Types),并引入了更加灵活的POD类型和标准布局类型的概念。
总之,POD类型的设计目的是为了保证其简单性和内存布局的确定性,以便在需要时直接操作内存,提高程序的效率和性能。
=default , =delete
- 明确默认行为: 在某些情况下,即使类的特殊成员函数可以被默认生成,也可以使用= default语法来显式地指示默认行为。这样做可以让代码更加清晰明了,减少歧义。
- 禁用默认实现: 在某些情况下,希望禁止编译器生成默认实现,可以使用= default来声明这些特殊成员函数,然后在声明的同时标记为delete。这样可以防止意外的默认行为,提高代码的安全性。
1
2
3
4
5
6
7
8class Myclass{
Myclass() = default;
~Myclass() = default;
// 禁用默认的拷贝构造函数和赋值运算符
Myclass(const Myclass&) = delete;
Myclass& operator=(const Myclass&) = delete;
}
左值,右值
- 左值 (lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。左值是一个有内存位置的对象
1
2
3
4int a; // a是一个左值
int& function(){
return temp; // 返回的是int&引用,也是一个左值
} - 右值 (rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。一个表达式不是左值就是右值!
1
2
3// 下面都是右值
var + 1;
3;
- 对于左值和右值,左值到右值的转换可以是隐式的,但是右值不能转换为左值(除非特殊操作),右值可以显式的赋值给左值,没有隐式转换
- 纯右值 (prvalue, pure rvalue),纯粹的右值或字面量。比如字面量:10,true;求值结果为字面量或匿名临时对象:1+2,3*5。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。
注意:字面量除了字符串字面量以外,均为纯右值。字符串字面量为一个左值,类型为const char数组1
const char (&left)[6] = "01234"; // 此时,"01234"为一个左值
- 将亡值:即将被销毁、却能够被移动的值。可以直接把纯右值和将亡值统一看做右值,不影响
1
2std::vector<int> vec = Get_Vector();
// vec是一个左值,而函数Get_Vector()返回的东西就是一个右值(将亡值) - 右值引用:使用符号
&&
,比如int&& b = 1合法, - 左值引用:就是原来普通的引用
&
,为变量取一个别名,所以左值引用不能接受一个右值(int& b = 1不合法) std::move
,将左值参数无条件转换为右值- 左值引用只能绑定左值,右值引用只能绑定右值。但是注意常量的左值引用可以绑定非常量左值、常量左值、右值(const int & a = 1;合法)
1
2
3
4// 左值引用,使用T&, 只能绑定左值
// 右值引用,使用T&&,只能绑定右值
// 常量左值,使用const T&, 既可以绑定左值又可以绑定右值
// 已命名的右值引用,编译器会认为是个左值 - 移动语义:(可以避免无意义的拷贝构造,加强性能),比如下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int main() {
std::string str = "Hello world."; // 因为std::string是资源管理类型,它的移动语义会转移底层的动态内存资源,从而影响原变量
std::vector<std::string> v;
// 将使用 push_back(const T&), 即产生拷贝行为
v.push_back(str);
// 将输出 "str: Hello world."
std::cout << "str: " << str << std::endl;
// 将使用 push_back(const T&&), 不会出现拷贝行为
// 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销
// 这步操作后, str 中的值会变为空
v.push_back(std::move(str));
// 将输出 "str: "
std::cout << "str: " << str << std::endl;
// 只有资源管理类型使用移动语义后才会失效,(int, double, float这些都不会),而像(unique_ptr, std::string, std::vector)这种就会失效
} - 完美转发:建议阅读https://changkun.de/modern-cpp/zh-cn/03-runtime/#%E5%AE%8C%E7%BE%8E%E8%BD%AC%E5%8F%91
设计到std::forward
来进行参数的转发(传递)
- 左值 (lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。左值是一个有内存位置的对象
auto类型推断,可以让编译器推理变量的类型
1
2
3
4
5
6
7
8
9
10auto x = 5; //int
auto y = 0.5; //double
std::vector<int> vec = {1,2,3,4}; //作为迭代器进行遍历
for(auto it = vec.begin(); it != vec.end(); ++it){
std::cout<< *it << std::endl;
}
// 在c++14后,下面的写法是可以的
template<typename T, typename U>
auto add(T x, U y){return x+y;}还可以用来做模板编程和用来表示一些复杂的类型,简化代码
列表初始化,使用{}来进行初始化
1
2std::vector<int> vec = {1,2,3,4}; //列表初始化容器
auto x = {1,2,3}; //推断auto类型为std::initializer_list<int>decltype,可以返回表达式的类型,也可以返回一个变量的类型,用法如下
1
2
3
4
5
6template <typename T1, typename T2>
auto Mul(const T1& t1, const T2& t2) -> decltype(t1 * t2) {
return t1 * t2;
}
int n = 5;
decltype(n) n1 = 6; //推导为int类型基于范围的for循环
1
2
3vector<int> vec = {1,2,3,4,5};
for(auto element : vec)
std::cout<< element << std::endl;多线程编程
相关库:thread,atomic
thread: 可以创建线程,提供多种方法,比如join(),wait()
atomic: 提供原子操作,保证对共享变量的原子访问,避免竞争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
atomic_llong num{0}; //使用atomic创建一个变量num,在t1和t2中都可以访问,而且不会出现竞争访问的情况
thread t1 (func, 0); //func为一个函数
thread t2 (func, 0);
t1.join();
t2.join();
8. 引用计数
- **引用计数**这种计数是为了防止内存泄露而产生的。 基本想法是对于动态分配的对象,进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次, 每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存
9. RAII
- **RAII (Resource Acquisition Is Initialization) 资源获取**(初始化技术):对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间
- 是一种依赖对象生命周期管理资源的技术
- 资源获取即初始化(RAII):在对象构造时获取资源,在对象析构时释放资源
- 核心原则:通过构造函数和析构函数来自动管理资源的申请和释放
- 常见资源:动态内存(new/delete),文件句柄,线程锁,网络连接(socket),数据库连接
- C++11对RAII的支持:
```cpp
// 智能指针
std::unique_ptr; std::shared_ptr; std::weak_ptr
// 线程锁
std::lock_guard; std::unique_lock
// 文件句柄,就是用来读取文件的
std::fstream智能指针
目的:防止内存泄露,设置的自动回收机制- unique_ptr: 不允许多个unique_ptr对象指向同一块内存,拥有内存的管理权。没有拷贝构造函数,只有移动构造函数。可以使用std::make_unique函数方便的创建unique_ptr对象
1
2
3
4
5
6
7
8std::unique_ptr<int> p = std::make_unique<int>(10); // 创建,c++14引入
// make_unique()方法
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
} // 底层还是使用的new方法
// 可以使用std::move()赋值给另外一个unique_ptr,原来的被销毁
std::unique_ptr<int> p1(std::move(p)); - share_ptr: 通过计数的方式,多个share_ptr可以共同管理同一块内存,当计数器为0,指针所指向的内存会被释放。可以使用std::make_shared函数方便的创建shared_ptr对象
1
2
3
4
5
6
7
8auto pointer = std::make_shared<int>(10);
std::cout << *pointer << std::endl; // 像正常指针一样访问指向的内存中存放的数据
auto pointer2 = pointer; // 计数+1
auto pointer3 = pointer; // 计数+1
int *p = pointer.get(); // 获取原始指针,这样不会增加计数
std::cout << pointer.use_count(); // 输出计数器的值
pointer3.reset(); // 来减少一个引用计数 - weak_ptr: 用来辅助share_ptr,是shared_ptr的弱引用,不会增加计数。可以使用lock()方法来将一个weak_ptr变为shared_ptr,expired()方法在资源未被释放时候,返回false,否则true
- unique_ptr: 不允许多个unique_ptr对象指向同一块内存,拥有内存的管理权。没有拷贝构造函数,只有移动构造函数。可以使用std::make_unique函数方便的创建unique_ptr对象
lambda表达式
基本语法如下1
2
3[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}捕获列表分为以下几种:
- 值捕获,写法为[value1]。值捕获的前提是变量可以拷贝,不同之处则在于,被捕获的变量在 Lambda 表达式被创建时拷贝, 而非调用时才拷贝
1
2
3
4
5
6
7
8void lambda_value_capture() {
int value = 1;
auto copy_value = [value] {
return value;
};
value = 100; //value = 100
auto stored_value = copy_value(); //stored_value = 1
} - 引用捕获,写法为[&value]。保存的是引用,值会发生变化
- 隐式捕获,&,=
- 表达式捕获,实例
1
2
3
4
5
6
7void lambda_expression_capture() {
auto important = std::make_unique<int>(1);
auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
return x+y+v1+(*v2);
};
std::cout << add(3,4) << std::endl;
} //important原本为一个独占指针,是不会被捕获列表捕获到的,但是使用std::move()方法将其转换为右值后,就可以被捕获到了。输出为9
泛型lambda表达式
从c++14开始,下面的写法是可以通过编译的
1
2
3auto add = [](auto x, auto y) { return x + y;};
add(1, 2);
add(2, 3.5);1
2
3
4
5
6
7
8
9// lambda表达式在排序的用法
vector<int> nums;
std::sort(nums.begin(), nums.end(), [](const auto& a, const auto& b){
return a > b; // 从大到小排序
});
vector<vector<int>> nums;
std::sort(nums.begin(), nums.end(), [](const auto& a, const auto& b){
return a[1] > b[1]; // 按照第二个元素,从大到小排序
});- 值捕获,写法为[value1]。值捕获的前提是变量可以拷贝,不同之处则在于,被捕获的变量在 Lambda 表达式被创建时拷贝, 而非调用时才拷贝
结构化绑定,可以提供多返回值的功能
1
2
3
4
5
6
7
8
9
10std::tuple<int, double, std::string> func(){
return std::make_tuple(1, 2.5, "hello");
}
int main(){
auto [x, y, z] = func();
// x=1, y=2.5, z="hello"
}
12. 变长参数模版
```cpp
template<typename... Args> class test;这个test类可以接受不限制个数的typename作为模板的形式参数,例如
1
2
3
4
5
6
7
8
9class test<int, std::vector<int>, std::string> //合法的
//如果要对传入的参数进行解包的话,可以使用如下的代码,使用lambda表达式和初始化列表的方法
template<typename T, typename... Ts>
auto printf3(T value, Ts... args) {
std::cout << value << std::endl;
(void) std::initializer_list<T>{([&args] {
std::cout << args << std::endl;
}(), value)...};
}noexcept:在c++11中,将异常的声明简化为:函数可能抛出异常,函数不能抛出任何异常。
使用 noexcept 修饰过的函数如果抛出异常,编译器会使用 std::terminate() 来立即终止程序运行。1
void func1() noexcept; // 不可能抛出异常
原始字符字面量:可以在一个字符串前方使用
R
来修饰字符串1
std::string str = R"(C:\File\Project)";
内存对齐:C++ 11 引入了两个新的关键字 alignof 和 alignas 来支持对内存对齐进行控制。
alignof
关键字能够获得一个与平台相关的std::size_t
类型的值,用于查询该平台的对齐方式,alignas
来重新修饰某个结构的对齐方式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19struct Storage {
char a;
int b;
double c;
long long d;
};
struct alignas(std::max_align_t) AlignasStorage {
char a;
int b;
double c;
long long d;
};
int main() {
std::cout << alignof(Storage) << std::endl;
std::cout << alignof(AlignasStorage) << std::endl;
return 0;
}C++类型转换
- static_cast
(data): 如int转double,const转非const,void* 转 int* - dynamic_cast
(data): 在运行时进行基类与派生类的转换,比如将基类指针转换为派生类指针 - const_cast(data): 将const/volatile转换为非const/volatile类型
- reinterpret_cast(data): 第一个的一种补充,慎重使用
- static_cast
函数签名(signature)
- 函数签名是函数声明的一部分,用于唯一标识一个函数。它包括函数的名称和参数的类型列表,但不包括返回值类型
- 由 函数名称 + 参数类型列表(包括参数的数量、顺序、类型)组成。函数签名不包括参数的名字和返回值类型
- 函数重载是通过函数签名实现的,同时也可以唯一标识一个函数
- 和函数定义(Definition)和函数声明(Declaration)不同
1
2
3
4
5void func(int a, double b); // 签名是 func(int, double)
int func(int a, double b); // 签名仍是 func(int, double),和上面的冲突
void func(int a); // 签名是 func(int)
void func(double a); // 签名是 func(double)
void func(); // 签名是 func()
std::function<>
- 通用、多态的函数包装器。可以存储、复制、调用任何符合特定函数签名的可调用对象(例如普通函数、lambda表达式、函数指针、成员函数指针等)
- 在使用的时候,注意要检查是否为空
1
2
3
4// 语法形式,signature是函数签名,但是不包含函数名字,因为std::function是类型擦除(type erasure)的实现。不关心存储的具体可调用对象的类型,只关心对象的函数签名
template <typename Signature>
class function;
// 签名只包括返回类型和参数列表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// 作为函数参数,调用的时候,传入一个签名为void(int)的函数
void execute(std::function<void(int)> func, int val){
if(func){ // 注意要检查检查是否为空
func();
}
}
void printVal(int val){ std::cout << val << std::endl; } // 签名为void(int)
// 也可以实现回调函数,动态行为(比如在设计模式中的策略模式,可以看成在运行的时候,动态设置不同的执行策略,通过设置不同的std::function<>)
// 可以存储lambda表达式
std::function<void()> func = [](){std::cout << "Lambda stored in std::function!" << std::endl;}
// 结合std::bind或std::function使用成员函数时,需要绑定对象
class Number{
public:
void printNum(int val){ std::cout << val << std::endl;}
};
int main(){
Number obj1; // 必须要绑定对象
// std::placeholders::_1这个是一个占位符,用于在使用std::bind时指示某个位置的参数将由调用时传入的实际参数替代。有多个参数占位符,可以使用 _2、_3 等
std::function<void(int)> func = std::bind(&Number::printNum, &obj1, std::placeholders::_1);
func(42); // 输出就是42
}
面向对象相关
- 委托构造函数,可以在同一个类的某个构造函数中调用另一个构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class test{
int value1;
int value2;
test(){ value1 = 1;}
test(int value): test() { //可以调用test类的默认构造函数了
value2 = value;
}
};
2. 显式虚函数重载
1. override,在重载虚函数的时候使用,编译器将检查基函数是否存在这样的其函数签名一致的虚函数,否则将无法通过编译
```cpp
struct Base {
virtual void foo(int);
};
struct SubClass: Base {
virtual void foo(int) override; // 合法
virtual void foo(float) override; // 非法, 父类没有此虚函数
};- final,在进行类继承的时候引入,防止被继续继承并终止虚函数继续重载
1
2
3
4
5
6
7
8
9
10
11
12struct Base {
virtual void foo() final;
};
struct SubClass1 final: Base {
}; // 合法
struct SubClass2 : SubClass1 {
}; // 非法, SubClass1 已 final
struct SubClass3: Base {
void foo(); // 非法, foo 已 final
};
- final,在进行类继承的时候引入,防止被继续继承并终止虚函数继续重载
- 强类型枚举
使用enum class的语法进行声明,实现了类型安全,而且不能被隐式转换为整数当希望获得枚举值的值时,将必须显式的进行类型转换,不过我们可以通过重载 << 这个算符来进行输出,比如:1
2
3enum class new_enum : int {
value1, value2, value3 = 100, value4 = 100
};1
2
3
4
5
6
7
8
9
10
template<typename T>
std::ostream& operator<<(
typename std::enable_if<std::is_enum<T>::value,
std::ostream>::type& stream, const T& e)
{
return stream << static_cast<typename std::underlying_type<T>::type>(e);
}
// 这样,下面的代码段是合法的
std::cout << new_enum::value3 << std::endl
容器
线性容器
std::array
与std::vector的不同:vector是自动扩容的,array的大小是固定的。当vector中删除一些元素的时候,多余的内存并不会释放1
2
3
4
5constexpr int len = 4;
std::array<int, len>arr = {1, 2, 3, 4};
std::sort(arr.begin(), arr.end(), [](int a, int b){
return b < a;
}); // 可以使用标准库中的一些容器算法,比如std::sortstd::forward_list
std::forward_list 是一个列表容器,使用方法和 std::list 基本类似
与std::list 的双向链表的实现不同,std::forward_list 使用单向链表进行实现, 提供了 O(1) 复杂度的元素插入,不支持快速随机访问(这也是链表的特点), 也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向迭代时,具有比 std::list 更高的空间利用率。
无序容器
- 有序容器:
std::map / std::set
,内部通过红黑树进行实现, 插入和搜索的平均复杂度均为O(log(size))
。在插入元素时候,会根据<
操作符比较元素大小并判断元素是否相同, 并选择合适的位置插入到容器中。当对这个容器中的元素进行遍历时,输出结果会按照<
操作符的顺序来逐个遍历。 - 无序容器:
std::unordered_map / std::unordered_multimap , std::unordered_set / std::unordered_multiset
无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为O(constant)
, 在不关心容器内部元素顺序时,能够获得显著的性能提升。用法和原有的类似
元组
三个核心函数:
- std::make_tuple: 构造元组
- std::get: 获得元组某个位置的值
- std::tie: 元组拆包
- std::tuple_cat: 元组合并
正则表达式
正则表达式描述了一种字符串匹配的模式。
参考https://changkun.de/modern-cpp/zh-cn/06-regex/
一般使用正则表达式主要是实现下面三个需求:
- 检查一个串是否包含某种形式的子串;
- 将匹配的子串替换;
- 从某个串中取出符合条件的子串。
- 相关方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// example1
std::regex txt_regex("[a-z]+\\.txt");
std::string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};
for (const auto &fname: fnames){
std::cout << std::regex_match(fname, txt_regex) << std::endl;
} // std::regex_match将会输出 0 或 1
//example2
std::regex base_regex("([a-z]+)\\.txt");
std::smatch base_match;
for(const auto &fname: fnames) {
if (std::regex_match(fname, base_match, base_regex)) {
// std::smatch 的第一个元素匹配整个字符串
// std::smatch 的第二个元素匹配了第一个括号表达式
if (base_match.size() == 2) {
std::string base = base_match[1].str();
std::cout << "sub-match[0]: " << base_match[0].str();
std::cout << fname << " sub-match[1]: " << base << std::endl;
}
}
}
并行与并发
相关库:std::thread, #include<thread>, #include<mutex>, std::<mutex>
- 互斥量与临界区
std::mutex是基本的mutex类,可以通过lock()对对象进行上锁,unlock()进行解锁。
可以使用c++提供的RAII语法的模板类std::lock_guard。这个方法可以保证在调用lock_guard的时候就自动加锁,同时当离开作用域的时候自动释放,避免手动上锁解锁std::unique_lock: 更加灵活,以独占所有权(没有其他的 unique_lock 对象同时拥有某个 mutex 对象的所有权) 的方式管理 mutex 对象上的上锁和解锁的操作。所以在并发编程中,推荐使用 std::unique_lock。1
2
3
4
5
6
7
8
9
10
11
12void critical_section(int param){
static std::mutex mutex;
std::lock_guard<std::mutex> lock(mutex); // 已经自动加锁
// 执行竞争操作
// 离开作用域的时候mutex会被释放
}
int main(){
std::thread t1(critical_section, 2), t2(critical_section, 3);
t1.join();
t2.join();
// 使用lock_guard()方法保证了在离开作用域的时候自动使用unlock()方法
}
std::lock_guard 不能显式的调用 lock 和 unlock, 而 std::unique_lock 可以在声明后的任意位置调用(当使用std::defer_lock参数), 可以缩小锁的作用范围,提供更高的并发度。1
2
3
4
5
6
7
8
9
10
11
12
13void critical_section(int param){
static std::mutex mutex;
// 如果没有加std::defer_lock, 就会自动上锁,离开作用域自动解锁
// 加了这个参数后,就可以任意的加锁解锁,非常自由
std::unique_lock<std::mutex> lock(mutex, std::defer_lock);
lock.lock();
// 执行竞争操作
lock.unlock();
// 此时可以抢夺v的竞争变量的使用权
lock.lock();
// 执行竞争操作
// 离开作用域的时候mutex会被释放
} - std::future:期物,可以作为一种线程同步的手段,即屏障(barrier),可以获取异步任务的结果
1
2
3
4
5
6// std::packaged_task 可以封装任何可以调用的目标
std::packaged_task<int()> task([](){return 123;});
std::future<int> result = task.get_future(); // 获得了task的期物
std::thread(std::move(task)).detach();
result.wait(); //在这里进行阻塞,一直到task任务完成才继续进行
std::cout << result.get() << std::endl; // 使用get()方法获取结果 - std::condition_variable:条件变量,可以解决死锁问题,当互斥操作不够用而引入的。 比如,线程可能需要等待某个条件为真才能继续执行, 而一个忙等待循环中可能会导致所有其他线程都无法进入临界区使得条件为真时,就会发生死锁。该变量用于唤醒等待线程从而避免死锁。
相关方法:notify_one()唤醒一个线程,notify_all()通知所有线程运行输出如下: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
37std::queue<int> produced_nums;
std::mutex mtx;
std::condition_variable cv;
bool notified = false; // 通知信号
// 生产者
auto producer = [&]() {
for (int i = 0; ; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(900));
std::unique_lock<std::mutex> lock(mtx);
std::cout << "thread " << std::this_thread::get_id() << " is producing " << i << std::endl;
produced_nums.push(i);
notified = true;
cv.notify_all(); // 此处也可以使用 notify_one
}
};
// 消费者
auto consumer = [&]() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
while (!notified) { // 避免虚假唤醒
cv.wait(lock);
}
// 短暂取消锁,使得生产者有机会在消费者消费空前继续生产
lock.unlock();
// 消费者慢于生产者
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
lock.lock();
while (!produced_nums.empty()) {
std::cout << "thread " << std::this_thread::get_id() << " is consuming " << produced_nums.front() << std::endl;
produced_nums.pop();
}
notified = false;
}
};
// 设置一个生产者线程,2个消费者线程
// thread 2 is producer, thread 3, 4 is consumer
在生产者中我们虽然可以使用 notify_one(),但实际上并不建议在此处使用, 因为在多消费者的情况下,我们的消费者实现中简单放弃了锁的持有,这使得可能让其他消费者 争夺此锁,从而更好的利用多个消费者之间的并发。
同时,这个example不能实现多个消费者并行分享队列中的资源 - 原子操作与内存模型
原子操作:std::atomic,为整数或者浮点数的原子类型提供了一些操作,可以保证很强的同步条件不是所有类型都提供原子操作,可以使用1
2
3
4
5
6
7
8
9
10std::atomic<int> a = {0};
std::thread t1([](){
a.fetch_add(1);
});
std::thread t2([](){
a++; // 等于fetch_add()
a += 1; // 等于fetch_add()
});
t1.join();
t2.join();std::atomic<T>::is_lock_free
来检查该类型是否提供原子操作
一致性模型:
存在4种不同的一致性模型,要求从强到弱
线性一致性 > 顺序一致性 > 因果一致性 > 最终一致性
内存顺序: 没看懂,建议找视频看看,参考资料https://zhuanlan.zhihu.com/p/382372072 - explicit 关键字
- 用于构造函数或转换运算符,以防止隐式转换或单参数构造函数引发的意外类型转换。它主要用于避免那些可能意外发生的隐式类型转换,从而使代码更加安全和可读
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class MyClass {
public:
explicit MyClass(int x) : value(x) {}
int value;
};
void func(MyClass obj) {
std::cout << "Value: " << obj.value << std::endl;
}
int main() {
MyClass obj1(10); // 正确,显式调用构造函数
func(obj1); // 正确
// MyClass obj2 = 10; // 错误,因为构造函数是 explicit 的
// func(10); // 错误,不能隐式地转换 int 到 MyClass
return 0;
}
- 用于构造函数或转换运算符,以防止隐式转换或单参数构造函数引发的意外类型转换。它主要用于避免那些可能意外发生的隐式类型转换,从而使代码更加安全和可读
- std::variant
- C++ 的 std::variant 是 C++17 引入的标准库特性之一,它提供了一种类型安全的方式来存储多种可能的类型。与联合体(union)类似,std::variant 可以在运行时保存不同类型的值,但相比 union 提供了更多的安全性和功能
- std::variant 是一个类型安全的联合体,它能够在一个变量中存储多种不同的类型,但在任何给定的时刻只能存储其中一种类型。相比传统的 union,std::variant 可以通过编译期检查,防止不安全的类型操作
1
2
3
4
5
6
7
8
9
10
std::variant<int, float, std::string> v; // v可以存储int, float 或 std::string
v = 10;
cout << std::get<int>(v); // 通过std::get<>()进行访问
// 通过std::visit进行访问
std::variant<int, std::string> v = "Hello";
auto visitor = [](auto&& arg) {
std::cout << "Type is: " << typeid(arg).name() << ", Value: " << arg << std::endl;
};
std::visit(visitor, v); // 根据当前存储的类型执行 visitor - 特点:类型安全(只能访问当前存储的类型),类型转换保护,可以通过std::get
和std::visit安全的访问 - std::variant 的内存分配等同于最大类型的大小
文件系统
c++基础知识
c++ new和malloc的区别
- new返回具体类型的指针,而malloc返回void*
- new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL
- 使⽤new操作符申请内存分配时⽆须指定内存块的⼤⼩,⽽malloc则需要显式地指出所需内存的尺⼨
- opeartor new /operator delete可以被᯿载,⽽malloc/free并不允许᯿载
- new/delete会调⽤对象的构造函数/析构函数以完成对象的构造/析构。⽽malloc则不会
- malloc与free是C++/C语⾔的标准库函数, new/delete是C++的运算符
C++ delete和free的区别
- delete会调⽤对象的析构函数,确保资源被正确释放。free只是简单地释放内存块
- delete可以正确释放通过new[]分配的数组。free 不了解数组的⼤⼩,不适⽤于释放通过malloc分配的数组
const和constexpr
- constexpr是常量表达式,如果一个变量为constexpr,则同样是const。但⼀个const的变量或函数,并不是constexpr的
指针常量(Constant Pointer)和常量指针(Pointer to Constant)
- 指针常量(Constant Pointer)是指指针本身是一个常量,也就是说,这个指针一旦初始化后,只能指向初始化时的地址,不能再指向其他地址。但是,指针指向的值可以被修改
- 指针本身不可修改(即指向的地址不能变),指针指向的值可以修改(前提是值本身不是常量)
1
int *const ptr; // 只可以修改指针指向对象的值
- 常量指针(Pointer to Constant)是指指针指向的值是一个常量,也就是说,不能通过该指针修改指向的值,但指针本身的指向可以改变
- 指针可以指向不同的地址, 不能通过指针修改指向地址上的值
1
const int *ptr; // 只可以修改指针指向的对象
- Constant Pointer to Constant,指针的指向,指针指向的对象都不能修改
1
const int *const ptr;
函数指针和指针函数
- 函数指针(Pointer to Function):指向函数的指针,它存储的是函数的地址。通过函数指针可以调用指向的函数
1
2
3
4int add(int a, int b){return a + b;}
int (*functPtr)(int, int);
functPtr = &add; - 指针函数(Function Returning a Pointer):返回值为指针的函数。它是一个普通的函数,函数返回的类型是指针类型
1
int* funct(){int x = 10; return &x;}
- 函数指针(Pointer to Function):指向函数的指针,它存储的是函数的地址。通过函数指针可以调用指向的函数
struct中的成员默认是public,class默认是private
c++栈和堆
- 栈和堆都是⽤于存储程序数据的内存区域。栈是⼀种有限的内存区域,⽤于存储局部变ᰁ、函数调⽤信息等。堆是⼀种动态分配的内存区域,⽤于存储程序运⾏时动态分配的数据
- 堆变量的生命周期由开发人员定义(new/malloc/delete/free)
内存泄漏:内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使⽤的内存的情况
- 堆内存泄漏(heap leak):使用new/malloc分配的内存没有通过delete/free释放
- 系统资源泄露(Resource leak):系统分配的资源比如bitmap, handle, socket没有用相关函数释放或者关闭,导致浪费,效能降低,运行不稳定
- 没有将基类的析构函数定义为虚函数:当基类指针指向⼦类对象时,如果基类的析构函数不是 virtual,那么⼦类的析构函数将不会被调⽤,⼦类的资源没有正确释放
- 可以通过将内存的分配封装在类中,构造函数分配内存,析构函数释放内存;使⽤智能指针防止内存泄漏
析构函数需要设置为虚函数,保证子类对象在释放的时候可以正确调用子类的析构函数。而构造函数不需要,没有意义
内存分配方式
- 静态存储区域分配:在程序编译的时候就已经分配好,比如全局变量,static变量
- 栈:在执⾏函数时,函数内局部变ᰁ的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放
- 堆:使用new/malloc分配,delete/free释放,生存期自定义
面向对象特性:继承(Inheritance),封装(Encapsulation),多态(Polymorphism)
- 对于public, private, protected继承,私有成员不能被“派⽣类”访问,基类中的公有和保护成员能被“派⽣类”访问
- 对于公有继承,只有基类中的公有成员能被“派⽣类对象”访问,保护和私有成员不能被“派⽣类对象”访问。对于私
有和保护继承,基类中的所有成员不能被“派⽣类对象”访问 - 多态:简单来说,就是允许将派生类指针赋值给基类指针
override和overload
- override是重写,覆盖,一般是派生类覆写基类的函数,⽅数列表,返回值,所抛出的异常要与基类中的一致
- overload是重载,一般是普通函数,有相同函数名但是不同参数列表
C++的多态(Polymorphism)通过虚函数(Virtual Function)和虚函数表(Virtual function table)实现
- 基类中声明虚函数,而在派生类中使用override进行覆写
- 使用基类指针指向派生类对象,调用虚函数
- 虚函数表:编译器在对象的内存布局中维护了⼀个虚函数表,其中存储了指向实际函数的指针。这个表在运⾏时⽤于动态查找调⽤的函数
相关概念:虚函数,纯虚函数,抽象类
- 纯虚函数是在抽象类中声明的虚函数,它没有具体的实现,只有函数的声明。派⽣类必须实现抽象类中的纯虚函数,否则它们也会成为抽象类
- 抽象类是不能被实例化的类,它存在的主要⽬的是为了提供⼀个接⼝,供派⽣类继承和实现
1
2virtual void funct1() {cout << 1;}
virtual void funct2() = 0; // 纯虚函数 - 常⻅的不能声明为虚函数的有:普通函数(⾮成员函数),静态成员函数,内联成员函数,构造函数,友元函数
深拷贝,浅拷贝
- 深拷⻉是对对象的完全独⽴复制,包括对象内部动态分配的资源。在深拷⻉中,不仅复制对象的值,还会复制对象所指向的堆上的数据
- 浅拷⻉仅复制对象的值,⽽不涉及对象内部动态分配的资源。在浅拷⻉中,新对象和原对象共享相同的资源,⽽不是复制⼀份新的资源
C++引用(reference)
- 不可以重新绑定对象
- 不可能创建空引用,必须初始化
- 可以作为函数返回值,作为函数参数传递,用于类的构造函数
assert
- 是一个宏,用法为assert(expression), 包含在
库中,当expression为true的时候,程序继续执行。当expression为false的时候,程序终止执行,并且输出错误信息
- 是一个宏,用法为assert(expression), 包含在
回调函数(callback function)
- 是一种函数,通过指针或引用传递给另一个函数,后者可以在适当的时间调用这个回调函数。回调函数的主要用途是实现一种“反向控制”的机制,使得被调用的函数在某些条件满足时能够通知调用者,并且根据调用者提供的行为做出响应
- 可以判断多个条件,如
assert(x > 0 && y > 0)
- 对性能的影响比较轻微(只在调试期间),而在软件发布版本(会禁用所有assert,所以没有任何影响)
- 允许将函数作为参数传递,并在合适的时间调用
- 可以使用函数指针,lambda表达式,类成员函数,仿函数进行调用
- 是事件驱动编程的核心概念,广泛应用于图形用户界面(GUI)编程、异步编程等领域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 回调函数实现,同时使用std::function<>特性
class Event {
public:
void setCallback(std::function<void()> callback) {
this->callback = callback;
}
void trigger() {
if (callback) {
callback();
}
}
private:
std::function<void()> callback;
};
int main() {
Event event;
// 设置回调函数
event.setCallback([]() { std::cout << "Event triggered!" << std::endl; });
event.trigger(); // 触发事件,调用回调函数
}
STL
- 由6部分组成:容器(Container),算法(Algorithm),迭代器(Iterator),仿函数(Function Object),适配器(Adapter),空间配置器(Allocator)
- container:vector, queue, list, set, map
- algorithm: sort(), find(), copy()
- map, set底层是基于红黑树进行实现的,而unordered_map, unordered_set底层是基于哈希表实现的
- emplace_back()比push_back()更高效,因为它避免了创建和销毁临时对象的开销
基本语法
- 数据结构相关
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// 在c++中,栈库为#include<stack>
stack<int> st;
// 相关方法:push(int i), pop(), top()
// #include<vector> 使用vector库
// 初始化长度为n,默认值为0
vector<int> nums = vector<int>(n, 0);
// 初始化n*m的二维数组
vector<vector<int>> nums(n, vector<int>(m, 0))
// 在函数中引用
void dfs(vector<vector<int>>& grid, int x, int y)
vector<vector<int>> grid(n, vector<int>(m, 0))
nums.push_back(); nums.pop_back(); // 从末尾压入元素 / 移除最后一个元素
auto it = find(nums.begin(), nums.end(), target); nums.erase(it); // 找到某元素然后删除
// #include<list> 使用list库,是一个双向链表
list<int> nums = {1, 2, 3};
nums.push_front(4); nums.push_back(4);// 在链表头插入元素,在链表尾插入元素
nums.pop_back(); nums.pop_front(); // 删除最后一个和第一个元素
nums.front(); nums.back(); // 访问第一个和最后一个元素
nums.size(); // 链表大小
nums.empty(); // true为空
for(int i:nums) cout << i << ' '; // 遍历list
// std::unordered_map 是一种关联容器,允许你存储键值对。每个键(key)是唯一的,并且与一个值(value)相关联。
std::unordered_map<int, int> map; // 储存一个键值对,类型都为int
map.insert({1, 2}) // or map[1]=2
map.erase(2) // 删除键为2的元素
auto it = map.find(2) // 查找键为2的元素,返回迭代器
if (map.find(3) != map.end()) // 检查元素是否存在
std::cout << "Key 3 exists" << std::endl;
map.contains(3); // c++20引入新的方法,检查元素是否存在,返回bool类型
for(const auto& pair:map)
std::cout << pair.first << pair.second; // first is key, second is value
// std::unordered_set 是一种无序集合容器,允许你存储唯一的元素,并提供快速的插入、删除和查找操作
std::unordered_set<int> set;
set.count(2) // 检查set中是否存在2这个元素,因为set只能储存唯一的元素,所以只能有1个2或者0个2,返回1或0
set.insert(3) // 插入元素
set.erase(3) // 删除元素
auto it = set.find(3) // 查找指定元素所在位置,返回迭代器
for(const int& elem:set)
std::cout << elem; // 遍历
set.contains(3); // 检查是否含有元素3,返回bool类型,比find好用简洁
// std::queue是队列,相关操作如下
queue<int> que;
int num1 = queue.front();
queue.front(); // 返回队头元素,不弹出
queue.pop(); // 先获取队头元素,然后弹出队头元素
queue.empty() // True为空,False为不空
queue.push(1); // 从队尾插入元素1
// priority_queue 优先队列,可以用来实现最大堆和最小堆,默认是最大堆
// 底层是基于二叉堆实现的,插入一个新元素需要经过Bubble up操作进行调整
// 调整需要O(logn)时间,所以插入元素时间复杂度是O(logn)
// 插入元素O(logn), 访问元素top() O(1), 删除元素pop() O(logn)
priority_queue<int> pq; // 最大堆,最大的元素在队头
priority_queue<int, std::vector<int>, std::greater<int>> pq_min; // 最小堆,最小的元素在队头
auto cmp = [](const DATATYPE* a, const DATATYPE* b) {
return a->val > b->val; // 最小堆, 这里的排序逻辑是a比b大,那么a优先级就低,b优先级高,优先队列把优先级高的放在最前面,所以这是一个最小堆
};
priority_queue<DATATYPE, vector<DATATYPE>, decltype(cmp)> pq; // 同样是一个最小堆,但是是自定义的数据结构
pq.pop();pq.top();pq.push();
// 函数参数
auto cmp = [&](const int a, const int b){
return a > b;
};
priority_queue<int, vector<int>, function<bool(int, int)>> pq(cmp);
void traversal(priority_queue<int, vector<int>, function<bool(int, int)>>& pq)
// set(),可以使用自定义的排序
struct cmp{
bool operate()(const int &a, const in &b){
return a > b;
}
};
set<int, cmp> nums; // 一个降序排序的set
map<int, int, cmp> ht; // 一个降序排序的map
// 同时也可以用std::greate<int>
// pair, #include<utility>
std::pair<int, int> obj1; // 默认构造一个pair
auto p = std::make_pair(1, "hello"); // 构造一个pair<int, string>
// deque, #include<deque>双端队列
std::deque<int> dq; // 可以使用初始化列表构造,比如{1, 2, 3}, (nums.begin(), nums.end())
dq[1], dq.at(2), dq.front(), dq.back(); // 按下标访问和访问头尾元素
push_front(), push_back(), pop_back(), pop_front();
// 在头部和尾部插入或删除元素的效率为常数时间O(1)
// 而Vector插入/删除头部元素是O(n),支持随机访问
// multiset,有序集合,可以存储重复元素
std::multiset<int> ms = {1, 2, 2, 3, 3, 3};
insert(), find(), erase(), count(); // 插入,寻找,清除指定迭代器下标,计算数量
// multimap
std::multimap<int, std::string> mm = {
{1, "one"}, {2, "two"}, {2, "dos"}, {3, "three"}
};
insert(), find(), erase(), count(); // 插入,寻找,清除指定迭代器下标,计算数量
auto range = mm.equal_range(2);
for (auto it = range.first; it != range.second; ++it) {
std::cout << it->second << std::endl;
} // 输出two, dos - 内存分配
1
2
3int memo[c][m][n];
memset(memo, -1, sizeof(memo)); // 给memo分配空间,初始化为-1
int &res = memo[k][i][j]; // 使用res直接引用memo中的[k][i][j],会随着res更改而更改,减少代码,保证安全 - 数据转换相关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// atoi(), stoi(), using by #include<cstring>, 把字符串转换为int类型输出
// atoi()不做范围检查,超出范围也进行输出
// stoi()做范围检查,超出范围则runtime error
- c++排序相关
```cpp
// 对于一个vector<int> arr数组,可以写一个排序函数,需要#include<algorithm>
// 当cmp返回true的时候,sort函数会认为a应该在b前面,所以当a > b返回true,sort()就把a排在b前面,所以就是一个降序排序,最大的在最前面
// 所以写return a < b, 返回true的时候,a排b前面,b比a大,所以是一个增序
// 这个排序的规则和优先队列那个排序规则不一样的
auto cmp = [](int a, int b){
return a > b;
}
sort(arr.begin(), arr.end(), cmp);
// 其中cmp是排序规则,默认是升序排序,下面是降序排序
sort(arr.begin(), arr.end(), [](int a, int b){
return a > b; //
}); - c++ string相关
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// 转字符串
string s = std::to_string(5);
// 对于一个vector<int> arr数组,可以写一个排序函数,需要#include<algorithm>
sort(arr.begin(), arr.end(), cmp);
// 其中cmp是排序规则,默认是升序排序,下面是降序排序
sort(arr.begin(), arr.end(), [](int a, int b){
return a > b;
});
// 分割字符串, 方法1
std::string sentence = "Hello my name is Jane";
std::vector<std::string> words;
size_t start = 0, end;
while ((end = sentence.find(' ', start)) != std::string::npos) {
words.push_back(sentence.substr(start, end - start));
start = end + 1; // 移动到下一个单词的起始位置
}
words.push_back(sentence.substr(start)); // 最后一个单词
// 分割字符串, 方法2,#include<sstream>
std::string sentence = "Hello my name is Jane";
std::vector<std::string> words;
std::istringstream iss(sentence);
string temp;
while(getline(iss, temp, ' ')){
words.push_back(temp); // 按照' '对sentence进行分割,每次找到一个' ',就存入temp中,然后再把temp压入vector
}
// 用空格连接字符串
vector<string> word_list = {"cat","bat","rat"};
string result = accumulate(word_list.begin() + 1, word_list.end(), word_list[0], [](auto a, auto b){
return a + " " + b;
});
// result结果,使用空格将字符串都连接起来了,结果就是cat bat rat
// string, push_back(), pop_back()
string str = "";
str.push_back('a');
str.pop_back(); // 合法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// lambda expression
void TryCall(int val, std::function<void(int)> callback){ // 对于接受回调函数为参数的这个函数
val += 1;
callback(val);
}
class Myclass{
void ClassCallBack(int val){
std::cout << val << endl;
}
};
int main(){
TryCall(10, [](int val){ // 使用lambda表达式进行调用
std::cout << val << endl;
});
// 或者使用成员函数进行调用
Myclass obj;
TryCall(10, std::bind(&Myclass::ClassCallBack, &obj, std::placeholders::_1));
// obj是一个对象,实际上会发生obj.ClassCallBack(), 而placeholders::_1是一个占位符,在调用时,_1会被传入的第一个参数替换
} - cpp相关实用方法
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 - Z: 65 ~ 90
// a - z: 97 ~ 122
// uppercase + 32 = lowercase
// 数字的范围是48 ~ 57(0 ~ 9)
// 取一个最大的数
int max_value = INT_MAX;
// 对于这种数据结构,通过键查找对于的数组的时候,一定要使用引用
// 这样就可以取到原来map中保存的vector地址。不使用引用会把原来的数据全部复制到一个新地址,就慢了
unordered_map<int, vector<int>> ht;
vector<int>& arr = ht[key]; // 一定要这么写!快很多!
int max_v = *max_element(nums.begin(), nums.end()); // 计算一个数组中最大的数
int min_v = *min_element(nums.begin(), nums.end()); // 计算一个数组中最大的数
accumulate(nums.begin(), nums.end(), 0); // 计算一个数组的和
reverse(nuns.begin(), nums.end()) // 反转一个数组
string str;
str.substr(0, 3); // substr(pos, len) 从pos开始,输出长度为len的子串
int a = 1, b = 2;
std::swap(a, b); // 交换a, b的值
// 锁的控制域限定,锁一般是在竞争区域中使用,比如
int a = 1, b = 2;
string s = "hello";
static std::mutex mutex;
{
std::lock_guard<std::mutex> lock(mutex); // 此时,进入这个大括号就会创建一个锁
// 这个大括号内部就是竞争区域,当离开这个区域的时候,就会自动解锁了
}
// C++对于某个数num,向上取整的方法
int res = (num - 1) / divisor + 1;
// 向下取整就直接用int类型就行了,会自动向下取整的
// 提取出哈希表中所有元素的方法
unordered_map<int, int>ht;
vector<pair<int, int>> kv(ht.begin(), ht.end());
// 取随机数
// 可以使用std::srand(std::time(0))设置随机种子,得到相同随机序列
int random_range = std::rand()%100; // 生成0~99中的随机数
int random_value = std::rand(); // 生成随机数
// 可使用<random>库,std::mt19937引擎生成随机数
std::mt19937 gen(std::random_device{}()); // 随机数生成器
std::uniform_real_distribution<double> dis(0.0, 1.0); // 0 到 1 的浮点数均匀分布
double value = dis(gen);
// 数学相关
gcd(x, y) // Greatest Common Divisor, GCD, 返回x和y的最大公因数