RAII and Scope in C++

1. RAII

1.1 Resources

在C++中,资源是程序管理的各类资源,比如内存、POSIX文件、可合并的进程和互斥锁等。我们能够通过某些表达(系统调用、库函数等)来获取(require)资源,同样,我们也能通过某些表达来释放(dispose)资源

资源的正确管理十分重要,错误的资源的管理会导致资源的泄漏,比如内存的泄漏(可能导致系统崩溃)、文件句柄的泄漏(可能导致无法创建文件)、持续上锁的互斥锁(可能导致死锁)等。除此,还可能造成资源的双重释放、释放后使用等问题。

1.2 C++ Lifetime of Class Objects

C++中,对象的生命周期是可以被定义的,对象生命的开始与结束都相应的会对应一个事件。这些事件背后的代码会在对象生命的开始与结束自动的执行对象初始化和对象消逝的代码。这些代码也被称作构造函数(constructors)析构函数(destructors)

当我们需要创建对象时,我们通过构造函数初始化对象。而当对象需要销毁时我们则需要考虑额外的因素。对于局部变量对象而言,当期超出作用域时,就会自动地调用析构函数。而对于哪些动态分配内存的对象,使用deletedelete[]显式地释放内存时,析构函数才会被调用。

1.3 Ownership

The RAII class is said to "own" the resource. It is responsible for cleaning up that resource at the appropriate time.

1.4 The Rule of Five

Resource acquisition is initializationRAII),或称为 Constructor Acquires, Destructor Releases (CADRe),即构造函数获取资源,析构函数释放资源。这是最初用于C++的编程惯用法,旨在通过对象的生命周期来管理资源的获取和释放。

通过RAII,我们可以显式地定义相应的构造函数和析构函数,在对象生命周期的开始通过构造函数自动初始化并获取资源,并在对象超出作用域或删除对象时自动调用析构函数来释放资源,避免资源泄漏。需要显式定义的函数包括析构函数拷贝构造函数拷贝赋值运算符移动构造函数移动赋值运算符,这被称为"The Rule of Five"。

标准库中大量使用RAII和The Rule of Five,从而达到自动释放资源的目的。通过使用标准库和RAII,我们能够尽量避免手动管理资源,从而简化代码,提高安全性和可维护性。

2. Example Using RAII

2.1 Build a RAII Class

局部对象就是在栈上创建的对象。当你在栈上创建对象时,它会自动调用构造函数。这是因为栈上的对象具有自动存储期(automatic storage duration),它们的生命周期由作用域(scope) 控制。当执行到 '}' 时,就会自动调用析构函数,随之栈帧销毁、函数返回。

#include <iostream>

class scope
{
public:
	scope(){
		std::cout << "scope constructor" << std::endl;
	}
	~scope(){
		std::cout << "scope deconstructor" << std::endl;
	}
};
struct test
{
	test(){
		std::cout << "test constructor" << std::endl;
	}
	~test(){
		std::cout << "test deconstructor" << std::endl;
	}
};

int main(){

	scope s;
	test t;
	return 0;
} // <- End of scope
du@DVM:~/Desktop/Cpp$ ./scope 
scope constructor
test constructor
test deconstructor
scope deconstructor

2.2 RAII of Objects on the Heap

由于堆上对象的生命周期通常比栈上对象的生命周期更长,因此堆上对象的构造和析构通常与 new 和 delete 关键字的使用相关联。当你使用 new 关键字创建一个新的对象时,会调用该对象的构造函数;而当你使用 delete 关键字销毁对象时,会自动调用该对象的析构函数。

3. RAII Provided by Operatornew and delete

mallocfree 函数改变为 newdelete 关键字的使用是从C语言过渡到C++内存管理方式的改进。虽然 newdelete 关键字在底层还是会调用 mallocfree,但是使用 newdelete 会调用对象的构造函数和析构函数,这是 mallocfree 所不具有的。

