C++中的继承&&多态博客

前言

是因为有了继承才有了多态的存在,我们明白继承的原理和特性才能去理解多态

文章目录

继承

什么叫做继承

继承机制是面向对象程序设计中实现代码复用的手段,它是我们能在原有的类的基础上,实现一个新的派生类,并扩展其他的功能和属性。

继承的定义

派生类继承基类有三中继承方式:公有继承(public),保护基础(protected),私有继承(private)

class Person
{
    public:
    string name;
    protected:
    int age;
    private:
    int weight;
};

class Student:public Person
{
   public:
   string id;
};

Student类公有继承了Person类

继承方式导致访问方式的变化

基类类成员/继承方式 public继承 protectd继承 private继承
基类的公有成员 成为派生类公有成员 成为派生类保护成员 成为派生类私有成员
基类的保护成员 成为派生类保护成员 成为派生类保护成员 成为派生类私有成员
基类的私有成员 在派生类中不可见 在派生类中不可见 在派生类中不可见

总结

(1) 基类的私有成员无论什么方式继承在派生类中都是不可见的,但是在基类中依然是有内存的,派生类对象类内和内外都不能访问它。

(2) protectd继承其实是为了继承而出现的,基类的private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。

(3) 其实有一个这样的关系 public>protected>private,基类的其他成员在子类的访问方式==Min(成员在基类的访问限定符,继承方式)

(4) class默认的继承方式是private,struct默认的继承方式是public

(5) 其实protected/private继承真的甚少用,因为这样的继承维护性和扩展性不强

切片-基类和派生类对象赋值转换

派生类对象可以赋值给基类的对象/基类的指针/基类的引用。其实就是派生类中父类的部分切来赋值过去。

基类的对象不能赋值给派生类对象

基类的指针可以通过强制类型转换复制给派生类指针,但是基类的指向派生类对象的时候才是安全的,不然就会出现内存访问错误,可以使用RTTI 来进行识别后进行安全转换。

继承中的作用域(同名问题)

同名的情况一定要少出现

父子都是独立作用域

(1) 在继承体系中的基类和派生类都是独立的作用域

同名会被隐藏,可以显示访问

(2) 子类和父类有同名成员,子类成员将屏蔽对父类成员的直接访问,这叫做隐藏(重定义),注意隐藏是它还可以存在,如果想访问被隐藏的成员,可以显式访问: 基类::基类成员

函数同名就构成隐藏

(3) 对于成员函数,只要同名就构成隐藏了

派生类的默认成员函数

默认成员函数是指我们不写就给我默认生产的函数

构造函数

派生类的构造函数必须调用基类的构造函数初始化基类的成员,如果基类没有默认构造函数,则必须在派生类构造的初始化列表阶段显示调用。

拷贝构造函数

派生类的拷贝构造函数必须调用基类的拷贝构造函数完成拷贝初始化

operator=

派生类的operator=必须要调用基类的operator=完成基类的复制

析构函数

派生类的析构函数会在被调用完成后自动调用基类的析构函数,这样才能保证派生类对象先清理派生类成员在清理基类成员的顺序。

const对象取地址函数

const对象不能被修改,所以可以重载这个函数,返回一个假地址来保护对象

普通对象取地址函数

实现不能被继承的类

传统写法

思路

将基类的构造函数私有化,因为派生类初始化对象的时候需要先调用基类的构造函数,而如果将基类构造函数设置为私有,派生类无法执行这个过程

这样写的话,要为基类提供一个静态成员函数结构,实现创建一个对象功能。

实现

class A
{
  public:
  static A GetInstance()
  {
      return A();
  }
  private:
  A();
};

C++11

思路

C++11提供了final关键字 加在类后面表示最终类,无法被继承

实现

class A final
{}

友元出现在继承中

友元关系是不能被继承的,你爸爸的朋友不一定是你的朋友

静态成员出现在继承中

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。

复杂的继承(菱形继承和菱形虚拟继承)

单继承

很简单就是一个派生类继承了一个基类

多继承

A继承了B,同时也继承了C

class B
{};
class C
{};

class A:public B,public C
{};

