C++ RAII初探+析構(gòu)函數(shù)
前言
早期編寫的C++是有缺陷的,舉些例子。比如裸指針滿天飛,多線程的數(shù)據(jù)競(jìng)爭(zhēng),雙重釋放等等。但如今的C++正在努力改善這些缺陷,RAII范式的編程在C++比重逐步增加。RAII(Resource Acquisition Is Initialization)是C++之父Bjarne Stroustrup在設(shè)計(jì)C++的時(shí)候就引入了。即:資源獲取即初始化。通俗點(diǎn),在對(duì)象創(chuàng)建的時(shí)候獲取資源,在對(duì)象銷毀的時(shí)候釋放資源。確保內(nèi)存的安全性。指針shared_ptr就是其中的杰作,下面也會(huì)講到。
本篇除了RAII之外,還會(huì)分析下其析構(gòu)函數(shù)的關(guān)聯(lián)。代碼部分,經(jīng)過(guò)C++20測(cè)試,均可跑通,可直接用。
RAII操作例子
一個(gè)非常簡(jiǎn)單的RAII操作,我們初始化對(duì)象的時(shí)候打開(kāi)了文件資源。然后在離開(kāi)對(duì)象的作用域的時(shí)候,會(huì)調(diào)用析構(gòu)函數(shù)釋放(關(guān)閉)文件資源,例子如下:
//filename:RAII.c
//compile:g++ -g -static -o RAII RAII.c
#include <iostream>
#include <memory>
class File {
public:
File(const std::string& filename) {
// 在構(gòu)造函數(shù)中打開(kāi)文件
std::cout << "Opening file: " << filename << std::endl;
file_ = fopen(filename.c_str(), "r");
if (!file_) {
throw std::runtime_error("Failed to open file");
}
}
~File() {
// 在析構(gòu)函數(shù)中關(guān)閉文件
if (file_) {
std::cout << "Closing file." << std::endl;
fclose(file_);
}
}
private:
FILE* file_;
};
int main() {
try {
// 創(chuàng)建 File 對(duì)象,RAII 確保文件在生命周期結(jié)束時(shí)自動(dòng)關(guān)閉
File f("example.txt");
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
}
return 0;
}
File對(duì)象的構(gòu)造函數(shù)里面打開(kāi)文件,上面代碼運(yùn)行的結(jié)果如下:
圖片
在File對(duì)象f離開(kāi)作用域也即是try塊的結(jié)尾大括號(hào)處,會(huì)調(diào)用析構(gòu)函數(shù),關(guān)閉文件。
關(guān)于這點(diǎn)我們lldb驗(yàn)證下,且簡(jiǎn)略分析下其原理。在~File()析構(gòu)函數(shù)下斷,其堆棧是在RAII.c:36也即是try塊大括號(hào)結(jié)尾的地方調(diào)用了析構(gòu)函數(shù)。
如下:
(lldb) b ~File()
Breakpoint 2: where = RAII`File::~File() + 16 at RAII.c:17:13, address = 0x000000000040582c
(lldb) r&c
Process 4510 resuming
Opening file: example.txt
Process 4510 stopped
* thread #1, name = 'RAII', stop reason = breakpoint 2.1
frame #0: 0x000000000040582c RAII`File::~File(this=0x00007fffffffe208) at RAII.c:17:13
14
15 ~File() {
16 // 在析構(gòu)函數(shù)中關(guān)閉文件
-> 17 if (file_) {
18 std::cout << "Closing file." << std::endl;
19 fclose(file_);
20 }
(lldb) bt
* thread #1, name = 'RAII', stop reason = breakpoint 2.1
* frame #0: 0x000000000040582c RAII`File::~File(this=0x00007fffffffe208) at RAII.c:17:13
frame #1: 0x00000000004055ef RAII`main at RAII.c:36:5
frame #2: 0x00000000004b7ec8 RAII`__libc_start_call_main + 104
frame #3: 0x00000000004ba090 RAII`__libc_start_main + 624
frame #4: 0x0000000000405475 RAII`_start + 37
當(dāng)我們運(yùn)行到try塊收尾大括號(hào)處,看此時(shí)程序剛好調(diào)用了File::~File
(lldb) n
Opening file: example.txt
Process 4552 stopped
* thread #1, name = 'RAII', stop reason = step over
frame #0: 0x00000000004055e3 RAII`main at RAII.c:33:5
30 File f("example.txt");
31
32 // 文件操作...
-> 33 } catch (const std::exception& e) {
34 std::cerr << e.what() << std::endl;
35 }
36
(lldb) di -s $pc
RAII`main:
-> 0x4055e3 <+110>: lea rax, [rbp - 0x58]
0x4055e7 <+114>: mov rdi, rax
0x4055ea <+117>: call 0x40581c ; File::~File at RAII.c:18:5
0x4055ef <+122>: mov eax, 0x0
0x4055f4 <+127>: mov rdx, qword ptr [rbp - 0x18]
0x4055f8 <+131>: sub rdx, qword ptr fs:[0x28]
也即是代碼:
0x4055ea <+117>: call 0x40581c ; File::~File at RAII.c:18:5
RAII風(fēng)格指針
現(xiàn)代C++的幾個(gè)指針
- std::unique_ptr:獨(dú)占所有權(quán)的智能指針。一個(gè) unique_ptr 只能有一個(gè)指針指向資源,因此它不支持復(fù)制,只支持轉(zhuǎn)移所有權(quán)。
- std::shared_ptr:共享所有權(quán)的智能指針。多個(gè) shared_ptr 可以共享對(duì)資源的所有權(quán),只有最后一個(gè)指針被銷毀時(shí),資源才會(huì)被釋放。
- std::weak_ptr:一種不影響資源生命周期的智能指針,用來(lái)打破循環(huán)引用的問(wèn)題
我們也來(lái)觀察下RAII指針自動(dòng)調(diào)用析構(gòu)函數(shù)釋放的例子
//filename:zhizhen.c
//compile:g++ -std=c++20 -g -static -o zhizhen zhizhen.c
#include <iostream>
#include <memory>
class Resource {
public:
Resource(const std::string& name) : name_(name) {
std::cout << name_ << " acquired!" << std::endl;
}
~Resource() {
std::cout << name_ << " released!" << std::endl;
}
void use() {
std::cout << "Using " << name_ << std::endl;
}
private:
std::string name_;
};
void demonstrateWeakPtr() {
// 創(chuàng)建 shared_ptr 管理 Resource 對(duì)象
std::shared_ptr<Resource> sharedResource = std::make_shared<Resource>("Resource1");
// 創(chuàng)建 weak_ptr 觀察 shared_ptr
std::weak_ptr<Resource> weakResource = sharedResource;
// weak_ptr 不增加引用計(jì)數(shù),它只是觀察資源
std::cout << "Weak pointer created, but it does not affect resource's reference count." << std::endl;
// 使用 weak_ptr 的 lock 方法來(lái)獲取 shared_ptr
if (auto lockedResource = weakResource.lock()) {
lockedResource->use(); // 使用資源
} else {
std::cout << "Failed to lock weak pointer, resource is not available." << std::endl;
}
// 當(dāng) shared_ptr 離開(kāi)作用域時(shí),資源會(huì)被釋放
}
int main() {
demonstrateWeakPtr(); // 資源由 shared_ptr 管理,weak_ptr 只是觀察
return 0;
}
它的結(jié)果如下,同樣的析構(gòu)函數(shù)在離開(kāi)作用域釋放
圖片
結(jié)尾
RAII并不是一個(gè)新鮮的特性,而是古早就有的一種范式。上面例子展示了對(duì)象創(chuàng)建的時(shí)候獲取資源,對(duì)象銷毀的時(shí)候釋放資源的例子。
我們只需要寫好代碼的規(guī)范,其它的編譯器都給做了,比如析構(gòu)函數(shù)的調(diào)用等。這種操作,有效的防范了部分內(nèi)存泄露的可能性。