也就是说,new和delete提供RAII这种机制。这也是 newdeletemallocfree 的最主要的差别。以下展示 newdelete的底层实现:

#include <iostream>
#include <cstdlib> // for malloc and free

void* operator new(size_t size) {
    void* ptr = std::malloc(size);
    if (!ptr) {
        throw std::bad_alloc();
    }
    return ptr;
}

void operator delete(void* ptr) noexcept {
    std::free(ptr);
}

class Entity {
public:
    Entity() {
        std::cout << "Entity created" << std::endl;
    }
    ~Entity() {
        std::cout << "Entity destroyed" << std::endl;
    }
};

int main() {
    Entity* e = new Entity();
    delete e;
    return 0;
}

4. What RAII can DO??

  1. 智能指针:C++11 引入了 std::unique_ptr 和 std::shared_ptr 智能指针。它们在构造时获取动态内存,在析构时自动释放内存。
  2. 互斥锁:在多线程编程中,std::lock_guard 和 std::unique_lock 利用 RAII 管理互斥锁的获取和释放。
  3. 文件操作:C++ 标准库中的 std::ifstream 和 std::ofstream 也使用 RAII 来管理文件的打开和关闭。

4.1 A Smart Pointer Example

当我们想要申请堆内存资源时,我们会用 newdelete 关键字来申请和释放我们的内存资源。这两者总是成对出现,即当我们使用 new ,就不要忘记使用 delete。但是智能指针为我们提供了省去使用 delete 的便利。

智能指针有三种:

  1. Unique pointers:是最简单的智能指针。unique_ptr可以看作一种作用域指针(scoped pointer),在超出作用域时会自动销毁所管理的对象。(RAII)
  2. Shared pointer:引用计数,多个shared_ptr可以共享同一个对象。当引用数为0,自动释放堆内存资源。(RAII)
  3. Weak pointer:不增加引用计数,解决shared_ptr之间循环引用的问题。辅助 shared_ptr 防止循环计数导致的内存资源泄漏。

智能指针实际上就是对裸指针的封装,实际上两个关键字仍然成对出现。其中规定 unique_ptr 所指向的堆对象只能有一个引用,不可以拷贝,也就是为什么叫做 unique pointer。例如:

std::unique_ptr<Entity> entity0 = std::make_unique<Entity>();
auto entity1 = entity0; // error

unique_ptr 还提供异常安全性(exception safety)。也就是说它能够在异常发生时确保资源的正确释放,避免资源泄漏。具体来说,unique_ptr 通过 RAII机制管理动态分配的内存,确保在对象生命周期结束时自动释放内存。

#include <iostream>
#include <memory>

class Entity
{
public:
	Entity(){
		std::cout << "Entity created" << std::endl;
	}
	~Entity(){
		std::cout << "Entity destroyed" << std::endl;
	}
	
};

template<class T>
class scopedPointer
{
private:
	T* m_ptr;
public:
	scopedPointer(const T* other_ptr) = delete;
	scopedPointer(T* other_ptr) = delete;
	scopedPointer& operator=(const scopedPointer& other) = delete;
	scopedPointer(T* ptr)
		: m_ptr(ptr)
	{
	}
	~scopedPointer(){
		delete m_ptr;
	}
    scopedPointer& operator=(scopedPointer&& other) noexcept {
        if (this != &other) {
            delete m_ptr;
            m_ptr = other.m_ptr;
            other.m_ptr = nullptr;
        }
        return *this;
	}
};
int main(){
	{
		scopedPointer* s_ptr = new Entity();

	}//<--s_ptr销毁的时刻
	{
		std::unique_ptr<Entity> entity(new Enity()); 
		std::unique_ptr<Entity> entity = std::make_unique<Entity>();
	}
	std::cin.get();
	return 0;
}

Copyable? NO! Movable? YES! 由于unique_ptr可移动不可拷贝,所以我们应当删去拷贝构造函数和对拷贝赋值运算符的重载。