C++多态 动态联编 静态联编 虚函数 抽象类 final override关键字

news2025/1/14 9:38:12

C++多态

  • 多态
    • 多态原理
  • 动态联编和静态联编
  • 纯虚函数和抽象类
  • C++11的final override关键字
  • 重载 隐藏 重写的区别

多态

1.派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。
否则被认为是同名覆盖,不具有多态性。
如基类中返回基类指针,派生类中返问派生类指针是允许的,这是一个例外(协变)。
2.只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。友元函数和全局函数也不能作为虚函数。因为要this指针
3.静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
4.内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
5.构造函数和拷贝构造函数不能作为虚函数。构造函数和拷贝构造函数是设置虚表指针。
6.析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
7.实现运行时的多态性,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现运行时的多态性。
8.在运行时的多态,函数执行速度要稍慢一些:为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价,但通用性是一个更高的目标。
9.如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。正确的定义必须不包括virtual。

名字粉碎技术是编译时的多态
运行时的多态:满足两个条件,把函数定义成需,用指针或者引用调用虚函数

运行时的多态:公有继承+虚函数+指针或者引用调用虚函数

当一个动物类型引用狗类型的时候,会调用狗的虚方法,不会调用动物的虚方法,

class Animal {
private:
	string name;
public:
	Animal(const string na) : name(na)
	{ }
	~Animal() {}
	virtual void eat() { cout << "eat ... " << endl; }//virtual void walk ( { cout <<"walk ... " enG
	virtual void PrintInfo() {}
	const string& GetName() const { return name; }
};
class Dog :public Animal {
private:
	string owner;
public:
	Dog(const string& own, const string& na) :Animal(na),owner(own) {

	}
	~Dog(){}
	virtual void eat() { cout << "eat :bone " << endl; }
	virtual void PrintInfo() {
		cout << "owner:  " << owner << endl;
		cout << "Dog name:  " << GetName() << endl;
	}
};
class Cat :public Animal {
private:
	string owner;
public:
	Cat(const string& own, const string& na) :Animal(na), owner(own) {

	}
	~Cat() {}
	virtual void eat() { cout << "eat :fish " << endl; }
	virtual void PrintInfo() {
		cout << "owner:  " << owner << endl;
		cout << "Cat name:  " << GetName() << endl;
	}
};
void funa(Animal& an) {
	cout << typeid(an).name() << endl;
	an.eat();
	
}
void funb(Animal*p) {

	if (p == nullptr)return;
	p->eat();
}
int main() {
	Dog dog("yhping", "hashiqi");
	Cat cat("tulun", "xiaofei");
	funb(&dog);
	funb(&cat);
	return 0;
}

在这里插入图片描述

...

void funa(Animal& an) {
	cout << typeid(an).name() << endl;
	an.eat();
	
}
void funb(Animal*p) {

	if (p == nullptr)return;
	p->eat();
}
int main() {
	Dog dog("yhping", "hashiqi");
	Cat cat("tulun", "xiaofei");
	funa(dog);
	funa(cat);
	return 0;
}

在这里插入图片描述

void func(Animal an) {
	cout << typeid(an).name() << endl;
	an.eat();

}
int main() {
	Dog dog("yhping", "hashiqi");
	Cat cat("tulun", "xiaofei");
	func(dog);
	func(cat);
	return 0;
}

在这里插入图片描述

多态原理

class Object
{
private:  int value;
public:Object(int x = 0) :value(x) {	}
	  virtual void add() { cout << "0bject: :add()" << endl; }
	  virtual void fun() { cout << "Object: :fun()" << endl; }
	  virtual void print() const { cout << "Object : :print()" << endl; }
};
class Base : public Object {
private:int num;
public: Base(int x = 0) :Object(x), num(x + 10) {  }
	  virtual void add() { cout << "Base: :add()" << endl; }
	  virtual void fun() { cout << "Base: :fun()" << endl; }
	  virtual void show() {
		  cout << "Base: : show()" << endl;
	  }
};

class Test : public Base {
private:
	int count;
public:
	Test(int x = 0) :Base(x), count(x + 10) {}
	virtual void add() { cout << "Test: :add()" << endl; }
	virtual void print() const { cout << "Test : :print()" << endl; }
	virtual void show() { cout << "Test : :show()" << endl; }
};

一旦在基类中指定某成员函数为虚函数,那么,不管在派生类中是否给出virtual声明,派生类(以及派生类的派生类,…)中对其重定义的成员函数均为虚函数

