IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++:多态性与虚函数 | 虚函数的注意点 | 汇编角度来看动态联编过程 -> 正文阅读

[C++知识库]C++:多态性与虚函数 | 虚函数的注意点 | 汇编角度来看动态联编过程

一.多态性

多态性是面向对象程序设计的关键技术之一。若程序设计语言不支持多态性,不能称为面向对象的语言。
利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功能。

接下来详细说明联编过程。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。

1.静态联编——编译时的多态

也叫做早期绑定(early binding),静态联编(static binding)。

通过函数的重载和运算符的重载来实现的。

静态联编示例

int Max(int a, int b) {
	return a > b ? a : b;
}
char Max(char a, char b) {
	return a > b ? a : b;
}
double Max(double a, double b) {
	return a > b ? a : b;
}

int main(void)
{
	int x = Max(12, 23);
	char ch = Max('a', 'b');
	double dx = Max(12.23, 34.45);

	return 0;
}

这种在编译时期就确定好了调用哪些函数的,就是早期绑定。

2.动态联编——运行时的多态

也叫做晚期绑定(late binding),(dynamic binding)

运行时的多态性是指在程序执行前,无法根据函数名和参数来确定该调用哪一个函数,必须在程序执行过程中,根据执行的具体情况来动态地确定
它是通过类继承关系public和虚函数来实现的。目的也是建立一种通用的程序。通用性是程序追求的主要目标之一。

二.虚函数的定义

虚函数是一个类的成员函数,定义格式为:

virtual 返回类型 函数名(参数表);

关键字virtual指明该成员函数为虚函数。
virtual仅用于类定义中,如果虚函数在类外定义,不可以加关键字。

动态联编示例

这里的基类为Animal类,派生类为Dog类和Cat类。
在派生类中,分别重写了基类的虚函数。

class Animal
{
private:
	string name;
public:
	Animal(const string& na) : name(na) {}
public:
	virtual void eat() {}
	virtual void walk() {}
	virtual void PrintInfo() {}

	string& get_name() { return name; }
	const string& get_name()const { return name; }
};

class Dog : public Animal
{
private:
	string owner;
public:
	Dog(const string& ow, const string& na) : Animal(na), owner(ow) {}

	virtual void eat() { cout << "Dog eat: bone" << endl; }
	virtual void walk() { cout << "Dog walk: run" << endl; }
	virtual void PrintInfo()
	{
		cout << "Dog owner 's name: " << owner << endl;
		cout << "Dog name: " << get_name() << endl;
	}
};

class Cat : public Animal
{
private:
	string owner;
public:
	Cat(const string& ow, const string& na) : Animal(na), owner(ow) {}

	virtual void eat() { cout << "Cat eat: fish" << endl; }
	virtual void walk() { cout << "Cat walk: silent" << endl; }
	virtual void PrintInfo()
	{
		cout << "Cat owner 's name: " << owner << endl;
		cout << "Cat name: " << get_name() << endl;
	}
};

运行示例:
如果将派生类传递给基类的引用或指针,再以基类指针调用虚方法,就会发生动态联编。

void fun(Animal& animal)
{
	animal.eat();
	animal.walk();
	animal.PrintInfo();
}

int main(void)
{
	Dog dog("Srh", "二哈");
	Cat cat("Sauron", "汤姆猫");

	fun(dog);
	fun(cat);

	return 0;
}

可以看出,虽然fun()函数里是拿基类的引用来调用虚方法,打印结果却不同,这就是动态联编。
在这里插入图片描述

注意点:

  1. 公有继承
  2. 使用虚函数
  3. 必须使用引用或指针来调用虚函数

为什么要使用公有继承?

因为公有继承代表 is-a 关系,即猫是动物的一种。不能使用私有继承。

三.虚函数的注意点

  1. 派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型否则被认为是重载,而不是虚函数如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外。

1的示例:这种情况下是函数重载

class Object
{
public:
	virtual Object* fun() {}
};

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

四.虚函数表和虚表指针的概念

实际上,运行时的多态是因为虚函数表的存在,如果设计的类里面有虚函数,那么在编译时期就会生成虚函数指针和虚函数表,里面存放各个虚函数的函数指针;
如果派生类重写了基类的虚函数,那么派生类的虚函数就覆盖虚函数表里面的基类虚函数。

示例:

class Object
{
private:
	int value;
public:
	Object(int x = 0) : value(x) {}

	virtual void add() { cout << "Object::add" << endl; }
	virtual void fun() { cout << "Object::fun" << endl; }
};

