现代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移动构造函数。

posted @   Destiny233  阅读(166)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示