Learning C++
练习库:learning-cxx
_CXX
【00】 输出与换行
std::cout << "xxx" << std:endl;- 插入换行符并刷新缓冲区
- 为了提升
磁盘IO效率,std::cout默认不会立刻写入目标设备(如显示器或文件),而会放在缓冲区中- 如果当缓冲区满了或者输入流被关闭,写入目标程序(提高
io效率)01
- 如果当缓冲区满了或者输入流被关闭,写入目标程序(提高
- 为了提升
【01】 定义变量
int xxx【02】 声明
从内向外读
总是优先读取
[]和()而不是*void ( *(*f[]) () ) ()*f[]:是个函数,每个元素是指针*(*f[]) ():表示每个指针指向一个函数所以是函数指针(是指针)
指针函数是函数,表示返回指针类型的函数
【03】参数传递

参数传递的方式
值传递
- 传副本
引用传递
- 引用改变原对象改变
常量引用传递
- 传递的引用不可以被修改
指针传递
- 通过地址修改数据
右值引用传递
和引用的区别是右值引用传递是移动语义,有资源的传递
- 移动资源而不是复制资源
如何选择传递方式
- 是否需要修改
- 是:指针/引用
- 否:按值/常量引用
- 参数大小
- 大:引用
- 否:按值
- 是否要移动语义
- 是否需要修改
【04】static 关键字
对变量
static 声明的值会被赋值为0,而普通声明不会(输出是一个垃圾值)
- 修饰全局变量
- 使得该变量只可以这个文件内部访问
- 修饰局部变量
- 无论调用多少次,只会初始化一次
- 在生命周期内,static 声明的变量会保留其值。
- 修饰全局变量
对函数
- 成员函数
- 可以通过类名直接访问
- 普通函数
- 该函数只有该文件可见,其他文件包括调用
- 成员函数
【05】constexpr
- 关键词作用
- 减少运行的计算开销
- 原理
- 编译出的结果直接储存起来
- 运行时候直接用——相当于函数不被调用,函数相当于值
- 编写要求
- 函数
- 使用
constexpr修饰的函数必须在编译时能够求值。
- 使用
- 常量
constexpr常量会在编译时求值,并在程序中直接作为常量使用。
- 函数
【06】array
三目运算符
<condition> ? <true expression> : <false expression>eg.:(arr[i] != 0) ? arr[i] : (arr[i] = fibonacci(i - 1) + fibonacci(i - 2))
类型大小
数据类型 描述 大小 (字节) bool布尔类型 (true/false) 1 char字符类型 1 int整型 (通常为 32 位) 4 unsigned int无符号整型 4 long长整型 (通常为 32 位) 4 long long长长整型 (通常为 64 位) 8 unsigned long long无符号长长整型 8 float单精度浮点型 4 double双精度浮点型 8 void*指针类型 (指向任意类型的指针) 8
【07】loop
纯函数
定义
- 输出只依赖于输入:纯函数的输出只依赖于它的输入参数,相同的输入永远会产生相同的输出。
- 没有副作用:纯函数在计算过程中不改变外部状态,也不与外部世界进行交互(比如打印到控制台、修改全局变量、进行文件操作等)。
好处
没有副作用,不会影响彼此状态
- 便于并行优化
- 测试方便
eg.static unsigned long long fibonacci(int i) { // TODO: 为缓存设置正确的初始值 static unsigned long long cache[96]; static int cached; cache[1] = 1; // TODO: 设置正确的循环条件 for (cached = 2; cached < i+1; ++cached) { cache[cached] = cache[cached - 1] + cache[cached - 2]; } return cache[i]; } // 这个 fibonacci 函数 不是纯函数,因为它有副作用——它修改了静态变量 cache 和 cached,并且缓存了计算结果。纯函数应该没有副作用,且输出仅依赖于输入。
【08】pointer
- 数组向指针退化
arr[i]等价于*(arr + i)
【09】enum&union
无作用域枚举 (
enum)常见的枚举类型,不具有类型安全,可能会污染命名空间。
enum ColorEnum : unsigned char { COLOR_RED = 31, COLOR_GREEN, COLOR_YELLOW, COLOR_BLUE, };
作用域枚举 (
enum class)引入了作用域,提供类型安全,可以避免命名空间污染。
enum class Color : int { Red = COLOR_RED, Green, Yellow, Blue, };
联合体 (
union)允许将不同类型的值存储在同一内存位置,常用于类型双关,但在 C++ 中这种做法可能导致未定义行为。
union TypePun { ColorEnum e; Color c; };
类型双关(Type Punning)
通过
union或其他方法,在 C++ 中进行类型转换,允许按不同类型访问同一内存,但需要小心,因为这可能引发未定义行为。TypePun pun; pun.c = c; // 将 Color 类型值存储到联合体成员 c 中 return pun.e; // 通过联合体成员 e 访问相同内存位置的值
【10】trivial
布局
类、结构或联合类型的对象的成员在内存中的排列方式
为了确保 C++ 和 C 跨语言转换或者需要进行底层内存操作不出问题, 这些布局类型非常重要
平凡类型
平凡类型没有自定义的构造函数、析构函数或拷贝/移动操作符,编译器提供默认的构造、拷贝和析构行为。
struct TrivialStruct { int a; double b; };
标准布局类型
适用于需要与 C 代码兼容或进行底层内存操作的情况。
- 所有成员按照定义的顺序存储,类或结构体的成员变量
- 而平凡类型,编译器可能会插入填充字节或改变成员的顺序以提高内存对齐和访问效率
- 不允许有虚函数、虚基类或非静态成员函数。
- 所有成员按照定义的顺序存储,类或结构体的成员变量
POD 类型
可以直接通过内存块的复制来传递(无附加的生命周期管理)
必须是平凡类型。
必须是标准布局类型。
不允许有用户定义的构造函数、析构函数或拷贝/移动操作符。
struct PODStruct { int a; double b; };
初始化
(){}表达式#include <string> std::string s1; // 默认初始化 std::string s2(); // 不是初始化! // 实际上声明了没有形参并且返回 std::string 的函数 “s2” std::string s3 = "hello"; // 复制初始化 std::string s4("hello"); // 直接初始化 std::string s5{'a'}; // 列表初始化(C++11 起) char a[3] = {'a', 'b'}; // 聚合初始化(C++11 起是列表初始化的一部分) char& c = a[0]; // 引用初始化
【11】method
class与struct的区别总结特性 class struct 默认成员访问权限 private(成员默认为私有)public(成员默认为公共)默认继承访问权限 private(继承时默认私有)public(继承时默认公共)方法定义方式 与 struct完全相同(公共或私有成员)与 class完全相同(公共或私有成员)
【12】CV 限定符
const限定符- 标记变量为常量,禁止修改其值。
const对象中的方法也要用const修饰
volatile限定符- 告诉编译器变量的值可能在外部环境中发生变化,防止编译器对该变量进行优化。
【13】构造器(构造函数)
- 初始化列表只在构造函数中使用
:,用于初始化类的成员变量。 - 对于普通变量(如局部变量或全局变量),则不需要使用
:
【14】class_destruct
RAIIresource aquisition is initalization- 资源获取即初始化是一种编程技术,
- 指的是通过对象的生命周期来管理资源的获取和释放。
- 在对象创建时获得资源,在对象销毁时释放资源
- 对应构造和析构中的操作
new和delete- 创建数组:
new 类型[大小] - 回收数组:
delete[] 数组名
- 创建数组:
size_t类型- 无符号整型
- 大小看平台架构(更动态)
stl中专门设计保存数组大小等信息。
【15】 class_clone
赋值构造函数
- 浅拷贝
- 两个指针指向同一个地址空间。如果一个对象把资源销毁,另一个对象的指针就会悬挂,出问题
- 深拷贝
- 两个内存空间
- 浅拷贝
显示弃用
编译器会默认生成构造函数,为了表避免意外错误,需要
= delete,让编译器拒绝生成这些函数。class MyClass { public: MyClass() = delete; // 禁用默认构造函数 MyClass(const MyClass& other) = delete; // 禁用复制构造函数 MyClass& operator=(const MyClass& other) = delete; // 禁用复制赋值运算符 };
【16】class_move
移动构造 / 移动赋值
左值和右值
- 左值可以取地址
- 左值引用:
int&,指向一个具名变量或可以被修改的对象。
- 左值引用:
- 右值的临时的,不会在内存中
- 右值引用:
int&&,指向一个临时对象或即将消失的对象
- 右值引用:
- 左值可以取地址
noexcept关键词- 意思:不会抛出异常
- 作用:让编译器可以进行一些优化
eg.:void foo() noexcept {}
辨析
移动构造函数 发生在新对象初始化时(例如
DynFibonacci f2 = std::move(f1);)。DynFibonacci(DynFibonacci &&a) noexcept{ if(this != &a){ *this = std::move(a); } }移动赋值运算符 发生在已有对象被赋值时(例如
f2 = std::move(f1);)。带有
noexcept的移动赋值函数例子class MyClass { private: int* data; public: // 带有 noexcept 的移动构造函数 MyClass(MyClass&& other) noexcept : data(other.data) { other.data = nullptr; // 移动资源后,原对象指针为空 } // 析构函数 ~MyClass() { delete data; // 删除资源 } };- 由于
noexcept标记,编译器可以在需要时优先使用此移动构造函数,提高性能。
- 由于
运算符重载
this隐式指针,指向对象自己
this->value = value; // 使用 this 指针来区分成员变量和参数DynFibonacci &operator=(DynFibonacci && rhs) noexcept { if (this != &rhs) { cache = rhs.cache; cached = rhs.cached; rhs.cache = nullptr; rhs.cached = 0; } return *this; // 返回引用,而不是指针 };
【17】class_derive
- 对象构造顺序:从基类到派生类。
- 拷贝构造顺序:先拷贝基类,再拷贝成员对象,最后拷贝派生类。
- 析构顺序:从派生类到基类。
class D : public B, private C{
pass
}【18】class_virtual
final关键词继承后不能改写
char virtual_name() const final { return 'C'; }
静态绑定(静态类型决定调用函数)/ 动态绑定(虚函数)
B &rbd = d: B是静态类型,d是它的动态类型非虚函数:调用时 静态类型决定。
struct A { void foo() { // 非虚函数 std::cout << "A's foo" << std::endl; } }; struct B : public A { void foo() { // 非虚函数,覆盖 A 中的 foo() std::cout << "B's foo" << std::endl; } }; int main() { A *a = new B(); a->foo(); // 输出 "A's foo" (静态绑定,a 是 A 类型指针) delete a; }- 静态类型决定了调用
foo()的版本。虽然a实际上指向的是B类型的对象,但因为foo()不是虚函数,所以调用的是A类中的foo(),输出A's foo。
- 静态类型决定了调用
虚函数:调用时 动态类型决定,实际对象的类型决定调用哪个版本的函数。
在 C++ 中,虚函数只需要在基类中声明一次,在派生类中重写虚函数时,
virtual关键字是可选的。即使在派生类中的函数签名中没有virtual,它依然会被视为覆盖基类的虚函数。struct A { virtual void foo() { // 虚函数 std::cout << "A's foo" << std::endl; } }; struct B : public A { void foo() override { // 虚函数,覆盖 A 中的 foo() std::cout << "B's foo" << std::endl; } }; int main() { A *a = new B(); a->foo(); // 输出 "B's foo" (动态绑定,a 是 A 类型指针,但实际对象是 B 类型) delete a; }- 动态类型决定了调用
foo()的版本。虽然a是A类型的指针,但它指向的是B类型的对象,foo()是虚函数,因此会调用B类中的foo(),输出B's foo。
- 动态类型决定了调用
【19】class_virtual_destruct
静态字段(类变量)
概念
- 需要在类外声明
- 只有一份,和对象的生命周期无关,和类的生命周期有关
- 需要在类外进行显式初始化——
int A::num_a = 0; - 可以通过类名直接访问
示例
public: static int num_b = 0; // 用 public 关键词包含可以外部直接访问
虚析构函数
- 何时用
- 设计有继承的基类时,基类的析构函数需要用虚析构函数
- 因为如果直接把基类的资源释放,继承的类对象资源也会被一起释放
- 设计有继承的基类时,基类的析构函数需要用虚析构函数
- 何时用
类型转换
类的类型转换
dynamic_cast会在运行时检查类型,如果转换不合法,它会返回nullptr(对于指针)或抛出std::bad_cast异常(对于引用)。B* bb = dynamic_cast<B*>(ab); // b* 表示指针指向b if (bb) { bb->show(); // 如果转换成功,调用派生类的方法 } else { cout << "Conversion failed!" << endl; // 如果转换失败,处理错误 }变量的类型转换
double d = static_cast<double>(i); // 将 int 转换为 double(安全的类型转换) doubel d = double(i) // 不安全的类型转换,如果失败不会报错
【20】function_template
函数模板
template <typename T> T add(T a, T b) { return a + b; }调用的时候可以加上类型
template<typename T> T digmoid_dym(T x) { return 1 / (1 + std::exp(-x)); }; ans.f = digmoid_dym<float>(x.f); ans.d = digmoid_dym<double>(x.d);
精度比较问题
实际上,浮点数在计算机中表示时存在微小的舍入误差。
举例
ASSERT(plus(0.1f, 0.2f) == 0.3f)这条断言会通过,原因在于
0.1f和0.2f都是 单精度浮点数(float),并且它们的和0.3f也被表示为 单精度浮点数。因为float类型的精度较低,因此在加法运算时,虽然float计算会有精度误差,但这些误差通常较小,不足以影响==比较。ASSERT(plus(0.1, 0.2) == 0.3)- 这条断言不能通过,原因是
0.1和0.2被当作 双精度浮点数(double)处理,而 双精度浮点数的精度更高,因此在计算0.1 + 0.2时,结果不是恰好等于0.3。
- 这条断言不能通过,原因是
思考
浮点数何时可以判断 ==?何时必须判断差值?
使用
==比较浮点数:- 常量值计,没有复杂的算术运算,外部输入。
必须使用差值比较:
当浮点数是由复杂的算术运算得到时,尤其是加法、减法等运算会引入舍入误差。
// 这时的 a 和直接计算的预期值可能有所差距 if (std::fabs(a - expected_value) < 1e-6) { // 判断差值是否在容忍范围内 }
【21】runtime_datatype
- 枚举
传统枚举
enum DataType { Float, Double, }; int a = Float; // 这种用法是允许的,会把 Float 映射成整数 0强类型枚举
而使用
enum class后,无法直接将enum class枚举值赋给其他类型:enum class DataType { Float, Double, }; int a = DataType::Float; // 错误:无法从 DataType 转换为 int
【22】class_template
类模板
基本语法
template <typename T> class ClassName { T data; // 使用模板参数T定义数据成员 public: void setData(const T& val) { data = val; } T getData() const { return data; } }; // 实例化 ClassName<int> intClass; // 使用 int 类型实例化 ClassName<double> doubleClass; // 使用 double 类型实例化
std::memcpy()C++ 标准库提供的一个低级内存操作函数,用于将一块内存的内容复制到另一块内存
void* std::memcpy(void* dest, const void* src, size_t count);当你需要复制大量的内存数据时,
std::memcpy通常比逐个元素的复制要高效得多dest是目标内存地址(即复制的目的地)。src是源内存地址(即要复制的原始数据)。count是要复制的字节数。
【23】template_const
C++模板 “模板非类型实参(non-type template parameters)”
模板非类型实参允许我们在模板定义时
而不是传统的类型参数
- 使用常量值
- 指针
- 引用
template <int N> struct Array { int data[N]; };
【24】std_array
std::array是 C++ 中的一种容器std::array的特点:- 固定大小:
std::array的大小在编译时确定,并且大小不能动态改变。这意味着,数组的大小在创建时就必须指定。 - 元素访问与传统数组类似:
std::array使用索引访问元素,且其下标从 0 开始,支持常规的迭代器、范围for循环等功能。 - 封装了 C 风格数组:
std::array是对 C 风格数组的包装,提供了与数组类似的语法,但添加了许多成员函数和操作,使得它比原生数组更加安全和灵活。 - 内存布局与传统数组相同:
std::array在内存中按值存储数据,与传统 C 风格数组类似。 - 类型安全:
std::array类型安全,要求在定义时指定元素类型和大小。
- 固定大小:
主要成员函数:
size():返回数组的元素个数。empty():检查数组是否为空(对于std::array,这个总是返回false,因为它有固定大小)。at():与operator[]类似,但提供了边界检查。如果索引越界,会抛出std::out_of_range异常。front()和back():返回数组的第一个元素和最后一个元素。fill():将数组的所有元素设置为给定值。swap():交换两个std::array对象的内容。data():返回指向数组内部数据的指针,便于与 C 风格的函数交互。
优点:
- 安全性:
std::array提供了比传统数组更安全的操作,如at()提供的越界检查。 - 与标准库兼容:
std::array支持 STL 算法(如std::sort()、std::find()),并且可以方便地与其他容器类型(如std::vector)进行互操作。 - 性能优势:
std::array在性能上与传统数组相似,因此可以用于性能要求高的场景。 - 类型安全:它提供了类型安全的操作,确保元素的类型正确。
- 安全性:
使用示例:
cpp复制代码#include <array> #include <iostream> int main() { // 定义一个固定大小为 5 的整型数组 std::array<int, 5> arr = {1, 2, 3, 4, 5}; // 使用at()进行安全访问 std::cout << "Element at index 2: " << arr.at(2) << std::endl; // 使用size()获取数组大小 std::cout << "Array size: " << arr.size() << std::endl; // 遍历数组 for (const auto& elem : arr) { std::cout << elem << " "; } std::cout << std::endl; // 使用fill()填充数组 arr.fill(10); for (const auto& elem : arr) { std::cout << elem << " "; } std::cout << std::endl; }
【26】std_vector
std::vector是 C++ 标准模板库中的动态数组容器。它可以动态调整大小,存储任意类型的元素,同时提供丰富的操作方法。- 特点
动态大小:可以根据需要自动调整大小,无需用户手动管理内存。
连续存储:存储在一段连续的内存中,因此支持高效的随机访问。
类型安全:支持任意类型的元素,包括用户自定义类型。
高效操作
- 在末尾添加或移除元素(
push_back和pop_back)的时间复杂度为均摊 O(1)- 在中间插入或删除元素的时间复杂度为 O(n),因为需要移动数据。
- 在末尾添加或移除元素(
- 特点
常用操作
构造函数
- 默认构造:
std::vector<T> vec; - 初始化构造:
std::vector<T> vec(n, value);创建包含
n个值为value的元素的向量。std::vector<bool> vec(100, true) // true 表示1,已激活
- 列表初始化:
std::vector<T> vec = {1, 2, 3};
- 默认构造:
大小操作
size():返回当前元素个数。capacity():返回当前分配的存储容量(可以容纳的元素数量)。resize(n):调整大小为n,多余的元素被销毁,不足的部分填充默认值。empty():判断容器是否为空。
元素访问
operator[]和at(index):通过索引访问元素。front()和back():访问首尾元素。data():返回指向底层数组的指针。
修改操作
push_back(value):在末尾添加元素。pop_back():移除末尾元素。insert(position, value):在指定位置插入元素。不能直接把
vec.begin()替换为1,它需要的是一个迭代器vec.insert(vec.begin() + 1, 1.5);- 在索引
1之前插入1.5,begin()是指向第一个元素的迭代器。
- 在索引
erase(position):移除指定位置的元素。clear():移除所有元素。- 但不会该改变
capacity
- 但不会该改变
迭代器
begin()和end():返回首尾元素的迭代器。rbegin()和rend():返回反向迭代器。rend:标记容器结束位置(不可访问区域)
容量管理
reserve(n):预分配至少能容纳n个元素的内存,减少频繁的重新分配。shrink_to_fit():释放未使用的容量。
比较
功能 std::vectorstd::array普通数组 大小是否动态调整 是 否 否 初始化方式 动态初始化 静态固定初始化 静态初始化 元素访问 []或at()[]或at()[]增加元素 push_back()、insert()不支持 不支持 删除元素 pop_back()、erase()不支持 不支持 容器大小获取 size()和capacity()size()sizeof(arr) / sizeof(arr[0])支持 STL 算法 是 是 否 容量管理 reserve()和shrink_to_fit()不支持 不支持 内存布局是否连续 是 是 是 操作丰富性 丰富 较少 少 内存管理是否自动 是 否 否 使用场景
- 数据大小动态变化且不可预测时。
- 需要便捷操作(插入、删除、搜索等)。
- 希望与 STL 算法(如
std::sort)良好兼容时。
注意事项
动态数组大小
64位
- 24个字节(指向数据指针,指向容量指针,指向当前大小指针)
【26】std_vector_bool
- 模板特化
- 通用模板:为所有类型提供一个通用的模板实现。
- 特化模板:针对特定类型提供一个特殊的实现。如果没有针对某个类型的特化,编译器会使用通用模板。
两种常见的模板特化:
完全特化:完全为某个特定类型提供一个实现。
template<typename T> struct MyType { void print() { std::cout << "General Template" << std::endl; } }; // 针对 int 类型进行模板特化 template<> struct MyType<int> { void print() { std::cout << "Specialized Template for int" << std::endl; } };偏特化:为模板的某些特定参数提供不同的实现,而不完全限定所有类型。。
template<typename T> struct MyType<T*> { void print() { std::cout << "Template Specialized for Pointer Types" << std::endl; } };
【27】strides
类型别名
两种方式
typedeftypedef unsigned int UInt;usingusing UInt = unsigned int;
逆向迭代器和普通迭代器的区别
普通迭代器:
strides.begin()指向的是容器的第一个元素。strides.end()指向的是容器的最后一个元素的下一个位置(超尾)。
反向迭代器:
strides.rbegin()指向的是容器的最后一个元素。strides.rend()指向的是容器的第一个元素的前一个位置(超首)。
#include <vector> #include <iostream> int main() { std::vector<int> v = {1, 2, 3, 4}; for (auto it = v.rbegin(); it != v.rend(); ++it) { std::cout << *it << " "; // 输出:4 3 2 1 } return 0; }
【28】std_string
std::basic_string:模板类,用于表示和操作字符串。operator""s:自定义字面量操作符,用于将字符串字面量转换为std::string。auto hello = "Hello"s;decltype:用于推导表达式的类型。std::is_same:用于判断两种类型是否相同。auto hello = "Hello"s; auto world = "world"; ASSERT((std::is_same_v<decltype(hello), std::string>), "Fill in the missing type."); ASSERT((std::is_same_v<decltype(world), const char *>), "Fill in the missing type.");
【29】std_map
std::map: 关联容器,用于存储键值对它通过 红黑树(或类似的平衡树)实现,确保元素按键的顺序排列。
主要特点:
排序:
std::map自动按键进行排序,默认使用键的<运算符进行比较,可以自定义比较规则(通过传入比较器)。唯一键:每个键只能出现一次。如果插入一个已有键的元素,新的元素会覆盖旧的元素。
时间复杂度:查找、插入和删除的平均时间复杂度为 O(log N),其中 N 是容器中的元素数量。
底层实现:基于自平衡的二叉搜索树(如红黑树),保持键的有序性。
常见操作:
插入元素:
map.insert({key, value})- 如果键存在不会更新,会返回
false
- 如果键存在不会更新,会返回
查找元素:
map.find(key)map.find(key)返回一个迭代器,当没有找到键时返回map.end()
删除元素:
map.erase(key)获取键值:
map[key](使用operator[]访问或插入)
示例
#include <iostream> #include <map> int main() { std::map<int, std::string> myMap; myMap[1] = "one"; myMap[2] = "two"; myMap.insert({3, "three"}); // 循环语法 —— 对象:容器 for (const auto& pair : myMap) { std::cout << pair.first << ": " << pair.second << std::endl; } return 0; }
std::unordered_map与
std::map不同,它并不保证元素的顺序,而是根据键的 哈希值 来存储元素。主要特点:
- 无序性:
std::unordered_map中的元素没有顺序,它们根据键的哈希值分布在不同的桶中,桶内的元素顺序不保证。 - 键的唯一性:每个键只能出现一次,插入已存在的键时会覆盖已有的值。
- 时间复杂度:平均情况下,查找、插入和删除的时间复杂度为 O(1),但是在哈希冲突时,最坏情况下可能会退化为 O(N)。
- 底层实现:基于哈希表实现,通过哈希函数将键映射到哈希桶中。
- 无序性:
常见操作:
- 插入元素:
unordered_map.insert({key, value}) - 查找元素:
unordered_map.find(key) - 删除元素:
unordered_map.erase(key) - 获取键值:
unordered_map[key](使用operator[]访问或插入)
- 插入元素:
示例:
#include <iostream> #include <unordered_map> int main() { std::unordered_map<int, std::string> myUnorderedMap; myUnorderedMap[1] = "one"; myUnorderedMap[2] = "two"; myUnorderedMap.insert({3, "three"}); for (const auto& pair : myUnorderedMap) { std::cout << pair.first << ": " << pair.second << std::endl; } return 0; } // 输出(顺序可能不同)std::map与std::unordered_map的区别特性 std::mapstd::unordered_map顺序 按照键的顺序(升序)排列 无序排列 底层实现 红黑树(或其他平衡树) 哈希表 查找复杂度 O(log N) 平均 O(1),最坏 O(N) 插入/删除复杂度 O(log N) 平均 O(1),最坏 O(N) 键的唯一性 键唯一且按顺序排列 键唯一,但无特定顺序 内存使用 需要额外存储树的结构 需要额外存储哈希表的结构 适用场景 需要保持有序键值对的场景 需要高效查找、插入、删除的场景
何时使用
std::map和std::unordered_map?使用
std::map:当需要保证元素的顺序时,或者有对元素排序的需求时(例如需要按键值顺序遍历容器)。使用
std::unordered_map:当需要频繁进行查找、插入、删除操作,并且顺序不重要时,unordered_map由于哈希表的高效性,通常会提供更好的性能。
【30】std_unique_ptr
智能指针
- 指针数量:
std::unique_ptr只包含一个原始指针,指向它所管理的对象。 - 所有权:
std::unique_ptr采用 唯一所有权 模型,意味着每个资源只能被一个unique_ptr拥有,不能共享。 - 复制和移动:
unique_ptr不能被拷贝,只有移动语义。资源的所有权可以通过移动来转移。 - 内存管理:当
unique_ptr被销毁时,它会自动删除它所管理的资源(调用delete)。
构造和初始化
auto ptr = std::make_unique<int>(10); // 更安全和简洁移动语义
std::unique_ptr<int> ptr1 = std::make_unique<int>(10); std::unique_ptr<int> ptr2 = std::move(ptr1); // ptr2 获得所有权,ptr1 变为空访问资源,用
*和->空指针检查
用
()操作符来检查if (ptr) { // 等价于 if (ptr != nullptr) // ptr 不为空 }
资源本身(
Resource对象)在堆上。智能指针对象在栈上。作用域和资源释放 :nerd_face:
资源释放和指针销毁是两码事
- 离开作用域时,
ptr本身被销毁,才会调用析构函数 - 手动分配
nullptr,资源会被释放。
- 离开作用域时,
eg.|------------------| | drop(R2) | -> 销毁 R2 |------------------| | reset(R1) | -> R2 |------------------| | forward(R1) | -> 已销毁 |------------------| | forward(R1) | -> 已销毁 |------------------| | reset(nullptr) | -> 已销毁 (销毁的是资源,在堆上。析构函数是栈上指针的资源,离开作用域才会被回收)=
【31】std_share_ptr
std::shared_ptr指针数量 (包含两个指针)
- 一个指向实际资源的原始指针。
- 一个指向引用计数的控制块指针。这个控制块包含资源的引用计数,用于管理对象的生命周期。
所有权:
std::shared_ptr采用 共享所有权 模型,多个shared_ptr可以共享同一个资源。每次复制或赋值时,引用计数会增加。引用计数:
std::shared_ptr使用引用计数来管理对象的生命周期。每当一个shared_ptr被销毁时,引用计数会减少;当引用计数变为零时,资源会被自动销毁。内存管理:当所有
shared_ptr的引用计数为零时,shared_ptr会自动删除它所管理的资源。示例
std::shared_ptr<int> ptr1 = std::make_shared<int>(10); std::shared_ptr<int> ptr2 = ptr1; // ptr1 和 ptr2 都指向相同的资源
std::weak_ptr指针数量 (也包含两个指针)
- 一个指向实际资源的原始指针(和
shared_ptr一样)。 - 一个指向控制块的指针,但它不会增加引用计数。因此,
weak_ptr不拥有资源,它只是“观察”一个shared_ptr所管理的资源。
- 一个指向实际资源的原始指针(和
所有权:
std::weak_ptr不拥有资源的所有权,无法阻止资源的销毁。它只能观察shared_ptr的资源。当shared_ptr被销毁后,weak_ptr变为空指针。用途:
std::weak_ptr主要用于 避免循环引用,特别是在需要两个或多个对象相互引用的情况下。如果shared_ptr之间相互持有weak_ptr,它们就不会增加引用计数,避免了循环引用导致的内存泄漏问题。示例
std::shared_ptr<int> ptr1 = std::make_shared<int>(10); std::weak_ptr<int> weakPtr = ptr1; // weakPtr 不增加引用计数
函数
lock()方法- 通过
weak_ptr的lock()方法,可以尝试获得一个有效的shared_ptr。- 如果所观察的对象仍然存在(即引用计数大于 0),
lock()返回一个有效的shared_ptr,并增加该对象的引用计数; - 如果对象已经被销毁(引用计数为 0),则返回一个空的
shared_ptr。
- 如果所观察的对象仍然存在(即引用计数大于 0),
- 通过
use_count()方法std::shared_ptr:use_count()返回当前shared_ptr持有资源的引用计数。std::weak_ptr:use_count()返回当前所观察的shared_ptr的引用计数,但它本身并不增加引用计数。
【32】std_transform
std_transform
定义:
std::transform是 C++ 标准库中的一个算法- 用于对一个范围内的元素进行变换,并将结果存储在另一个容器中。
语法:
template <class InputIt, class OutputIt, class UnaryOperation> OutputIt transform(InputIt first, InputIt last, OutputIt d_first, UnaryOperation op);InputIt first, InputIt last:输入范围的开始和结束迭代器。OutputIt d_first:输出范围的开始迭代器,变换后的元素将存储在此范围。UnaryOperation op:应用于输入元素的操作,通常是一个函数或函数对象。
用途:
- 处理容器中的数据,如对数组或向量中的元素进行加法、平方、转换等操作。
- 生成新容器或修改原容器的元素。
例子:
#include <algorithm> #include <vector> #include <iostream> int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; std::vector<int> result(vec.size()); // 使用 std::transform 对 vec 中的每个元素进行平方 std::transform(vec.begin(), vec.end(), result.begin(), [](int x) { return x * x; }); for (int num : result) { std::cout << num << " "; // 输出: 1 4 9 16 25 } return 0; }
lambda 表达式
[](int x) { return x * x; }是一个 lambda 表达式,用于在 C++ 中定义一个匿名函数。[]是 lambda 表达式的捕获列表,用于捕获外部变量。在这个例子中,[]表示没有捕获任何外部变量。(int x)是参数列表,表示 lambda 函数接收一个整数类型的参数x。{ return x * x; }是函数体,表示该 lambda 函数返回x的平方。
【33】std_accumulate
用于计算序列中所有元素的累加值。
- 函数原型:
template< class InputIterator, class T, class BinaryOperation > T accumulate( InputIterator first, InputIterator last, T init, BinaryOperation op );参数说明:
InputIterator first:指向序列第一个元素的迭代器。InputIterator last:指向序列最后一个元素之后的元素的迭代器。T init:初始化值。累加操作会从init值开始进行。BinaryOperation op:一个二元操作函数或函数对象,用于执行累加操作。它定义了如何合并两个元素(如加法、乘法等)。
eg.int result = std::accumulate(v.begin(), v.end(), 0, [](int a, int b) { return a + b * b; // 返回 a + b^2 });