Effective C++ 38-42

最后更新于:2022-04-01 15:49:58

38.绝不要重新定义继承而来的缺省参数值。 重新定义函数缺省参数值意味着重新定义函数,而非虚函数不能重新定义,所以将就考虑不能重新定义虚函数的缺省参数值的原因:虚函数是动态绑定的而缺省参数值是静态绑定的。 静态类型是指程序中声明的类型,而动态类型是指实际对象的类型,举个栗子: ~~~ class A{ public: virtual void fun(int a=0) const{cout<<a<<endl;} }; class B:public A{ public: virtual void fun(int a =2)const{cout<<a<<endl;} }; int main(){ B* pb = new B();//pb的静态类型为 B* A* pa = pb;//pa 的静态类型 为 A*, //但是一个指针的静态类型不一定为其动态类型,如pa它的动态类型却是B类型的对象,这是由动态绑定实现的 pb->fun(); pa->fun(); ~~~ 虚函数是动态绑定的,但缺省参数值是静态绑定的,即对于pb,pa调用的虚函数,其使用的默认参数值都为静态绑定的,pa绑定的是 A类中的 a= 0,而pb绑定的是B类中的 a=2,两者不同,虽然函数都是调用B中动态绑定的虚函数,但是默认参数不同,输出结果也不同。 39.避免 ”向下转换“ 继承层次。 从一个基类指针到一个派生类指针称为向下转换,一般使用static_cast将基类指针强制转换为派生类指针。向下转换难看,容易导致错误,且难以理解,升级和维护。 向下转换的消除:使用虚函数调用来代替。第一种方法很简单理解,但是对于一些类不适用,如基类范围太大导致7有些派生类不应该有这个函数的功能,所以要将这些类的每个虚函数称为一个空操作,或者作为一个纯虚函数,并默认实现返回错误的操作。第二个方法是加强类型约束,使得指针的声明类型和你所知道的真的指针类型相同。即对于用到这些向下转换时,通过一些设定,滤去那些不拥有真正指针类型的指针,只留下需要进行操作的指针并以其真实的类型来调用其函数。 如果遇到必须要转换的情况,也不要使用static_cast,而是使用安全的用于多态的向下转换 dynamic_cast,当对一个指针使用dynamic_cast时,先尝试转换,如果成功返回一个合法的指针,否则返回空指针。 40.通过分层来体现”有一个“或”用..来实现“。 使某个类的对象成为另一个类的数据成员,从而实现将一个类构筑在另一个类之上,这个称为分层 Layering,也被称为构成,包含或嵌入。 对于有一个的概念很容易理解,对于一个 Person, 有 Name,Address,Phone等属性,但是不能说Person是一个Name。 对于"用..来实现”,其实就是调用其它类的对象作为类的主要数据成员,使用这个类的的函数接口来实现新的类中的功能与接口。 41.区分模版和继承。 根据依赖的类的用途来区分,如过依赖的类是类的行为,则为继承,如果依赖的类是类所操作的对象类型,则是模版。如,企鹅类依赖于鸟类,鸟类中的接口决定的是企鹅类中的行为,即两者是继承关系,而当实现一个集合时,集合类依赖与类T,是由于类T为集合类的进行操作的对象,这是模版。模版的实现会假设类可以调用T的构造析构赋值等函数,模版的特性是类模版的行为在任何地方都不依赖于T,行为不依赖于类型。 当对象的类型不影响类中函数的行为时,就使用模版来生成这样一组类。 当对象的类型影响类中函数的行为时,就用继承来得到这样一组类。 42.明智地使用私有继承。 私有继承不为 “是一个” 的关系。如果两个类之间的继承关系为私有继承,编译器一般不会将派生类对象转换为基类对象。私有继承时,基类的公有和protected 类型的成员都变成派生类的私有成员。私有继承意味着”用...实现“,私有继承纯粹是一种实现 技术。私有继承只是继承实现,而忽略接口。私有继承在 软件 ”设计“过程中毫无意义,只是在软件”实现“时才有用。 对于分层,也有 用...实现的含义,对于分层与私有继承,尽可能使用分层,必要时才使用私有继承。而建议使用私有继承在用到保护成员和有虚函数介入时。 对于一个基类,只作为其他类的实现来使用,使用分层作为其他类的私有成员,但其不是抽象类,导致其可能被其他人随意调用导致出错。这是就需要使用到私有继承,对于这种具有实现但是只能用于特定用途的基类,将其接口都改为protected类型,而正确使用它的类不用分层而使用私有继承来安全的使用基类。 对于模版,其为C++中最有用的组成部分之一,但是,实例化一个模版,就可能实例化实现这个模版的代码,如构成set<int> 和set<double>的代码是完全分开的两份代码,模版会导致代码膨胀。改进的方法:创建一个通用类,储存对象的void*指针。创建另一组类来保证类型安全使用通用类。以实现栈stack为例,先构建一个stack的通用类: ~~~ class GenericStack{ protected://实现类使用私有继承继承这个通用类,所以将接口保护起来 GenericStack(); ~GenericStack(); void push(void* object);//使用指针 void* pop(); bool empty() const; private: struct StackNode{ void *data; StackNode *next; //在stack中使用指针来传递数据和保存数据,则节点析构时不用释放void指针指向的内存。 StackNode(void *newData,StackNode *nextNode) :data(newData),next(nextNode){} }; StackNode *top; GenericStack(const GenericStack&);//防止拷贝和赋值 GenericStack& operator=(const GenericStack&); }; ~~~ 而要实现stack的具体类通过私有继承这个类来实现功能,而且可以使用模版来完美的完成这个工作: ~~~ template <class T> class Stack:private GenericStack{ public: void push(T* objectPtr){GenericStack::push(objectPtr);} T* pop(){return static_cast<T*>(GenericStack::pop());} bool empty() const {return GenericStack::empty();} }; ~~~ 这里使用私有继承,将通用类GenericStatck作为其实现,而其接口函数都是内联函数,几乎没有消耗,而且使用模版实现了类型安全的判断。对于创建任意类型的stack只要重新编译这个三个简单的内联函数即可,而不是通用类中复杂的实现,极大的降低了程序的开销。 这样的代码是令人惊叹的,近乎完美的。首先使用了模版,编译器会根据你的需要来自动生成所有的接口。因为使用模版,这些类是类型安全的,类型错误会在编译期间就能发现。因为GenericStack的成员函数是保护类型,用户不可能绕过接口类来调用它。因为这个模版的接口成员函数都被隐式的声明为内联,使用这些类时不会带来运行开销,生成代码就想用户直接使用GenericStack来编写的一样。因为GenericStack是使用void*指针,操作栈的代码只需要一份,而不同类型只要简单的编译类模版中的简单的内联函数就行。简而言之,这个设计使代码达到了最高的效率和最高的类型安全。
';