[笔记]Effective C++改善程序与设计的55个具体做法_第二章 析构_构造_赋值运算

条款 05: 了解 C++ 默默编写并调用哪些函数

在 C++ 中,即使定义一个看似空无一物的类,编译器也会在需要时隐式生成一系列特殊成员函数。这些函数如同幕后工作者,支撑着对象的基础操作。

空类的默认函数生成机制

当定义class Empty {};这样的空类时,编译器会隐式声明以下特殊成员函数,且仅在被调用时才生成实际实现:

class Empty {
    public:
        Empty() {} // 默认构造函数 
        Empty(const Empty& rhs) {} // 拷贝构造函数 
        ~Empty() {} // 析构函数 
        Empty& operator=(const Empty& rhs) {} // 拷贝赋值操作符 
};

调用示例

Empty e1;         // 触发默认构造函数调用 
Empty e2(e1);     // 触发拷贝构造函数调用 
e2 = e1;          // 触发拷贝赋值操作符调用 

C++11 后的特殊成员函数扩展

随着 C++ 标准的演进,特殊成员函数的生成规则也更加丰富和细化。

  1. 移动构造与移动赋值(C++11+)

如果类未定义拷贝 / 移动操作或析构函数,编译器会生成默认的移动操作,用于高效转移资源所有权:

class MoveExample { 
    int* data; 
public: 
    MoveExample(MoveExample&&) = default;    // 移动构造 
    MoveExample& operator=(MoveExample&&) = default;  // 移动赋值 
};
  1. 默认构造函数的限制

若类包含未初始化的const成员或引用成员,编译器将不会生成默认构造函数,此时必须手动定义构造函数,否则会导致编译错误:

class RefConstClass { 
    const int c; 
    int& ref; 
};  // 需手动定义构造函数,否则编译错误 
  1. 拷贝操作的限制

对于内含const成员或引用成员的类,编译器不会自动生成拷贝构造和拷贝赋值函数,开发者必须手动定义,以满足特殊成员的初始化和赋值需求:

class SpecialMem { 
    const int id; 
    int& ref; 
public: 
    SpecialMem(int i, int& r) : id(i), ref(r) {} 
    SpecialMem(const SpecialMem& o) : id(o.id), ref(o.ref) {}  // 手动定义拷贝构造 
    SpecialMem& operator=(const SpecialMem& o) { 
        if (this != &o) { /* 处理非自我赋值 */ } 
        return *this; 
    } 
};

条款 06: 若不想使用编译器自动生成的函数,就该明确拒绝

在某些场景下,我们需要禁止编译器自动生成特定的成员函数,以满足程序设计的特殊要求。

传统解决方案(C++98)

在 C++98 时代,通过继承Uncopyable基类来禁止类的拷贝操作是常见做法:

class Uncopyable { 
protected: 
    Uncopyable() {} // 允许派生类构造 
    ~Uncopyable() {} 
private: 
    Uncopyable(const Uncopyable&);       // 声明为私有且不实现 
    Uncopyable& operator=(const Uncopyable&); 
}; 
class HomeForSale : private Uncopyable { 
    // 无需声明拷贝相关函数,继承后自动禁用 
};

禁止拷贝场景

HomeForSale h1, h2; 
HomeForSale h3(h1);  // 编译错误:无法访问私有拷贝构造 
h1 = h2;             // 编译错误:无法访问私有拷贝赋值 

C++11+ 改进方案(=delete 语法)

C++11 引入了更简洁直观的=delete语法,可直接在类内禁用默认函数:

class HomeForSale { 
public: 
    HomeForSale() = default; 
    HomeForSale(const HomeForSale&) = delete;    // 禁用拷贝构造 
    HomeForSale& operator=(const HomeForSale&) = delete;  // 禁用拷贝赋值 
    ~HomeForSale() = default; 
};

扩展:禁用移动操作

若需要同时禁用移动构造和移动赋值,也可使用=delete实现:

class NonMovable { 
public: 
    NonMovable(const NonMovable&) = delete; 
    NonMovable& operator=(const NonMovable&) = delete; 
    NonMovable(NonMovable&&) = delete;         // 禁用移动构造 
    NonMovable& operator=(NonMovable&&) = delete; // 禁用移动赋值 
};

条款 07: 为多态基类声明 virtual 析构函数

在涉及多态的继承体系中,析构函数的设计关乎资源的正确释放。

析构函数与多态资源释放

错误示例(内存泄漏)

当基类析构函数不是虚函数时,通过基类指针删除派生类对象会导致资源泄漏:

class TimeKeeper { 
public: 
    TimeKeeper() {} 
    ~TimeKeeper() {}  // 非虚析构函数 
}; 
class AtomicClock : public TimeKeeper { 
    int* data = new int(0); 
public: 
    ~AtomicClock() { delete data; }  // 派生类析构 
}; 
TimeKeeper* ptk = new AtomicClock(); 
delete ptk;  // 仅调用TimeKeeper::~TimeKeeper(),AtomicClock资源泄漏! 

正确实现(虚析构函数)

为基类声明虚析构函数,确保通过基类指针删除派生类对象时,能正确调用派生类析构函数:

class TimeKeeper { 
public: 
    virtual ~TimeKeeper() {}  // 虚析构函数 
}; 
delete ptk;  // 先调用AtomicClock::~AtomicClock(),再调用基类析构,资源正确释放 

标准库陷阱与 C++11 规范

继承非虚析构函数的标准库类(如std::string)

