-
2021-03-09 07:24:21
科技信息 1.引言 多态性(polymorphism)一词来源于拉丁语 poly(表示多的意思)和 mor-phos(意为形态),其字面的含义是多种形态。在面向对象系统中,多态性是其核心内容之一,反映了人们在求解问题时,对相似性问题的一种求解方法。Java 语言实现了两种多态性:静态多态性和动态多态性。本文重点论述其采用重写机制实现动态多态性的原理,并结合实例讲解动态多态性的应用。 2.关于多态的概念 2.1 继承 继承描述了两个类之间的一种关系。当一个类拥有另外一个类的所有非私有数据和操作时,就称这两个类之间具有继承关系。被继承的类称为父类或超类,由继承而得到的类称为子类。Java 语言不支持多重继承,一个类只能有一个直接父类,父类可以同时拥有多个子类。Java 中用关键字 extends 实现继承。 2.2 重写 在类的继承的过程中,子类可以根据需要对从父类继承来的方法进行重新定义,这时父类和子类中的两个方法功能不同,但是要求两者名字、参数列表和返回类型要一致,就称子类重写了父类的该方法。对于子类重写的父类的方法,若父类的对象调用这个方法时,则调用父类中的定义;若子类的对象使用这个方法时,则调用子类中的定义,父类中的定义如同被 “屏蔽”了。 2.3 抽象类 在 Java 中,提供了抽象类来表示现实世界中的抽象概念,即用抽象类来定义一般状态和行为。在抽象类中通常包括有抽象方法,所谓抽象方法是指抽象类中有定义但没有给出其实现的方法。因此,抽象类不能创建实例对象,只能通过抽象类派生出新的子类,再由子类来创建对象,子类必须提供抽象方法的实现细节。抽象类和抽象方法用 abstract 标识。 代码示例一: abstract class Shape // 定义抽象类 // 定义抽象方法 { abstract public double getArea(); } // 定义子类 Circle 继承抽象类 Shape class Circle extends Shape { private double pi=3.14; public double radius=5; // 实现父类的抽象方法 getArea() public double getArea() { return pi*radius*radius;} } public class TestAbstract { public static void main(String[]args) // 创建子类的实例对象 { Circle c=newCircle(); } } 2.4 接口 接口是 Java 语言提供的另一种重要功能,接口可以具有数据成员 和方法,但与类相比有其特殊性: (1)接口用 interface 声明,数据成员必须初始化,方法必须全部都声明为抽象的。 (2)接口之间的继承为多重继承,即一个接口可以有多个父接口。 (3)接口不能创建实例对象,必须利用接口的特性来建造一个新的类,称之为接口的实现,用 implements 表示,实现接口的类可以创建对象。一个类可以同时实现多个接口。 代码示例二: // 定义接口 Shape2D public interface Shape2D { double pi=3.14; public abstract double getArea(); } // 定义接口 Color public interface Color { public abstract void setColor(Stringstr); } // 定义接口 Shape3D继承 Sh
更多相关内容 -
C++的静态联编和动态联编
2020-09-02 15:51:41本文阐述了静态联编和动态联编的概念和区别,通过具体实例分析了实现动态联编的条件,指出了虚函数是实现动态联编的基础。 -
C++:多态性与虚函数 | 虚函数的注意点 | 汇编角度来看动态联编过程
2022-03-20 15:47:55多态性与虚函数 | 虚函数的注意点 | 汇编角度来看动态联编过程 以对象调用函数时的动态联编目录
一.多态性
多态性是面向对象程序设计的关键技术之一。若程序设计语言不支持多态性,不能称为面向对象的语言。
利用多态性技术,可以调用同一个函数名的函数,实现完全不同的功能。接下来详细说明联编过程。将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(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()函数里是拿基类的引用来调用虚方法,打印结果却不同,这就是动态联编。
注意点:
- 公有继承
- 使用虚函数
- 必须使用引用或指针来调用虚函数
为什么要使用公有继承?
因为公有继承代表 is-a 关系,即猫是动物的一种。不能使用私有继承。
三.虚函数的注意点
- 派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是重载,而不是虚函数。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外。
1的示例:这种情况下是函数重载
class Object { public: virtual Object* fun() {} }; class Base : public Object { public: virtual Base* fun() {} }
- 只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。
- 静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
- 实现动态多态性时,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现动态的多态性。
- 内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
- 析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
- 函数执行速度要稍慢一些。为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价,但通用性是一个更高的目标。
- 如果定义放在类外,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为虚表的指针,指向虚函数表,里面存放虚函数的指针。- 在构造的过程中,先构造基类,此时的虚表指针指向基类的虚表。
- 在构造完基类后再构造派生类时,虚表指针就会指向派生类的虚函数表(如果重写了基类的虚函数,就会覆盖基类的虚函数)。
注意点:
如果拿对象名加(.)调用方法,那么不管该方法是否为虚函数,都调用该对象的方法。如果是以基类的指针或引用指向派生类,那么调用虚方法时就会采用动态联编,调用派生类的虚方法。
五.以汇编角度来看动态联编过程
示例:
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++多重继承及多态性原理实例详解
2020-08-18 22:47:02主要介绍了C++多重继承及多态性原理实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 -
C++的多态性以及静态联编和动态联编
2014-07-19 22:29:47联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存...按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编首先要介绍一下C++的多态性:
多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。多态(polymorphisn),字面意思多种形状。
C++多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。(这里我觉得要补充,重写的话可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性)而重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。但这并没有体现多态性。
多态与非多态的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。
那么多态的作用是什么呢,封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。
最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是一定的,而固定的地址将始终调用到同一个函数,这就无法实现一个接口,多种方法的目的了。
C++多态的笔试题目:
#include<iostream> using namespace std; class A { public: void foo() { printf("1\n"); } virtual void fun() { printf("2\n"); } }; class B : public A { public: void foo() { printf("3\n"); } void fun() { printf("4\n"); } }; int main(void) { A a; B b; A *p = &a; p->foo(); p->fun(); p = &b; p->foo(); p->fun(); return 0; }
第一个p->foo()和p->fuu()都很好理解,本身是基类指针,指向的又是基类对象,调用的都是基类本身的函数,因此输出结果就是1、2。
第二个输出结果就是1、4。p->foo()和p->fuu()则是基类指针指向子类对象,正式体现多态的用法,p->foo()由于指针是个基类指针,指向是一个固定偏移量的函数,因此此时指向的就只能是基类的foo()函数的代码了,因此输出的结果还是1。而p->fun()指针是基类指针,指向的fun是一个虚函数,由于每个虚函数都有一个虚函数列表,此时p调用fun()并不是直接调用函数,而是通过虚函数列表找到相应的函数的地址,因此根据指向的对象不同,函数地址也将不同,这里将找到对应的子类的fun()函数的地址,因此输出的结果也会是子类的结果4。
笔试的题目中还有一个另类测试方法。即
B *ptr = (B *)&a; ptr->foo(); ptr->fun();
问这两调用的输出结果。这是一个用子类的指针去指向一个强制转换为子类地址的基类对象。结果,这两句调用的输出结果是3,2。
并不是很理解这种用法,从原理上来解释,由于B是子类指针,虽然被赋予了基类对象地址,但是ptr->foo()在调用的时候,由于地址偏移量固定,偏移量是子类对象的偏移量,于是即使在指向了一个基类对象的情况下,还是调用到了子类的函数,虽然可能从始到终都没有子类对象的实例化出现.
而ptr->fun()的调用,可能还是因为C++多态性的原因,由于指向的是一个基类对象,通过虚函数列表的引用,找到了基类中fun()函数的地址,因此调用了基类的函数。由此可见多态性的强大,可以适应各种变化,不论指针是基类的还是子类的,都能找到正确的实现方法。补充:
令人迷惑的隐藏规则
本来仅仅区别重载与覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然增加。这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
1.如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)
2.如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)C++纯虚函数
一、定义
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
virtual void funtion()=0
二、引入原因
1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
三、相似概念
1、多态性
指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。
a、编译时多态性:通过重载函数实现
b、运行时多态性:通过虚函数实现。
2、虚函数
虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)
3、抽象类
包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。
静态联编和动态联编
联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址,它是计算机程序彼此关联的过程。按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。
静态联编是指在编译阶段就将函数实现和函数调用关联起来,因此静态联编也叫早绑定,在编译阶段就必须了解所有的函数或模块执行所需要检测的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型,C语言中,所有的联编都是静态联编。
动态联编是指在程序执行的时候才将函数实现和函数调用关联,因此也叫运行时绑定或者晚绑定,动态联编对函数的选择不是基于指针或者引用,而是基于对象类型,不同的对象类型将做出不同的编译结果。C++中一般情况下联编也是静态联编,但是一旦涉及到多态和虚拟函数就必须要使用动态联编了。下面将介绍一下多态。
多态:字面的含义是具有多种形式或形态。C++多态有两种形式,动态多态和静态多态;动态多态是指一般的多态,是通过类继承和虚函数机制实现的多态;静态多态是通过模板来实现,因为这种多态实在编译时而非运行时,所以称为静态多态。
动态多态例子:
#include <stdio.h> #include <iostream> /** *Shape */ class CShape { public: CShape(){} virtual ~CShape(){} virtual void Draw() = 0; }; /** *Point */ class CPoint : public CShape { public: CPoint(){} ~CPoint(){} void Draw() { printf("Hello! I am Point!/n"); } }; /** *Line */ class CLine : public CShape { public: CLine(){} ~CLine(){} void Draw() { printf("Hello! I am Line!/n"); } }; void main() { CShape* shape = new CPoint(); //draw point shape->Draw();//在这里shape将会调用CPoint的Draw()函数 delete shape; shape = new CLine(); //draw Line shape->Draw();//而在这里shape将会调用CLIne 的Draw()函数 delete shape; return ; }
由上面的例子,大家应该能理解什么是多态了:也就是一个Draw() 可以有两种实现,并且是在运行时决定的,在编译阶段不知道,也不可能知道!只有在运行的时候才能知道我们生成的shape是那种图形,当然要实现这种效果就需要动态联编了,在基类我们会把想要多态的函数声明为虚函数,而虚函数的实现原理就使用了动态联编。
静态多态的例子:
在上面例子的基础之上添加模板函数:
<pre name="code" class="cpp">template <class T> void DrawShape(T* t) { t->Draw(); }
修改main函数为如下:
void main() { CShape* shape = new CPoint(); //draw point shape->Draw(); DrawShape<CPoint>((CPoint*)shape); delete shape; shape = new CLine(); //draw Line shape->Draw(); DrawShape<CLine>((CLine*)sh; delete shape; return ; }
-
C++之多态性
2019-06-05 00:25:311.初探多态性 在面向对象方法中,所谓多态性就是不同对象收到相同消息,产生不同的行为。在C++程序设计中,多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作,这样就可以用同一个函数名调用不同...1.初探多态性
在面向对象方法中,所谓多态性就是不同对象收到相同消息,产生不同的行为。在C++程序设计中,多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作,这样就可以用同一个函数名调用不同内容的函数。换言之,可以用同样的接口访问功能不同的函数,从而实现“一个接口,多种方法”。
事实上,在程序设计中经常会使用到多态性。最简单的例子就是运算符了,例如我们使用运算符+,就可以实现整型数、浮点数、双精度类型之间的加法运算,这三种类型的加法操作其实是互不相同的,是由不同内容的函数实现的。这个例子就是使用了多态的特征。
在C++中,多态性的实现和联编(也称绑定)这一概念有关。一个源程序经过编译、链接,成为可执行文件的过程是把可执行代码联编(或称装配)在一起的过程。其中在运行之前就完成的联编成为静态联编(前期联编);而在程序运行之时才完成的联编叫动态联编(后期联编)。
静态联编支持的多态性称为编译时多态性(静态多态性)。在C++中,编译时多态性是通过函数重载和模板实现的。利用函数重载机制,在调用同名函数时,编译系统会根据实参的具体情况确定索要调用的是哪个函数。
动态联编所支持的多态性称为运行时多态(动态多态)。在C++中,运行时多态性是通过虚函数来实现的。
再举一个通俗易懂的例子:比如买票这个行为,普通人买是全价;学生买是半价票等。
2.多态的定义和实现
2.1 多态定义构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person.Person买票就是全价,而Student买票就是半价。
那么在继承中要构成多态还需要两个条件:
a. 调用函数的对象必须是指针或者引用。
b. 被调用的函数必须是虚函数,且完成了虚函数的重写。什么是虚函数?
虚函数:在类的成员函数前加virtual关键字。class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } };
什么是虚函数的重写?
虚函数的重写:派生类中有一个跟基类的完全相同的虚函数,我们就称子类的虚函数重写了基类的虚函数。“完全相同”是指:函数名、参数、返回值都相同。另外,虚函数的重写也叫做虚函数的覆盖。示例代码:
#include <iostream> #include <stdlib.h> using namespace std; class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { public: virtual void BuyTicket(){ cout << "买票-半价" << endl; } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person ps; Student st; Func(ps); Func(st); system("pause"); return 0; }
不规范的重写行为
在派生类中重写的成员函数可以不加virtual关键字,也是构成重写,因为继承后基类的虚函数被继承下来,在派生类中依旧保持虚函数的属性,我们只是重写了它。这是非常不规范的,在平时尽量不要这样使用。
注意:若子类中的函数有virtual修饰,而父类中没有,则会构成函数隐藏。
基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。这里他们的函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这也说明基类的析构函数最好写成虚函数。
这里贴一个链接,专门解释了为什么基类的析构函数最好写成虚函数:https://blog.csdn.net/komtao520/article/details/82424468
接口继承与实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以,如果不实现多态,不要把函数定义成虚函数。3. 抽象类
在虚函数的后面写上 = 0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象。只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现了接口继承。
示例代码:
#include<iostream> #include <stdlib.h> using namespace std; 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(); } int main() { Test(); system("pause"); return 0; }
结果: Benz-舒适
BMW-操控4. 多态的原理
4.1 虚函数表
//计算sizeof(b)为多少??? #include<iostream> #include <stdlib.h> using namespace std; class Base { public: virtual void Fun1(){ cout << "Func1()" << endl; } private: int _b = 1; }; int main() { Base b; cout << "sizeof(b):" << sizeof(b) << endl; system("pause"); return 0; }
通过测试我们发现sizeof(Base)大小为8字节。除了_b成员,还多了一个_vfptr放在对象的前面(注意有些平台可能会放在对象的后面,这个跟平台有关),对象中的这个指针我们称它为虚函数表指针。一个含有虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表(虚表)中。示例代码:
//1.增加一个派生类Derive去继承Base //2.Derive中重写Func1 //3.Base再增加一个虚函数Fun2和一个普通函数Fun3 class Base { virtual void Func1() { cout << "Base::Func1()" << endl; } virtual void Func2(){ cout << "Base::Func2()" << endl; } void Func3(){ cout << "Base::Func3()" << endl; } private: int _b = 1; }; class Derive : public Base { public: virtual void Func1(){ cout << "Derive::Func1()" << endl; } private: int _d = 2; }; int main() { Base b; Derive d; system("pause"); return 0; }
这里我们打开监视窗口,不难发现以下几点问题:
a. 派生类对象也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。
b.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫覆盖。
c.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但不是虚函数,所以不放在虚表中。
d.虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
总结:派生类的虚表生成:
(1)先将基类中的虚表内容拷贝一份到派生类虚表中;
(2)如果派生类重写了基类中的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;
(3)派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类的虚表的最后。多态实现的原理:
分析了这么多,我们依旧拿上边“买票”为例,Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket.class Person { public: virtual void BuyTicket() { cout << "买票-全价" << endl; } }; class Student : public Person { public: virtual void BuyTicket(){ cout << "买票-半价" << endl; } }; void Func(Person& p) { p.BuyTicket(); } int main() { Person Mike; Func(Mike); Student John; Func(John); system("pause"); return 0; }
这样就实现了不同对象去完成同一行为时,展现出不同的形态。动态绑定与静态绑定:
a. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态 ,例如:函数重载。
b. 动态绑定又称为后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。下边给大家贴了一个链接,这个博文更好地讲解了多态,希望对大家有所帮助。
https://blog.csdn.net/u012630961/article/details/81226351 -
C++多态性原理详解(静态多态、动态多态、虚函数、虚函数表)
2021-03-01 21:03:20C++多态性原理详解(静态多态、动态...静态联编支持的多态性称为编译时多态(静态多态),通过函数重载或函数模板实现;动态联编支持的多态性称为运行时多态(动态多态),通过虚函数表实现。 2 静态多态性 2.1 函数重载 首 -
【转】C++多态篇1一静态联编,动态联编、虚函数与虚函数表vtable
2016-04-20 11:43:00首先,说起多态就必须要讲静态联编,动态联编。...谭浩强写的C++程序设计直接叫静态多态性和动态多态性。 文章转载自:原文链接 转载于:https://www.cnblogs.com/Blackmanba-xzl/p/5411965.html... -
【C++】5.1 编译时的多态性与运行时的多态性
2019-09-20 19:16:025.1 编译时的多态性与运行时的多态性 1、什么时多态性 【定义】 (1)在面向对象方法中,不同对象收到相同的消息时,产生不同的行为(即方法) (2)在C++程序设计中,多态性是指用一个名字定义不同的函数,函数... -
《随笔八》—— C++中的“ 多态中的静态联编 、动态联编”
2018-11-18 12:04:38在编译时的动态联编 在运行时的动态联编 C++中的虚函数在这篇:《随笔九》—— C++中的 “ 虚函数 ”https://blog.csdn.net/qq_34536551/article/details/84375823 下面先给出一些代码示例,引用出要使用多态的... -
c++动态联编与静态联编
2017-10-23 11:12:33这个主要是虚函数实现的多态性. 1.静态联编 静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数... -
C++面向对象(五):多态性
2020-05-16 09:32:12C++面向对象:多态性 会有点长,不过读过就全学会喽!!!!!! 会有点长,不过读过就全学会喽!...在 C + + 中,多态性的实现和联编这一概念有关。一个源程序经过编译、连接, 成为可执行文件的过程是把可执行代码 -
动态联编与静态联编的区别
2019-03-18 14:40:57摘要】:本文阐述了静态联编和动态联编的概念和...按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。 1.静态联编 静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行... -
c++中 动态联编,静态联编和虚函数
2017-02-23 08:50:30按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。 1. 静态联编 静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在... -
C++中的静态联编和动态联编详解
2019-02-28 22:53:36按照联编所进行的阶段不同,可分为静态联编和动态联编。 例如A类中有fun这个函数, B类中也有fun这个函数,现在我在类外的main函数里面调用fun 函数。那么main函数就是函数调用,调用fun函数,而A... -
C++程序设计(八)—— 多态性和虚函数
2018-09-24 00:17:41动态联编所支持的多态性称为运行时的多态性,这由虚函数来支持。虚函数类似于重载函数,但与重载函数的实现策略不同,即对虚函数的调用使用动态联编。 1、静态联编中的赋值兼容性及名字支配规律 派生一个类的... -
C++动态联编与静态联编
2020-03-16 09:12:57按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。 1. 静态联编 静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行... -
C++第八章 多态性与虚函数(二).ppt
2022-06-16 01:57:07C++第八章 多态性与虚函数(二).ppt -
动态联编
2015-10-08 20:38:37首先我们知道的是,动态联编 和 静态联编 都是多态性的一种体现。 关于面向对象的三个基本要素:封装(类型抽象), 继承 和 多态。 首先我们从概念性上面了解了 动态联编 和 静态联编 的功能:实现了多态性。... -
动态联编和静态联编
2021-07-09 10:14:04按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。 1. 静态联编 静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译... -
C++:多态性
2021-06-08 19:40:57●多态的实现类型 ●多态性实现的相关技术 ●虚函数的定义和使用 ●纯虚函数与抽象类 ●函数重载和运算符重载 -
C++ 虚函数与多态性
2020-09-26 21:06:43多态性 面向对象程序设计中的多态性是指向不同的对象发送同一个消息,不同对象对应同一消息产生不同行为。在程序中消息就是调用函数,不同的行为就是指不同的实现方法,即执行不同的函数体,也可以这样说就是实现了... -
C++面试题-面向对象-多态性与虚函数
2019-01-11 00:16:21C++面试题-面向对象-多态性与虚函数 问:在C++程序中调用被C编译器编译后的函数,为什么要加extern "C"? 答:C++语言支持函数重载,C语言不支持函数重载。函数被C++编译后在库中的名字与C语言的不同。... -
虚函数与多态性
2020-06-18 22:25:08一.静态联编 1.联编是指一个程序自身彼此关联的过程,在这个联编过程中,需要确定...为了实现多态性,利用虚函数机制,可部分地采用动态联编。 二.类指针的关系 基类指针和派生类指针与基类对象和派生类对象4种可能匹配 -
静态联编和动态联编
2019-03-16 18:25:03首先我们知道的是,动态联编 和 静态联编 都是多态性的一种体现。 关于面向对象的三个基本要素:封装(类型抽象), 继承 和 多态。 首先我们从概念性上面了解了 动态联编 和 静态联编 的功能:实现了多态性。 ... -
C++ 虚函数、静态联编、动态联编
2016-01-05 17:12:04C语言中,所有的联编都是静态联编,据我所知道的,任何一种编译器都支持静态联编(废话)。 动态联编是指在程序执行的时候才将函数实现和函数调用关联, 因此也叫运行时绑定或者晚绑定 ,动态联编对函数的选择不是... -
c++多态性答案
2014-12-06 15:03:27c++多态性与虚函数习题答案,这些复习资料供大家使用 -
虚基类 和多态性c++
2020-10-14 20:48:14面向对象程序设计重要概念之一就是多态性 同一个名字定义若干个功能相近的...概念 编译时的多态是通过静态联编 实现的 运行时的多态性则是通过动态联编实现的 动态联编核心是虚函数 虚函数是一种在基类中定义为 virt