DJの小站

记录生活,发现自己

0%

Effective C++ 资源管理

本章提出的不少思想和方法,在目前的modern C++中已经得到了实践和改进,因此,曾思考过是否要写本章笔记,后来觉得,了解一下来龙去脉也挺好的,所以有了本次总结。

一、以对象管理资源

所谓资源,便是使用之后须还给系统,C++常见的资源类型有:动态申请的内存,文件描述符,互斥锁,数据库连接,网络socket连接等等,由于可能存在的各种问题,推荐以对象管理资源的形式对资源进行管理。

  1. 为何以对象管理资源

    通常情况下,我们可能会这样写一个程序:

    1
    2
    3
    4
    5
    6
    class Example{};
    void func(){
    Example *ptre = new Example;
    ......
    delete ptre;
    }

    但这样的内存管理具有一定缺陷,若是控制流由于异常,过早的return,或是被魔改而提前退出而抵达不了delete ptre语句,我们便可能将Example所保存的那部分资源泄露。

    故而,C++提出了以对象管理资源的方式,把资源放入对象中,当控制流离开func,该对象对应的析构函数便可以自动释放那些资源,这正是利用了C++析构函数自动调用机制来解决问题(即系统自动释放栈内资源)

  2. 以对象管理资源

    以对象管理资源应当遵循两个基本守则:

    • 获得资源后立刻放入对象
    • 管理对象调用析构函数时确保释放资源
  3. C++的智能指针

    遵循以对象管理资源的理念,C++提供了几个智能指针:auto_ptrshared_ptrunique_ptr以及weak_ptr

    首先须说明的是,auto_ptr由于并不符合copy语义,而更像是移动语义,所以C++1x中已被unique_ptr代替,接下来康康auto和 shared两种ptr的用法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //智能指针使用示例
    #include<memroy>

    //auto_ptr
    std::auto_ptr<int> ptr(new int(2));
    std::auto_ptr<int> ptr2 = ptr; //ptr为null,根本不符合copy语义

    //shared_ptr
    std::shared_ptr<int> ptr = std::make_shared<int>(200);
    std::cout << *ptr << std::endl; //输出200,隐式转换
    std::shared_ptr<int> ptr2 = ptr; //二者指向同一个对象,计数+1
    std::cout << ptr.use_count() << std::endl; //获得计数
    ptr2.reset(); //指向空,此时ptr计数为1
    int * t = ptr.get(); //从shared_ptr中获取资源,不推荐这么做
    //当ptr使用reset或本段代码结束时,自动调用shared_ptr的delete,结束

二、在资源管理类中当心copying行为

当一个RAII对象被复制,常见有几种可能可供选择:

  • 禁止复制【不拷贝】
  • 对底层资源实行引用计数并写好它的deleter(删除器)【浅拷贝+计数】
  • 复制底部资源【移动】
  • 转移资源拥有权【深拷贝】

其中,auto_ptr总是会销毁对象,而其余智能指针可以自定义删除器,带自定义删除器的shared_ptr写法如下:

1
2
3
4
5
6
7
8
struct FileCloser {
void operator()(FILE* fp) const {
if (fp != nullptr) {
fclose(fp);
}
}
};
std::shared_ptr<FILE, FileCloser> sptr(fopen("test_file.txt", "w"));

使用lambda表达式定义删除器:

1
2
3
4
5
std::shared_ptr<FILE> sptr(
fopen("test_file.txt", "w"), [](FILE* fp) {
std::cout << "close " << fp << std::endl;
fclose(fp);
});

三、在资源管理类中提供对原始对象的访问

因为历史原因,总会有API的参数不是智能指针,这时需要我们对于RAII管理的原始资源提供访问。

一种方式是使用get函数,这种方法较为繁琐,但更为安全,另一种方式是采用()运算符重载来提供隐式转换,但有时会造成程序员写出不安全的代码,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//C API:
FontHandle getFont();
void releaseFont(FontHandle fh);
class Font{
private:
FontHandle f;
public:
explicit Font(FontHandle fh):f(fh){}
~Font(){releaseFont(f);}
operator FontHandle() const{return f;}
}

//use
Font f1(getFont());
FontHandle f2 = f1;
//本意是Font f2 = f1,但写错成了FontHandle f2 = f1,编译器不会发出警告,然而若是f1被释放,f2则会虚吊

值得一提的是,在C++1x的智能指针中,同时提供了隐式转换和显示转换,但自己写RAII类时,一般采用显式的get方法写更佳。

四、成对地采用new,delete

1
2
3
4
int *ptr = new int(10);
delete ptr;
int *ptr = new int[100];
delete []ptr;

因为new出的空间排布很可能是这样的:

单对象: [Object]

多对象:[计数n]|[Object | Object|……]

调用单对象的new,多对象的delete [],可能会delete掉其它一些部分,造成错误;调用多对象的new T[n],单对象的delete,会导致内存没被释放完,造成内存泄漏

五、以独立语句将newed对象放入指针

1
2
3
4
void handle(std::auto_ptr<int>, int);
int nowsum();

handle(std::auto_ptr<int>(new int(10)), nowsum());

上述的handle使用起来可能会造成内存泄漏,因为编译器要调用handle函数,需要执行三个操作:

  • nowsum()
  • new int(10)
  • 调用auto_ptr构造函数

我们无法确定编译器怎样完成这一过程,可能因为O3优化,它生成了如下步骤的代码:

  1. new int(10)
  2. nowsum()
  3. 调用auto_ptr函数

这种情况下,如果nowsum阶段异常,就铁定造成资源泄露,故而需要以独立语句将newed对象放入智能指针

1
2
std::auto_ptr<int> ptr(new int(10));
handle(ptr, nowsum());