C++面向对象程序设计_Part2

part1讲述了基于对象,part2则是在基于对象的基础上,创建类与类之间的联系,即面向对象编程以及面向对象设计。ios

主要讲述如下三点:git

  • Inheritance (继承)
  • Composition(复合)
  • Delegation (委托)

另外,我把补充内容中的对象模型放入到Part2,我以为放入这里更加合适。github

markdown文件和一些代码能够从GitHub中下载,连接:https://github.com/FangYang970206/Cpp-Notes, 推荐使用typora打开。编程

转发请注明github和博客园地址,谢谢~设计模式

Composition(复合)

1558453874580

Composition(复合)就是has a, 上面的事例就是队列(queue)类中有一个双端队列(deque)类,队列中的成员函数经过双端队列的操做函数完成,这是类与类之间的第一个关系。(黑色菱形表明内部拥有)bash

deque中可能拥有不少方法,但queue中只经过deque提供了很是少的方法,这是一个设计模式Adapter,将一个拥有许多功能的类改造一下,获得另外一个类。markdown

内存视角下的composition(复合)

1558459151904

能够看到有两个复合关系,最后queue的内存是40.框架

composition(复合)关系下的构造与析构

1558457528106

因为Container类是拥有Component类,因此在构造方面,先调用Component类的默认构造函数,而后再调用Container的构造函数,由内而外的构造,里面作好了,再作外面。析构则相反,先对Container进行析构,而后再对Component进行析构,过程是由外而内,将外面的去掉,才能看到里面去掉里面,符合常识。函数

Delegation (委託) —— Composition by reference

1558457888072

若是一个类(string)中拥有一个指针(StringRep*),该指针指向的是另外一个类(StringRep),这种关系是Delegation(委托),更好的说法就是Composition by reference(学术界不说by pointer),两种类的生命周期不同,与复合两种类会同时初始化不一样,委托当须要用的时候再进行初始化。上图中的实例是一种很是有名的设计,叫handle/body,指针指向的类负责具体实现,能够看到有一个//pimpl,意思是pointer to implement,而拥有那个指针的类只提供外界接口,就是基于委托这种方式,Handle(string)是提供给外界的接口,body(StringRep)就是实现部分,为何这么有名,这是由于String类设计好了就不需修改了,只须要修改实现的那一个类,具备很好的弹性,另外,还有能够进行共享机制(减少内存),下图的a,b,c共享一个StringRep,这种方式叫作reference counting,当须要修改其中一个时,须要把内容copy出来一份进行修改,另外两个依然指向原StringRep。(白色菱形表明指针指向)

Inheritance (继承)

1558491481526

继承的语法就是在类名后加上:public(还能够是protected,private)你想要继承的类,若是想继承多个类,用逗号隔开就能够了。何时用继承,肯定一个关键点,子类is a 父类(例如,狗is a动物)。上述的List_nodes是继承了List_node_base全部的数据,另外还有本身的数据。

Inheritance (继承)关系下的构造与析构

1558716277153

继承的类(derived object)的一部分是基类(base part),对于要被继承的基类,它的析构函数必须是virtual,否则会出现问题,这个问题将在后面说。继承的构造函数会首先调用基类的构造函数,而后调用本身的构造函数(由内而外)。析构则相反,先析构本身,而后再调用基类的析构函数。

Inheritance (继承)with virtual functions(虚函数)

1558716850059

子类继承了父类的两样东西,一种是父类的数据,一种是父类函数的调用权。对于一个类而言,它的子类均可以访问因此的public方法,而子类要不要从新定义父类的函数呢?这时候就须要虚函数了,当public里面的函数不是虚函数时,则但愿子类不从新定义该函数。当函数是虚函数时(在返回类型前加入关键字virtual),则但愿子类从新定义它,而且父类已经有了默认定义。当函数是纯虚函数时(在结束符;前面加上=0),则但愿子类必定要从新定义它,父类没有默认定义(但能够有默认定义)。该事例是定义了一个基类shape,而后矩形Rectangle和椭圆Ellipse对shape进行继承,基类的objectID是无需继承的,能够提早定好,在父类调用便可,而error函数,父类有默认的错误信息,若是子类有更精细的错误信息,父类容许子类能够从新定义的,打印出子类调用时的错误,而draw函数则必须从新定义,父类没有定义(draw shape没有意义),子类不一样,所画出的形状天然不一样。

