首页 > 其他分享 >多态

多态

时间:2024-05-31 14:11:21浏览次数:12  
标签:函数 派生类 多态 virtual 基类 重写 public

概念:通俗的来讲多态就是多种形态,具体就是去完成某个行为,不同的对象去完成时会产生出不同的状况。
例如:当同样去买票时,学生买的是学生票,而普通人则只能买普通票。

多态的构成条件

  1. 必须通过基类的指针或引用调用虚函数。
  2. 被调用的函数必须是虚函数,而且派生类必须对基类的虚函数进行重写。
class Person {
public:
	virtual void BuyTicket() {
		cout << "Person::全价" << endl;
	}
};

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

虚函数

虚函数:被virtual修饰的成员函数称为虚函数。

注意:这里的virtual与虚拟继承的virtual没有关系,仅仅是关键字相同。

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

虚函数的重写

虚函数的重写又称为覆盖,即派生类中有一个与基类完全相同的虚函数(完全相同是指返回值类型相同,函数名相同,函数参数列表相同。)

class Person {
public:
	virtual void BuyTicket() {
		cout << "Person::全价" << endl;
	}
};
/*在重写基类虚函数时,virtual也可以不用写,但一般不建议这样*/
class Student : public Person {
public:
	virtual void BuyTicket() {
		cout << "Student::半价" << endl;
	}
};

void test(Person& p) {
	p.BuyTicket();
}

int main() {
	Person per;
	Student stu;

	test(per);
	test(stu);
}

从上面可以看出在调用虚函数时,与指针或引用的类型无关,与指针或引用对象的类型有关。

在test函数中,参数的类型为什么是Person而不是Student类型呢?这就与继承有关了,因为子类的对象可以赋值给基类的对象、指针、引用。

虚函数重写的例外

  1. 协变(基类与派生类虚函数的返回值不同)

    基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用,这种情况称为协变。

    class A {};
    class B : public A {};
    class Person {
    public:
    	virtual A* BuyTicket() {
    		cout << "Person::全价" << endl;
    		return nullptr;
    	}
    };
    
    class Student : public Person {
    public:
    	virtual B* BuyTicket() {
    		cout << "Student::半价" << endl;
    		return nullptr;
    	}
    };
    
  2. 析构函数的重写(基类与派生类析构函数的名字不同)

    如果基类的析构函数为虚函数,那么此时的派生类的析构函数无论是否有virtual关键字,都与基类的析构函数构成重写。从表面上看析构函数的名字不同,但是编译器会将析构函数统一处理为destructor。

    //构成重写
    class Person {
    public:
    	virtual ~Person() {
    		cout << "~Person()" << endl;
    	};
    };
    
    class Student : public Person {
    public:
    	virtual ~Student() {
    		cout << "~Student()" << endl;
    	}
    };
    

重载、重写、重定义的对比

  • 重载
    • 两个函数在统一作用域。
    • 函数名、参数相同。
  • 重写(覆盖)
    • 两个函数分别在基类和派生类的作用域。
    • 函数名、参数、返回值必须相同(协变例外)。
    • 两个函数必须是虚函数。
  • 重定义(隐藏)
    • 两个函数分别在基类和派生类的作用域。
    • 函数名相同。
    • 两个基类和派生类的同名函数不构成重写就是重定义。

c++中的final和override

  1. final:修饰虚函数,表示该虚函数不能再被重写。

    class Person {
    public:
        //此时不能被重写
    	virtual void BuyTicket() final {
    		cout << "Person::全价" << endl;
    	}
    };
    
    class Student : public Person {
    public:
    	virtual void BuyTicket() {
    		cout << "Student::半价" << endl;
    	}
    };
    
  2. override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写编译报错。

抽象类

纯虚函数:虚函数后面写上 =0 。

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象,因此纯虚函数规范了派生类必须重写。

class Car {
public:
	virtual void Drive() = 0;//纯虚函数
};

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

class BMW : public Car {
public:
	virtual void Drive() {
		cout << "BMW" << endl;
	}
};

void Test() {
	Car* pBenz = new Benz;
	pBenz->Drive();

	Car* pBMW = new BMW;
	pBMW->Drive();

	Car pBenz;//实例化不出来
}

接口继承和实现继承

接口继承:虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。

实现继承:普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数。

综上,如果不实现多态,就不要将函数定义为虚函数。

虚函数表

//在X86程序下
class Base {
public:
	virtual void Fun() {
		cout << "Fun()" << endl;
	}

private:
	int _b = 1;
};

int main() {
	cout << sizeof(Base) << endl;
}

在上述程序中发现Base是8byte。那为什么不是4byte呢?因为在Base类中除了有_b成员,还会有一个_vfptr在对象的前面,这个_vfptr称为虚函数表指针。一个含有虚函数的类中都至少有一个虚函数表指针,因为类中的虚函数地址都要放到虚函数表中。

那么在派生类中的虚函数表又是什么样子呢?

class Base {
public:
	virtual void Fun1() {
		cout << "Base::Fun1()" << endl;
	}

	virtual void Fun2() {
		cout << "Base::Fun2()" << endl;
	}

	virtual void Fun3() {
		cout << "Base::Fun3()" << endl;
	}

private:
	int _b = 1;
};

class Derive : public Base {
public:
	virtual void Fun1() {
		cout << "Derive::Fun1()" << endl;
	}

private:
	int _d = 2;
};

int main() {
	Base b;
	Derive d;
	cout << sizeof(b) << endl;
	cout << sizeof(d) << endl;
}

