前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《Effective C++》读书笔记(5):实现

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

作者头像
C语言与CPP编程
发布2023-09-06 17:24:01
1650
发布2023-09-06 17:24:01
举报
文章被收录于专栏:c语言与cpp编程c语言与cpp编程

今天继续更新《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;
  ...
};
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-08-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 C语言与CPP编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档