Object::RTTI 运行时的类型识别信息
Object::vftable

继承虚表,需要重写,重复的虚函数更改所属类别,没有重复的继承,新的虚函数添加就行
基类和派生类有相同的函数,没有定义为虚,就是同名隐藏,
基类给一个虚函数,派生类重写虚函数,符合三同,就是同名覆盖,覆盖虚表里面函数的地址

虚表存储示意图
在这里插入图片描述

Object obj 定义一个对象,开辟了8个字节,一个是存储整型val,另外存储虚表指针__vfptr,首先将__vfptr指向第一个虚函数首地址,即Object::add函数的入口地址,再接着构建val的值0,由构造函数设置虚表指针
再构建Base base对象 base有一个基对象 Object,该基对象也有虚表指针
构建过程为:到达Base的构造函数,但是并不构建base,先构建公有继承的基类,到达obj的构造函数,用x初始化val值之前,使该虚表指针指向obj虚表的首地址,用x初始化val值,构建完基类型,回到base的构造函数,构造成员num之前,对base的虚表重新构建,使虚表指针指向base的地址,然后再构建num的值,obj的大小为8字节,base的大小为12字节
每个对象的虚表指针最终指向该对象的虚表

在这里插入图片描述

int main() {
	Object* op = nullptr; 
	Test test;
	Base base;
	op = &test;
	op->add(); 
	op->fun(); 
	op->print();
	op = &base; 
	op->add(); 
	op->fun(); 
	op->print();
	return 0;
}

这里op指向test的地址,调用函数的话,就查该对象自己的虚表,即调用
Test::add Base::fun Test::print
拿指针或者引用来调用虚函数时,需要查虚表
指针指向哪个对象,调用虚方法的时候,就查哪个对象的虚表
虚表只有一份,同类型的对象共享一份,虚表存放在代码区或者数据区

如果对虚函数表理解不到位,可以看这一篇博客
链接: 虚函数表

动态联编和静态联编