Inheritance (继承)with virtual ——经典实例

1558718409179

对于在powerpoint打开ppt文件而言,有如下几步,先点打开命令的菜单栏,而后出现文件界面,选择咱们要打开的文件名,而后程序会检查文件名是否符合规范,符合规范则在磁盘上搜索文件,搜索到了打开文件便可。而遇到注意的是,因此打开文件的过程都是这样,只有最后打开文件可能会不一样(可能会打开不一样格式的文件),因而有团队就将除文件打开函数之外的函数进行打包,子类直接继承,只要子类从新定义父类打开文件的函数便可。以下图所示:

1558718827030

团队开发了CDocument类,定义Serialize函数须要从新定义,在OnFileOpen函数中的省略号即为打包好的过程。用CDocument类的人只需从新定义Serialize函数便可,则在main函数中,先建立一个CMyDoc实例myDoc,调用myDoc.OnFileOpen函数,子类没有定义这个函数,实则调用的是父类的函数,即CDocument::OnFileOpen(&myDoc), 进入父类函数中,运行打包好的过程,当运行到Serialize函数时,发现子类从新定义了它,则调用子类从新定义的Serialize函数,最后再返回到CDocument::OnFileOpen,继续下面的过程。不再用写通常的步骤了,完美!这是一种很是有名的设计模式Template method(不是说C++ template),提供了一种应用框架,它将重复同样的操做写好,不肯定的步骤留给实际应用设计者从新实现。十年前最有名的产品MFC就是这样一种应用框架。

深层次的理解,谁调用函数,this就是谁,当调用Serialize函数是,编译器是经过this->Serialize()调用,因而就调用到了this从新定义的Serialize函数。

1558752779674

上图就是CDocument和CMyDoc的实例,用cout来模拟步骤,呼应上面两张图片。

Inheritance + Composition关系下的构造与析构

1558753009197

当同时存在继承和复合,类是如何进行构造和析构呢?这一节要讨论的问题:

  1. 子类有父类的成分还有包含着另外一个类;
  2. 子类继承父类,父类拥有另一个类。

状况2就很明显了,构造依然是自内而外,析构是由外而内。

对于状况1,这是侯捷老师留的做业,本身写代码判断,我写了一个:

#include <iostream>

using namespace std;

namespace fy1{
    class Base;
    class Derived;
    class Component;

    class Base{
    public:
        Base() {cout << "Base Ctor" << endl;}
        virtual ~Base() {cout << "Base Dtor" << endl;}
    };

    class Component{
    public:
        Component() {cout << "Component Ctor" << endl;};
        ~Component() {cout << "Component Dtor" << endl;};
    };

    class Derived : Base{
    public:
        Derived() {cout << "Derived Ctor" << endl;}
        ~Derived() {cout << "Derived Dtor" << endl;}
    protected:
        Component c;
    };

    void fy1_test(){
        Derived d;
    }
}

int main() {
    cout << "Ctor and Dtor test:" << endl;
    fy1::fy1_test();
    return 0;
}

运行结果为:

Ctor and Dtor test:
Base Ctor
Component Ctor
Derived Ctor
Derived Dtor
Component Dtor
Base Dtor

能够看到先初始化父类(Base),而后再初始化Component类,再初始化本身,析构与构造相反。

下图也给出告终论。

1558760439215

至此,面向对象的概念说完了,下面进入实例环节。

Delegation (委托) + Inheritance (继承) (一)

1558873574510

上述代码解决的是下图所示的问题,对同一份文件使用四个不一样窗口去查看,或者右下角所示的,同一个数据,三种不一样的viewer查看。数据只有一份,表现多种形式,数据变化,表现形式也会发生变化,要表现这样的特性,这就对表现文件的class和存储数据的class之间关系要有要求,上图就是下图的一种解法,23种设计模式之一。Subject类是存储数据的类,不过类中有delegation,使用了一个vector类用来存放Observer类的指针,这样Observer类以及它的全部子类均可以导入这个vector中,Observer类至关于表现形式类的父类,能够有多种表现形式,这些都是子类。update则是子类须要从新定义的方法,不一样表现形式能够有不一样的更新方法。对于Subject类来讲,当咱们想建立新的窗口(新的observer类)去查看它的时候,须要对将新的Observer类进行注册,函数attach就是实现这样的功能,能够将新的observer子类的指针加到vector中,注销的函数没有写出来,另外,当数据发生变化时,使用set_val函数,须要使用一个函数去更新全部的observer子类,这就是notify函数干的事,遍历vector每个observer指针,调用指针指向的update方法,对表现形式进行更新。Delegation + Inheritance真的感受好强大呀。

