c++的组合/继承与多态

来源:互联网 发布:浴霸松下奥普 知乎 编辑:程序博客网 时间:2024/06/05 23:00

c++的组合/继承与多态

类与类之间,存在组合关系与继承关系。组合关系是比较简单方便的,能用就用,别滥用继承关系

1.类的组合关系

  • 所谓组合关系,就是一个类中包含了其他类。具体的实现方法很简单,将其他类的对象作为当前类的成员使用,那么就构成了组合关系
class Computer //电脑类{    Memory mMem;  //内存类的对象    Disk mDisk;   //硬盘类的对象    CPU mCPU;   //cpu类的对象    MainBoard mMainBoard;   //主板类的对象public:    Computer()    {    }};
  • 这些对象仅仅就是普通的成员变量罢了,没什么值得注意的

2.类的继承关系

继承,是面向对象中代码复用的重要手段

继承关系的特性

  • 所谓继承关系,就是子类继承父类所有的特质(元素),同时可新增父类没有的特质(元素),也可重写继承得到的特质(元素)。继承关系有一些独特的性质:
    • 子类重写/新增的成员函数,无法直接访问继承到的成员变量。为了克服这一点,父类中一般不用private权限,而用protected权限。如下面,Dell中的test便可直接访问mv
    • 尽管子类在概念上是父类的子集,但是子类所含的特质(元素)是父类所含特质(元素)的超集,所以子类可以直接赋值给父类
class Computer {protected:    int mv;public:    Computer()    {        mv = 100;    }    void change()    {        mv = 100;    }};class Dell : public Computer {public:    void test()   //这个成员函数是子类新增的    {        mv = 123;    }};int main(){       Dell dell;       Computer pc = dell;  //可以直接用子类初始化父类    pc = dell;   //也可以用子类赋值父类    return 0;}
  • c++支持三种继承方式,上面代码中用的是最常见的public方式,其实在工程中一般只使用public继承,不推荐其他继承方法!!
    • public继承:父类成员在子类中保持原有的访问级别
    • private继承:父类所有成员在子类中变为private权限
    • protect继承:父类public成员在子类中变为public权限,其他成员保持不变

父子间的同名覆盖

  • 子类中可以定义父类中同名的成员变量,子类中的同名成员变量将覆盖(隐藏)父类成员变量,可以用父类名::成员变量名来获取被覆盖(隐藏)的父类成员
  • 子类中可以定义父类中同名的成员函数,不论参数是否相同,子类中的同名成员函数都会覆盖(隐藏)父类成员函数,可以用父类名::成员函数名来获取被覆盖(隐藏)的父类成员函数。不难得出结论,子类不能重载父类的成员函数
class Parent{public:    int mi;    void add(int v)    {        mi += v;    }};class Child : public Parent{public:    int mi;    void add(int a, int b)    {        mi += (a + b);    }};int main(){    Child c;    c.mi = 100;        c.Parent::mi = 1000;//只能这样访问父类中的成员变量    c.add(1);    c.Parent::add(4, 5, 6);//只能这样访问父类中的函数    return 0;}

3.继承和组合类的构造与析构

当一个类是子类,同时它又包含了其他类,那么当它实例化时,如何初始化成员变量?

  • 先执行父类中的构造函数:若子类初始化列表中,调用了父类中的构造函数,则执行之;初始化列表中若未调用,则执行父类中显式定义的无参构造函数
  • 之后执行自己内部对象的构造函数
  • 最后执行子类自己的构造函数
  • 口诀:先父母,后客人,再自己
  • 当对象的声明周期结束时,析构函数的触发顺序和构造顺序完全相反,即:先自己,后客人,再父母

4.重写、多态、虚函数

多态的目的,其实是用来在子类中实现良好的、可靠的函数重写