多继承情况下的切片和多继承不一样,多继承的切片基类A的指针会指向A的部分,B基类的指针会指向B的部分,而不是都指向地址的开始位置。同理对于基类的引用/赋值给基类对象也是一个道理。

菱形继承

继承的关系就像一个菱形一样
A,B,C,D四个类,

B继承了D , C也继承了D, A继承了B和C。

class D
{
    public:
    int d;
};

class B:public D
{};
class C:public D
{};

class A:public B,public C
{};

这种继承是明显有数据冗余和数据的二义性问题的。A对象中D的成员会有两份。

如果要访问D中的d必须要通过显式访问

int main()
{
    A a;
    a.B::d;
    a.C::d;
    return 0;
}

虚拟菱形继承

虚拟继承作用和使用

虚拟继承可以解决菱形的二义性和数据冗余问题

上面的例子我们使用虚拟继承就不会出现二义性问题

class D
{
    public:
    int d;
};

class B:virtual public D
{};
class C:virtual public D
{};

class A:public B,public C
{};

因为是虚拟继承D中的成员只有一份
访问数据就不会出现二义性了,也不会有数据冗余了。

int main()
{
    A a;
    a.d;
}

虚拟继承的实现原理

虚拟继承的公共对象会被放到对象的最后面

虚拟继承基类的派生类里面会多出一个指针变量,这个指针变量指向一个表,这个表中存放了找到公共成员位置的偏移量。

image

继承和组合的讨论

继承和组合的区别

继承是一种is-a的关系,就是说每个派生类对象都是基类的对象

组合是has-a的关系,假设B组合了A,每个对象都有一个A的对象

推荐使用组合

继承允许你根据基类的实现来定于派生类的实现,这种通过派生类的复用通常被称为白箱复用(对可视性而言),在继承方式中,基类的内部细对子类是可见的,在一定程度上这破坏了基类的封装,基类的改变也势必导致派生类的改变,耦合度太高,依赖关系强

组合也称为黑箱复用,对象组合要求被组合的对象具有良好的定义接口,这种复用风格被称为黑箱复用,因为对象的内部细节不可见,组合类之间没有很强的依赖关系,耦合度低,优先使用对象组合有助于保持每个类被封装,

要实现多态就必须要用继承

一般情况下能用组合就用组合,多态的情况就需要是用继承了

继承和组合对比

继承 高耦合,破坏基类封装,依赖关系强

组合 低耦合,保护了封装,但是需要被组合的类有良好的接口

吐个槽

C ++ 就不应该出现菱形继承,为了解决的它,有虚拟菱形继承,但是这导致在性能上很差,这个反而是C++的缺陷,其他OO语言(面向对象)就没有多继承,比如java

多态

多态存在的意义

我们买票的时候,针对不同的人,比如说学生和成人买票,票的价格是不一样的,这其实就是一种多态

多态的定义和实现

多态的实现要借助虚函数,实现对函数的覆盖(重写)

虚函数

成员函数前加上virtual,这个成员函数就是一个虚函数

虚函数的重写条件

一般情况下,函数的名字,类型,返回值都必须相同,如果只是函数名相同就变成了隐藏了(继承中讲到的),两个函数必须是虚函数。

当然也有特殊情况,下面介绍不同种情况下的重载。

正常重写

class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}

重写的例外协变

这是一个例外,当返回值一个是基类指针一个是派生类指针的时候,可以构成重写

class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};

不规范的重写

派生类的函数可以不加virtual,基类加virtual,这样也可以构成重写,但是这样非常不规范,我们平时不要这样使用。

class Person {
public:
virtual void BuyTicket() {cout << "买票-全价" << endl;}
};
class Student : public Person {
public:
void BuyTicket() {cout << "买票-半价" << endl;}
};

为什么把析构函数也定义为虚函数

看如下例子即可明白

class Person {
public:
~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};

int main()
{
    Person* p = new Student();
    delete p;
    return 0;
}

因为p调用析构函数只与类型有关,p是Person类型的指针,delete就只会析构基类部分,只调用Person

虽然析构函数的名字不一样,但是其实编译后的名称都是destructor,这里可以理解为特殊处理,所以也算重写

接口继承和实现继承

多态就是接口继承,子类继承父类的某个函数并不是为了要父类的实现,而是要父类的接口,然后自己再重写这个函数

