Effective C++ 笔记(2)

发布于 2024-01-08 07:11:51 字数 6766 浏览 30 评论 0

6. 若不想使用编译器自动生成的函数 就该明确拒绝

Explicitly disallow the use of compiler-generated functions you do not want

class Uncopyable {
 protected:
  Uncopyable() {}  // 允许 derived 对象构造和析构
  ~Uncopyable() {}

 private:
  Uncopyable(const Uncopyable&);  // 但阻止 copying
  Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale : private Uncopyable {
  // class 不再声明 copy 构造函数或 copy assign 操作符
};

int test_item_6() {
  HomeForSale h1;
  HomeForSale h2;
  // HomeForSale h3(h1); // 不会通过编译
  // h1 = h2; // 也不该通过编译

  return 0;
}

请记住:为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为 private 并且不予实现。使用像 Uncopyable 这样的 base class 也是一种做法

7. 为多态基类声明 virtual 析构函数

Declare destructors virtual in polymorphic base classes

class AWOV {  // AWOV = "Abstract w/o Virtuals"
 public:
  virtual ~AWOV() = 0;  // 声明 pure virtual 析构函数
};

AWOV::~AWOV() {}  // 必须为这个 pure virtual 析构函数提供一份定义

任何 class 只要带有 virtual 函数都几乎确定应该也有一个 virtual 析构函数。如果 class 不含 virtual 函数,通常表示它并不意图被用做一个 base class。当 class 不企图被当作 base class,令其析构函数为 virtual 往往是个馊主意。无端地将所有 classes 的析构函数声明为 virtual,就像从未声明它们为 virtual 一样,都是错误的。

vptr(virtual table pointer)指向一个由函数指针构成的数组,称为 vtbl(virtual table)。每一个带有 virtual 函数的 class 都有一个相应的 vtbl。当对象调用某一 virtual 函数,实际被调用的函数取决于该对象的 vptr 所指的那个 vtbl—-编译器在其中寻找适当的函数指针。

析构函数的运作方式是,最深层派生(most derived)的那个 class 其析构函数最先被调用,然后是其每一个 base class 的析构函数被调用。编译器会在 AWOV 的 derived classes 的析构函数中创建一个对~AWOV 的调用动作,所以你必须为这个函数提供一份定义。

“给 base classes 一个 virtual 析构函数”,这个规则只适用于 polymorphic(带多态性质的)base classes 身上。这种 base classes 的设计目的是为了用来“通过 base class 接口处理 derived class 对象”。并非所有 base classes 的设计目的都是为了多态用途。例如标准 string 和 STL 容器都不被设计作为 base classes 使用,更别提多态了。

请记住:

  • polymorphic(带多态性质的)base classes 应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。
  • Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性(polymorphically),就不该声明 virtual 析构函数。

8. 别让异常逃离析构函数

Prevent exceptions from leaving destructors

C++并不禁止析构函数吐出异常,但它不鼓励你这样做。

请记住:

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。

9. 绝不在构造和析构过程中调用 virtual 函数

Never call virtual functions during construction or destruction

derived class 对象内的 base class 成分会在 derived class 自身成分被构造之前先构造妥当。由于 base class 构造函数的执行更早于 derived class 构造函数,当 base class 构造函数执行时 derived class 的成员变量尚未初始化。确定你的构造函数和析构函数都没有(在对象被创建和被销毁期间)调用 virtual 函数,而它们调用的所有函数也都服从同一约束。

请记住:在构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至 derived class(比起当前执行构造函数和析构函数的那层)。

10. 令 operator=返回一个 reference to *this

Have assignment operators return a reference to *this

class Widget {
 public:
  Widget &operator=(const Widget &rhs) {  // 返回类型是个 reference,指向当前对象
    return *this;
  }

  Widget &operator+=(const Widget &rhs) {  // 这个协议适用于+=、-=、*=等等
    return *this;
  }

  Widget &operator=(
      int rhs) {  // 此函数也适用,即使此一操作符的参数类型不符协定
    return *this;
  }
};

请记住:令赋值(assignment)操作符返回一个 reference to *this

11. 在 operator=中处理 自我赋值

Handle assignment to self in operator=

class Widget11 {
 public:
  void swap(Widget11 &rhs) {}  // 交换*this 和 rhs 的数据

  Widget11 &operator=(const Widget11 &rhs) {
    Widget11 temp(rhs);  // 为 rhs 数据制作一份复件(副本)
    swap(temp);          // 将*this 数据和上述复件的数据交换
    return *this;
  }
};

int test_item_11() {
  Widget11 w;
  w = w;  // 赋值给自己

  return 0;
}

在 operator=函数内手工排列语句(确保代码不但”异常安全”而且”自我赋值安全”)的一个替代方案是,使用所谓的 copy and swap 技术。

请记住:

  • 确保当对象自我赋值时 operator=有良好行为。其中技术包括比较”来源对象”和”目标对象”的地址、精心周到的语句顺序、以及 copy-and-swap。
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

12. 复制对象时勿忘其每一个成分

Copy all parts of an object

void logCall(const std::string &funcName) {}  // 制造一个 log entry

class Customer {
 public:
  Customer(const Customer &rhs)
      : name(rhs.name)  // 复制 rhs 的数据
  {
    logCall("Customer copy constructor");
  }

  Customer &operator=(const Customer &rhs) {
    logCall("Customer copy assignment operator");
    name = rhs.name;  // 复制 rhs 的数据
    return *this;
  }

 private:
  std::string name;
};

class PriorityCustomer : public Customer {
 public:
  PriorityCustomer(const PriorityCustomer &rhs)
      : Customer(rhs),  // 调用 base class 的 copy 构造函数
        priority(rhs.priority) {
    logCall("PriorityCustormer copy constructor");
  }

  PriorityCustomer &operator=(const PriorityCustomer &rhs) {
    logCall("PriorityCustomer copy assignment operator");
    Customer::operator=(rhs);  // 对 base class 成分进行赋值操作
    priority = rhs.priority;
    return *this;
  }

 private:
  int priority;
};

如果你为 class 添加一个成员变量,你必须同时修改 copying 函数(包括 copy 构造函数和 copy assignment 操作符)。(你也需要修改 class 的所有构造函数以及任何非标准形式的 operator=。如果你忘记,编译器不太可能提醒你。)

任何时候只要你承担起 为 derived class 撰写 copying 函数 的重责大任,必须很小心地也复制其 base class 成分。那些成分往往是 private,所以你无法直接访问它们,你应该让 derived class 的 copying 函数调用相应的 base class 函数。

令 copy assignment 操作符调用 copy 构造函数是不合理的,因为这就像试图构造一个已经存在的对象。令 copy 构造函数调用 copy assignment 操作符同样无意义。构造函数用来初始化新对象,而 assignment 操作符只施行于已初始化对象身上。

如果你发现你的 copy 构造函数和 copy assignment 操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是 private 而且常被命名为 init。这个策略可以安全消除 copy 构造函数和 copy assignment 操作符之间的代码重复。

请记住:

  • Copying 函数应该确保复制 对象内的所有成员变量 及 所有 base class 成分。
  • 不要尝试以某个 copying 函数实现另一个 copying 函数。应该将共同机能放进第三个函数中,并由两个 copying 函数共同调用。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

羞稚

暂无简介

文章
评论
26 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文