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

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

作者头像
C语言与CPP编程
发布2023-08-10 08:13:23
1980
发布2023-08-10 08:13:23
举报
文章被收录于专栏:c语言与cpp编程

本文包括第6章设计基于锁的并发数据结构与第7章设计无锁数据结构,后者实在有些烧脑了。此外,发现吴天明版的中译本有太多太离谱的翻译错误了,还得是中英对照才行:

条款18、让接口容易被正确使用,不易被误用

好的接口容易被正确使用,不易被误用;应使自己的所有接口努力达成这一点。

“容易被正确使用”的办法包括:

1、接口的一致性。例如STL的容器几乎都有类似的接口。

2、与内置类型行为类似。例如对于int类型来说a*b=c是非法的,那么自己定义的operator*也应该避免a*b=c的操作。

“不易被误用”的方法包括:

1、建立新类型。例如对于以int类型的年、月、日作为参数的接口来说,用户很可能搞混顺序(不同国家年月日的常用顺序不一),那么可以建立年类、月类、日类作为参数类型。

2、限制类型上的操作。例如令返回值为const,即可阻止用户写出“a*b=c”的代码。

3、限制对象值。例如参数像月份这样取值有限,即可使用枚举类或者预先定义一系列函数返回所有月份。

4、消除用户的资源管理责任。例如条款13中提过一个工厂方法:

代码语言:javascript
复制
Widget* create_Widget() { ... }

如果只是返回一个裸指针,那么删除指针释放资源的责任就落在用户身上,而这常常会带来问题;如果把返回值改为shared_ptr,那么自动释放资源。

shared_ptr还有个性质是会使用每个指针专属的删除器。如果对象在一个DLL中被new创建而在另一个DLL内被delete,会引起运行期错误;而shared_ptr使用的删除器来自创建时所在的DLL,不存在上述问题。


条款19、设计class犹如设计type

当定义了一个新class,也就定义了一个新type。在定义一个新type前考虑以下问题:

新type的对象应该如何被创建和销毁?

对象的初始化和对象的赋值该有什么样的差别?

新type的对象如果被passed by value意味着什么?

什么是新type的合法值?

你的新type需要配合某些继承图系吗?

你的新type需要什么样的转换?

什么样的操作符和函数对此新type而言是合理的?

什么样的标准函数应该驳回?

谁该取用新type的成员?

什么是新type的未声明接口?

你的新type有多么一般化?

你真的需要一个新type吗?


条款20、宁以pass-by-reference-to-const替换pass-by-value

默认情况下C++以by value方式传参。这意味着函数参数都是由实参拷贝构造而来,调用端获得的也是函数返回值拷贝构造而来(不过有各种优化方式),多次拷贝的成本非常大。

C++中引用通常以指针来实现,传引用的成本相当低。因此,绝大多数情况下,用pass-by-reference-to-const替换pass-by-value更加高效。

此外,如果采用pass-by-value,如果误将派生类对象传给基类参数,那么派生类成员将被截断,仅留下基类成员;使用pass-by-reference-to-const即可避免这样的切割问题。

以上规则并不适用于内置类型以及STL的迭代器和函数对象,因为它们本质上很小,pass-by-value更适当。


条款21、必须返回对象时,别妄想返回其reference

虽然条款20中介绍了对于函数参数而言pass-by-reference-to-const相比pass-by-value的优势,但是对于函数返回值而言情况又不一样了。

当要返回的对象是一个局部栈对象时,函数退出后该对象就被销毁,无法通过指针或引用访问。因此函数返回值不能是指针或引用,必须是值传递。

当要返回的对象是函数内分配的堆对象时,如果返回的是堆对象的引用,那么函数退出后指针无法被删除,该堆对象无法被回收。因此需要返回堆对象的指针。

代码语言:javascript
复制
const Widget& f(){
  Widget* p = new Widget();
  return *p;
}

当返回的对象是局部静态变量的指针或引用时,如果程序的逻辑可能同时需要多个这样的对象,那么显然会出错:

代码语言:javascript
复制
const Rational& operator*(const Rational& lhs, const Rational& rhs){
  static Rational result;
  ...
  return result;
}

Rational a,b,c,d;
...
if((a*b)==(c*d)) //此时等号两边是同一个对象   

