Effective C++ Chapter 4 设计与声明 总结

  • Post author:
  • Post category:其他




Chapter 4 设计与声明



Item 18 把接口设计的易于正确使用而难于错误使用

遵守以下原则使得接口易于正确使用而难于错误使用:

  • 保证正确性:保证接口的一致性, 行为能够兼顾内置类型(比如把常量0换成static_cast<object*>(0)作为形参给shared_ptr的构造函数)

  • 预防错误:

    • 引入新类型(比如书中的Date类型就通过把构造函数的参数从数字换成了能自定义输入规则,封装性更强的Day,Month,Year三个类)

    • 限制运算符的使用(比如三个分数类a,b,c 你只想它们之间被比较而不是赋值,你就应该把operator=声明成delete以防止出现if(a*b = c) 这样的意外错误)

    • 自定义 shared_ptr的deleter 以防止跨DLL问题 (比如Item14的用shared_ptr自动释放mutex锁)



Item 19 把设计class看做设计一个type ⭐⭐


个人评价:十分重要,可以看做一整个Chapter的总结

设计一个类时思考如下问题:

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

    (如何设计其构造函数,构析函数,内存分配函数,和释放函数(operator new, operator new[], operator delete, operator delete[] ))

  • 对象的初始化和赋值该有什么样的区别?(如何设计构造函数,该不该重载operator=,注意别混淆初始化和赋值,见item4)

  • 新type如果被以值传递,意味着什么?(比如如何避免在循环里反复调用返回拷贝的函数导致资源浪费这种行为)

  • 什么是新type的合法值? (做好输入验证,比如Item 18的Date类设计)

  • 你的新type需要配合哪个继承体系吗?记住,如果你设计新派生类,你将受到父类设计的束缚(比如父类的函数是否为virtual, -> item 34, 36 构析函数是否为virtual -> item7)

  • 新type需要什么样的转换,如果从T1到T2, 应该用隐式装换还是显式转换?(比如T1声明operator T2 或 T2 写一个接受T1的explicit构造函数)

    例子:item15的Font类

  • 什么样的操作符和函数对这个新type是合理的?(item 23 24 46)

  • 把你不希望使用的标准函数声明为private(item 6,其实现在C++11只要把不想用的标准函数设置成delete就行了)

  • 什么是新type的“未声明接口”?(item 29)

  • 你的新type有多么一般化?应该定义一个新class还是class template?

(书中无具体案例,个人理解:如果你要设计一个Shape体系(Shape体系下有长方形,正方形,圆形,菱形等等类), 与其设计一个庞大的继承树, 不如设计为一个class template)

  • 你真的需要一个新type吗?(呼吁第五条,与其添加新派生类不如使用non member non friend函数整合类的相关功能,见item 24)



Item 20 用常量引用传递代替值传递

  • 用常量引用传递(pass by reference to const)代替值传递的好处是,不仅能避免不必要的拷贝开销,在多态场景下还能防止派生类作为参数向基类形参隐式转换导致的“切片”问题(即实参拷贝的派生类部分被切掉,失去派生类特性)

学完C++ Primer 15章的TextQuery设计就不难理解pass by reference to const的好处了,另外const的另一个好处是允许传入C风格字符串给string引用

  • 内置类型,STL迭代器,函数对象一般不受值传递限制(后两个懂,第一个不绝对吧,作者本人也说了不要因为build-in type拷贝开销小就可以对开销不置一顾)



Item 21 若必须返回拷贝 不要返回引用

  • 不要把函数内变量,引用或指针,或静态对象作为返回对象(函数结束后销毁本地变量导致ub),对于必须返回拷贝的情形(比如两个分数类的乘法必须生成一个新的分数对象(存储积)),乖乖接受拷贝返回的命运,但可以让编译器帮你做优化,比如加inline声明:
#include<iostream>
using std::cout; using std::endl; using std::ostream;
class Rational{
    public:
        Rational(int numerator = 0, int denominator = 1);
    private:
        int n,d;
    const Rational operator*(const Rational &rhs) const;
};

inline const Rational operator*(const Rational& lhs, const Rational& rhs) {
	return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}



Item 22 声明数据成员为private

  • 将数据成员声明为private的好处:

    • 保证句法一致性(syntactic consistency), 也就是把类内所有数据都设计为只能通过函数获取,客户就不用思考该用函数还是变量名调用数据了,全是函数
    • 可细分访问控制权限(对于public变量,即可读也可写,没有细分权限可言,但如果声明为private, 可以通过函数设计逐步细分访问控制,比如设计为只读,只写,可读写,不可读写四个权限)
    • 加强类的约束性并提供给类作者实现上的灵活性 (书中以速度收集器应该设计成记录所有速度(以空间换时间)还是必要时才计算(以时间换空间)说明 <== 莫名联想到动态规划),比如上述的访问权限控制,设计相关函数的前提和后置条件,针对多线程环境做对应处理等
  • protected 变量的封装性并不比public高到哪去(public变量改变废掉客户端代码,protect变量改变废掉其所有派生类代码,无论哪种情况最后都会导致大量的重构需求)