class Base : public Object
{
private:
	int sum;
public:
	Base(int x = 0) : Object(x + 10), sum(x) {}
	virtual void add() { cout << "Base::add" << endl; }
	virtual void fun() { cout << "Base::fun" << endl; }
};

int main()
{
	Base base(10);
	Object* op = &base;

	return 0;
}

运行结果:
vfptr为虚表的指针,指向虚函数表,里面存放虚函数的指针。

  1. 在构造的过程中,先构造基类,此时的虚表指针指向基类的虚表。

在这里插入图片描述

  1. 在构造完基类后再构造派生类时,虚表指针就会指向派生类的虚函数表(如果重写了基类的虚函数,就会覆盖基类的虚函数)。

在这里插入图片描述

注意点:
如果拿对象名加(.)调用方法,那么不管该方法是否为虚函数,都调用该对象的方法。

如果是以基类的指针或引用指向派生类,那么调用虚方法时就会采用动态联编,调用派生类的虚方法。

五.以汇编角度来看动态联编过程

示例:

int main()
{
	Base base(10);
	Object* op = &base;

	op->add();
	op->fun();
	return 0;
}

如图,在调用op->add()时,先将op的地址加入到eax中,再从eax中取出虚表的地址给edx;

在这里插入图片描述

eax再从edx中取地址,这时取得的地址为第一个虚函数的地址,调用该函数。
在这里插入图片描述
在这里插入图片描述

在执行到op->fun()时,和刚才的区别就是eax取地址时是 [edx + 4]。
原因是虚函数表里存放的都是虚函数指针,每个指针都占四字节,对其加4,就是取第二个虚函数指针。

在这里插入图片描述

六.习题:多重继承时的虚表

1.多重继承时虚表的内存模型

示例:

class Object
{
private:
	int value;
public:
	Object(int x = 0) : value(x) {}

	virtual void add() { cout << "Object::add" << endl; }
	virtual void fun() { cout << "Object::fun" << endl; }
	virtual void Print() { cout << "Object::Print" << endl; }
};

class Base : public Object
{
private:
	int sum;
public:
	Base(int x = 0) : Object(x + 10), sum(x) {}
	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 num;
public:
	Test(int x = 0) : Base(x + 10), num(x) {}
	virtual void add() { cout << "Test::add" << endl; }
	virtual void Print() { cout << "Test::Print" << endl; }
	virtual void show() { cout << "Test::show" << endl; }
};

虚表内存模型:

基类Object的虚表为:

在这里插入图片描述

派生类Base的虚表为:
因为Base重写了add()和fun()这两个虚方法,所以会覆盖基类的。没有重写Print(),那么基类的虚函数还会存在虚表中。

在这里插入图片描述

派生类Test的虚表为:
重写了Base的add(),和show()这两个虚方法,所以替换为Test的虚方法;Base的fun()未重写,继续留在虚表;重写了Object的Print()方法,所以替换为Test的虚方法。

在这里插入图片描述

2. 以对象调用普通方法时产生的动态联编

在上述代码不变的情况下,如果基类Object中有一个普通方法,里面调用虚方法:

class Object
{
private:
	int value;
public:
	Object(int x = 0) : value(x) {}

	virtual void add() { cout << "Object::add" << endl; }
	virtual void fun() { cout << "Object::fun" << endl; }
	virtual void Print() { cout << "Object::Print" << endl; }

	void function()
	{
		fun();
	}
};

那么在通过对象调用该方法时,都分别调用什么?

int main(void)
{
	Test t1;
	Base b1;
	Object obj;

	t1.function();
	b1.function();
	obj.function();

	return 0;
}

实际上,虽然是以对象调用,很多人会认为这个就是普通的静态联编。但这种调用方式是通过this指针调用,所以函数内部调用的虚函数也是通过this指针。

运行示例:
首先记住这三个对象的虚表

在这里插入图片描述
第一步
在对象t1调用function()时,this指针里的虚函数指针指向的是Test类的虚表,那么他调用fun()函数会在自己的虚表里找,此时的fun()函数并未重写,所以会调用它的基类Base的fun()函数。

在这里插入图片描述

第二步
通过b1对象调用,那么虚表指针此时指向Base的虚表,会调用Base::fun()。

在这里插入图片描述

第三步
通过对象obj来调用,那么虚表指针此时指向Object的虚表,就会调用Object::fun()。

在这里插入图片描述

运行结果:
在这里插入图片描述

end

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-03-22 20:20:07  更:2022-03-22 20:21:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 1:36:21-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码