正确的做法是:对于返回值而言,该用值传递是就用值传递

代码语言:javascript
复制
const Rational operator*(const Rational& lhs, const Rational& rhs);

条款22、将成员变量声明为private

将成员变量声明为private有各种好处:

1、客户访问数据的一致性。客户只需记住用访问器函数来得到数据,无需考虑哪些是成员函数、那些是成员变量。

2、可细微划分访问控制。public成员全都可读可写,而private成员可以通过访问器、修改器的不同设置来控制读写权限。

3、为所有可能的实现提供弹性。private成员完全封装,日后完全可以在不影响用户使用的前提下被修改或替换。

protected并不比public更具封装性。假设取消一个已存在的protected/ic成员变量,那么所有派生类中使用到它的都需相应调整;而取消一个private成员变量则无需这样。


条款23、宁以non-member、non-friend替换member函数

越多成员被封装,则越少用户能访问它们,则修改它们时弹性就越大。而对于成员变量来说(首先应该是private),能访问它的函数越多,则其封装性越低。

那么,如果在成员函数与非成员/非友元函数之间抉择,并且两者提供相同的技能,那么非成员/非友元函数能访问的成员变量更少,封装性更强。

代码语言:javascript
复制
class WebBrowser{
public:
  void clear_cache();
  void clear_history();
  void clear_all{
    clear_cache();
    clear_history;
  }
};

void clear_webbrowser(WebBrowser& wb){
  wb.clear_cache();
  wb.clear_history();
}

C++中比较自然的做法是选择clear_webbrowser而非WebBrowser::clear_all,并将其置于WebBrowser的命名空间中。


条款24、若所有参数皆需类型转换,请为此采用non-member函数

如果需要让某函数的所有参数都进行类型转换,包括this指针所指的参数,那么该函数必须是non-member的。

虽然让类支持隐式类型转换通常很糟糕,但有个例外是建立数值类型时:

代码语言:javascript
复制
class Rational{
public:
  //参数分别为有理数的分子与分母
  Rational(int numerator = 0,int denominator = 1);
  int numerator() const;
  int denominator() const;
  const Rational operator*(const Rational& rhs) const;
}

当operator*是成员函数时,无法解决这样的代码:

代码语言:javascript
复制
Rational r1,r2;
...
r1 = 2 * r2;
//实质上等于 r1 = 2.operator*(r2)

对于有理数类的乘法,如果要支持字面量隐式转换为有理数对象,那么该类的operator*的所有参数都可能需要类型转换。因此成员函数无法解决这个问题,必须转为非成员函数:

代码语言:javascript
复制
const Rational operator*(const Rational& lhs,const Rational& rhs);

条款25、考虑写出一个不抛异常的swap函数

swap函数目前已是异常安全性编程的脊柱(copy and swap写法)。

如果对于你的类/模板类来说,swap的默认实现的效率可接受,那么无需额外做什么;而如果默认实现的效率不足,通常源于类为了二进制兼容性使用了pimpl技法,就需要实现一个确保不抛出异常的swap:

1、首先,提供一个public的swap成员函数,让它高效地置换两个对象。

2、在类/模板类所在的命名空间提供一个non-member的swap,调用swap成员函数。

3、对于类(而非模板类),还需特化std::swap,调用swap成员函数。

4、真正使用swap时先声明using std::swap,再直接使用swap,让编译器寻找最合适的swap实现版本。

代码语言:javascript
复制
namespace WidgetStuff{
  // WidgetImpl对象非常大
  class WidgetImpl { ... }; 
  // Widget是上面的pimpl类,用指针指向Widget
  class Widget{
  public:
    Widget& operator=(const Widget& rhs){
      *(p_impl)=*(rhs.p_impl);
    }
  //swap时只需交换指针,所以默认实现效率不足
    void swap(Widget& other){
      using std::swap;
      swap(p_impl, other.p_impl);  
    }
  private:
    WidgetImpl* p_impl;
  }
  
  // 同一命名空间下提供一个non-member的swap 
  void swap(Widget& lhs, Widget& rhs){
    lhs.swap(rhs);
  }

}
//std命名空间内提供特化版本的swap
namespace std{
  template<>
  void swap<Widget>(Widget& lhs, Widget& rhs){
    lhs.swap(rhs);
  }
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-08-02,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档