通过调试可以看出:

  1. 派生类的虚表继承自基类,当发生重写时基类的虚表和派生类的虚表是不一样的,d的虚表存的是重写的Fun1()的地址。
  2. Fun3()也继承下来了,但由于它不是虚函数所以不会被放到虚函数表中。
  3. 虚函数表本质是一个存虚函数指针的指针数组,一般情况下数组的最后面放了一个nullptr
  4. 派生类虚表生成过程:
    1. 先将基类中的虚表内容拷贝一份到派生类的虚表中。
    2. 如果派生类重写了基类中的某个虚函数,则用派生类自己的虚函数覆盖虚表中基类的虚函数
    3. 派生类自己新增的虚函数按其在派生类中的声明次序增加到派生类虚表的后面。

动态绑定与静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称静态多态。

动态绑定又称后期绑定(晚绑定),在程序运行期间,根据具体的类型确定程序的举起行为,调用具体的函数,也称为动态多态。

只有虚函数有动态绑定,其他的都是静态绑定。

class Base {
public:
	virtual void Fun1() {
		cout << "Base::Fun1()" << endl;
	}

	void Fun3() {
		cout << "Base::Fun3()" << endl;
	}

private:
	int _b = 1;
};

class Derive : public Base {
public:
	virtual void Fun1() {
		cout << "Derive::Fun1()" << endl;
	}

	void Fun3() {
		cout << "Derive::Fun3()" << endl;
	}

private:
	int _d = 2;
};

int main() {
	Base b;
	Derive d;
	//静态绑定,发生在编译期
	Base* ptr = &b;
	ptr->Fun3();
	ptr = &d;
	ptr->Fun3();

	//动态绑定,发生在运行期
	ptr = &b;
	ptr->Fun1();
	ptr = &d;
	ptr->Fun1();
}

标签:函数,派生类,多态,virtual,基类,重写,public
From: https://www.cnblogs.com/zhiheng-/p/18224440

相关文章

  • C++:虚表指针、虚表、虚函数和动态多态
    classBase{public:virtualvoidshow(){std::cout<<"Baseshow"<<std::endl;}};classDerived_1:publicBase{public:voidshow()override{std::cout<<"Derivedshow"<<std::endl;}};class......
  • C#中多态的实际例子及好处与风险
    好处:代码复用:基类的通用行为可以在派生类中重用。灵活性和扩展性:新增派生类时,无需修改现有代码,符合开闭原则。简化设计:使用抽象接口而非具体类进行编程,提高代码抽象层次。潜在风险:性能损失:虚方法调用相比直接调用非虚方法略慢。设计复杂度:过度使用多态可能导致设计过于复......
  • 伴生对象与多态
    packagecom.tencent.bk.devops.atom.task.repositoryimportcom.tencent.bk.devops.atom.task.utils.RepoAuthTypedataclassCodeBitbucketRepository(overridevalaliasName:String,overridevalurl:String,overridevalcredentialId:String,overrideval......
  • java 三大特性之多态
    多态多态就是基于继承条件下,具有对象多态(一个人可以是儿子,可以是父亲等),行为多态(都可以跑,但跑的有快有慢)。特点1.多态存在方法重写2.多态编译看左边,运行看右边3.变量没有多态性4.多态下不能使用子类的独有功能好处多态可以使程序有良好的扩展,并可以对所有类的对象进行通......
  • C++技能进阶指南——多态语法剖析
            前言:多态是面向对象的三大特性之一。顾名思义,多态就是多种状态。那么是什么的多种状态呢?这里的可能有很多。比如我们去买火车票,有普通票,学生票;又比如我们去旅游,有儿童票,有成人票等等。这些都是多态的例子。具体转化为我们的编程思想就是:让不同类型......
  • 【Java笔记】第8章:面向对象的三大特性(封装、继承、多态)
    前言1.三大特性概述2.封装3.继承4.多态结语#include<GUIQU.h>intmain{上期回顾:【Java笔记】第7章:面向对象个人主页:C_GUIQU归属专栏:【Java学习】return一键三连;}前言各位小伙伴大家好!上期小编给大家讲解了Java中的面向对象,接下来讲讲Java中面向......
  • aardio 实现封装继承多态
    //Car实现封装继承多态importconsole//父类classCar{ctor(make,model,color,year){//构造函数,用于初始化对象的属性this.make=make//制造商this.model=model//型号this.color=color//颜色this.year=year//年......
  • java —— 封装、继承、接口和多态
    一、封装封装是将数据和操作这些数据的方法整合成一个类。在这个类中,用private修饰符将某些数据隐藏起来,只通过特定的方法实现这些数据的访问和修改,以此实现数据的完整和安全性。封装的步骤:二、继承 继承是指把子类共有的某些属性或方法抽离出来,编写为父类,这样多个子类......
  • OOP笔记 —— 多态(Polymorphism)
    多态就是同一个方法的不同实现(即:相同的函数名,不同的函数体)多态的精髓在于父类指针的使用:将子类的地址赋给父类指针,即父类指针指向子类对象注意:用指针去调用成员方法时,通过“->”符号1.虚函数(VirtualFunction)此处的虚函数是指非纯虚函数。定义:非纯虚函数是一个带有virt......
  • 多态性
    定义一个shape抽象类,派生出rectangle类和circle类,计算各类派生类对象的面积area().#include<iostream>usingnamespacestd;classshape{public:virtualdoublearea()=0;};classrectangle:publicshape{public:rectangle(doublew,......