比较清楚的一片博客:动态联编和静态联编
静态联编(static binding)早期绑定:静态联编是指在编译和链接阶段,就将函数实现和函数调用关联起来。
C语言中,所有的联编都是静态联编,并且任何一种编译器都支持静态联编。C++语言中,函数重载和函数模板也是静态联编。
C++语言中,使用对象名加点".“成员选择运算符,去调用对象虚函数,则被调用的虚函数是在编译和链接时确定。(称为静态联编)。
动态联编(dynamic binding)亦称滞后联编(late binding)或晚期绑定:动态联编是指在程序执行的时候才将函数实现和函数调用关联起来。
C++语言中,使用类类型的引用或指针调用虚函数(成员选择符”>"),则程序在运行时选择虚函数的过程,称为动态联编。

class Object {
private:
int value; public:
	Object(int x = 0) : value(x) {
		memset(this,0, sizeof(Object));
	}
	void func() { cout << "Object : : func: " << value << endl; }
	virtual void add(int x) { cout << "0bject::add: " << x << endl; }
};
int main() {
	Object obj;
	Object* op = &obj;
	obj.add(1); //这里可以
	op->add(2);
}

memset(this,0, sizeof(Object)) 在虚表构建完成后,初始化x的值后,又将this指针指向的对象的所有值都置为0,即__vfptr和val都置为了0
obj.add(1);这里是静态联编,op->add(2);这里需要查表,此时虚表指针已经变成了nullptr,程序会崩溃

拿指针或者引用调用虚函数是动态联编,和拿对象调用虚函数是静态联编,
add(1); //this->add(1);add(this,1) 类的成员函数调用其他成员函数,都有一个this指针,所以需要查虚表

class Object{
private:
int value; 
public:
	Object(int x = 0) : value(x) {}
void print()
{
	cout << "Object : : print" << endl; 
	add(1);
}
virtual void add(int x)
{
	cout << "0bject: :add: " << x << endl;
}
};
class Base : public Object {
private: int num; public:
	Base(int x = 0) :Object(x + 10),num(x) { }
	void show()//Base*const this
	{
		cout << "Base : : show" << endl; 
		print();//this->print() 
	}
	virtual void add(int x)
	{
		cout << "Base: :add: " << x << endl;
	}
};
int main() {
	Base base; 
	base.show();
	return 0;
}

在这里插入图片描述

class Object {
private:
int value; public:
	Object(int x = 0) : value(x) {
		cout << "Create Object: " << endl; 
		add(11);
	}
	~Object() {
		cout << "Destory 0bject" << endl; 
		add(12);
	}
	virtual void add(int x)
	{
		cout << "Object: : add: " << x << endl;
	}
};
class Base : public Object{
private: int num; public:
Base(int x = 0) : Object(x + 10),num(x) {
cout << "Create Base " << endl; 
add(21);
}
~Base() {
cout << "Destroy Base" << endl; 
add(22);
}
virtual void add(int x)
{
 cout << "Base: :add: " << x << endl;
} };
int main() {
	Base base; 
	return 0;
}

凡是在构造函数和析构函数调用虚函数,都是静态联编
在这里插入图片描述

指针加一,跟指向对象没有关系,只跟自己的类型有关系

class Object{
private:
int value; public:
	Object(int x = 0) : value(x) {
		cout << "Create Object: " << endl;
	}
	~Object() {
		cout << "Destory 0bject" << endl;
	}
	virtual void Print()const
	{
		cout << "value:"<<value << endl;
	}
};
class Base : public Object {
private: int num; public:
	Base(int x = 0) : Object(x + 10), num(x) {
		cout << "Create Base " << endl;
	}
	~Base() {
		cout << "Destroy Base" << endl;
	}
	virtual void Print()const {
		cout << "num" << num << endl;
	}
};

int main() {
	Object* op = new Base(10);
	op->Print();
	delete op;
	return 0;
}

在这里插入图片描述
delete op; 这里派生类对象没有调用析构函数,所以将基类析构函数定义为虚

class Object{
private:
int value; public:
	Object(int x = 0) : value(x) {
		cout << "Create Object: " << endl;
	}
	virtual ~Object() {
		cout << "Destory 0bject" << endl;
	}
	virtual void Print()const
	{
		cout << "value:"<<value << endl;
	}
};
class Base : public Object {
private: int num; public:
	Base(int x = 0) : Object(x + 10), num(x) {
		cout << "Create Base " << endl;
	}
	virtual ~Base() {
		cout << "Destroy Base" << endl;
	}
	virtual void Print()const {
		cout << "num" << num << endl;
	}
};

int main() {
	Object* op = new Base(10);
	op->Print();
	delete op;
	return 0;
}

在这里插入图片描述

析构函数可以定义成虚,构造函数和拷贝构造函数不能定义为虚
如果一个类型不具备派生对象,将析构函数定义为虚就没有意义
在继承关系,并且基类有虚方法,就要把基类析构函数定义为虚,
如果对为什么基类析构函数定义为虚不理解,可以看看这一篇博客
链接: C++中虚析构函数的作用及其原理分析

纯虚函数和抽象类

抽象类的概念:含有纯虚函数的类是抽象类。
抽象类是一种特殊的类,它是为抽象的目而建立的,它处于继承层次结构的较上层。
抽象类不能实例化对象,因为纯虚函数没有实现部分,所以含有纯虚函数类型不能实例化对象;

虚函数实现依赖派生类,基类就是抽象类,析构函数定义为虚函数
无法定义对象,但是可定义指针,定义指针的时候不用实例化对象

抽象类只能用作其他类的基类,不能创建抽象类的对象。
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类型。

class Shape {
private:
	std::string _sname;
public:
	Shape(const std::string& name) : _sname(name) {}
	virtual ~Shape() {}
public:
	virtual void draw() const = 0;
	virtual void area() const = 0;
};
class Circle : public Shape {
private:
	static const float pi;
	float _radius;
public:
	Circle(const string& name, float r = 0) :Shape(name), _radius(r) {}
	~Circle(){}
	virtual void draw() const {}
	virtual void area() const {}
};
const float Circle::pi = 3.14;
int main() {
	//Shape a;//err
	Circle cir("ddd", 2);
	return 0;
}

如果没有将继承的纯虚函数给出具体的实现,那么继承的派生类也是抽象类,所以继承的派生类需要将基类的纯虚函数给出具体的实现,否则无法定义对象
接口就是 函数的返回类型 函数名 形参列表
//应用类型,不提供派生,也不继承;

class cDateTime
{};

//节点类型,提供了继承和多态的基础,但没有纯虚函数,

class shape
{
string sname;
public:
virtual float area() const { return 0.0f;}
string getName() const;
};

抽象类型;抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出;

class Shape{
	string sname;
	public:
	virtual float area()const=0;
	string getName()const;
};
	

//接口类;没有属性,所以的函数都是纯虚函数;

class Shape{
public:
virtual void draw() const = 0;
virtual float area() const = 0;
};

//实现类﹔是继承了接口或抽象类型,定义了纯虚函数的实现;

class circle : public Ishape
{
public:
virtual void draw() const 
virtual void erea() const { return 0;}
}

有时希望派生类只继承成员函数的接口(声明),纯虚函数;
有时希望派生类同时继承函数的接口和实现,但允许派生类改写实现,虚函数。
有时则希望同时继承接口和实现,并且不允许派生类改写任何东西,非虚函数。

C++11的final override关键字

C++11中增加了final关键字来限制某个类不能被继承,或者某个虚函数不能被重写。如果修饰函数,final只能修饰虚函数,并且要放到类或者函数的后面。

virtual void fun() final =0;

这是矛盾冲突的,=0说明是纯虚函数,就是等着重写,而final后面不准重写

重载 隐藏 重写的区别

重载:是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型

隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/679174.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

NCI Architecture

2.1 组成部分 NCI 可分为以下逻辑组件&#xff1a;  NCI 核心 NCI 核心定义了设备主机 (DH) 和 NFC 控制器 (NFCC) 之间通信的基本功能。 这使得 NFCC 和 DH 之间能够进行控制消息&#xff08;命令、响应和通知&#xff09;和数据消息交换。  传输映射 传输映射定义 N…

Excel百万级别数据的导入和导出【详细代码】

代码层级结构 DurationAspect package com.zhouyu.aspect;import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.A…

MFC 截图功能实现(2)

上篇文章里面所用的截图是点击按钮就截取当前打开的界面&#xff0c;链接&#xff1a; MFC 截图功能实现_春蕾夏荷_728297725的博客-CSDN博客 这里所用的截图是可以选中区域&#xff0c;另存为目录等的操作&#xff1a; 效果&#xff1a; 选中区域&#xff1a; 菜单&#xf…

javaee 任务调度,定时任务 多个任务同时执行

错误的写法 如果按照下图的写法&#xff0c;两个任务不会同时执行&#xff0c;因为是一个线程&#xff0c;两个任务是串行的关系。 可以看到第二个任务是等第一个任务执行完以后&#xff0c;才执行的。 正确的写法 使用线程池&#xff0c;为每一个任务创建一个线程 可以看…

Markdown中使用 LaTeX 编辑数学公式

Markdown中使用 LaTeX 编辑数学公式 1 介绍TeX&#xff08;计算机排版系统&#xff09;LaTeX&#xff08;TeX宏集&#xff09;KaTeX 和 MathJax 2 注意点单双美元符号包裹问题KaTeX 有些不支持 3 语法保留字符希腊字母希伯来字母二元运算符二元关系符几何符号逻辑符号集合符号箭…

Linux下一切皆文件的理解

目录 一. 回顾上文 Linux底层进程和被打开文件的映射关系图&#xff1a; Linux部分源代码示意图如下&#xff1a; ​编辑 二.Linux下一切皆文件的核心理解 一. 回顾上文 在前两篇文章中&#xff0c;我论述了Linux系统中关于文件基础IO的一些内容&#xff1a; 1.有关于文件…

RT-Thread-05-空闲线程和两个常用的钩子函数

空闲线程和两个钩子函数 空闲线程是一个比较特殊的系统线程&#xff0c;它具备最低优先级&#xff0c;当系统中无其他就绪线程可运行时&#xff0c;调度器将调度到空闲线程&#xff1b;空闲线程还负责一些系统资源回收以及将一些处于关闭状态的线程从线程调度列表中移除&#x…

Nautilus Chain:模块化Layer3架构为RWA赛道构建基础设施

DeFi Summer后&#xff0c;加密行业经历了新一轮的爆发、增长后&#xff0c;整体的发展逐渐陷入滞缓。传统金融是一个经过了上百年发展的成熟市场&#xff0c;将800万亿美元体量的传统金融不断引入到链上有望推动加密行业迎来新一轮的质变。将传统资产以加密代币化的形式引入到…

【从零开始学习JAVA | 第十七篇】抽象类与抽象方法

目录 前言&#xff1a; 抽象类与抽象方法&#xff1a; 抽象类&#xff1a; 抽象方法&#xff1a; 特点&#xff1a; 抽象类 抽象方法 注意事项 抽象类 与 抽象方法的意义 抽象类 抽象方法 前言&#xff1a; 在之前&#xff0c;如果我们要构建两个相同类型的类的时候&…

【C++】泛型编程——模板进阶

文章目录 前言1. 模板参数的分类2. 非类型模板参数2.1 非类型模板参数的概念2.2 铺垫2.2 非类型模板参数的使用2.4 注意2.5 array的了解 3. 模板的特化3.1 概念3.2 函数模板特化3.3 类模板特化3.3.1 全特化3.3.2 偏特化部分特化参数更进一步的限制 4. 模板分离编译4.1 什么是分…

基于TF-IDF算法个人文件管理系统——机器学习+人工智能+神经网络(附Python工程全部源码)

目录 前言总体设计系统整体结构图系统流程图 运行环境模块实现1. 数据预处理2. 词频计算与数据处理3. 数据计算与对比验证 系统测试工程源代码下载其它资料下载 前言 本项目旨在通过应用TF-IDF算法&#xff0c;将新下载的课件进行自动分类整理。我们的方法是通过比较新文件中的…

Matlab统计分析-相关系数

统计分析-相关系数 相关系数 (pearson与spearman) 皮尔逊 person相关系数和斯皮尔曼spearman等级相关系数&#xff0c;它们可用来衡量两个变量之间的**(线性)**相关性的大小&#xff0c;根据数据满足的不同条件&#xff0c;我们要选择不同的相关系数进行计算和分析。 基础概…

C生万物 | 常见的六种动态内存错误

学习过C语言中的动态内存函数&#xff0c;例如【malloc】、【calloc】、【realloc】、【free】&#xff0c;那它们在使用的过程中会碰到哪些问题呢&#xff0c;本本文我们一起来探讨下~ 1、对NULL指针的解引用操作 代码&#xff1a; void test() {int *p (int *)malloc(INT_…

Sui链上事务处理概述

Sui通过其混合式交易处理方法&#xff0c;实现比其他区块链更快速和高效的性能。这种方法使得Sui的交易测试吞吐率达到每秒297,000次。从实际应用的角度来看&#xff0c;使用Sui的用户在apps和游戏中几乎能够获得实时响应。 在区块链世界中&#xff0c;交易是apps运作的基础&a…

AutoHotKey脚本的循环:While和Loop

While AHK提供三种循环方法&#xff0c;分别是Loop, While和For&#xff0c;其中While和For在其他语言中都很常见&#xff0c;下面通过while循环&#xff0c;实现一个鼠标框选矩形尺寸的脚本 ; 来自官网的案例 CoordMode "Mouse", "Screen"~LButton:: {M…

【文献分享】基于感知质量的滚动优化无人机导航

论文题目&#xff1a;Perception-aware Receding Horizon Navigation for MAVs 作者&#xff1a;Zhang, Zichao ; Scaramuzza, Davide 作者机构&#xff1a;苏黎世大学 论文链接&#xff1a;https://files.ifi.uzh.ch/rpg/website/rpg.ifi.uzh.ch/html/docs/ICRA18_Zhang.pd…

【雕爷学编程】Arduino动手做(122)---BH1750光照传感器

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

使用近10年,说说向大家推荐Linux系统的理由

使用linux已经快十年了&#xff0c;一直想推荐身边的使用linux,但是一直没有分享。但是现在我想分享推荐了。下面我们一起聊聊为什么我向大家推荐linux。 为什么现在我想推荐了呢&#xff1f;是因为我自认为相对于同龄人来说&#xff0c;我使用桌面版的时间算是挺长的了&#…

彻底搞懂什么是阿里云服务器vCPU?

阿里云ECS服务器vCPU和CPU是什么意思&#xff1f;CPU和vCPU有什么区别&#xff1f;一台云服务器ECS实例的CPU选项由CPU物理核心数和每核线程数决定&#xff0c;CPU是中央处理器&#xff0c;一个CPU可以包含若干个物理核&#xff0c;通过超线程HT&#xff08;Hyper-Threading&am…

走向计算机视觉的通用人工智能:从GPT和大型语言模型中汲取的经验教训 (下)...

点击蓝字 关注我们 关注并星标 从此不迷路 计算机视觉研究院 公众号ID&#xff5c;计算机视觉研究院 学习群&#xff5c;扫码在主页获取加入方式 论文地址&#xff1a;https://arxiv.org/pdf/2306.08641.pdf 计算机视觉研究院专栏 Column of Computer Vision Institute 人工智能…