1.组合与继承
1.1组合
1.1.1设计模式 Adapter-改造
现在我们设计了一个 功能很强大的类,但是我们考虑到由于版本发布的问题(精简版,企业版,旗舰版。。。),我们不可能发布一个版本就去开发一个,这样成本实在太高了,仔细思考以下,既然有了一个很强大的旗舰版,那么其他削弱的版本从旗舰版删删减减不就好了?
这种面向对象的设计模式就是改造
标准库里头有一个例子:queue & deque
deque 是两头都能出,queue是先进先出(只有一个出口)voidpush(constvalue_type&x){c.push_back(x);}voidpop(){c.pop_front();}};
deque 是两头都能出,queue是先进先出,deque的功能明显比queue强大的多,所以queue完全可以由deque实现。
当然改造不只限于删删减减,也可以再类里加上其他的类对象,在现实生活中我们的汽车都是万国牌的,这就好比在汽车这个类是中有许许多多的其他东西(座椅类,引擎类,安全气囊类。。。),我们在写类的时候也可以效仿。
在C中,在struct包涵其他的struct就相当于改造。
5.1.2复合下的构造和析构如何调用?
构造函数由内而外调用:
容器的构造函数首先调用组成部分的默认构造函数,然后执行自己的,如果想指定调用组成的构造函数如下
Container::Container(...):Component(){...};
析构函数由外而内调用:
容器先调用自己的析构函数,在调用组成的析构函数,如果想指定调用如下
Container::~Container(...){...~Component()};
1.2.1 设计模式 Delegation-委托
客户端+服务器的架构叫C/S架构,客户端把信息收集起来发送到服务器,由服务器统一处理,这样在功能升级的时候,只需要在服务器端的有较大的改动,而客户端几乎不变,例如:有一个客户端提供银行账号登录,后来由于公司业务拓展,现在可以支持QQ账号登录,这种改动对应客户端只是用户改变输入的账号与密码,主要的改动只在服务器上进行。
为了满足这种设计需求,就可以使用 设计模式 委托,在类中使用一个指针指向别的功能类,自身的功能完全借由别的类来完成(可以随时改变指向),保持自身面向客户不变。这种手法也叫编译防火墙。
现在有a,b,c三个String类的对象,同时共享一个StringRep对象:
这个时候如果a想要更改内容,那么就必须拷贝一份副本给a改,不能影响到b和c。
5.2继承
Dog属于动物类,从动物类继承:
继承就是一种传承,这里涉及到两个类,一个叫父类(基类)另一个叫子类(派生类),子类可以传承父类的所有的数据,还可以对父类进行扩展。
继承的作用在于代码复用与扩展,继承后的子类一定是大于等于父类的。
5.2.1继承下的构造与析构调用
继承中构造 和 析构的调用顺序:
构建子类对象一定会先调用父类的构造函数,子类默认调用父类的无参构造,当然也可以指定调用父类的构造函数,需要通过初始化列表指定调用
Derived::Derived(...):Base(){...}
析构函数的调用顺序 和 构造函数调用顺序相反
Derived::~Derived(...){...~Base()}
注意:
base class 的析构必须是virtual,否则可能会出现undefined behaviour
1.2.2c++中的继承方式
(1)公开继承 class Child:public Parent{};
公开继承下 父类数据到子类之后的权限变化
父类的公开数据 到子类之后是公开的
父类的保护数据 到子类之后是保护的
父类的私有数据 到子类之后是隐藏的
(2)保护继承 class Child:protected Parent{};
保护继承下 父类数据 到子类之后的权限变换
父类的公开数据 到子类之后是保护的
父类的保护数据 到子类之后是保护的
父类的私有数据 到子类之后是隐藏的
(3)私有继承 class Child:private Parent{};
私有继承下 父类数据 到子类之后的权限变换
父类的公开数据 到子类之后是私有的
父类的保护数据 到子类之后是私有的
父类的私有数据 到子类之后是隐藏的
总结:所谓的继承方式,就是父类能给子类的最高权限,实际权限小于等于这个权限,父类的私有数据 到子类之后一定是隐藏的。
class A: B 没写权限就是默认私有的
2.虚函数
Shape是一个很抽象的概念,下面的长方形,椭圆是一个具体的概念,考虑到两个图形都有是一种形状,为了增加代码复用,抽象出Shape这个类用来描述我们希望图形对象所具有的共性。
纯虚函数draw在基类中,这样就迫使子类里头必需出现纯虚函数的定义,确保子类图形能够被绘制。
虚函数error,由于不同的图形可能在操作中会出现错误,针对不同的对象需要重新定义错误处理,当子类出错那么会调用子类对应的error,基类的error提供了一个通用的错误处理,如果子类没有重新定义error,那么就延续父类的处理方式。
成员函数objectID,我们希望给创建的所有对象编号,这不需要子类去重新定义编号方式,所以使用成员函数。
2.1运行时绑定
virtual让类具有多态性,有的功能函数不是父类可以实现的 或者 父类只能提供一个泛用的方法,那么使用virtual让方法在子类中实现,这样在调用子类时不会受到父类的干扰。
在软件开发中那些泛用的,谁写都一样的东西叫框架,框架大量的使用了virtual,最牛逼的就是MFC(虽然这东西现在名声不咋滴)。
在C++中支持相关对象的不同的成员函数(原型相同),并允许对象与适当的成员函数进行运行时的绑定,C++通过重写(overwrite)支持这种机制----多态
在上面代码中我们希望从键盘输入一个非零的整数让程序输出不同的消息,但是上述代码无论输入什么,打印的都是Animal。这是因为指针p的指向的say函数是编译的时候就确定的。
接着在基类函数前面加上virtual
现在编译后,输出的结果就和我们从键盘输入的内容有关了。
多态这种性质只发生在父类型的指针(引用) 指向子类对象时,通过父类型的指针调用
虚函数,如果子类重写了这个虚函数 ,则调用的表现是子类的,否则就是父类型中对应的实现。继承是构成多态的基础,虚函数是构成多态的关键 ,函数重写是必备条件
2.1.1 重载,覆盖,隐藏的区别
(1)重载:
在同一个类中
函数名字相同,参数不同
virtual关键字可有可无
(2)覆盖:
在不同的类中(基类与派生类)
函数名字相同,参数相同
基类必需有virtual关键字
(3)隐藏
在派生类中函数与基类中的函数同名,参数不同,无论有没有virtual,基类函数都会被隐藏
在派生类中函数与基类中的函数同名,参数相同,但是基类没有virtual,此时发生隐藏
2.1.2多态的应用 (类型更加通用 根据具体的对象做出具体行为)
用在函数的参数上
void testAnimal(Animal* a);
用在函数的返回值上
Animal* getAnimal(int s);
2.1.3类型识别
因为多态让子类对象富有个性化,也做到了类型通用,但是通用就意味着失去个性化,那么在我们设计程序时可以通过类型识别恢复子类的个性。
2.1.3.1 typeid
typeid这个运算符可以获得类型或者对象的类型信息,使用时需要包涵#include 。
typeid 返回的信息存入一个type_info 类型的对象中,这个类型 重载 == 和 !=运算符。并且有个成员函数 name()返回类型的名称。
如果父子类之间 没有多态性,则当父类对象指针指向子类对象时,通过指针取值识别出的对象是父类型的。
运行后输出结果如下:
i指的是int,Pi指的是int*,P6Animal指的是Animal*,6Animal指的是Animal。
2.2 虚析构
有以下继承:
我们通过两种不同的方式创建对象
(1)
没有任何问题。
(2)
少了调用了一次B的析构函数,这样就造成了内存泄露。
解决方法:
使用虚析构后,资源被释放。
2.2.1虚表
virtual之所以能呈现出多态的特性,这是因为在底层使用了虚函数表,通过指针连接不同的代码块。
如果基类的析构函数没有被声明成virtual函数,那么在虚表中就不会出现析构函数,那么class B中的析构函数会将class A中的析构函数隐藏,然而指针是A*类型,在调用析构的时候只会调用基类的析构,这样就导致了内存泄露,加上virtual之后,delete时会先去调用子类的析构函数,当子类的析构被调用,父类的析构会自动被调用,这样就避免了内存泄露。
2.2.2使用指针直接操作虚表
指针是一个大流氓,得到了地址一切的权限都只是君子协议
3.Delegation(委托)+Inheritance(继承)
设计一个Subject类,其中使用向量容器来保存观察窗口,同时需要更新数据时直接调用set_val函数。
在写一个基类Observer
子类PPT(举例而已)
主函数
编译运行后,当我们改变了其中一个的数据,其余的4个对象跟着一起改了,这样就可以实现窗口联动或者一些需要关联的操作,这叫面向对象设计。
作者:rrreal
链接:http://www.jianshu.com/p/39abc5336dcf
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。