当前位置: 首页 > article >正文

详解c++多态---上

virtual关键字

1.可以修饰原函数,为了完成虚函数的重写,满足多态的条件之一。

class Person {
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
 
class Student : public Person {
public:
 virtual void BuyTicket() { cout << "买票-半价" << endl; }
 /*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后
基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
 /*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
 
void Func(Person& p)
{ p.BuyTicket(); }
 
int main()
{
 Person ps;
 Student st;
 
 Func(ps);
 Func(st);
 
 return 0;
}

2.可以在菱形继承中,去完成虚继承,解决菱形继承数据冗余和二义性。

多态的两个条件

1.虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类 型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

2.父类对象的指针或引用(父类可以接受子类->切割/切片,子类不一定能接受父类)去调用虚函数

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

将&去掉后就无法满足多态。 

满足多态:跟指向的对象有关,指向那个对象就调用其虚函数。

不满足多态:跟调用对象的类型有关,类型决定其调用的虚函数。

虚函数重写的两个例外:

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

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

父类写了virtual子类可以不写->但是这样的话不太标准。~~~~~~~~~~

析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的 析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规 则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处 理成destructor。

class Person{
public:
    virtual ~Person() {cout<<"~Person()"<<endl;}
};
class Student : public Person {
public:
 virtual ~Student() { cout << "~Student()" << endl; }
};
 
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成
多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
 Person* p1 = new Person;
 Person* p2 = new Student;
 
 delete p1;
 delete p2;
 
 return 0;
}
 virtual ~Student() { cout << "~Student()" << endl; }
};

 子类和父类的析构函数的函数名均会被处理为destructor。

这样使用没有问题。

 delete p2;不构成多态时,调用的指针(Person)类型决定了要调用(Person)的析构函数。

构成多态时,调用的指针指向谁,就调用谁的析构函数。

C++11 override 和 final

final

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

final:修饰类,表示该类不能被继承<----->构造函数私有化

override

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

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

重载、覆盖(重写)、隐藏(重定义)的对比

 重写时为了重写函数实现。

重写比重定义要求更严格。

抽象类

在虚函数的后面写上 =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;
 }
};

 纯虚函数

virtual void Drive() = 0;

1.强制子类去完成重写。 

2.表示抽象(在现实中没有对应的实体)的类型。

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的 继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所 以如果不实现多态,不要把函数定义成虚函数。

vfptr

虚函数表指针(简称虚表指针)。

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

private:
	int _b = 1;
};

 虚函数表其实就是一个函数指针数组。通常以0x00000000 结束。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	int _p = 1;
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	int _s = 2;
};

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

int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);

	return 0;
}

 

 

 满足多态时:

 运行时 到指向对象的虚函数表中查找对应的虚函数的地址

不满足多态时:

编译时 直接通过p的类型决定要调用函数的地址。

重写之后子类的虚函数会将父类的虚函数(virtual void BuyTicket())覆盖掉。

虚函数存在那?

普通函数和虚函数最终都会编译为指针,存在代码段。

注:虚函数不是存在虚表,虚表中存的是虚函数的指针。

虚函数表存在那?

代码段(常量区)同类型的对象共用一张虚表。

代码证明

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

private:
	int _b = 1;
};
void func()
{
	//取出对象的前4个字节
	Base b1;
	printf("vfptr虚表地址: %p\n", *(int*)&b1);

	int i = 0;
	int* p1 = &i;
	int* p2 = new int;
	const char* p3 = "hello";
	printf("栈变量: %p\n", p1);
	printf("堆变量: %p\n", p2);
	printf("代码段常量: %p\n", p3);
	printf("虚函数地址: %p\n", &Base::Func1);
	printf("普通函数地址: %p\n", func);

}

动态绑定与静态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数 重载

2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用 具体的函数,也称为动态多态。 (运行时到虚表中找虚函数地址)

单继承和多继承关系的虚函数表

单继承中的虚函数表

函数指针的定义

void(*p)();
class Base{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};

class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};

 打印虚函数表

typedef void(*VF_PTR)();
void PrintVFTable(VF_PTR* pTable)
{
	for (size_t i = 0; pTable[i] != 0; ++i)
	{
		printf("vfTable[%d] : %p-> ", i, pTable[i]);
		VF_PTR f = pTable[i];
		f();
	}
	cout << endl;
}

多继承中的虚函数表

class Base1 {
public:
 virtual void func1() {cout << "Base1::func1" << endl;}
 virtual void func2() {cout << "Base1::func2" << endl;}
private:
 int b1;
};
 
class Base2 {
public:
 virtual void func1() {cout << "Base2::func1" << endl;}
 virtual void func2() {cout << "Base2::func2" << endl;}
private:
 int b2;
};
 
class Derive : public Base1, public Base2 {
public:
 virtual void func1() {cout << "Derive::func1" << endl;}
 virtual void func3() {cout << "Derive::func3" << endl;}
private:
 int d1;
};
 

}

sizeof(Derive)=20(8+8+4)

子类继承父类,会继承父类的虚表,子类直接覆盖父类的虚表即可。

char*+1---->+1

int*+1----->+4

	Derive d;
	PrintVFTable((VF_PTR*)(*(int*)&d));
	PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))));

多继承时func3()往Base1的虚表里放。

多态练习题

1.子类缺省参数不起作用/虚继承

class A
{
public:
 virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
 virtual void test(){ func();}
};
 
class B : public A
{
public:
 void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
 
int main(int argc ,char* argv[])
{
 B*p = new B;
 p->test();
 return 0;
}

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

virtual void test(){//A* this
 func();
}
p->test();//p->test(p);
class B : public A
{
public:
 void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};

子类缺省参数不起作用。

虚函数继承的是接口(函数名,参数,返回值),唯独不继承函数体。

重写指的是对其实现进行重写。

{ std::cout<<"B->"<< val <<std::endl; }

1. 下面哪种面向对象的方法可以让你变得富有( )

A: 继承 B: 封装 C: 多态 D: 抽象

2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的 调用则可以关联于具体的对象。

A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定(多态)

3. 面向对象设计中的继承和组合,下面说法错误的是?()

A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为 白盒复用

B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也 称为黑盒复用

C:优先使用继承,而不是组合,是面向对象设计的第二原则

D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现

4. 以下关于纯虚函数的说法,正确的是( )

A:声明纯虚函数的类不能实例化对象

B:声明纯虚函数的类是虚基类

C:子类必须实现基类的纯虚函数

D:纯虚函数必须是空函数

5. 关于虚函数的描述正确的是( )

A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型

B:内联函数不能是虚函数

C:派生类必须重新定义基类的虚函数

D:虚函数可以是一个static型的函数

6. 关于虚表说法正确的是( )

A:一个类只能有一张虚表-》多重继承

B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表

C:虚表是在运行期间动态生成的-》编译时生成,运行时初始化

D:一个类的不同对象共享该类的虚表

7. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )

A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址

B:A类对象和B类对象前4个字节存储的都是虚基表的地址

C:A类对象和B类对象前4个字节存储的虚表地址相同

D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

多继承中指针偏移问题

下面说法正确的是( )

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
 
int main(){
 Derive d;
 Base1* p1 = &d;
 Base2* p2 = &d;
 Derive* p3 = &d;
 return 0;
}


http://www.kler.cn/news/305731.html

相关文章:

  • 移动应用开发与测试赛题2
  • 将 YOLOv10 模型从 PyTorch 转换为 ONNX
  • 前端开发的单例设计模式
  • Leetcode面试经典150题-202.快乐数
  • 人工智能时代,程序员如何保持核心竞争力?
  • CSP-J 计算机网络
  • CSS 圆角渐变边框
  • Linux软件安装
  • 虚幻5|使用F插值到,击打敌人使UI血条缓慢缩减|小知识(3)
  • 利用 Vue.js 自定义指令实现权限控制:问题解析与最佳实践20240912
  • 网络通信安全:全面探索与深入分析
  • python的流程控制语句之制作空气质量评估系统
  • 国产化中间件正在侵蚀开源中间件
  • 使用 Vue.js 将数据对象的值放入另一个数据对象中
  • Redis 集群高可用详解及配置
  • mfc140u.dll文件错误的相关修复方法,4种方法修复mfc140u.dll
  • 计算机毕业设计选题推荐-推拿知识互动平台-Java/Python项目实战
  • 通信工程学习:什么是UNI用户网络接口
  • 漏洞复现-泛微-E-Cology-SQL
  • 基于python+django+vue的视频点播管理系统
  • 半导体制造技术中的沉积和驱入(Deposition and drive-in)过程
  • Vue3 全文知识点
  • Spring Boot 驱动的在线读书笔记平台
  • Vue与React的Diff算法
  • 让人物照片跳舞vigen追影
  • Pytorch+Anaconda+Pycharm+Python
  • 工程师 - HUE(Humans in User Experience)介绍
  • Puppeteer-Cluster:并行处理网页操作的新利器
  • MyBatis中多对一关系的三种处理方法
  • 四、Django模型