继承就是实现继承,子类就是要继承父类的某个函数的实现,子类就是需要已经实现好了的

重载,覆盖,隐藏的对比

重载:

两个函数在同一个作用域

函数的重载,操作符的重载,要求函数名必须得相同,同时参数列表必须不同

隐藏(重定义)

两个函数分别在基类和派生类作用域

隐藏那是继承里面的东西,只要函数名相同并且不满足覆盖(重写)的条件,派生类就会隐藏基类的同名函数。

覆盖(重写)

两个函数分别在基类和派生类的作用域

函数名/参数/返回值都必须相同(协变例外)

两个函数必须是虚函数

抽象类

概念

包含纯虚函数,包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象,纯虚函数规范了派生类必须重写,纯虚函数也更加体现了接口继承。

定义实现

汽车必须要轮胎,但是具体使用哪个轮子由派生类实现

class Car
{
    public:
    virtual void  tyre() = 0;
};

class Benz :public Car
{
    public:
    virtual void  tyre()
    {
        cout << "Benz-越野轮胎" << endl;
    }
};

class BMW :public Car
{
    public:
    virtual void  tyre()
    {
        cout << "BMW-花纹轮胎" << endl;
    }
};

void Test()
{
    Car* pBenz = new Benz;
    pBenz-> tyre();
    Car* pBMW = new BMW;
    pBMW-> tyre();
}

C++11 override 和 final

final阻止类的进一步派生和虚函数的进一步重写

override确保在派生类中声明的函数跟基类的虚函数有相同的签名

override 相当于为重写检查语法

实际中我们建议多使用纯虚函数+ overrid的方式来强制重写虚函数,因为虚函数的意义就是实现多态,如果
没有重写,虚函数就没有意义

相当于为重写加上一个语法检查机制

class Car{
public:
    virtual void Drive(){}
};


//加上override后相当于加上了一个重写的语法检查
class Benz :public Car {
public:
virtual void Drive() override {
    cout << "Benz-舒适" << endl;}
};

//这样写就是不正确的,因为不满足重写的条件
class Benz :public Car {
public:
virtual int Drive() override {
    cout << "Benz-舒适" << endl;
    return 0;
}
};

final 禁止虚函数被重写

修饰基类的虚函数不能被派生类重写

class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};

上面对于Drive的重写会报错,因为语法不允许

纯虚函数的作用

纯虚函数可以实现抽象类,而抽象类是为了实现接口继承,其实目的是为了实现多态。

多态实现的原理

虚表指针

基类中定义了虚函数,派生类对象和基类的对象中会多处一个虚表指针
,这个虚表指针指向一个虚函数表

虚函数表

虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。

通过查看汇编代码我们可以看到虚函数的调用过程

//eax获得对象的指针
mov     eax,dword ptr [p]
//edx获得对象的第一个字节的内容
mov     edx,dword ptr[eax]
//ecx加载this指针
mov     ecx,dword ptr[p]
//获得对应的虚函数指针,如果对应的虚函数指针是第二个位置,就会执行 eax,dword ptr [edx+4]
mov     eax,dword ptr [edx]
//调用对应的虚函数
call    eax 

父类虚函数表和子类虚函数表关系和区别

(1) 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基
类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数,派生类自己新增加的虚函数按其在,派生类中的声明次序增加到派生类虚表的最后。(监视窗口可能看不到)

//查看看不到的虚函数指针,可以通过打印虚函数表来判断

(2) 覆盖其实就是虚函数表中的函数地址被替换了

(3) 派生类首先继承了父类的虚函数表,然后根据自己是否重写虚函数来判断是否要修改或者添加虚函数表中的内容

(4) 使用对象接收的时候是不会产生多态现象的,因为在拷贝构造的时候不会拷贝虚函数指针

//实参是一个对象,不会有多态现象
void Func(Person p)
{
    p.BuyTicket();
}

int main()
{
    Student mike;
    Func(mike);
    mike.BuyTicket();
    return 0;
}

虚函数表存在哪

(1) 验证虚函数表存在哪个位置:直接打印出对象的表的值,就是对象的第一个字节,然后再打印一个函数地址比较,现象如果比较近,说明虚函数表和函数一样,都是存放在代码区,反之则不是。

