《Effective C++》读书笔记(5):实现

2023-09-06 17:24:01 浏览数 (1)

今天继续更新《Effective C 》和《C 并发编程实战》的读书笔记,下面是已经更新过的内容:

《C 并发编程实战》读书笔记(1):并发、线程管控

《C 并发编程实战》读书笔记(2):并发操作的同步

《C 并发编程实战》读书笔记(3):内存模型和原子操作

《C 并发编程实战》读书笔记(4):设计并发数据结构

《Effective C 》读书笔记(1):让自己习惯C

《Effective C 》读书笔记(2):构造/析构/赋值运算

《Effective C 》读书笔记(3):资源管理

《Effective C 》读书笔记(4):设计与声明

大多数情况下,适当地提出声明与定义是花费心力最多的事情,而相应的实现大多直截了当。但仍有一些细节值得注意。


条款26、尽可能延后变量定义式的出现时间

当程序运行到对象的定义式时就肯定会多出了一次构造、一次析构的成本。

过早地声明某对象,如果因为种种原因(条件分支、过早返回、异常等)没有使用该对象,那么不仅降低了程序的清晰度,还浪费了上述的构造、析构的成本。


条款27、尽量少做转型动作

C 中兼容C式的转型操作,还有四个新式转型;后者容易被辨识,目标也更狭窄,易于编译器、程序员诊断。

代码语言:javascript复制
//C
(T)expression
T(expression)
//C  
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)

转型破坏了类型系统,可能导致任何种类的麻烦;它并非只是让编译器将某类型视为另一种类型,而是往往真的产生一些代码。

如果可以,尽量避免转型。在注重效率的代码中避免dynamic_cast,因为它的很多实现版本执行得很慢;尤其要避免一连串的判断dynamic_cast,不仅又大又慢,而且基础不稳,每次类有修改该代码也需要调整。

对于需要转型的设计,试着发展无需转型的替代设计。

代码语言:javascript复制
class Widget { ... };
class SpecialWidget : public Widget{
public:
  void f();
};
//需要转型
std::vector<std::shared_ptr<Widget>> ptrs;
for(auto iter = ptrs.begin(); iter != ptrs.end();   iter){
  if(SpecialWidget *psw = dynamic_cast<SpecialWidget*>(iter->get())){
    psw->f();
  }
}
//无需转型
std::vector<std::shared_ptr<SpecialWidget>> ptrs;
for(auto iter = ptrs.begin(); iter != ptrs.end();   iter){
  (*iter)->f();
}
//或者将f()实现为虚函数,则也无需转型

如果转型是必要的,试着将它隐藏于某函数背后;用户调用该函数而不是使用转型。


条款28、避免返回handles指向对象内部成分

避免返回handles(包括引用、指针、迭代器)指向对象内部。即使使用const修饰返回值,仍然可能存在handles所指对象或所属对象不存在的问题。

代码语言:javascript复制
class Foo {
public:
    Foo() { m_name = new std::string("foo"); }
    ~Foo() { delete m_name; }

    void dtor() { m_name = nullptr; }

    const std::string& getName() const { return *m_name; }

private:
    std::string* m_name;
};

void exit(const Foo& foo) {
    // m_name指针为nullptr时,访问它会导致未定义行为
    std::cout << foo.getName() << std::endl;
}

int main() {
    Foo foo;
    foo.dtor();
    exit(foo);
    return 0;
}

遵守该条款可增加封装性,帮助const成员函数的行为像个const,并将空悬handles的可能性降至最低。


条款29、为“异常安全”而努力是值得的

抛出异常时,异常安全的函数会不泄露任何资源、不允许数据败坏。函数的“异常安全保证”等于所调用的各个函数的“异常安全保证”中的最弱者。

异常安全函数提供以下三个保证之一:1、基本承诺。异常时所有对象处于内部前后一致的情况,但显示状态不可预料。2、强烈保证。异常时程序状态不改变。3、不抛掷保证。承诺不抛出异常。

“强烈保证”往往能够以“copy and swap”实现出来。

代码语言:javascript复制
class Widget {
public:
  Widget() { value = new int(0); }
  Widget(const Widget &rhs) {
    value = new int(*rhs.value);
  }
// copy and swap
  Widget& operator=(Widget rhs) {
    swap(rhs);
    return *this;
  }
  void swap(Widget &rhs) {
    using std::swap;
    swap(value, rhs.value);
  }
  ~Widget() { delete value; }

private:
  int *value;
};

条款30、透彻了解inlining的里里外外

将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化。

不过目前inline更多代表允许多重定义,例如head-only库可以用inline在头文件中定义变量。


条款31、将文件间的编译依存关系降至最低

该原则是为了减少不必要的编译时间和编译错误,提高代码的可维护性。其基本思想是:依赖于声明式而非定义式,头文件仅有声明式。基于此有两个手段:

1、Handle classes。把类分割为两个类,一个只包含接口与真正对象的指针,另一个负责对象实现的细节;这种设计称为pimpl。

类的用户完全与其实现细节分离,任何实现的修改都不需要客户端重新编译,真正实现接口与实现分离。

代码语言:javascript复制
#include<string>
#include<memory>

//Person实现类的前置声明
class PersonImpl;
//Person接口用到的类 前置声明
class Data;
class Address;

class Person{
public:
  Person(const std::string& name, const Data& birtyday, const Address& addr);
  ...
private:
  std::shared_ptr<PersonImpl> pImpl;
};

2、Interface classes。提供一个抽象基类,目的是描述派生类的接口,因此它不提供成员变量、构造函数,只提供虚析构函数与一组纯虚函数来描述所有接口。

代码语言:javascript复制
class Person{
public:
  virtual ~Person();
  virtual std::string name() const = 0;
  ...
};

0 人点赞