由于std::string的析构函数不是虚函数,继承它并添加额外资源管理时会引发未定义行为:

class SpecialString : public std::string { 
    char* extraData = new char[10]; 
public: 
    ~SpecialString() { delete[] extraData; }  // 派生类析构 
}; 
std::string* ps = new SpecialString("test"); 
delete ps;  // 未定义行为:仅调用std::string::~string(),extraData泄漏! 

C++11 纯虚析构函数规范

在 C++11 中,若基类定义了纯虚析构函数,必须提供实现:

class AbstractShape { 
public: 
    virtual ~AbstractShape() = 0;  // 纯虚析构函数需声明 
}; 
AbstractShape::~AbstractShape() {}  // 必须提供实现 

条款 08: 别让异常逃离析构函数

析构函数中的异常处理不当,会导致程序出现严重问题。

异常逃离的风险与解决方案

风险示例

当析构函数抛出异常时,可能导致程序调用std::terminate而异常终止:

class DBConnection { 
public: 
    void close() { throw std::exception(); }  // 可能抛异常 
    ~DBConnection() { close(); }  // 析构函数抛异常,导致程序终止 
}; 
void process() { 
    DBConnection db;  // 析构时close抛异常,程序调用std::terminate() 
}

C++11 改进方案(noexcept修饰符)

C++11 引入的noexcept修饰符可明确声明析构函数不会抛出异常,提升程序稳定性:

class DBConn { 
private: 
    DBConnection db; 
    bool closed = false; 
public: 
    ~DBConn() noexcept {  // 声明析构不抛异常 
        if (!closed) { 
            try { 
                db.close(); 
                closed = true; 
            } catch (...) { 
                std::cerr << "Error closing DB" << std::endl; 
                std::abort();  // 安全终止程序 
            } 
        } 
    } 
    void close() { db.close(); closed = true; }  // 显式关闭接口 
};

条款 09: 绝不在构造和析构过程中调用 virtual 函数

在对象的构造和析构阶段调用虚函数,会导致意想不到的结果。

构造 / 析构期间的多态失效

示例:构造函数中调用虚函数

在基类构造函数中调用虚函数,实际调用的是基类版本,而非派生类重写后的版本:

class Base { 
public: 
    Base() { virtualFunc(); }  // 调用Base::virtualFunc() 
    virtual void virtualFunc() { std::cout << "Base\n"; } 
}; 
class Derived : public Base { 
public: 
    Derived() : Base() {} 
    void virtualFunc() override { std::cout << "Derived\n"; } 
}; 
Derived d;  // 输出"Base",而非"Derived" 

替代方案:使用非虚初始化函数

为避免问题,可通过非虚函数进行初始化和清理操作:

class Base { 
public: 
    Base() { init(); }  // 调用非虚函数 
    void init() { /* 初始化逻辑 */ } 
}; 

条款 10: 令 operator = 返回一个 reference to *this

为operator=返回对象引用,可支持链式赋值操作。

链式赋值的规范实现

class Widget { 
public: 
    Widget& operator=(const Widget& rhs) { 
        // 赋值逻辑 
        return *this; 
    } 
}; 
Widget w1, w2, w3; 
w1 = w2 = w3;  // 等价于w1.operator=(w2.operator=(w3)) 

条款 11: 在 operator = 中处理 “自我赋值”

处理operator=中的自我赋值情况,可确保赋值操作的安全性。

证同测试与 Copy-and-Swap 技术

传统证同测试

通过检查是否为自我赋值,避免不必要的资源释放和复制:

Widget& operator=(const Widget& rhs) { 
    if (this == &rhs) return *this;  // 自我赋值检测 
    // 安全的资源复制逻辑 
    return *this; 
}

C++11 推荐:Copy-and-Swap

利用值传递自动生成副本,并通过交换实现安全赋值:

Widget& operator=(Widget rhs) {  // 值传递自动生成副本 
    swap(*this, rhs);  // 交换资源 
    return *this; 
}

条款 12: 复制对象时勿忘其每一个成分

在实现拷贝函数时,需确保基类和派生类的所有成员都被正确复制。

派生类拷贝的常见错误与修正

错误示例(遗漏基类拷贝)

派生类拷贝构造函数若未调用基类拷贝构造函数,会导致基类部分未被正确复制:

class Base { 
    int value; 
public: 
    Base(int v) : value(v) {} 
    Base(const Base& b) : value(b.value) {} 
}; 
class Derived : public Base { 
    double data; 
public: 
    Derived(int v, double d) : Base(v), data(d) {} 
    Derived(const Derived& d) : data(d.data) {}  // 错误:未调用Base拷贝构造 
};

正确实现

显式调用基类拷贝构造函数,确保基类成员也被正确复制:

Derived(const Derived& d) : Base(d), data(d.data) {}  // 显式调用基类拷贝构造 

C++11 统一初始化语法

C++11 引入的统一初始化语法,可使成员初始化更简洁、安全:

Derived(int v, double d) : Base(v), data{d} {}  // 使用花括号初始化 

C++11+ 关键特性总结

特性 相关条款 说明
=default 5,6 显式要求编译器生成默认函数,如 MoveExample() = default;
=delete 6 显式禁用函数,如 HomeForSale(const&) = delete;
移动语义 5,11 优化资源转移,如 MoveExample(MoveExample&&) noexcept;
noexcept 8 声明函数不抛异常,如 ~DBConn() noexcept;
override 7,9 确保虚函数正确重写,如 void f() override;
暂无评论

发送评论 编辑评论


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