CPP 继承,virtual(转)

本文详细介绍了C++面向对象的三大特性:封装、继承和多态。通过具体实例讲解了不同继承方式下成员的访问权限,构造和析构顺序,同名成员处理方式等。并深入探讨了菱形继承问题及解决方案,以及多态性的实现机制。

from https://www.cnblogs.com/embedded-linux/p/9613557.html?ivk_sa=1024320u

CPP_封装_继承_多态

 

C++面向对象的三大特性:封装,继承,多态。

封装:使用一整套方法去创建一个新的类型,这叫类的封装。

继承:从一个现有的类型基础上,稍作改动,得到一个新的类型的方法,叫类的继承。

多态:当有几个不同的子类对象时,对象调用的函数会依据对象类型来调用相应类型的成员函数。==> virtual function

1. 继承

继承的主要目的是为了代码复用。

class 子类: 继承方式 父类

==> 继承方式: describes the access restriction of the filial class ==> "protected" all inherited variables are protected execpt for private ones which are NOT visible to the filial class itself.

子类也称派生类,父类也称基类。

1.1 访问权限:public, protected, private

protected表示这种成员可以被子类继承和访问,但是外界却像private一样不可访问。

父类的private成员并不是不继承,而是对子类扩展的成员函数不可见

public派生规则:

父类成员访问权限子类继承自父类成员的访问限定
publicpublic
protectedprotected
private不可见

protected派生规则:

父类成员访问权限子类继承自父类成员的访问限定
publicprotected
protectedprotected
private不可见

private派生规则:

父类成员访问权限子类继承自父类成员的访问限定
publicprivate
protectedprivate
private不可见

1.2 继承中构造和析构顺序

创建子类对象时,先创建父类,再创建子类
析构时,先析构子类,再析构父类。

1.3 继承中同名成员处理方式

当子类与父类出现同成员时,如何通过子类对象,访问到子类或父类中同名的数据呢?

  • 访问子类同名成员,直接访问即可。
  • 访问父类同名成员,需要加作用域
s.m_A
s.Base::m_A
s.func();
s.Base::func()

注:如果子类出现和父类同名的成员函数,子类的同名成员函数会隐藏父类中所有的同名成员函数(包括父类中各种重载函数)。

1.4 继承同名静态成员处理方式

问题:继承中同名的静态成员在子类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域

同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象和通过类名)

1.5 菱形继承

两个派生类继承同一个基类,又有某个派生类同时继承这两个派生类,这种继承被称为菱形继承,或者钻石继承。

菱形继承问题

  • 羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时,就会产生二义性
  • 羊驼继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以

菱形继承问题

虚继承:继承之前加上关键字virtual,变成虚继承。

#include <iostream>
using namespace std;

//动物类
class Animal
{
public:
    int m_Age;
};

//继承之前加上关键字virtual,变为虚继承
//Animal类称为 虚基类

//羊类
class Sheep :virtual public Animal {};
//驼类
class Camel :virtual public Animal {};
//羊驼类
class Alpaca :public Sheep, public Camel {};

int main()
{
    Alpaca alpaca;
    alpaca.Sheep::m_Age = 18;
    alpaca.Camel::m_Age = 28;
    //菱形继承时,两个父类拥有相同的数据,需要加以作用域区分
    //这份数据我们只需要一份,菱形继承导致数据有两份,造成资源浪费
    //利用虚继承解决菱形继承的问题
    cout << "alpaca.Sheep::m_Age = " << alpaca.Sheep::m_Age << endl;
    cout << "alpaca.Camel::m_Age = " << alpaca.Camel::m_Age << endl;
    cout << "alpace.m_Age = " << alpaca.m_Age << endl;

    return 0;
}
------
alpaca.Sheep::m_Age = 28
alpaca.Camel::m_Age = 28
alpace.m_Age = 28

1.6 类型转换

父类A 子类B

A *p = new B; 可以
B *p = new A; 不可以

父类指针可以指向新子类,反之不然。

p调用的成员方法为父类(A)的方法,非子类B的成员方法。属编译时绑定(因p的指针类型为A *),如要访问子类,需要使用多态(父类相应方法定义为virtual)。

A a;
B *p;
p = &a; 可以

