C++多态与多态原理以及重载和重写(覆盖)的区别

C++多态与多态原理以及虚函数表

1. 多态

多态分为两类:静态多态(编译时多态)和动态多态(运行时多态)
  静态多态就是我们所熟悉的函数重载和运算符重载,而派生类和虚函数实现动态多态。
  静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)。

#include<iostream>
using namespace std;

class Father{
public:
    void Func(){
        cout<<"Father::Func()"<<endl;
    }
};
class Son:public Father{
public:
    void Func(){
        cout<<"Son::Func()"<<endl;
    }
};
void doFunc(Father& person){
    person.Func();
}
int main(){
    Father f;
    f.Func();//自身成员函数调用
    doFunc(f);//自身成员函数调用
    cout<<"======================"<<endl;
    Son s;
    s.Func();//同名隐藏
    doFunc(s);//隐式类型转换之后调用父类成员函数
    cout<<"======================"<<endl;
    Son* ps = &s;
    ps->Func();//同名隐藏
    cout<<"======================"<<endl;
    Father* pf = &s;
    pf->Func();//隐式类型转换之后调用父类成员函数
}

运行结果:

在这里插入图片描述
  如果仅是简单的继承关系,不同类型对象调用相同接口(void doFunc(Father& person))想完成不同的行为是不可能的。但是如果做一个简单的转变(在父类成员函数前加个virtual关键字),这就使不同类型对象调用相同接口完成不同的行为变为可能:

#include<iostream>
using namespace std;

class Father{
public:
    virtual void Func(){
    //void Func(){
        cout<<"Father::Func()"<<endl;
    }
};
class Son:public Father{
public:
    void Func(){
        cout<<"Son::Func()"<<endl;
    }
};
void doFunc(Father& person){
    person.Func();
}
int main(){
    Father f;
    f.Func();//自身成员函数调用
    doFunc(f);//自身成员函数调用
    cout<<"======================"<<endl;
    Son s;
    s.Func();//同名隐藏
    doFunc(s);//多态
    cout<<"======================"<<endl;
    Son* ps = &s;
    ps->Func();//同名隐藏
    cout<<"======================"<<endl;
    Father* pf = &s;
    pf->Func();//多态
}

运行结果:
在这里插入图片描述
这就完成了多态。
多态成立的三个条件
1.继承
2.子类重写(覆盖)父类虚函数
  a) 返回值,函数名字,函数参数,必须和父类完全一致(析构函数除外)
  b) 子类中virtual关键字可写可不写,建议写
3.父类指针/引用指向子类

虚函数定义规则
  1.如果虚函数在基类与派生类中出现,仅仅是名字相同,而形式参数不同,或者是返回类型不同,有无const.那么即使加上了virtual关键字,也是不会覆盖
  2.只有类的成员函数才能说明为虚函数,因为虚函数仅适合用与有继承关系的类对象,所以普通函数不能说明为虚函数。
  3.静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。
  4.内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义,但是在编译的时候系统仍然将它看做是非内联的。
  5.构造函数不能是虚函数,因为构造的时候,对象还是一片未定型的空间,只有构造完成后,对象才是具体类的实例。
  6.析构函数可以是虚函数,而且通常声明为虚函数。

2. 多态原理

当父类中有了虚函数后,内部结构就发生了改变,其内部多了一个 vfptr(virtual function pointer虚函数表指针),vfptr指向 vftable (虚函数表)
C/C++虚函数和虚函数表概念及实现原理

虚函数表
虚函数表是一个存储类成员函数指针的数据结构
虚函数表是由编译器自动生成和维护的
virtual函数会被编译器放入虚函数表中
存在虚函数时,每个对象当中都有一个指向虚函数表的指针(vptr指针)

分析下面代码:

#include<iostream>
using namespace std;
class Animal{
public:
	virtual void speak(){
		cout << "动物在说话" << endl;
	}
	virtual void eat(){
		cout << "动物在吃饭" << endl;
	}
};
class Cat :public Animal{
public:
	virtual void speak(){
		cout << "小猫在说话" << endl;
	}
	virtual void eat(){
		cout << "小猫在吃鱼" << endl;
	}
};
void doSpeak(Animal & animal) {//Animal & animal = cat
	animal.speak();
}
//如果发生了继承的关系,编译器允许进行类型转换
void test01(){
	Cat cat;
	doSpeak(cat);
}
int main(){
	test01();
	return EXIT_SUCCESS;
}

运行结果:

小猫在说话

在这里插入图片描述

  1. 当父类声明了虚函数之后,会在类内产生一个vfptr指针,指向其虚函数表,虚函数表内容为&Animal::speak
  2. 在Cat类创建对象的时候,调用构造函数,会将其所有的vfptr都指向自己的虚函数表,表内容为&Cat::speak,这一步,子类写speak父类的虚函数,这种写法叫做重写(override),又叫做覆盖
  3. 这样正在执行时,Animal* animal = new Cat; animal.speak();程序就会直接调用虚函数表里的内容

3. 重载和重写(覆盖)的区别

No.重载重写(覆盖)
1重载要求函数名相同,但是参数列表必须不同,返回值可以相同也可以不同。覆盖要求函数名、参数列表、返回值必须相同。
2在类中重载是同一个类中不同成员函数之间的关系。在类中覆盖则是子类和基类之间不同成员函数之间的关系。
3重载函数的调用是根据参数列表决定。覆盖函数的调用是根据对象类型决定。
4重载函数是在编译时确定调用一个函数。覆盖函数是在执行时确定调用个函数。
  • 14
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值