这是一系列关于C++的学习笔记。
著名的C++内存安全
2019年,微软发布了一篇著名的文章,A proactive approach to more secure code,主要结论是有大概70%的安全漏洞来自于不安全的内存。那么作为“现代”的程序员,怎样可以不容易Shoot yourself in the foot呢?同时,因为exception的设计,程序运行的path有的时候不可预测,有的时候很难管理。如果再throw exception的时候,资源没有被释放,那么就会造成资源泄漏。
RAII
两个个人理解的重点:
- 应该基于内存管理更少的自由。
- 应该基于资源的所属权做出明确的划分。
由此引出了对于有 明确生命周期(invariant)的资源的管理方式:RAII,即Resource Acquisition Is Initialization。
一个简单的例子
RAII适合管理有明确获取(acquire)和释放(release)的资源,比如文件句柄、内存、锁等。下面是一个简单的例子:
手动管理文件
#include <iostream>
#include <fstream>
int main() {
FILE* file = fopen("example.txt", "w");
some_complex_function_that_may_throw_exception();
file << "Hello, RAII!";
file.close(); // release
return 0;
}可以看到,如果在some_complex_function_that_may_throw_exception中抛出了异常,那么file.close()就不会被调用,造成资源泄漏。
使用RAII管理文件
#include <iostream>
#include <fstream>
class File {
public:
File(const std::string& filename) : file(filename) {}
~File() { file.close(); }
std::ofstream& get() { return file; }
private:
std::ofstream file;
};
int main() {
File file("example.txt");
some_complex_function_that_may_throw_exception();
file.get() << "Hello, RAII!";
return 0; // 资源会在File析构的时候被释放
}另一个lock_guard的例子
RAII与其说是一种模式,不如说是一种思想。STL中已经存在了很多RAII的实现,比如std::lock_guard,它的构造函数会lock一个mutex,析构函数会unlock这个mutex。这样就可以保证在任何情况下,mutex都会被正确释放。
#include <iostream>
#include <mutex>
std::mutex mtx;
void print_block(int n, char c) {
std::lock_guard<std::mutex> lck(mtx); // acquire lock
for (int i=0; i<n; ++i) { std::cout << c; }
std::cout << '\n';
// automatically release lock
}
int main() {
std::thread th1(print_block,50,'*');
std::thread th2(print_block,50,'$');
th1.join();
th2.join();
return 0;
}Smart Pointers
另一个C++允许你做的蠢事是使用new和delete来手动管理堆上的内存。
别这么做。
C++11引入了std::unique_ptr和std::shared_ptr,它们是RAII的实现,可以帮助你管理堆上的内存。
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> p1(new int(5));
std::cout << *p1 << std::endl;
return 0; // p1会在main函数结束的时候被释放
}