首页 > 编程语言 >详解c++多态---上

详解c++多态---上

时间:2024-09-15 17:24:27浏览次数:12  
标签:函数 继承 多态 c++ class --- virtual 重写 public

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;
}

标签:函数,继承,多态,c++,class,---,virtual,重写,public
From: https://blog.csdn.net/2301_77479435/article/details/142280838

相关文章

  • 帝国cms备份的数据库文件夹-帝国CMS备份中心
    帝国CMS提供了内置的备份工具,称为“帝国备份王”,用于备份和恢复数据库。备份的数据库文件通常会被保存在一个特定的文件夹中。以下是关于帝国CMS备份的一些基本信息和备份文件夹的位置:备份文件夹位置帝国CMS备份的文件通常保存在网站根目录下的/data/backup/文件夹中。在这个文......
  • 中间件知识点-消息中间件(Kafka)二
    Kafka一、Kafka介绍及基本原理kafka是一个分布式的、支持分区的、多副本、基于zookeeper的分布式消息系统/中间件。kafka一般不会删除消息,不管这些消息有没有被消费。只会根据配置的日志保留时间(log.retention.hours)确认消息多久被删除,默认保留最近一周的日志消息(日志......
  • 博客建站9 - hexo网站如何提升markdown文档的编辑效率和体验
    1.本网站的系统架构2.场景概述3.影响效率的问题和解决方案3.1.图片插入-根据文章来分类管理3.1.1.效率问题3.1.2.解决方案3.2.图片插入-从剪贴板中插入图片3.2.1.效率问题3.2.2.解决方案3.3.图片插入-在VSCode中预览图片3.3.1.效率问题3.3.2.解决......
  • c++走出迷宫改良版2
    本文对上期做了删改话不多说上代码:注彩色输出部分代码出自博主夜若渊#include<bits/stdc++.h>#include<windows.h>#include<stdlib.h>#include<cstdio>#include<iostream>#include<string>#include<stdio.h>#include<ctime>#include<conio.h&g......
  • 使用fake-useragent库伪装请求头
    部分网站做了反爬虫机制,不允许程序访问网站的数据,而使用同一个useragent(用户代理)短时间爬取大量数据也可能被网站反爬虫程序识别。为了更好地模拟浏览器地工作,可以使用第三方库fake-useragent生成假的useragent字符串伪装浏览器,从而绕过一些网站的反爬虫措施。首先在命令行中输入......
  • 深入解析C++函数指针:掌握游戏开发中的关键技术
    深入解析C++函数指针:掌握游戏开发中的关键技术C++作为一门经典的编程语言,因其强大的性能和灵活性,被广泛应用于游戏开发。而函数指针作为C++中的一个重要概念,在游戏开发中更是扮演着不可或缺的角色。对于想要深入掌握C++并在游戏开发领域站稳脚跟的开发者来说,理解并灵活运用函数指......
  • 帝国cms数据库在哪-帝国CMS
    帝国CMS(EmpireCMS,简称ECMS)的数据库文件通常位于服务器上的某个特定目录内。根据提供的信息,帝国CMS数据库文件的位置大致如下:主数据库文件:empiremaker.sql:包含完整的数据结构。empiremaker_add.sql:包含增补的数据结构。empiremaker_config.sql:包含系统的配置信息。模块......
  • 华为OD机试真题-水仙花数-2024年OD统一考试(E卷)
    最新华为OD机试考点合集:华为OD机试2024年真题题库(E卷+D卷+C卷)_华为od机试题库-CSDN博客  题目描述所谓水仙花数,是指一个n位的正整数,其各位数字的n次方和等于该数本身。例如153是水仙花数,153是一个3位数,并且153=1^3+5^3+3^3。输入描述第一行输入一个整数n,表示一个n位的......
  • UG 二次开发-菜单与工具条(二、 应用DLL入口编写)
    在上一篇中,我们编写了UG加载DLL入口写入,这一篇,我们将继续编写应用DLL入口,上一篇中,最后是入加DLL,加载应用DLL,并入口函口数。首先,我们新建应用DLL主工程,工程名称我们就叫做IUGMain。完成工程创建。添加一个新的CPP,作为DLL的主程式文件由于我们这是由空白工程创建的应用......
  • Django的高校学生信息管理系统的设计与实现-附源码02553
    摘 要本研究旨在设计和实现基于Django框架的高校学生信息管理系统,涵盖了系统用户、学生信息管理、教师信息管理、课程分类管理、开课信息管理、选课信息管理、课表信息管理、成绩信息管理、系统管理、网站公告管理和校园资讯等多个功能模块。通过系统的建立,旨在提供一个全面......