[笔记]Effective C++改善程序与设计的55个具体做法_第三章 资源管理

常见的资源类型

内存、文件描述器(file descriptors)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接、网络socks。

条款13: 以对象管理资源

普通示例:

class Investment {...}; // "投资类型"继承体系中的root class

// 投资类的具体创建,指向Investment继承体系内的动态分配对象。调用者有责任释放
Investment * createInvestment();

void f()
{
    Investment* pInv = createInvestment(); // 调用factory函数
    ...
    delete pInv; // 释放pInv所指对象
}

使用智能指针方式自动析构:

void f()
{
    std::auto_ptr<Investment> pInv(createInvestment());
    ...
    // 经由auto_ptr的析构函数自动删除pInv
}

注:现代C++中已将auto_ptr废弃,建议使用更安全的unique_ptr等智能指针。

展示的两个关键想法:

  • 获得资源后立刻放进管理对象(managing object)内:上面示例放入auto_ptr作为初值, “以对象管理资源”就是通常所说的RAII(Resource Acquisition Is Initialization; 资源取得时机便是初始化时机)。
  • 管理对象(managing object)利用析构函数确保资源被释放:使用析构释放简化流程。

注意点: 不要让多个auto_ptr指向同一个对象,会导致未定义的多次销毁。这一点由auto_ptr的性质:若通过copy构造函数或者copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权。

std::auto_ptr<Investment> pInv1(createInvestment());

sd::auto_ptr<Investment> pInv2(pInv1); // pInv2指向对象, pInv1被设置为null

pInv1 = pInv2; // pInv1指向对象,pInv2为null

使用RCSP(reference-counting smart pointer; RCSP)的版本:

void f()
{
    ...
    std::shared_ptr<Investment> pInv(createInvestment());
    ...
    // 释放经由shared_ptr析构函数自动删除pInv
}

指针复制:

void f()
{
    ...
    std::shared_ptr<Investment> pInv1(createInvestment());
    std::shared_ptr<Investment> pInv2(pInv1); // pInv1和pInv2指向同一个对象
    pInv1 = pInv2; // 同上
    ...
}

注意: 不论auto_ptr和shared_ptr两者都在其析构函数内做delete而不是delete[]动作,不要在动态分配的array身上使用。编译上可以通过!

std::auto_ptr<std::string> aps(new std::string[10]);
std::shared_ptr<std::string> spi(new std::int[1024]);

现代指针管理资源补充说明:

  • 现代智能指针对比:
// C++11+ 推荐使用 unique_ptr(独占所有权)
std::unique_ptr<Investment> pInv(createInvestment());  // 无法复制,但可移动

// C++11+ 推荐使用 shared_ptr(共享所有权)
std::shared_ptr<Investment> pInv(createInvestment());  // 支持复制,引用计数管理

// C++14+ 推荐使用 make_unique(更安全的创建方式)
auto pInv = std::make_unique<Investment>(args...);

// C++11+ 推荐使用 make_shared(更高效的创建方式)
auto pInv = std::make_shared<Investment>(args...);
  • 禁止使用裸指针初始化多个智能指针:
Investment* ptr = new Investment;
std::shared_ptr<Investment> p1(ptr);  // 正确
std::shared_ptr<Investment> p2(ptr);  // 错误!多个独立的shared_ptr管理同一资源,导致重复释放

条款14: 在资源管理类中小心copying行为

针对某些非heap-based的资源,需要自己按照RAII(Resource Acquisition Is Initialization, 资源取得时机便是初始化时机“)自己定义管理类。RAII类型的资源管理保证资源在构造期间获得,在析构期间释放。

例如mutex。

但是在使用时,如果资源管理类进行copy操作,其结果不一符合预期,此时需要小心处理。采用的策略通常有两种:禁止和引用计数。

  • 禁止
class Lock : private Uncopyable { // 条款6

};
  • 引用计数
class Lock {
    public:
        explicit Lock(Mutex *pm) : mutexPtr(pm, unlock) // 这里通过指定unlock为删除器
        {
            lock(mutexPtr.get());
        }
    private:
        std::shared_ptr<Mutex> mutexPtr;
};