p访问成员变量时可能越界(因p的成员变量多于a)。

 

2. 多态

2.1 动态多态

多态分为两类:静态多态和动态多态。

  • 静态多态:函数重载和运算符重载属于静态多态,复用函数名
  • 动态多态:派生类和虚函数实现运行时多态

区别

  • 静态多态的函数地址早绑定--编译阶段确定函数地址。
  • 动态多态的函数地址晚绑定--运行阶段确定函数地址。

动态多态满足条件

  • 有继承关系
  • 子类重写父类的虚函数(重写:函数返回值 函数名 参数列表 完全相同

动态多态使用

父类的指针或引用指向子类的对象

#include <iostream>

using namespace std;

class Animal
{
public:
    void virtual speak(){  // 虚函数
        cout << "Animal speak" << endl;
    }
};

class Cat: public Animal
{
public:
// 重写:函数返回值、函数名、参数列表 完全相同
    void speak(){
        cout << "Cat speak" << endl;
    }
};

class Dog: public Animal
{
public:
    void speak(){
        cout << "Dog speak" << endl;
    }
};

// 地址早绑定,在编译阶段确定函数地址
// 如果想实现cat speak,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定--地址完绑定
// 父类中成员函数前加virtual
void doSpeak(Animal &animal)
{
    animal.speak();
}

int main()
{
    Cat cat;
    Dog dog;

    doSpeak(cat);
    doSpeak(dog);

    return 0;
}
-----------------
Cat speak
Dog speak

声明为vitual的成员函数称为虚函数,在运行时确定执行体,属动态绑定,dynamic binding,调用子类的具体实现。

一旦某个函数在父类中声明为virtual,在所有继承它的子类中这个函数也是virtual的,子类可以不必写virtual关键字

多态原理
多态原理

有虚函数的类都有一个隐含的指针成员vptr,指向一个虚函数表。(一个类一个)

每个父类或子类都各自有一张虚函数表。在构造对象时,对象的vptr成员指向该类的虚函数表vbtable(virtual base table)。

包含虚函数的类的sizeof应多加4个字节,用于存放vptr(虚函数表指针)

2.2 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要是调用子类重写的内容。因此可以将虚函数改为纯虚函数。

如果一个成员函数声明为virtual并在末尾加上=0而没有提供函数的实现,称为纯虚函数。拥有纯虚函数的类称为抽象类,抽象类不能实例化(或者说不能创建该类的对象),而只能被其他类继承。

纯虚函数语法:

virtual 返回值类型 函数名(参数列表)= 0;

抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数否则也属于抽象类

抽象类也称为接口,继承了某个抽象类也称实现了某个接口。

2.3 虚析构和纯虚析

多态使用时,如果子类中有属性开辟到堆区(malloc,new),那么父类指针在释放时无法调用到子类的析构代码。解决方法:将父类中的析构函数改为虚析构或纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象。

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名(){} = 0;
类名::~类名(){}

注:如果子类中没有堆区数据,可以不写虚析构或纯虚析构。

#include <iostream>


using namespace std;

class Animal
{
public:
    Animal(){cout << "Animal constructor" << endl;}
    //纯虚函数
    void virtual speak() = 0;
    //利用虚析构可以解决父类指针释放子类对象时不干净的问题
    //virtual ~Animal(){cout << "Animal destructor"}
    virtual ~Animal() = 0; //纯虚析构:需要声明,也需要具体实现
};

Animal::~Animal()
{
    cout << "Animal pure virtual deconstructor" << endl;
}

class Cat: public Animal
{
public:
    Cat(string name){
        cout <<"Cat constructor" << endl;
        m_Name = new string(name);
    }
    void speak(){
        cout << *this->m_Name << " cat speak" << endl;
    }
    ~Cat(){
        if(m_Name != NULL){
            cout << "free cat memory" << endl;
            delete m_Name;
            m_Name = NULL;
        }
    }

    string *m_Name;
};

int main()
{
    Animal *animal = new Cat("Tom");
    animal->speak();

    //父类指针析构时不会调用子类析构函数,导致如果子类有堆区数据,造成内存泄漏
    delete animal;

    return 0;
}
------------------
Animal constructor
Cat constructor
Tom cat speak
free cat memory
Animal pure virtual deconstructor
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值