Item 23 以non-member non-friend 函数取代member 函数

  • 以non-member non-friend 函数取代member 函数的优点:

    • 比将相关功能声明为member函数封装性更强

    (用非成员非friend函数和成员函数(或者friend函数)实现相同功能的对比:

    成员函数(或者friend函数)不仅仅能获取类内private变量,还有private成员函数,枚举体,别名等等。。

    但是非成员非friend函数除了类内的public变量和public函数,其他免谈。)

    • 使得程序(文中例子为浏览器)相关功能的包装灵活性(packaing flexiblity)更强

    (不知道怎么理解,但是看到一个有趣的例子是书中用namespace把实现类和用于扩展功能的non-member non-friend函数 都定义到一个namespace里了,这种namespace > class, function 的程序体系其实也是C++标准库的实现方式)

    • 所需的依赖更低(不难理解,声明为类内成员会被类给限制,声明为friend函数还得往类头文件多加一行,不雅), 使得扩展性(extensiblity) 更强

个人理解:弄成非成员非friend函数的做法而非类函数其实把功能从类体系中解耦出来了,使得设计更灵活,类似操作系统中用户程序对系统的调用,本质上是遵守了软件工程中的开放闭合原则。



Item 24 当参数必须被隐式转换时声明非成员函数

  • 如果你的函数参数(包括指针)涉及类的隐式转换,则你的函数必须是非non-member函数

(比如文中的分数类,其实这类operator只要参数超过两个都得是非成员,比如operator>>也是如此)

  • 一个函数不是成员函数不代表它就应该是friend函数,看情况设计



Item 25 设计一个安全高效的swap函数

  • 如果用std提供的初始swap函数交换类的导致的拷贝成本较高如何处理:

    • 将类的数据成员提取出来成为一个独立的数据结构,并在原来的类中用指针指向这个数据结构(pimpl技巧)
    • 为类设计其独有的swap成员函数,在这种自定义的swap函数里实现交换数据的方法是在函数体中直接写交换两个类的指针来实现交换,这样相比拷贝就高效了。不过注意swap函数要保证exception-safe
    • 另为swap成员函数提供一个非成员的swap函数(为对应类的全特化)用以调用类的成员swap函数,这个非成员的swap函数应该定义在类所在的命名空间里(回想24页的namespace > class, function的体系)。

    注意:模板函数不支持部分参数特化,应该用函数重载的方法来实现模板函数的部分特化

    • 可以把类专属的非成员swap函数放在std里,但最好还是为类自己定义一个命名空间,比如书中的

      Widget

      类就在

      namespace WidgetStuff

      下,不要引入对std来说全新的东西(原则上也不应该, 会破坏STL的封装性)

    • 注意在类的成员swap里声明std::swap, 一般编译器会自己找到最适合类的swap版本,如果你没有定义类特化的swap函数,则std::swap的作用就体现了:提供了一个垫底的调用。

class Widget { 
public:
...
void swap(Widget& other) {
    using std::swap; 
    swap(pImpl, other.pImpl); 
} ...
};

疑问:既然能在类内部就声明一个public且类专属的成员swap函数,为什么还要额外定义一个非成员的swap来调用上述swap?

查了点资料后懂了:

在客户端调用swap时,编译器会自动查找最适合类的swap版本,有利于接口的一致性(和原swap接口一致)和简易性。

调用关系:

calss Widget{
public:
    ……
    void swap(Widget& other)
    {
        using std::swap;//这个声明有必要
        swap(pImpl, other.pImpl);
    }
    ……
};
namespace std{
    template<> //修订后的swap版本
    void swap<Widget>(Widget& a, Widget& b)
    {
        a.swap(b);  //调用其成员函数
    }
}

int main(){
	Widget w1,w2;
	swap(w1,w2);//调用名空间的全特化Widget版本的swap.
}

当类都是模板类时,可以另声明一个新命名空间(当然也可以在std里,但为了后面的良好设计建议声明):

namespace WidgetStuff{
   ……//模板化的WidgetImpl等
   template<typename T>//内含swap函数
   class Widget{……};
   ……
   template<typename T>
   void swap(Widget<T>& a,//non-member,不属于std命名空间
             Widget<T>& b)
   {
       a.swap(b);
   }
}
int main(){
	Widget<int> w1,w2;
	swap(w1,w2);//调用名空间WidgetStuff的Widget<int>版本的swap.
}



版权声明:本文为Tracker647原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。