补充说明:

  • C++11+的禁止拷贝方法
class Lock {
public:
    Lock(const Lock&) = delete;             // 禁用拷贝构造
    Lock& operator=(const Lock&) = delete;  // 禁用赋值运算符
    ...
};
  • 移动语义支持
class Lock {
public:
    Lock(Lock&& other) noexcept { ... }     // 移动构造函数
    Lock& operator=(Lock&& other) noexcept { ... }  // 移动赋值运算符
};

条款 15:在资源管理类中提供对原始资源的访问

场景:

std::shared_ptr<Investment> pInv(createInvestment());

int daysHeld(const Investment *pi); // 返回投资天数

int days = daysHeld(pInv); // 编译报错

// 正确方式
int days = daysHeld(pInv.get());

智能指针重载了->和*操作符。

class Investment {
    public:
        bool isTaxFree() const;
        ...
};

Investment * createInvestment();
std::shared_ptr<Investment> pi1(createInvestment());

bool taxable1 = !(pi1->isTaxFree()); // 经由operator->访问资源
...

std::shared_ptr<Investment> pi2(createInvestment());
bool taxable2 = !((*pi2).isTaxFree()); // 经由operator*访问资源

使用隐式转换使调用更自然,不需要使用get()获取原始指针,但是会有副作用,需要再使用时注意。

class Font {
    public:
        ...
        operator FontHandle() const { return f; } // 隐式转换函数
};

FontHandle getFont(); 
void changeFontSize(FontHandle f, int newSize);

Font f(getFont());

int newFontSize;
...
changeFontSize(f, newFontSize); // 使用隐式转换方式
...
FontHandle f2 = f1; // 错误,原意是想拷贝一个Font对象,却反而将f1隐式转换为其底部的FontHandle然后才复制它。 如果f1被销毁,那么f2将变为虚吊的

条款 16:成对使用 new 和 delete 时要采取相同形式

错误示例:

std::string *stringArray = new std::string[100];
...
delete stringArray;

正确示例应为:

std::string *stringArray = new std::string[100];
...
delete [] stringArray;

同样的,如果你使用new创建一个非数组的指针,那么不应当使用delete []方式删除。

需要明白new: 是先创建内存,然后调用构造函数,delete: 是先调用析构函数,然后再调用释放内存。数组和非数组的场景下申请和释放是存在次数差异的。

补充:

数组的智能指针管理:

// C++11+ 使用 unique_ptr 管理数组
std::unique_ptr<int[]> arr(new int[10]);  // 自动使用 delete[]

// C++17+ 推荐使用 std::array 或 std::vector 替代动态数组
std::array<int, 10> arr;                  // 栈上分配,无需手动释放
std::vector<int> vec(10);                 // 动态大小,自动管理内存

条款 17:以独立语句将 newed 对象置入智能指针

可能的因时序导致问题的代码:

int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);

// 可能产生异常的调用
void processWidget(new Widget, priority()); //假设构造函数是一个explicit的函数,此时无法隐式转换

void processWidget(std::shared_ptr<Widget>(new Widget), priority()); // 参数执行顺序在C++不确定

对第二种情况主要的问题点:

  • 执行new Widget
  • 执行priority
  • 执行shared_ptr构造函数

如果执行priority函数抛出异常,那么shared_ptr将得不到执行,从而无法管理申请到的new Widget原始指针。

所以为了避免上面的情况,建议使用下面的方式进行处理:

std::shared_ptr<Widget> pw(new Widget); // 在单独语句内执行 
processWidget(pw, priority());

补充:

C++17 规定函数参数按从左到右的顺序求值,因此以下代码在 C++17+ 中是安全的:

processWidget(std::shared_ptr<Widget>(new Widget), priority());  // C++17+ 安全

但为了兼容性,仍建议使用独立语句初始化智能指针。

也可以使用工厂函数消除手动new:

// 提供工厂函数创建对象并返回智能指针
auto createWidget() {
    return std::make_unique<Widget>();
}

// 安全调用
processWidget(createWidget(), priority());
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