(2) 对象的虚函数表指针是在调用拷贝构造函数的时候初始化的

动态绑定和静态绑定

通过汇编代码分析,可以看出,满足多态以后的调用函数,不是在编译时确定的,是运行起来以后通过对象的虚函数指针找到函数的地址的,而不满足多态的函数调用时是编译时就确定好的。

静态绑定–早起绑定

在编译器器件就确定的程序行为,也叫静态多态,比如说函数重载

动态绑定–后期绑定

在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

父类指针和引用可以实现多态,但是父类对象不能实现多态

class Person {
public:
    virtual void BuyTicket() 
    {
        cout << "买票-全价" <<     endl;
    }
};

class Student : public Person {
    public:
    virtual void BuyTicket() {
        cout << "买票-半价"<<     endl; 
    }
};

void Func(Person p)//用父类对象来接受子类对象是不会实现多态的
//因为父类对象拷贝了子类对象的时候,没有拷贝虚函数表指针,所以父类依然是用的父类的虚函数表中的虚函数
{
    p.BuyTicket();
}

int main()
{
    Person Mike;
    Func(Mike);
    Student Johnson;
    Func(Johnson);
    return 0;
}

多继承和多继承中的虚函数表

多继承中派生类会有两个基类的虚函数指针,如果派生类再定义一个虚函数,新的虚函数的指针会储存在其中一张虚函数表中,而不是两个虚函数都添加
,记住:只在其中一个虚函数表添加,只在其中一个虚函数表中添加,只在其中一个虚函数表中添加,重要的事情说三遍!!!

#include <iostream>
#include <windows.h>
#include <stdlib.h>

using namespace std;


class A
{
public:
	virtual void fun1()
	{
		cout << "A::fun1()" << endl;
	}
	int a;
};

class B:virtual public A
{
public:
	int b;
};

class C : virtual public A
{
public:
	int c;
};

class D :public B, public C
{
public:
	int d;
};


int main()
{
	D w;
	w.a = 10;
	system("pause");
	return 0;
}

image

小结

构成多态由对象决定,对象里面的虚函数表不一样

不构成多态由类型决定

继承和多态我们一定要知道

什么是多态?

多态就是同一个行为不同对象去做的时候产生不同的结果,就比如说同样是人,买票的时候成人全票,学生半票。

搞清楚重载和重写(覆盖)和重定义(隐藏)是什么,不要混淆

重载: 一个作用域下的两个函数,他们的函数相同参数不相同就构成了重载

重写: 两个函数,一个在父类作用域,一个在子类作用域,他们的函数名,参数,返回值都相同,并且父类中的函数是个虚函数,或者两个都是虚函数,就构成重写,函数的重写是实现多态的条件。

重定义: 也是两个函数,一个父类作用域,一个是子类作用域,他们的函数名相同,参数不同,换句话说,父类子类作用域下没有满足重写的条件,就是重定义。

实现多态的原理

其实实现多态的原理就是因为有了虚函数表的存在,虚函数表中记录了属于对象的行为。

inline函数可以是虚函数吗

当然不可以是,在内敛成功的情况下,内敛函数连函数的地址都没有,无法放到虚函数表中。

静态成员可以是虚函数吗

不可以,因为静态成员函数没有this指针,而访问虚函数表是需要对象的。因为对象中存了虚函数表的地址

构造函数是可以定义为虚函数吗

不可以,虚函数表指针都是调用构造函数的时候初始化的,如果可以那虚函数指针都没初始化好,怎么调用构造函数呢。

析构函数可以是虚函数吗

可以,并且最好定义为虚函数,不然就可能出现因为对象的不同,释放用户空间的时候,导致内存泄漏

对象的访问普通函数快还是虚函数快

这个显然是前者快,前者编译的时候就确定好了,后者还要不断去虚函数表中找。

虚函数表在什么阶段生产

当然在编译的时候生成了,只不过对象的虚函数表指针是在调用构造函数的时候初始化

C++菱形继承的问题

上面有讲到,需要用心感悟

什么是抽象类,抽象类的目的

首先呢抽象类是为了强制实现重载,而重写是为了实现接口继承,他们其实目的就是为了实现多态。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章