首页 > 代码库 > Effective c++(笔记)之继承关系与面向对象设计
Effective c++(笔记)之继承关系与面向对象设计
1.公有继承(public inheritance) 意味着"是一种"(isa)的关系
解析:一定要深刻理解这句话的含义,不要认为这大家都知道,本来我也这样认为,当我看完这章后,就不这样认为了。
公有继承可以这样理解,如果令class D以public 的形式继承了class B ,那么可以这样认为,每一个类型为D的对象同时也可以认为是类型为B的对象,但反过来是不成立的,对象D是更特殊化更具体的的概念,而B是更一般化的概念,每一件事情只要能够施行于基类对象身上,就一定可以应用于派生类对象上(注意:在此处指的是每件事情,也就是基类的每个成员函数)。感觉这点非常容易混淆,同样举例子来说明一下,
企鹅是一种鸟,鸟可以飞,然后我们用c++的继承关系描述如下:
//鸟类 class Bird{ public: virtual void fly(); }; //企鹅类,注意是共有继承 class Penguin : public Bird{ };
按照上面这个继承来看,企鹅也可以fly(),不管fly()是虚函数还是非虚函数,公有继承后企鹅可以飞,但是这不符合常理,但是企鹅是一种鸟,但是这绝对不是public inheritance的关系,因为,它不满足凡是在基类上可以施行的行为都可以在派生类上施行,所以,可以看出来与我们的直觉不同,当确定public inheritance时一定要看是不是isa的关系,看是否满足规则。
很明显,上面我们认为企鹅isa鸟直接用public inheritance是不正确的,可以进行下面的改正
//鸟类 class Bird{ //没有fly函数 }; //会飞的鸟类 class FlyingBird : public Bird{ public: virtual void fly(); }; //不会飞的鸟类 class NonFlyingBird : public Bird{ //没有fly函数 }; //企鹅类,共有继承NonFlyingBird class Penguin : public NonFlyingBird{ //没有fly函数 };
上面的描述可能更能准确描述我们的思想,由上面的例子看出来,不要总是"自己认为"和"自己直觉",有时候是错误的。
因为我也觉得public inheritance 等于 isa 的关系这点不是很好理解,所以再举个例子加深印象
正方形是(isa)矩形,这是我们公认的,但是用c++继承方式描述我们认为的如下:
class Rectangle{ public: //设置矩形的高度和宽度 virtual void setHeight(int Height); virtual void setWidth(int Width); //传回当前矩形对象的高度和宽度 virtual int height() const; virtual int width() const; //使矩形的面积变大,改变宽度 void makeBigger(Rectangle &r); }; void Rectangle::makeBigger(Rectangle &r) { int oldHeigth = r.height(); //改变矩形对象的宽度 r.setWidth(r.width() + 10); assert(r.height() == oldHeigth); } class Square : public Rectangle{ };
按照上面的描述。makeBigger函数中只改变了矩形的宽度并没有改变高度,但是这个行为如果施行于正方形时,便不成立,所以将正方形的类public inheritance于矩形类是不合理的,不恰当的。
在c++中应该确定了解这些类之间的相互关系,后面会说到has-a和 is- implemented in terms of 将这些关系搞清楚,并应该知道如何在c++中塑造他们。
结论:
public inheritance 公有继承意味着isa的关系,凡是在基类B上施行的行为都可以在派生类D上施行。
2.怎么去区分接口继承和实现继承
同问:纯虚函数,(非纯)虚函数,非虚函数他们分别给派生类继承了什么?
答:
声明一个纯虚函数的目的是为了让派生类只继承其接口
纯虚函数是可以有函数体的,大家都知道声明为纯虚函数的类是抽象类,抽象类不能实体化,抽象类最大的作用就是给其他类来继承,而抽象类中的纯虚函数则在派生类中必须重新定义实现,当然并不是说抽象类中的纯虚函数不能有函数体,也可以有,这个函数体常常放置函数的缺省行为,也就是当抽象类的大多数派生类在实现这个纯虚函数时都需要这个行为,可以直接在函数体内显示调用该纯虚函数,看下面例子就知道了我在说什么了
class Airplane{ public: virtual void fly(const Ariport &destination) = 0; }; void Airplane::fly(const Ariport &destination) { //default opreation } class ModelA : public Airplane{ public: virtual void fly(const Ariport &destination) { Airplane::fly(destination); } }; class ModelB : public Airplane{ public: virtual void fly(const Ariport &destination) { Airplane::fly(destination); } };
声明(非纯)虚函数的目的就是为了让派生类继承接口和缺省行为
这是什么意思类,上述的纯虚函数可以没有函数体,如果有函数体的话常常放置缺省行为,但在派生类中必须显式调用基类的这个函数,这样达到代码的复用,而虚函数常常就会有函数体,当派生类继承基类后,如果派生类想改写这个虚函数,可以在派生类中自己重写该函数,因为虚函数是动态绑定的(执行的时候指针或者引用指向的对象),根据动态绑定的对象来决定执行哪个对象的虚函数,如果动态绑定的是派生类对象,而此派生类对象中没有改写此虚函数,也就是找到该函数的名字,此时就向基类中查找,一直向上查找,直到找到这个虚函数然后执行,由上可见虚函数不仅为想改写此函数的派生类提供了几口而且还为那些不想改写此虚函数的派生类提供了缺省行为。
声明非虚函数的目的就是使派生类继承接口和实现
在类中声明那些非虚函数的成员函数说明这个函数不给派生类改写,也就是不变性大于变异性,下一个问题会详细的讲述,当派生类继承基类后,那些非虚函数的成员函数不用在派生类中重写,派生类会继承了这个函数的接口和实现,同时也说明了派生类和基类在这个行为上是共有的,是不变的,比如说基类和派生类都需要同样的计算方法来返回一个ID号,而这个返回ID号的成员函数就不用定义虚函数。
3.千万不要重写从基类继承下来的非虚拟函数
答:从概念上来讲,当基类定义了这个非虚拟函数成员函数时也就表明了,凡是属于该基类也就是isa基类的派生类在这个行为上是一致的,因为isa的关系上面已经说明了,那么这个非虚成员函数在派生类中就不需要重写定义并且改写,大家的行为都是一样,并且基类给你提供了,何必再去花费功夫去重写定义呢?
再来说说如果在派生类中重写了基类中的非虚成员函数,会造成什么后果,这就会造成模棱两可的后果,并且在派生类中如果定义了重名的非虚成员函数将会覆盖掉基类中的这个函数,非虚成员函数的调用是静态绑定的,根据声明时的对象来执行,如果此时定义了派生类对象调用了这个非虚成员函数,我们本来的目的是调用它继承基类的,结果你重写了这个函数便会覆盖掉基类中的,所以会造成奇怪的结果。
其实如果你在派生类中重写了非虚成员函数后,也会违反我开始所说的public inheritance 意味着isa的这个规则,大家可以想想对么?
结论-----无论什么情况下都不要重新定义一个继承而来的非虚拟函数,非虚拟函数的不变性凌驾于变异性之上
4.在派生类中不要改变从基类继承的带有缺省参数值的虚函数的这些缺省参数值
答:感觉我写的好拗口,这么来说吧,当基类中有虚函数时,我们常常在派生类中来重写这个虚函数,如果这个虚函数带有缺省参数值时,我们尽量不要在派生类中改变这些缺省参数值。
还是举个例子来说明,这样更具体些哈!
enum ShapeColor{RED , GREEN , BLUE}; class Shape{ public: virtual void draw(ShapeColor color = RED) const = 0; }; class Rectangle : public Shape{ public: virtual void draw(ShapeColor color = GREEN); };
由上面代码可见,在派生类中必须重写纯虚函数,并且可以看到派生类中重写这个虚函数的缺省参数值从RED 改为了GREEN
这有什么影响呢?
Shape *pr = new Rectangle; pr->draw(BLUE); pr->draw(); // 调用的是Rectangle(RED)而不是Rectangle(GREEN)
我们知道虚函数是动态绑定的,是根据动态的类型决定,而这些缺省参数却是静态绑定的,根据静态对象的类型来决定的。
静态类型:程序在声明时所采用的类型
动态类型:程序目前所代表的或者所参考的类型
当我们执行pr->draw(),静态类型是基类,而基类的缺省参数是RED,而pr指针指向的类型是派生类,派生类中的缺省参数是GREEN,这就会导致这个函数有基类和派生类共同完成的,这样效果很不好,往往给读者觉得很怪的现象,所以我们强烈建议不要在派生类中去修改虚函数的缺省参数值。
5.尽量不要在继承体系下进行向下转型动作(即由基类指针转向派生类指针)
答,有人可能会说,我们什么时候才能有向下转型的动作,或者说如果某天我遇到这个动作我该怎么解决,什么解决办法最好?
下面就以例子来开始解决上面的问题
//银行账户类 class BankAccount{ public: //存款 virtual void makeDeposit(double amount) = 0; //提款 virtual void makeWithdrawal(double amount) = 0; //余额 virtual void balance() const = 0; }; //储金账户类 class SavingAccount{ public: void creditInterest(); }; //存放基类BankAccount*指针 list<BankAccount*> allAccounts; for(list<BankAccount*>::iterator p=allAccounts.begin(); p != allAccounts.end(); ++p) { //将BankAccount*转换成SavingAccount* static_cast<SavingAccount*>(*p)->creditInterest(); }
首先说明代码中,为什么要存放基类的指针而不存放基类的对象,因为,在容器中存放的都是对象的副本,存放指针可以避免出现很多对象的的副本,所以存放指向对象的指针而不是对象本身,这个也是常用的做法。
当我们调用派生类中的方法时,此时就涉及到了向下转型,将基类指针转换为派生类的指针,这种转换就称为向下转型。
如果这样干是一种解决方法的话,假如此时银行又增加了支票业务,那么在for循环中就会出现ifelse的分支每个分支都要进行static_cast,这样做在维护是真的是不恰当。
请看下面的一种方法----抽象出一个抽象类的方法
//银行账户类 class BankAccount{ public: //存款 virtual void makeDeposit(double amount) = 0; //提款 virtual void makeWithdrawal(double amount) = 0; //余额 virtual void balance() const = 0; }; //抽象出一个抽象类,用来代表会生利息的账户 class InterestBearingAccount : public BankAccount{ public: virtual void creditInterest() = 0; }; //储金账户类 class SavingAccount : public InterestBearingAccount{ virtual void creditInterest(); } class CheckAccount : public InterestBearingAccount{ virtual void creditInterest(); }; //存放基类BankAccount*指针 list<BankAccount*> allAccounts; for(list<BankAccount*>::iterator p=allAccounts.begin(); p != allAccounts.end(); ++p) { //将BankAccount*转换成SavingAccount* static_cast<InterestBearingAccount*>(*p)->creditInterest(); }
这是增加支票账户后,抽象出了类,但相对于前者只需要一次的向下转型即可,向下转型的动作取消的最佳办法就是以虚函数代替,上述还可以将creditInterest()函数加到基类BankAccount中,这样就完全避免了向下转型的动作
//银行账户类 class BankAccount{ public: //存款 virtual void makeDeposit(double amount) = 0; //提款 virtual void makeWithdrawal(double amount) = 0; //余额 virtual void balance() const = 0; virtual void creditInterest() = 0; }; //储金账户类 class SavingAccount : public BankAccount{ virtual void creditInterest(); } class CheckAccount : public BankAccount{ virtual void creditInterest(); }; //存放基类BankAccount*指针 std::list<BankAccount*> allAccounts; for(std::list<BankAccount*>::iterator p=allAccounts.begin(); p != allAccounts.end(); ++p) { //此时完全不用向下转型 (*p)->creditInterest(); }
另外,除了虚函数的方法解决向下转型的问题,还有种safe downcasting -- 安全向下的转型动作也不得不掌握。
这种虽然也是需要向下转型,但是起码是安全的哈!用 dynamic_cast运算符。
for(std::list<BankAccount*>::iterator p=allAccounts.begin(); p != allAccounts.end(); ++p) { if(SavingAccount *psa = dynamic_cast<SavingAccount*>(*p)) { psa->creditInterest(); }else if(CheckAccount *pca = dynamic_cast<CheckAccount*>(*p)) { pca->creditInterest(); }else{ error("......"); } }
结论--可以记住,尽量在自己的程序中不要进行向下转型,当没有办法时,可以首选虚函数的方法,看能否避免向下转型,当不能避免时应该使用dynamic_cast安全的向下转型的方法。
6.什么是继承,什么是模板,这两者的区别是什么?给定一个实际的例子,我该怎么去选择呢?
答:开始看到继承和模板时,感觉就是容易混淆,可能是以前看c++的时候没有仔细的去思考这个问题,但是看了书之后,才发现,它们之间是多么容易区分哈!当某个例子列出了很多行为,当你感觉到这些行为与类型没有关系时此时就可能是模板了,当感觉到不同类型的对象有不同的行为可能要重写等很明显就是继承的关系了哈!
比如:让你设计一个栈类,实现栈的初始化、入栈、出栈、判断栈是否为空等行为,并没有告诉你该栈中要存放什么类型的元素,其实无论存放什么类型,都可以轻松的写出这些行为,此时很明显就应该是模板,写出一个栈的模板类应该就ok(个人猜测STL源码分析中肯定有这个源码,可以水货的作者还有看),下面是栈的简单实现的代码
template<typename T> class stack{ public: stack(); ~stack(); void push(const T &object); T pop(); bool empty() const; private: struct StackNode{ T data; StackNode *next; StackNode(const T &newData , StackNode *nextNode):data(newData) , next(nextNode){} }; StackNode *top; //拒绝编译器产生的成员函数 //拷贝构造函数 stack(const stack &rhs); //赋值操作符 stack& operator=(const stack &rhs); }; template<typename T> stack<T>::stack(): top(0){} template<typename T> stack<T>::~stack() { while(top) { StackNode *toDie = top; top = top->next; delete toDie; } } template<typename T> void stack<T>::push(const T &object) { top = new StackNode(object , top); } template<typename T> T stack<T>::pop() { StackNode *topofstack = top; top=top->next; T data = http://www.mamicode.com/topofstack->data;>上面的代码对于stack而言是不完整的,还有很多没实现,但以足够表达出我的用意,我们并不知道stack中放置什么类型,便能写出以上的代码,说明栈stack这些行为与其中放置什么类型是没有关系的,所以,当行为与类型无关时,我们应该选择模板。
请看下面一个例子:
现在准备设计一个猫类,用来表现猫的一些行为,比如,猫吃,猫睡等行为,但是不同种类的猫吃的行为是不同的,睡的行为也不同,那么此时我们应该怎么设计这个猫类呢?
因为,不同种类的猫吃睡的行为不同,但是每个猫都具有吃和睡的这个行为,只不过这个行为不同而已,说到这很显然,应该使用继承inheritance,将吃和睡这个猫都具有的行为但不同种类猫这个行为不同的设计为虚函数,这样便可以在子类中进行重写,应该很明朗了,见下面的代码
class Cat{ public: virtual ~Cat(); virtual void eat() = 0; virtual void sleep() = 0; }; class Cat_One : public Cat{ virtual void eat(); virtual void sleep(); }; class Cat_Two : public Cat{ virtual void eat(); virtual void sleep(); };
结论---template 应该用来产生一群classes , 其中对象的类型不会影响到class的函数行为。---------inheritance 应该用于一群classes身上,其中对象类型会影响到class的函数行为。
7.什么是(has-a)的关系?什么是(is-implemented-in-terms-of)的关系?这两种关系与我们前面说的(isa)的区别又是什么呢?
答:has-a 和 is-implemented-in-terms-of的关系是通过layering来实现的,也就是说某个class中的数据成员中含有layered class 。
感觉说的还是模模糊糊,直接上一段代码就知道什么意思了
class Address; class PhoneNumber; class Person{ public: private: string name; Address address; PhoneNumber voiceNumber; PhoneNumber faxNumber; };
Person这个类中的数据成员有Address类和PhoneNumber类所构成的成员。这就是layering技术,我们前面说过,公有继承public inheritance 就是意味着是一种isa的关系,对比的,layering技术意味着有一种has-a和根据某物实现is-implemented-in-terms-of。可能又有人疑问了,那么通过layering技术的这种关系与前面的isa的关系的区别又在哪里呢?
这绝对是这个好问题,因为这就涉及到,当我们在起初面向对象的设计时,我们是应该选择public inheritance 还是选择layering技术来实在,这对后期的设计起到很大的影响。
比如,我们要设计set集合,当然首先想到的是STL中的模板set,但是模板中的set中的元素必须是有序的,也就是当你放到set集合中的类型可以比较,可以进行排序,这在很多情况下是很难实现的,比如我们想在set集合总放置我们自己定义的颜色对象,而颜色对象没办法比较衡量,此时我们就要设计自己的set集合,现在问题就来了,我们是要通过什么方法来设计自己的set集合呢?
可能又有人说首先选择STL中的模板,在模板上进行改进,我觉得这是个好方法,因为达到了代码的复用,起码这些代码不用我们自己写了,选择模板中的list,接下来,可能很多人都会不多想的选择去继承这个list(这是大家的通病,一般怎么去设计,总是想不想就去public inheritance,好像c++类中只有公有继承似的)我们要考虑是否可以进行公有继承。
公有继承满足对于任何行为凡是在基类B上为真的,在派生类D上也为真
如果我们进行public inheritance的话,如下所示
template<typename T> class set : public list<T>{ };
我们就拿插入行为来看,当我们向基类list中插入两次相同的元素时,那么list容器中便会有两个这样的元素,但是我们要设计的set集合中是不允许有相同元素产生的,我们只是不想插入set容器中排序而已,所以就拿插入这个行为而言就不成立,所以,公有继承public inheritance 是不成立的。所以只能通过layering技术来实习了,如下所示
template<typename T> class set{ public: bool member(const T &item) const; void insert(const T &item); void remove(const T &item); int cardinality() const; private: list<T> rep; }; template<typename T> bool set<T>::member(const T &item) const { if(find(rep.begin() , rep.end() , item) != rep.end()) return true; return false; } template <typename T> void set<T>::insert(const T &item) { if(!member(item)) rep.push_back(item); } template<typename T> void set<T>::remove(const T &item) { list<T>::iterator iter = find(rep.begin() , rep.end() , item); if(iter != rep.end()) rep.erase(iter); } template<typename T> int set<T>::cardinality() const { return rep.size(); }
可见用layering技术来模拟set和list的关系更加合适,所以,在是用public inheritance 还是 layering 技术时,先要搞清楚你要实现的这些类之间到底是什么关系,然后再去写代码。
8. 什么是私有继承private inheritance?什么是protected inheritance保护继承? 私有继承有什么用?
答:首先说保护继承protected inheritance , 这个。。。。几乎不用,我在这里写了,只是为了提醒大家,在c++代码中这个没有人知道是干什么的,也有可能我见识短。下面我们就私有继承privateinheritance来展开问题的讨论哈!
如果classes之间的继承关系是private,编译器通常不会自动将派生类对象转换为基类对象,而公有继承可以自动转化
然后,私有继承而来的所有数据成员,在派生类中都会变成私有属性,纵然它们在基类中原本是protected或者public属性。
除了上述两点,应该也没有什么了, 但是此时可能会有人跳出来说,私有继承private inheritance有什么用呢?
Effective c++为我们展示了它的用途,但是个人感觉一般情况下好像也用不到。
还是拿先前栈stack的例子,前面我们写了个stack template,用来产生一些类,分别放置不同的对象,但是当你经常使用时就会发现缺点,对于放置不同对象的stack它们成员函数的代码是完全分开的,不是复用的。这样的话将会导致问题,当我们大量使用stack template 后,目标代码的量会增加,会导致程序代码膨胀的现象。
那么怎么去解决这种代码膨胀的现象呢?
书上提到了使用泛型指针的方法,使我们压入栈和弹出栈的不再是对象,而是指向对象的指针,这样只需要一份拷贝代码的成本,即可来处理堆栈 ,如下所示
template<typename T> class GenericStack{ public: GenericStack(); ~GenericStack(); void push(void *object); void* pop(); bool empty() const; private: struct StackNode{ T data; StackNode *next; StackNode(const T &newData , StackNode *nextNode):data(newData) , next(nextNode){} }; StackNode *top; //拒绝编译器产生的成员函数 //拷贝构造函数 stack(const stack &rhs); //赋值操作符 stack& operator=(const stack &rhs); };
仔细看上面的代码,我们其实只是将T 换成了void*其余的根本没变,但是这种情况容易产生类型误用的操作,比如,我们将double类型的对象用push操作压入了int对象类型的栈,因为push中存放的是void*,它没有区分类型的功能,只要是指针都可以,所以我们还要对其进行改进,使其不能访问到GenericStack中的函数,如下所示template<typename T> class GenericStack{ protected: GenericStack(); ~GenericStack(); void push(void *object); void* pop(); bool empty() const; private: struct StackNode{ T data; StackNode *next; StackNode(const T &newData , StackNode *nextNode):data(newData) , next(nextNode){} }; StackNode *top; //拒绝编译器产生的成员函数 //拷贝构造函数 stack(const stack &rhs); //赋值操作符 stack& operator=(const stack &rhs); };
其实很容易,只是将public改成了protected,这样的话便安全了,直接访问不到类中的成员函数,然后我们用stack私有继承这个类template<typename T> class stack : private GenericStack{ public: void push(T *objectPtr){GenericStack::push(objectPtr);} T* pop() {return dynamic_cast<T*>(GenericStack::pop());} bool empty() const{return GenericStack::empty()} };
这个例子还是要好好体会,话说现在我也不是很理解,可能自己水平还没达到。上述的代码达到了安全性和效率上的双赢,在此也表达了根据某物实现的另一种方式---private inheritance 私有继承的方式。
9. 多继承 multiple inheritance , MI
答:多继承是,class,继承于多个类,但是它的复杂性不言而喻
多继承的形式如下所示:
class B_1{ }; class B_2{ }; class D : public B_1 , public B_2{ };
复杂性在于,当所继承的多个基类中都含有同名的成员函数时,那么此时就会造成模棱两可的情况,可能当调用基类中同名的函数时,必须显式的指出你要调用哪个基类中的函数。另外,多继承可用于下面这个例子
有个Person的抽象类,现在要产生一个具体的对象类MyPerson,此时还有一个与数据库相关的类PersonInfo类,PersonInfo类的设计目地是为了以各种形式进行数据库字段的打印,在每个字段的前后,以特殊字符串作为标记。设计的具象类MyPerson类与这两个类之阿金的关系又是什么呢?
MyPerson类与Person类之间关系很明确,Person类是个抽象类,MyPerson与Person之间是isa的关系故用public inheritance 公有继承,重要的是MyPerson与PersonInfo的关系是什么,它们之间的关系是根据某物实现,且MyPerson类中还要重写PersonInfo类的虚函数,根据某物实现有两种方式,通过layering技术或者私有继承,此处因为还要在Person类中重写虚函数,所以应该使用私有继承private inheritance 的方式实现。
class Person{ public: virtual ~Person(); virtual string name() const = 0; virtual string birthDate() const = 0; virtual string address() const = 0; virtual string nationality() const = 0; }; class DataBaseID; class PersonInfo{ public: PersonInfo(DataBaseID pid); virtual ~PersonInfo(); virtual const char* theName() const; virtual const char* theBirthDate() const; virtual const char* theAddress() const; virtual const char* theNationality() const; virtual const char* valueDelimOpen() const; virtual const char* valueDelimClose() const; }; class MyPerson : public Person , private PersonInfo{ public: MyPerson(DataBaseID pid) : PersonInfo(pid){} //重新定义私有继承下来的虚函数 virtual const char* valueDelimOpen() const{ return " "; } virtual const char* valueDelimClose() const { return " "; } //实现公有继承Person基类继承下来的接口 string name() const{ return PersonInfo::theName(); } string birthDate const{ return PersonInfo::theBirthDate(); } string address() const{ return PersonInfo::theAddress(); } string nationality() const{ return PersonInfo::theNationality(); } };
上面的代码便是集合了多继承multiple inheritance , 公有继承public inheritance ,私有继承private inheritance,其实个人觉得还是蛮复杂的,应该把这些知识运用到日常的c++项目开发中,这样我觉得才能对c++有更深的理解哈!