effective c++ 条款18 19 20 21

条款18:让接口容易被使用,不易被误用

在设计之前需要了解到用户可能会犯什么样的错误。原书中举例,设计一个表现日期的class

class Date{
public:
    Date(int month, int day, int year);
    //...
};

这个最初的接口看起来没什么毛病但是,用户可能在输入的时候弄错次序并且可能会输入错误的不符合实际情况的日期格式。

可以使用类型系统(type system),可以简单的导入一个外覆类型来区别天数、月份和年份。然后再Date构造函数里面使用这些类型,比如

struct Day{
explicit Day(int d):val(d){}
  int val;
};

struct Year{
explicit Day(int d):val(d){}
  int val;
};

struct Month{
explicit Day(int d):val(d){}
  int val;
};

如果出现类型不一致,函数就会报错,所以杜绝的用户填写次序的错误问题。

Date d(Day(30),Month(3),Year(2001)); //错误不正确的类型

Date d(Month(3),Day(30),Year(2001)); //正确!

再次对上面的这些结构体封装,将它们封装成类有利于接口的使用。

封装的思路:一年只有12个月都是固定的,所以Month就应该反映这个事实。所以使用enum类型是很恰当的,但是enum不具备安全性。所以,我们可以将enum当做是int类型的变量进行使用。

class Month{
public:
    static Month Jan(){ return Month(1);}
    static Month Feb(){ return Month(2);}
    ...
    static Month Dec(){ return Month(1);} 
private:
    explicit Month(int M); //阻止生成新的月份,将构造函数放在私有域里面。
    ...
};

Date d(Month::Mar(),Day(20),Year(2019));

在设计接口的时候如果“一致性”较好的话就使得接口十分容易被学习和使用,比如STL容器的借口都比较整齐。size就是返回容器的大小。

如果接口设计的时候要求用户必须记得某些事情,就等于给用户提供了范某种错误的倾向。比如一个函数返回一个指针,通常为了保证资源被正确释放都会使用共享指针来对资源进行控制。如果用户忘记将指针放进share_ptr里面的话就会导致内存泄露或者重复释放等情况发生。所以比较好的接口直接返回一个智能指针回来。

但是还会有问题,比如我们希望使用特定的删除器去实现析构

std::tr1::share_ptr<Investment> pInv(0,getRidOfInvestment);//这样无法编译成功需要对0进行强转
std::tr1::share_ptr<Investment> pInv(static_cast<Investment* >(0),getRidOfInvestment);

而不是delete调用的那一个,那怎么办?

在设计share_ptr的时候,设计人员就已经想到传入删除器作为计数为零时调用的析构函数。

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

设计时候需要考虑的一些问题

  • 新的type类型需要如何被创建和销毁?
  • 对象的内存初始化和对象的赋值需要什么样的区别?
  • 新的type的对象如果被pass by value,意味着什么?
  • 什么是新type的”合法值“?
  • 你的新type需要配合某个继承图系吗?
  • 你的新type需要什么样的类型转换吗?
  • 什么样的操作符和函数对此类型是合理的?
  • 什么样的标准函数应被放在private里面?
  • 决定成员的权限
  • 新的type有多"一般化"?
  • 真的有必要设计这个type吗?如果设计一个派生类不如设计多个non-member function或者templates,更能达到目的。

条款20:以pass-by-reference-const 替换 pass-by-value

主要的原因就是by-value的方法可能会导致性能上的影响,会调用copy构造函数,如果它是一个类的话可能会调用很多父类或者自己本身带的变量的拷贝构造函数,这样比较影响性能。

  • 但是一般按值传递就是为了不改变外面变量的值,从而传递一个副本进去,所以在传值的时候加入const修饰之后就防止值被修改。
  • 同时也可以避免对象切割的问题,当一个派生类对象以by value的方式传递并被视为一个基类,基类的拷贝构造会被调用,所以子类的特性就会被切割掉,除非有virtual copy函数。
  • pass by reference在编译之后的底层通常是传递指针,所以对于像一些内置类型来说使用按值传递更为简单。比如STL的容器就使用的是按值传递,迭代器和函数的使用者有责任看看他们是否高效且不受切割问题的影响。
  • 小型的用户自定义类型不一定成为按值传递的候选者。因为现在是小型用户定义的类型,并不代表以后这个类型不会扩充,而且通常小型的用户自定义类型,并不代表着开销就会小。

条款21:必须返回对象时,别妄想返回其引用

其实所谓的引用只是个名称,代表某个既有对象,任何时候看到一个引用的声明时,你都应该立即问自己它的另一个名称是什么,因为它一定是某物的另一个名称。

  1. 如果一个函数被设计为返回引用,但是这个引用指向的是一个局部的变量就会导致问题的出现,因为函数一旦执行完毕的话,这个局部的变量就会被析构。
  2. 如果说不能将引用指向一个局部的变量,那我们可以选择在堆区创建一个变量,并且用引用指向它,再返回这个引用。但是还是不可避免地付出了一个”构造函数“调用的代价,因为分配所得的内存以一个适当的构造函数完成初始化动作。而且你还需要面临一个问题:怎么样释放这一个变量?
  3. 你可能又想到可以将一个引用指向一个类里面的静态变量。但是这么做可能在多线程里面显现出严重的安全性问题。而且对于一个静态变量来说,如果在一个函数的里面对静态变量操作了两次,那么静态变量的值可能会改变两次。第一次改变之后。可能会对第二次的操作造成影响

猜你喜欢

转载自blog.csdn.net/weixin_42427338/article/details/86656827
今日推荐