CPP_继承_多态

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

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

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

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

1. 继承

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

class 子类: 继承方式 父类

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

1.1 访问权限:public, protected, private

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

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

public派生规则:

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

protected派生规则:

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

private派生规则:

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

注:protected成员子类可访问,对象不可访问。

#include <iostream>
#include <string>
using namespace std;

class Animal
{
public:
    Animal(){m_id = 0;}
    Animal(int id){ m_id = id;}
    virtual void speak(){cout << "animal speak" <<endl;}

protected:
    int m_id;
};

//class Rabbit: protected Animal
class Rabbit: public Animal
{
public:
    Rabbit(int id){ m_id = id;}
    void speak(){cout << "dog speak " << m_id <<endl;}
};

void doSpeak(Animal &animal)
{
    animal.speak();
}

int main()
{
    Rabbit rab(1);
    doSpeak(rab); // Animal& animal = cat
    //doSpeak(rab); //protected报错:'Animal' is an inaccessible base of 'rabbit' 

    return 0;
}

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;

抽象类特点:

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

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

#include <iostream>

using namespace std;

class CPU
{
public:
    virtual void calc() = 0;
};

class GPU
{
public:
    virtual void display() = 0;
};

class RAM
{
public:
    virtual void store() = 0;
};

class Computer
{
public:
    Computer(CPU *cpu, GPU *gpu, RAM * ram);
    void doWrok();
    ~Computer();



    CPU* cpu_;
    GPU* gpu_;
    RAM* ram_;
};

Computer::Computer(CPU* cpu, GPU* gpu, RAM* ram)
{
    cpu_ = cpu;
    gpu_ = gpu;
    ram_ = ram;
}

void Computer::doWrok()
{
    cpu_->calc();
    gpu_->display();
    ram_->store();
}

Computer::~Computer()
{
    if(cpu_ != NULL){
        delete cpu_;
        cpu_ = NULL;
    }

    if(gpu_ != NULL){
        delete gpu_;
        gpu_ = NULL;
    }
    
    if(ram_ != NULL){
        delete ram_;
        ram_ = NULL;
    }
}

class IntelCpu : public CPU
{
    virtual void calc(){
        cout << "Intel CPU" << endl;
    }
};

class AmdCpu : public CPU
{
    virtual void calc(){
        cout << "AMD CPU" << endl;
    }
};

class NvidiaGpu: public GPU
{
    void display(){
        cout << "Nvidia GPU" << endl;
    }
};

class AmdGpu: public GPU
{
    void display(){
        cout << "AMD GPU" << endl;
    }
};

class CorsairRam: public RAM
{
    void store(){
        cout << "Corsair RAM" << endl;
    }
};

class GskillRam: public RAM
{
    void store(){
        cout << "Gskill RAM" << endl;
    }
};

int main(int argc, const char *argv[])
{
    CPU *tIntelCpu = new IntelCpu;
    GPU *tNvidiaGpu = new NvidiaGpu;
    RAM *tCorsairRam = new CorsairRam;

    cout << "first Computer" << endl;
    Computer *tcomputer = new Computer(tIntelCpu, tNvidiaGpu, tCorsairRam);
    tcomputer->doWrok();

    cout << "------------------------------" << endl;
    Computer *scomputer = new Computer(new AmdCpu, new AmdGpu, new GskillRam);
    scomputer->doWrok();
    
    return 0;
}

2.3 虚析构和纯虚析

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

虚析构和纯虚析构共性:

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

虚析构和纯虚析构区别:

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

虚析构语法:

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
posted @ 2018-09-09 14:10  yuxi_o  阅读(504)  评论(0编辑  收藏  举报