现代C++编程实战(1)
堆、栈和RAII
这一讲主要说的是内存管理。C++中主要的内存管理有
- 栈上内存交给编译器管理
- 堆上内存需要用户自己管理
栈上内存分配很简单,移动几个指针就行了。
RAII
以下几种情况,就不能使用栈上的空间的了,必须用户自己管理内存。
- 对象很大
- 对象在编译时候不确定
- 对象的生命周期超过了函数
RAII的思想是,构建一个管理这种资源的类,在这个类的构造函数中申请资源,而在析构函数中释放资源。
实现智能指针
- 构造与析构
- 拷贝与赋值
- 模板化
- 子类指针向基类指针转化,写一个模板构造函数就好了
UniquePtr
独占指针,合理运用移动语义
#include <algorithm>
template<class T>
class UniquePtr {
public:
UniquePtr(T* p) : ptr_(p) {}
// 子类指针向基类指针转换
template<class U>
UniquePtr(UniquePtr<U>&& other) { // 参数是个右值,但不是移动构造
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
~UniquePtr() {
delete ptr_;
}
// 处理拷贝与赋值
// 在UniquePtr中不允许拷贝,只需允许移动
// 定义了移动以后,默认拷贝是禁止的
UniquePtr(UniquePtr&& other) { // 由于移动需要修改other,让其指向null,所以不能加const
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
// 这里不加引用是让编译器自己决定参数rhs是用移动还是拷贝
// 但是拷贝是禁止的,所以只能是移动了
UniquePtr& operator=(UniquePtr rhs) {
std::swap(this, &rhs); // 也可以处理自我赋值
return *this;
}
// 智能指针通用的
T* operator->() { return ptr_; }
T& operator*() { return *ptr_; }
operator bool() { return ptr_ != nullptr; }
private:
T* ptr_;
};
SharedPtr
共享指针的核心在于引用计数
引用计数
template <class T>
class SharedPtr
{
public:
explicit SharedPtr(T* ptr) : ptr_(ptr) {};
~SharedPtr() {
(*count_)--;
if (count_ == 0) {
delete ptr_;
}
}
// 拷贝构造和赋值
SharedPtr(const SharedPtr& rhs) {
ptr_ = rhs.ptr_;
if (ptr_) {
rhs->count_++;
count_ = rhs->count_;
}
}
// 赋值必须要处理自我赋值
SharedPtr& operator=(const SharedPtr& rhs) {
SharedPtr(rhs).swap(this); // 构造临时对象是为了防止swap抛出异常破坏了this内容
return *this;
}
// 子类向父类转换
template<class U> // 因为SharedPtr<T> 不能访问 SharedPtr<U>的私有成员
friend class SharedPtr;
template<class U>
SharedPtr(const SharedPtr<U> &other) {
ptr_ = other.ptr_;
if (ptr_) {
other.count_++;
count_ = other.count_;
}
}
void swap(const SharedPtr& rhs) {
std::swap(count_, rhs.count_);
std::swap(rhs.ptr_, ptr_);
}
T* operator->() { return ptr_; }
T& operator*() { return *ptr_; }
operator bool() const { return ptr_; }
private:
T* ptr_;
int* count_;
};
右值和移动
广义左值,左值,右值,将亡值
左值
- 变量名,函数名
- 返回左值引用的表达式,++x,x=1,cout << 1;
- 字符串字面量
纯右值
- 返回非引用类型的表达式
- 除了字符串以外的字面值,45,true这种
将亡值
- std::move()
生命周期延长规则
对于纯右值
实现移动
- 实现拷贝构造和移动构造
- 实现swap成员函数
- 有一个全局的swap函数
- 实现通用的赋值运算符(交给编译器决定调用哪个)
- 如果不抛异常,应该声明为noexcept
返回值优化
返回右值的时候,编译器会自动将构造在调用栈内存中,而不进行移动。
但是如果用了std::move就会禁止返回值优化
如果有分支也会禁止
完美转发
T&&有时候并不一定代表了右值引用:在模板推导的时候
- 如果参数是个左值引用,则T&&推导得到左值引用 T&
- 如果T是一个实际类型,则T&&是右值引用
问题在于:右值引用在传入函数后,会坍缩成左值引用,失去了原本右值的属性。这时候我们仍希望告知编译器,这是个右值。
可以用std::forward,强制转化成右值引用。
其实可以大概推出来,forward和move底层都是类型转换
容器汇编
这一节内容很简单,就是介绍STL中的容器。之前的STL源码剖析弄过了,这里就不细说了。
有个很关键的问题是:STL大多数情况只会调用noexcept移动构造函数。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通