C++语法理解记录
3.11
C++构造函数有几种?
C++构造函数有几种?
1.默认构造函数(简单)
2.带参构造函数(简单)
3.拷贝构造函数
class MyClass {
public:MyClass(const MyClass &other) {// 拷贝构造函数//通过other中的数据成员赋值给当前对象的数据成员变量//必须是引用传参,如果是传值方式,会产生递归调用(报错)}
};
4.移动构造函数(关键标识:noexcept):移动构造函数用于将资源(如动态内存、文件句柄等)从一个对象“移动”到另一个对象,而不是复制。它通常用于优化性能,特别是在处理临时对象或动态内存管理时。移动构造函数通过“窃取”资源来避免不必要的复制操作。
class MyClass {
public:MyClass(MyClass &&other) noexcept {// 移动构造函数//如果other中有指向堆区的指针//那么other中的指针会被赋值到当前对象,并将other中的指针变量赋值为nullptr,实现资源窃取}
};
5.委托构造函数:委托构造函数允许一个构造函数调用同一个类中的另一个构造函数,以避免代码重复。
class MyClass {
public:MyClass() : MyClass(0) { // 委托构造函数}MyClass(int value) {// 带参数的构造函数}
};
6.显式构造函数(关键标识:explicit)
class MyClass {
public:explicit MyClass(int value) {// 显式构造函数}
};
显式调用构造函数:MyClass obj1(10);
隐式调用构造函数:MyClass obj1 = 10;
如果没有 explicit 关键字,MyClass obj2 = 20;合法,因为编译器会隐式调用 MyClass(int v)
构造函数。使用 explicit 后,必须显式调用构造函数,避免意外的隐式转换。
补充:
类成员初始化次序: 就地初始化;构造函数初始化列表; 构造函数体内部对成员赋值
构造函数初始化列表是为了方便对内嵌类对象成员进行初始化.
移动构造函数加深理解
// 拷贝构造函数(深拷贝)MyString(const MyString& other) {data = new char[strlen(other.data) + 1];strcpy(data, other.data);std::cout << "拷贝构造函数调用,复制资源:" << data << std::endl;}// 移动构造函数(右值引用)MyString(MyString&& other) noexcept {data = other.data; // 直接“偷取”资源other.data = nullptr; // 清空源对象的指针std::cout << "移动构造函数调用,转移资源:" << data << std::endl;}
&是左值引用,&&是右值引用。使用移动构造函数可以提示这个输入的右值引用数据可转移。暂时的理解还是不够深,等后面用到再说。
MyString obj2 = obj1;(隐式调用拷贝构造函数)
MyString obj2(obj1);(显式调用拷贝构造函数)MyString obj2 = move(obj1);(隐式调用移动构造函数)obj1中的资源被窃取
MyString obj2(move(obj1));(显式调用移动构造函数)obj1中的资源被窃取
obj1的析构函数必须处理资源被重复释放的问题。
构造函数和析构函数执行顺序
当创建派生类对象时,构造函数按以下顺序调用:基类的构造函数(按继承声明顺序,从左到右);成员变量的构造函数(按类中声明顺序,与初始化列表顺序无关);派生类自身的构造函数。
析构函数的执行顺序:派生类自身的析构函数;成员变量的析构函数(按声明顺序的逆序);基类的析构函数(按继承声明顺序的逆序)。
对于多重继承,基类构造函数按继承声明顺序调用,与派生类构造函数初始化列表顺序无关。
C++编译器会默认生成的一些函数和运算符
1.默认构造函数
2.默认析构函数
3.默认拷贝构造函数(浅拷贝)(深拷贝需要自行定义)
4.默认移动构造函数
5.拷贝赋值运算符
6.移动赋值运算符
建议:如果在使用过程中涉及动态管理内存,自行定义拷贝构造函数和移动构造函数,析构函数。确保动态内存能够清晰的申请和释放,也便于实际维护。
3.12
虚继承?
这个是为了处理菱形继承问题:
什么是菱形继承问题?
class Base {
public:int data;
};
class Derived1 : public Base {}; // 继承Base
class Derived2 : public Base {}; // 继承Base
class Final : public Derived1, public Derived2 {}; // 菱形继承
Final中保留了两份Base的成员变量和成员函数,在调用Base类的成员函数时会产生歧义
虚继承:
class Base { /* ... */ };// 使用 virtual 关键字声明虚继承
class Derived1 : virtual public Base {};
class Derived2 : virtual public Base {};class Final : public Derived1, public Derived2 {};
这样Final对象中只会保留一份Base的成员变量及成员函数
继承多态?
抽象类:含有纯虚函数(virtual)的类,不能创建对象;派生类必须对纯虚函数进行覆写(override).
其他情况:含有虚函数的类,其派生类可以对虚函数进行覆写,确保通过基类指针可以正确调用派生类对象的成员函数(覆写基类的成员函数)。
虚析构函数?
基类中将析构函数标记为虚析构函数(virtual);
派生类可以对析构函数进行覆写(override);
主要作用:确保通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,
从而释放对象所占用的资源。
什么是虚函数表?
每一个含有虚函数的类都会有一个虚函数表。
每一个含有虚函数的类的对象创建时在内存空间里会先创建一个虚函数表指针,虚函数表指针指向虚函数表,虚函数表中存放了每个虚函数的地址。
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1 = 6;
};
class Derive :public Base1 {
public:void func1() override{ cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1 = 9;
};
int main() {Base1* a = new Base1{};Derive* b = new Derive{};Base1* c = new Derive{};a->func1();b->func3();c->func1();return 0;
}
a的内存空间分布如下
vptr(8字节) ->指向虚函数表(Base1::func1();Base1::func2();)
int b1 = 6(4字节)
padding(4字节填充)
b的内存空间分布如下:
vptr(8字节) ->指向虚函数(Derive::func1();Base1::func2();Derive::fun3())
int b1=6,int d1 = 9 (总共8字节)
c的内存空间分布如下:
vptr(8字节) ->指向虚函数(Derive::func1();Base1::func2();Derive::fun3())
int b1=6,int d1 = 9 (总共8字节)
基类指针指向派生类对象和派生类指针指向派生类对象的内存空间分布是一致的。
补充虚函数表测试
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1 = 6;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2 = 8;
};
class Derive :public Base1,public Base2 {
public:void func1() override{ cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1 = 9;
};
int main() {Base1* d = new Derive{};Derive* e = new Derive{};Base2* f = new Derive{};d->func1();e->func3();f->func1();return 0;
}
d内存布局:总共(48字节)
vptr(8字节)(Derive::func1();Base1::func2();Derive::func3();)
int b1=6(4字节) padding(4字节)
vptr(8字节)(Derive::func1();Base2::func2();)
int b2 =8,d1=9(共8字节)
内存对齐(无效内容)(16字节) e内存布局:总共48字节
vptr(8字节)(Derive::func1();Base1::func2();Derive::func3();)
int b1=6(4字节) padding(4字节)
vptr(8字节)(Derive::func1();Base2::func2();)
int b2 =8,d1=9(共8字节)
内存对齐(无效内容)(16字节) f内存布局:
vptr(8字节)(Derive::func1();Base2::func2();)
int b2 =8,d1=9(共8字节)
内存对齐(无效填充)(共32字节)
多态虚继承表特别理解:
派生类对象创建过程中,会先将基类的虚函数表复制过来然后根据派生类是否覆写或者新添虚函数,对虚函数表中的内容进行修改调整。
多继承过程中,多个基类的虚函数表都会被复制过来,如果派生类进行了覆写,则对虚函数表进行修改。如果派生类新增虚函数,这个虚函数会被添加到继承的第一个基类的虚函数表中。
动态联编的理解:编译器在编译时可以无法确定输入对象的类型,只能在执行时确定。比如一些需要跟用户交互的场景,交互过程中才能确定对象的类型。此时使用动态联编就非常必要了。多态调用的逻辑猜测:
void makeSpeak(Animal* animal) {animal->speak();
}
当输入的animal指针指向的时dog派生类对象,执行speak通过虚函数表中的函数执行地址进行执行。这个虚函数表中按顺序记录了Animal类的虚函数,但是在创建派生类对象的时候对这个虚函数表进行了覆写,调用speak会跳到Animal的speak的虚函数对应的位置,但实际上这个位置已经被覆写了,所以最后调用的是dog的speak函数。(不要扣的太细,一口气吃不成大胖子)
单例模式复习
核心思想:一个类只有一个实例,并提供一个全局访问点来访问这个实例。
C++:
利用静态成员函数作为全局访问点;
利用静态成员函数中的局部静态变量作为唯一的实例;
静态成员函数和静态成员变量属于类,不属于某一个类对象,可以通过类名::静态成员函数直接调用。类的静态成员变量还要在类外进行初始化赋值。
3.13
lambda表达式
Lambda表达式是一种简洁的定义匿名函数的方式。它允许你在需要函数的地方
直接定义函数,而无需显式声明一个函数。Lambda表达式非常灵活,可以捕获
上下文中的变量,并且可以像普通函数一样使用。
基本语法:
[捕获列表](参数列表)[可变规则]->[返回类型][函数体]
[capture](parameters)->return_type {函数体}
示例1:
auto lambda = [](int a, int b) -> int {return a + b;
};
std::cout << lambda(3, 4);
捕获列表方式:
[] 不捕获任何外部变量。
[=] 以值捕获所有外部变量(拷贝)。
[&] 以引用捕获所有外部变量。
[var] 以值捕获特定变量var。
[&var] 以引用捕获特定变量var。
[=, &var] 以值捕获所有外部变量,但以引用捕获特定变量var。
[&, var] 以引用捕获所有外部变量,但以值捕获特定变量var。
[this] 表示值传递方式捕捉所在类的this指针。mutable修饰符, 默认情况下Lambda函数总是一个const函数,mutable
可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。
Lambda默认不会改变捕获参数的值,如果使用mutable则可以改变。mutable成员变量:如果需要在const成员函数中修改某个成员变量,可以将该变量声明为mutable。int main()
{int m = 0;int n = 0;[&, n] (int a) mutable { m = ++n + a; }(4);cout << m << endl << n << endl;
}Lambda表达式工作原理
编译器会把一个Lambda表达式生成一个匿名类的匿名对象,并在类中重载函数调用运算符,实现了一个operator()方法。
auto print = []{cout << "Hello World!" << endl; };
翻译成下面的:
class print_class
{
public:void operator()(void) const{cout << "Hello World!" << endl;}
};
// 用构造的类创建对象,print此时就是一个函数对象
auto print = print_class();
C++内存分区
内存分区从低地址到高地址:
.text(代码区存放程序代码) ->.rodata(常量区字符串等)->.data(已初始化的全局,静态变量)->.bss(未初始化的全局/静态区)->.heap-> .stack->内核空间
.bss(block started by symbol)
.rodata(Read-only Data)
STL仿函数
STL仿函数本质上是一个类,其内部重载了()运算符从而实现了像函数一样的功能。
相比普通函数,仿函数不仅实现了函数功能,由于仿函数本身是一个类,可以存储成员变量,比如可以存储调用次数,可以有更多的用法。
仿函数还可以被编译器内联,性能更好。
lambda本身是一个简单的仿函数(在类中重载()运算符),只不过没有成员变量,没有仿函数那么灵活。
仿函数比较常用的点就是作为STL的算法的回调函数。
结构体和类的区别
没有区别,类默认私有,结构体默认公有。
C语言中结构体定义时必须有struct关键字
C++直接用结构体名称就可以。
struct ListNode node;(C语言)
ListNode node;(C++)
右值引用底层探索(主要管移动语义和完美转发,其他先忽略,用到再说)
左值通常指明确内存地址的对象
右值指临时的,即将被销毁的值
右值引用:标识可以被移动的临时对象,从而支持移动语义和完美转发
MyString obj2(move(obj1));
move的作用为类型转换,把左值obj1对象转换为右值引用,使得调用移动构造函数创建obj2对象,将动态内存直接转移而不是创建新内存并进行内容复制。
特别的:如果没有定义移动构造函数,编译器会尝试调用拷贝构造函数。右值引用就是为了标识使用移动构造函数创建对象。究极理解:move和移动构造函数是搭配的,用了move就肯定要定义移动构造函数。
对于普通右值:
int&& r = 15;相当于把立即数右值保存到1个位置,该位置称为r用来保存15的值。r有地址所以r为左值。可以对r的值进行更改,如更改为16.
特殊的在移动语义中和完美转发中特别的用到右值(直接做动态内存转移,完美转发左值和右值)
右值引用的主要作用:
移动语义:允许临时对象的资源(如内存、文件句柄等)被“移动”到另一个对象中,而不是进行拷贝。这可以避免不必要的资源复制,提高程序效率。
完美转发:在模板编程中,能够将函数的参数以左值或右值的形式完美转发给另一个函数。(std::forward)完美转发示例:#include <iostream>
#include <utility> // 包含 std::forward// 一个简单的函数,接受左值引用
void process(int& x) {std::cout << "左值引用函数调用, 值: " << x << std::endl;
}// 一个简单的函数,接受右值引用
void process(int&& x) {std::cout << "右值引用函数调用, 值: " << x << std::endl;
}// 一个接受任意参数的模板函数,并完美转发给 process 函数
template <typename T>
void forwardToProcess(T&& arg) {process(std::forward<T>(arg)); // 完美转发
}int main() {int a = 5;// 调用时传入左值forwardToProcess(a);// 调用时传入右值forwardToProcess(10);return 0;
}
process(int& x):这是一个接收左值引用的函数。如果传入的是一个左值(如变量a),这个函数会被调用。process(int&& x):这是一个接收右值引用的函数。如果传入的是一个右值(如字面量10),这个函数会被调用。forwardToProcess(T&& arg):这是一个模板函数,使用了T&&,它可以接受任何类型的参数(无论是左值还是右值)。这个函数使用std::forward<T>(arg)来将参数转发给process函数,从而实现完美转发。如果arg是一个左值,std::forward<T>(arg)会把它当作左值传递。
如果arg是一个右值,std::forward<T>(arg)会把它当作右值传递。
完美转发是按需传递左值或右值,让接收方决定是否转移资源。临时对象的销毁仍遵循标准生命周期规则(用在模板编程中)。深度总结:
移动语义:
核心目的:高效转移资源所有权,尤其是针对堆内存,文件句柄等昂贵资源。场景:显式将左值转为右值(std::move),允许资源被“窃取”,常用于实现移动构造函数、移动赋值运算符。
完美转发:
在泛型代码中保持参数的原始值类别(左值/右值),不主动参与资源管理。模板函数需要将参数原样传递给下层函数。
3.14-3.16
内联函数
类继承过程中的访问权限变化
手撕vector和string
智能指针
typeid
模板元编程
C++抛异常以及printf,以后写代码要常用,答题的时候不能debug的。
排序算法回顾
next和nextint 的用法。
new 数组和delete的具体写法
LRU实现(手撕)
友元函数的用法回顾
4.7日
c++异常有哪些?
标准异常类层次结构
1.顶层基类:std::exception
std::exception 是所有标准异常类的基类
主要接口:virtual const char* what() const noexcept
返回一个 C 风格字符串,描述异常的基本信息。通常在捕获异常时用于输出调试信息或日志记录。
用途:
为所有异常提供一个统一的捕获基类,这使得捕获语句可以轻松捕获所有衍生自 std::exception 的异常.
2.逻辑错误类(std::logic_error 及其派生类)
std::logic_error 用于表示程序内部的设计错误或逻辑矛盾。
派生类:
std::invalid_argument:当传递给函数的参数不符合要求时抛出。
std::domain_error:例如在数学函数中,如果参数不在定义域内(比如取负数的平方根)就会触发。
std::length_error:当长度超过允许的最大范围时使用。
std::out_of_range:例如数组或容器索引超出有效范围时抛出。
3.运行时错误类(std::runtime_error 及其派生类)
std::runtime_error用于表示运行时发生的错误,它不一定是程序逻辑上的错误,而可能是外部条件变化导致的,例如文件 I/O 错误或网络故障。
派生类:std::range_error:用于表示数值计算中结果超出可表示范围。
std::overflow_error 和 std::underflow_error:分别表示数值溢出或下溢。
3.新建异常类(来自 <new> 头文件)
std::bad_alloc当使用 new 操作符进行内存分配且内存不足时抛出此异常,是一种运行时错误。
4.I/O 异常类 std::ios_base::failure
主要用于输入/输出流错误,当流操作(例如读取文件)失败时抛出。
其中logic_error类和runtime_error类可以传入字符串
bad_alloc,bad_exception不能传入字符串
不同错误的使用不太一样,可以先使用简单的类型。
1.抛异常以及assert相关
简单错误简单办(if else)
复杂错误异常办(try throw catch)
C++是标准库所有异常类的基类exception
异常对象的what成员函数能够获取异常的相关信息
C++内建异常类:
#include<exception>
try{throw;//扔出当前异常
} catch(std::exception & e) {
} catch(...){接收任何异常
}
noexcept 异常安全性声明:告诉编译器某个函数不会抛出
异常,帮助编译器优化代码。
assert 调试模式下非常适合调试
非调试模式下可以在编译选项中加入NDEBUG编译选项,程序将不会执行assert语句
assert为调试使用。
assert用于调试阶段处理不可能出现的错误,检查代码是否正确。
perror则是针对可能出现的错误给予提示。
assert发行阶段就不再使用,perror发行阶段也在使用
//自定义异常类,覆写what成员函数
class MyException : public std::exception {
public:const char* what() const noexcept override {return "My custom exception";}
};
try {throw;// 可能抛出异常的代码
} catch (异常类型1 参数名) {// 处理异常类型1
} catch (异常类型2 参数名) {// 处理异常类型2
} catch (...) {// 处理所有其他类型的异常
}
2.new和delete数组
int* p = new int;
int* p = new int(35);
delete p;
int* p = new int[NUM];
delete [] p;
3.类继承过程中访问权限变化
基类:public private protected
公有继承:public private protected(不变)
私有继承:private private private(全私有)
保护继承 protected private protected
(public变protected)
protected特点:类外部无法访问 继承类可以直接访问
protected 主要用于基类向派生类暴露实现细节,同时阻止外部直接访问。
特别的:
对于对于继承类来说,继承类中不能访问基类的私有成员。但是可以访问基类的公有成员。
4.友元函数
一些情况下类的私有成员变量不能在类外被访问,但是有些时候又需要被访问,此时就是友元函数登场。
在类内通过friend关键字声明,定义在类外部,与静态成员函数相同,没有this指针,调用不需要通过对象实例。
5.内联函数
理想的内联函数:
函数体很小;
被频繁调用;
不包含循环或复杂控制结构;
非递归函数;
6.constexpr
函数在编译期就会被计算;消除运行时计算开销;
例:
constexpr int factorial(int n) {return n <= 1 ? 1 : n * factorial(n-1);
}
7.typeid
简单用法:获取类型名称
int p;
cout << typeid(p).name() << endl;
8.值得借鉴的条款
1.调用empty()而非size() == 0
太多了,慢慢学习吧
9.模板元编程和模板编程
模板编程:主要关注运行时的泛型行为;操作值和常规类型
模板元编程:完全在编译期执行,生成运行时代码;操作类型和编译期常量
后面再看
4.8日
1.智能指针
unique_ptr 独占所有权 单一所有者,不可复制,可移动
#include <memory>// 创建 unique_ptr
std::unique_ptr<int> p1 = std::make_unique<int>(42); // C++14 推荐方式
// 转移所有权
std::unique_ptr<int> p2 = std::move(p1); // p1 变为 nullptr,资源被转移shared_ptr 共享所有权 多个所有者,通过引用计数管理生命周期
// 创建 shared_ptr
std::shared_ptr<int> p1 = std::make_shared<int>(42); // 推荐:内存分配优化
std::shared_ptr<int> p2 = p1; // 引用计数+1
weak_ptr 无所有权(观察) 解决 shared_ptr 循环引用问题,需配合 shared_ptr 使用
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;加深理解:
unique_ptr可以看作是一个类,定义一个unique_ptr可以看作是定义一个类的实例,这个类中维护了一个唯一的指针ptr,包括删除实例的析构函数
unique_ptr()复现
自己编写代码理解更加深刻
这里是引用
#include <iostream>
#include <stdexcept>
template<typename T>
class unique_ptr_ {//智能指针,维护唯一的一个动态类型指针,带有析构函数会被释放掉
private:T* ptr;//维护一个唯一的动态内存指针
public://构造函数,禁止隐式转换explicit unique_ptr_(T* ptr_ = nullptr) : ptr{ ptr_ } {};//移动构造//明确std:move()才会执行这个函数unique_ptr_(unique_ptr_&& other) noexcept : ptr{ other.ptr } {other.ptr = nullptr;}//移动赋值运算符//移动要先删除当前有的,再添加新的unique_ptr_& operator=(unique_ptr_&& other) noexcept {if (this != &other) {reset();ptr = other.ptr;other.ptr = nullptr;}return *this;}~unique_ptr_() {reset();}//维护唯一的一个指针即创建实例对象时的动态内存指针//所以不接受赋值构造和拷贝构造(不可能用两个unique_ptr维护同一个动态内存),仅允许一个显示构造unique_ptr_(unique_ptr_& other) = delete;unique_ptr_& operator=(unique_ptr_& other) = delete;/*//释放资源void reset() {if (ptr) {delete ptr;}ptr = nullptr;}//释放并更新资源void reset(T* ptr_) {reset();ptr = ptr_;}*/void reset(T* ptr_ = nullptr) {if (ptr) delete ptr;ptr = ptr_;}//放弃所有权,返回管理的原始指针,并将自身置空T* release() {T* result = ptr;ptr = nullptr;return result;}//返回管理的原始指针(不释放所有权T* get() const noexcept { return ptr; }void swap(unique_ptr_& other) noexcept {std::swap(ptr, other.ptr);}//运算符重载:解引用指针,->T& operator*() const { if (!ptr) throw std::runtime_error("Dereferencing null pointer");return *ptr;}T* operator->() const noexcept{ return ptr; }
};
shared_ptr()以及weak_ptr复现
线程安全问题未考虑;基本实现了对于shared_ptr和weak_ptr的深入理解。
1.友元模板类的写法,即在该模板类中可以访问另一个模板类的私有成员变量
#include <iostream>
#include <stdexcept>
// 模拟控制块,保存 shared 和 weak 的引用计数
struct ControlBlock {int shared_count = 1;int weak_count = 0;
};
template<typename T>
class shared_ptr_;template<typename T>
class weak_ptr_ {
private:T* ptr;ControlBlock* ctrl;
public:// 默认构造weak_ptr_() noexcept : ptr(nullptr), ctrl(nullptr) {}//从shared_ptr构造weak_ptr_(const shared_ptr_<T>& other) noexcept : ptr{ other.ptr }, ctrl{ other.ctrl } {if (ctrl) ctrl->weak_count++;}//拷贝构造weak_ptr_(const weak_ptr_& other) noexcept : ptr{ other.ptr }, ctrl{ other.ctrl } {if (ctrl) ctrl->weak_count++;}//移动构造weak_ptr_(weak_ptr_&& other) noexcept : ptr{ other.ptr }, ctrl{ other.ctrl } {other.ptr = nullptr;other.ctrl = nullptr;}// 析构函数~weak_ptr_() {reset();}weak_ptr_& operator=(const weak_ptr_& other) noexcept {if (this != &other) {reset();ptr = other.ptr;ctrl = other.ctrl;if (ctrl) ctrl->weak_count++;}return *this;}// 移动赋值weak_ptr_& operator=(weak_ptr_&& other) noexcept {if (this != &other) {reset();ptr = other.ptr;ctrl = other.ctrl;other.ptr = nullptr;other.ctrl = nullptr;}return *this;}// 重置 weak_ptrvoid reset() noexcept {if (ctrl) {ctrl->weak_count--;//shared_count == 0 即这部分内存已经释放了weak_count==0说明这是一个唯一的弱引用也要被释放//weak_count!=0说明还有的weak_ptr在监视所以不能释放。//shared_count!=0说明还在共享内存,需要去监视不能释放if (ctrl->weak_count == 0 && ctrl->shared_count == 0)delete ctrl;}ptr = nullptr;ctrl = nullptr;}// 检查对象是否已过期(即 shared_ptr 数量为 0)//使用weak_ptr_去创建shared_ptr时要用到这个判断bool expired() const noexcept { return !ctrl || ctrl->shared_count == 0; }//lock使用weak_ptr_构建shared_ptrshared_ptr_<T> lock() {if (expired()) {return shared_ptr_<T>();//空的对象}return shared_ptr_<T>(*this);}// 获取引用计数(对应 shared_ptr 的数量)int use_count() const noexcept { return ctrl ? ctrl->shared_count : 0; }friend class shared_ptr_<T>;
};
template<typename T>
class shared_ptr_ {//智能指针,共享同一个的一个动态类型指针,会进行引用计数带有析构函数会被释放掉
private:T* ptr;ControlBlock* ctrl;
public://默认构造函数,定义一个为赋值的对象shared_ptr_() noexcept : ptr{ nullptr },ctrl{ nullptr } {}//显式带参构造函数,new int可能抛出异常,不能标注noexceptexplicit shared_ptr_(T* ptr_) : ptr(ptr_), ctrl(ptr_ ? new ControlBlock() : nullptr) {}//拷贝构造shared_ptr_(const shared_ptr_& other) noexcept : ptr{ other.ptr }, ctrl{ other.ctrl } {if (ctrl) ctrl->shared_count++;}//利用weak对象构造explicit shared_ptr_(const weak_ptr_<T>& weak) {if (weak.expired()) {ptr = nullptr;ctrl = nullptr;}else {ptr = weak.ptr;ctrl = weak.ctrl;ctrl->shared_count++;}}//移动构造shared_ptr_(shared_ptr_&& other) noexcept : ptr{ other.ptr }, ctrl{ other.ctrl } {other.ptr = nullptr;other.ctrl = nullptr;}//拷贝赋值运算符shared_ptr_& operator=(const shared_ptr_& other) noexcept {if (this != &other) {reset();//释放当前的ptr = other.ptr;ctrl = other.ctrl;if (ctrl) ctrl->shared_count++;}return *this;}//移动赋值运算符shared_ptr_& operator=(shared_ptr_&& other) noexcept {//首先要删除当前的if (this != &other) {reset();//释放当前的ptr = other.ptr;ctrl = other.ctrl;other.ptr = nullptr;other.ctrl = nullptr;}return *this;}//析构函数~shared_ptr_() {reset();}//get获取原始指针T* get() const noexcept { return ptr; }//release,释放资源,shard_ptr不会返回原始指针void release() {if (ctrl) {ctrl->shared_count--;if (ctrl->shared_count == 0) {delete ptr;ptr = nullptr;if (ctrl->weak_count == 0) {delete ctrl;ctrl = nullptr;}}}}//reset重置资源,释放资源并选择性更新资源void reset(T* ptr_ = nullptr) {release();if (ptr_) {try {ctrl = new ControlBlock();ptr = ptr_;}catch (...) {delete ptr_; // 避免泄漏throw;}}}//获取引用计数size_t use_count() const noexcept {return ctrl ? ctrl->shared_count : 0;}//检查是否唯一所有者bool unique() const noexcept {return use_count() == 1;}//重载解引用运算符T& operator*() const {if (!ptr) throw std::runtime_error("Dereference of null pointer");return *ptr;}// 重载箭头运算符T* operator->() const noexcept { return ptr; }// 转换为 bool 类型判断是否非空explicit operator bool() const noexcept { return ptr != nullptr; }// 交换两个 shared_ptr 的内容void swap(shared_ptr_& other) noexcept {std::swap(ptr, other.ptr);std::swap(ctrl, other.ctrl);}// 允许 weak_ptr 访问 private 成员friend class weak_ptr_<T>;
};int main() {shared_ptr_<int> sp(new int(42));weak_ptr_<int> wp(sp);// 尝试从 weak_ptr 提升为 shared_ptrif (auto p = wp.lock()) {std::cout << "提取到的值: " << *p << std::endl; // 输出 42}sp.reset(); // 释放资源,引用计数归零,delete ptr 执行if (wp.expired()) {std::cout << "资源已释放" << std::endl;}return 0;
}
2.并发
看不懂,用了才能会
thread
创建和管理线程,执行并发任务。通过 RAII(Resource Acquisition Is Initialization) 自动管理线程生命周期。
std::thread t1(task, 1); // 创建线程,执行 task(1)
std::thread t2(task, 2); // 创建线程,执行 task(2)
**join()**:阻塞当前线程,直到目标线程完成。
**detach()**:分离线程,使其在后台独立运行。
lock_guardunique_lock
提供比 lock_guard 更灵活的锁管理:延迟加锁(Deferred locking)。
手动加锁/解锁。
条件变量支持(必须与 std::condition_variable 配合)。
所有权转移(支持移动语义)。我的理解:
thread创建线程,特性有join()(主线程回收)和detach()(独立运行,系统自动回收)
std::mutex mtx;
lock_guard()作用域内自动加锁解锁
std::mutex mtx;
{
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
unique_lock()作用域内可以手动加解锁,定义时即加锁。
std::mutex mtx;
{
std::unique_lock<std::mutex> lock(mtx); // 立即加锁
value += 1;
lock.unlock(); // 手动解锁
}
3.LRU,LFU算法(已经处理ok)
4.9日
手撕vector和string
4.11日原子操作
需要在应用中慢慢体会,不要急。
#include <atomic>
可以将普通变量用 std::atomic<T> 包装,使其所有操作
都成为原子的。
举例:均为原子操作
定义:
std::atomic<int> counter(0);
加载
int value = counter.load(std::memory_order_acquire);
存储
counter.store(42, std::memory_order_release);
修改操作:加上一个值
int old_value = counter.fetch_add(1, std::memory_order_relaxed);
将变量替换为新值,并返回旧值
int previous = counter.exchange(100, std::memory_order_acq_rel);
比较与交换(CAS(compare_and_swap))
int expected = 0;
int desired = 1;
bool success = counter.compare_exchange_strong(expected, desired, std::memory_order_acq_rel, std::memory_order_relaxed);
区别:compare_exchange_weak 在某些平台上可能会因 spurious failure(虚假失败)而返回 false,即使值相等;因此通常在循环中使用,而 compare_exchange_strong 则更坚决,但可能效率略低。
内存顺序:
看不懂,后面用了之后再说
简单示例:无锁计数器
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>std::atomic<int> counter(0);void increment(int times) {for (int i = 0; i < times; ++i) {counter.fetch_add(1, std::memory_order_relaxed);}
}int main() {const int numThreads = 4;const int incrementsPerThread = 10000;std::vector<std::thread> threads;for (int i = 0; i < numThreads; i++) {threads.emplace_back(increment, incrementsPerThread);}for (auto& t : threads) {t.join();}std::cout << "Final counter value: " << counter.load() << std::endl;return 0;
}
无锁栈的示例:
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>// 无锁栈模板
template<typename T>
class LockFreeStack {
private:// 定义栈的节点struct Node {T data;Node* next;Node(const T& value) : data(value), next(nullptr) {}};// 使用原子指针来保存栈顶std::atomic<Node*> head;public:// 构造函数,初始化空栈LockFreeStack() : head(nullptr) {}// 析构函数,释放所有节点~LockFreeStack() {Node* node = head.load(std::memory_order_relaxed);while (node) {Node* next = node->next;delete node;node = next;}}// push 操作:将元素压入栈中void push(const T& value) {Node* new_node = new Node(value);// 读取当前栈顶(不加锁),并赋值给 new_node->nextnew_node->next = head.load(std::memory_order_relaxed);// 循环进行 CAS 操作,直到成功将 head 设为 new_nodewhile (!head.compare_exchange_weak(new_node->next, // expected:当前栈顶,新节点的 next 已经设置成 head 当前值new_node, // desired:新节点std::memory_order_release,std::memory_order_relaxed)) {// 如果 CAS 失败,new_node->next 已更新为最新的 head,再重新尝试}}// pop 操作:从栈中弹出元素bool pop(T& result) {Node* old_head = head.load(std::memory_order_relaxed);// 如果栈不为空,尝试使用 CAS 将栈顶更新为 old_head->nextwhile (old_head && !head.compare_exchange_weak(old_head, // expected:当前栈顶old_head->next, // desired:将 head 设为 old_head->nextstd::memory_order_acquire,std::memory_order_relaxed)) {// CAS 失败后,old_head 会更新为最新的 head,然后再次尝试}if (!old_head) {return false; // 栈为空,无法 pop}// 获取出栈的数据,删除旧节点result = old_head->data;delete old_head;return true;}
};//
// 测试无锁栈
int main() {LockFreeStack<int> stack;const int numThreads = 4;const int pushesPerThread = 1000;// 多线程 push 操作auto pushTask = [&stack, pushesPerThread](int base) {for (int i = 0; i < pushesPerThread; ++i) {stack.push(base + i);}};std::vector<std::thread> threads;for (int i = 0; i < numThreads; ++i) {threads.push_back(std::thread(pushTask, i * pushesPerThread));}for (auto& t : threads) {t.join();}// 多线程 pop 操作int popCount = 0;int value;auto popTask = [&stack, &popCount, &value]() {while (stack.pop(value)) {++popCount;}};threads.clear();for (int i = 0; i < numThreads; ++i) {threads.push_back(std::thread(popTask));}for (auto& t : threads) {t.join();}std::cout << "Total popped elements: " << popCount << std::endl;return 0;
}