1558873750010

下图是一个更详细的Observer解法构建

1558875339692

Delegation (委托) + Inheritance (继承) (二)

1558887391028

第二个实例,构建一个文件系统。能够把Primitive类看成文件类,而Composite类看成目录类,与平常使用的文件系统同样,目录里面能够包含目录,也能够包含文件,因此目录里面存放的不止是目录自己还能够是文件,可是须要放入到同一个容器中,想法是使用指针,但文件和目录是不太同样的,因此解决方案是将文件和目录共同继承Component类,而后Composite类中的容器存放的是Component的指针,这样就能够既存放文件又能够存放目录了,这是一种经典的解决方案,也是23种设计模式之一,Composite。另外,Component类中还有一个虚函数add,这是给目录进行继承的,由于目录能够新建目录和文件,这里不能设置为纯虚函数,由于文件不能继承这个函数,文件是不能在进行添加的。

Prototype

1558923111947

这又是23种设计模式之一的Prototype,Prototype要解决的是要设计一个类,这个类是为将来的子类所设计的,他能够访问和操做将来的子类,这就颇有意思了,都不知道将来的子类是啥,要去访问这个子类,这是怎么去作呢?原来在子类内部会申明一个关于子类的静态变量,就是上图中的LSAT : LandSatImage,另外这个子类会定义一个私有的构造方法(前面有一个负号,能够经过定义静态变量调用私有的构造方法),构造方法里面会调用父类的addPrototype函数,将静态变量的指针传到父类,父类就把传入的指针(经过父类的指针形式)加入到本身的容器当中,这样父类就知道子类的类型,就能够操做子类了,上述操做是这样的,父类有一个findAndClone函数,根据输入参数i选择父类容器中的子类进行clone,返回子类的指针,而clone父类定义的是一个纯虚函数,子类必须从新定义,上图中子类从新定义的是返回新建一个子类,返回它的指针,不过这个新建是调用的另一个构造函数(有一个#号,表明protect),使用private里面的构造函数是不行的,它是为父类打通桥梁的,为了与private里面的构造函数区分开,形参有一个int类型,这个int类型不会进行使用。

下面的图片是相关代码,解释上面的文字已经说的很清楚了。

父类Image

1558925338958

两个子类代码:

1558925377433

主函数:

1558925446700

Prototype例子来自于《Design Patterns Explained Simply》这本书,经典致敬!

1558925624955

面向对象的例子讲完了,下面介绍更加深层次的内容,理解面向对象更底层的东西。

对象模型

1558939321858

这张图所蕴含的信息量很大,如今一步步来看,首先最右边有三个类,A,B,C,A是爷爷,B是父亲,C是孙子,向上继承的关系,首先咱们从内存的角度来看类的布局,对于有虚函数(无论有多少个虚函数)的类来讲,它在内存中不只有本身定义的数据,还会多出一个指针,这个指针学名叫作虚指针(vptr),虚指针会指向一个虚表(vtbl),虚表里面定义的是各个虚函数所在的地址。A类内存中第一个就是虚指针,指向虚表,虚标里面有两个指针指向A的两个虚函数,下面两个是A类的数据,B类的前三个是继承了A类的数据以及虚指针,不一样的是B类从新定义了vfunc1函数,这将更新虚表,会将原来指向A::vfunc1函数的指针改成指向B::vfunc1函数的指针,如图就是将黄色的0x401ED0变为了浅蓝的0x401F80,C类继承B类的数据和虚指针,另外还有本身的数据,同时又重载了vfunc1,因此对应虚表中的vfunc1指针也要发生变化。在调用的时候,根据类中虚指针定义的虚表所指向的函数进行调用虚函数,这就是继承的根本原理,虚函数则是面向对象最强大的武器。非虚函数就是普通的函数,不过前面加上了类做用域。

静态绑定与动态绑定:当new一个C类时,获得一个指针p(上图所示),当经过p调用vfunc1的时候,其实是最下面中间语句进行调用的(其中n为编译器给虚函数在虚表中的索引),这是函数的调用方式与c很不同,在c的时代,当编译器看到函数调用,编译器会直接调用call XXX(XXX表明地址),地址是静态的,不会发生变化,这种方式叫作静态绑定,而C++经过类指针找到虚指针,根据虚指针找到虚表,从虚表中取出虚函数地址进行调用,这是一种动态的调用,不一样类的指针所调用的虚函数是不同的,这种方式叫作动态绑定,也叫虚机制。

下图就是一个实例,展现了这种动态绑定的强大威力。

1558943366112

我要设计一个powerpoint,要画出各类不一样的形状,咱们能够用一个List,承载A类的指针(放指针是由于容器只能放内存大小一致的东西,不一样的形状内存不一致),这样它的全部子类均可以放入List中了,由于都是A类,放入List后,循环遍历直接调用各自形状的draw函数就能够完成要求,这里走的依然是动态绑定,这是很好的,为了更加突出动态绑定的好处,这里要写出c语言中是如何作的,c语言只有静态绑定,上述过程须要条件判断当前的指针是指向哪个类,各类形状要使用多重判断,另外,当须要添加新的形状类时,又要修改条件判断代码,这是很很差的,不符合直观理解,应该像C++的虚函数同样,指针指向什么形状,就应该调用那种类型的draw。因而可知,C++动态绑定很棒,很强大。C++支持动态绑定和静态绑定,符合下面三个条件,C++采用动态绑定,条件以下:

  • 必须使用指针调用函数
  • 该指针必须是向上转型 (List中定义的类型是A的指针,但能够存储C的指针,经过继承向上转型)
  • 调用的函数必须是虚函数

这里还有一个概念——多态,List申明的时候,是经过pointer-to-A进行申明的,但实际是List能够存储各类不一样的形状,都属于A,确是不一样形态,因此叫多态。

因此,所谓的多态,虚函数,动态绑定,虚指针以及虚表,全部的故事都是同一件事情,真的了然于胸啊!😄

1558945922875

能够再来看看以前讲的Template method,这就是动态绑定的一个应用,子类CMyDoc调用父类的OnFileOpen函数,调用的时候会将子类的地址传入到父类的函数中,也就是this pointer(成员函数都是一个默认参数,表明的就是this pointer,谁调用成员函数,谁的地址就是this pointer),在OnFileOpen中,全部的调用前面都会加上一个this pointer,而对于Serialize函数来讲,它是符合上述咱们说的三个条件的,首先调用者是this,是指针,而后指向的是子类,向上转型,调用的Serialize函数是虚函数,因此会使用动态绑定,调用CMyDoc的Serialize函数。这是很好的!

1558948644501

如今从汇编代码的角度来看函数调用,初始化B类,讲B类强制转型为A类,获得a,调用a.vfunc1()函数,这里是静态绑定,由于是经过类调用函数,不是指针调用,汇编代码也说明了这个问题,使用的是call xxx形式编译函数。

1558949507638

而新建立了一个指针B,给的类型是指针A的类型,经过而后调用vfunc1函数,符合三个条件,是动态绑定,汇编代码的形式也不同了,汇编表示看不懂,不过call一行连同上面几行最后的表示在c语言中的描述确实是动态绑定的描述。另外,将b的地址赋给pa,从新调用vfunc1,同样是动态绑定,与new B是同样的。

#include <iostream>

using namespace std;


namespace fy2{
    class A{
    public:
        virtual void vfunc1() {cout << "A::vfunc1()" << endl;}
    private:
        int data1;
    };

    class B : public A{
    public:
        virtual void vfunc1() {cout << "B::vfunc1()" << endl;}
    private:
        int data2;
    };

    void fy2_test(){
        B b;
        A a = (A) b;
        a.vfunc1();

        A* pa = new B;
        pa->vfunc1();

        pa = &b;
        pa->vfunc1();
    }
}

int main() {
    cout << "object model test:" << endl;
    fy2::fy2_test();
    return 0;
}

输出结果:

object model test:
A::vfunc1()
B::vfunc1()
B::vfunc1()

面向对象的笔记到此结束,深深感觉到了C++面向对象程序的power,fighting!

相关文章
相关标签/搜索