  • 我们可以在子类中重写父类的成员函数,但是有时会发生一种子类退化的现象,那么这是重写就会失效
  • 子类退化:当我们用一个父类类型的指针/引用,去指向一个子类对象时,该指针和引用指向的将会是一个退化为父类的子类对象
  • 造成的结果就是,利用这种指针/引用,无法访问子类中独有的成员,只能访问父类中的成员,重写也会失效。根本原因是,编译器只能根据指针/引用的类型,来判断指向的对象的类型
class Parent{public:    void add()    {       cout << "123" << endl;    }};class Child : public Parent{public:    void add()//重写过的函数    {       cout << "abc" << endl;    }};int main(){    Child c;    Parent *p = c;//父类型的指针指向了子类对象    p ->add();//由于指针类型的关系,这里访问的是父类中原本的函数,访问不了子类重写过的函数    return 0;}
  • 由此,我们为了避免子类退化引起的重写失效,引入了多态。即使用virtual来修饰父类、子类中重写的函数,使其成为“虚函数”。这样,即使子类退化,仍然可以调用重写过的成员函数
class Parent{public:    virtual void add()//理论上父类子类中重写的函数都要修饰,其实只需在父类中修饰即可    {                 //因为virtual属性也会被继承给子类中的add函数       cout << "123" << endl;    }};
  • 值得注意的是,构造函数不能成为虚函数!只有构造函数执行后才可建立虚函数表。不过作为补偿,编译器对构造函数还是实现了多态(详见第5节继承和组合类的构造与析构),也没必要设置为虚函数了。与之相反,析构函数一定要设置为虚函数来实现多态,否则可能会内存泄漏
  • 构造函数和析构函数本身可以多态,但是内部却不能发生多态行为(调用虚函数的效果仅仅为调用当前类中对应的那个成员函数),因为虚函数表的生命周期“始于构造,终于析构”
  • 所以,会被重写的函数以及析构函数,必须使用在父类中使用virtual修饰,以此让它们成为虚函数,以实现多态

多态的本质,就是动态链编:让程序在运行时才去确认函数的具体调用,而不是在编译期间就确认具体的函数调用

5.安全多重继承

c++中一个子类可以继承多个父类,这也是对现实世界的一种描述,然而直接使用多重继承是不可靠的,工程中不会使用纯粹的多重继承

  • 实际开发中,常常使用单继承多接口来实现多重继承
class Base{protected:    int mi;public:    Base(int i)    {        mi = i;    }};class Interface1{public:    virtual void add(int i) = 0;};class Interface2{public:    virtual void multiply(int i) = 0;};class Derived : public Base, public Interface1, public Interface2{public:    Derived(int i) : Base(i)    {  }    void add(int i)    {        mi += i;    }    void multiply(int i)    {        mi *= i;    }};
  • 这种方法实现的多重继承,可以完美的支持多态和重写,

6.用继承实现抽象类

所谓抽象类,就是用类表达一种概念而非实体,比如定义一个电脑类

抽象类的概念

  • 抽象类有如下的特质
    • 抽象类作为父类而存在,只能被继承
    • 抽象类自己不能实例化,因为其实例化没有意义
    • 抽象类中,通常有些成员函数不提供具体实现
  • 一般来说,如果一个父类没有实例化的意义,那么我们就应该将其实现为抽象类

抽象类的实现方法

只可惜,c++中没有原生的抽象类,需要用纯虚函数来实现

  • 当虚函数没有具体实现,并且我们对其赋值为0时,编译器就会认为这个虚函数是纯虚函数(若不赋值为0,链接器将报错)
  • 将父类中的成员函数定义为纯虚函数(即函数没有具体的实现,只能由子类重写来实现),那么这个父类将无法被实例化,这样这个父类就成功变成抽象类了
class Shape{public:    virtual double area() = 0;//纯虚函数,通过给虚函数赋值0来实现};

接口的概念

所谓接口,就是一组行为的规范(一组函数集合)

  • c++中没有原生的接口,而是通过抽象类实现。接口是一种特殊的抽象类,它要满足:
    • 抽象类中没有任何成员变量,只有成员函数
    • 所有的成员函数都是public的、都是纯虚函数
  • 如下,就实现了一个接口,该接口可以被继承为串口、usb、蓝牙等等具体的通信方式
class Channel{public:    virtual bool open() = 0;    virtual void close() = 0;    virtual bool send(char* buf, int len) = 0;    virtual int receive(char* buf, int len) = 0;};
0 0
原创粉丝点击