——虚方法
注意:
(1)virtual修饰符不能与static、abstract或者override修饰符同时使用;
(2)由于虚方法不能是私有的,所以,virtual修饰符不能与private修饰符同时使用。
public virtual int Add(int x, int y) //定义一个虚方法 { return x + y; //返回两个数的和 }
——有关虚函数的疑问
为什么构造函数不能定义为虚函数?
Avirtual call is a mechanism to get work done given partialinformation. In particular, "virtual" allows us to call afunction knowing only an interfaces and not the exact type of theobject. To create an object you need complete information. Inparticular, you need to know the exact type of what you want tocreate. Consequently, a "call to a constructor" cannot bevirtual.
从C++之父Bjarne的回答我们应该知道C++为什么不支持构造函数是虚函数了,简单讲就是没有意义。虚函数的作用在于通过子类的指针或引用来调用父类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。
网络上还有一个很普遍的解释是这样的:虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
本人对这个观点并不认同,这主要是因为用什么方式实现虚函数是编译器的事情,使用Vtable只是大多数编译器采用的一种手段,并不代表编译器实现不了虚构造函数,编译器之所以不支持虚构造函数主要原因就是没有必要,所以正好这种实现方式也不支持,巧合而已。
情况1:用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构
情况2:用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。为什么会出现这种现象呢,个人认为析构的时候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就要根据指针绑定的对象来调用对应的析构函数了。
——重写方法
注意:
(1)override修饰符不能与new、static或者virtual修饰符同时使用,另外,重写方法只能用于重写基类中的虚方法,不能用来单独声明方法;
(2)重载和重写是不相同的,重载是指编写一个与已有方法同名,但参数列表不同的方法,而重写是指在派生类中重写基类的虚方法。
class BaseClass //定义一个基类 { public virtual int Add(int x, int y) //定义一个虚方法 { return x + y; //返回两个数的和 } } class Program:BaseClass //定义一个派生类,继承于BaseClass { static int z = 0; //定义一个静态变量,用来作为第3个被加数 public override int Add(int x, int y) //重写基类中的虚方法 { return base.Add(x, y) + z; //计算3个数的和 } static void Main(string[] args) { z = 698; //为静态变量赋值 BaseClass baseclass = new Program(); //使用派生类对象实例化基类对象 Console.WriteLine(baseclass.Add(98, 368)); //调用派生类中重写之后的方法 Console.ReadLine(); } }
说明:
在Main方法中使用基类对象调用的Add方法是在派生中重写之后的方法,这主要是因为虚方法的实现由派生类中的重写方法进行了取代。
技巧:
在派生类中重写基类中的虚方法时,可以使用base关键字调用基类中的虚方法。
——C++中虚函数的作用和多态
虚函数: 实现类的多态性
关键字:虚函数;虚函数的作用;多态性;多态公有继承;动态联编
C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,😮子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要在派生类中声明该方法为虚方法。
当子类重新定义了父类的虚函数后,当父类的指针指向子类对象的地址时,[即B b; A a = &b;] 父类指针根据赋给它的不同子类指针,动态的调用子类的该函数,而不是父类的函数(如果不使用virtual方法,请看后面★*),且这样的函数调用发生在运行阶段,而不是发生在编译阶段,称为动态联编。而函数的重载可以认为是多态,只不过是静态的。注意,非虚函数静态联编,效率要比虚函数高,但是不具备动态联编能力。下面的例子解释动态联编性: class A{ private: int i; public: A(); A(int num) :i(num) {}; virtual void fun1(); virtual void fun2(); }; class B : public A{ private: int j; public: B(int num) :j(num){}; virtual void fun2();// 重写了基类的方法 }; // 为方便解释思想,省略很多代码 A a(1); B b(2); A *a1_ptr = &a; A *a2_ptr = &b; // 当派生类“重写”了基类的虚方法,调用该方法时 // 程序根据 指针或引用 指向的 “对象的类型”来选择使用哪个方法 a1_ptr->fun2();// call A::fun2(); a2_ptr->fun2();// call B::fun1(); // 否则 // 程序根据“指针或引用的类型”来选择使用哪个方法 a1_ptr->fun1();// call A::fun1(); a2_ptr->fun1();// call A::fun1();
——虚函数的底层实现机制
实现原理:虚函数表+虚表指针
关键字:虚函数底层实现机制;虚函数表;虚表指针
编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。
举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:
如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。
如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。
下面的图片体现了上述的底层实现机制:
使用虚函数后的变化:
(1) 对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)。
(2) 每个类编译器都创建一个虚函数地址表
(3) 对每个函数调用都需要增加在表中查找地址的操作。
虚函数的注意事项
- 构造函数不能为虚函数。
- 基类的析构函数应该为虚函数。
- 友元函数不能为虚,因为友元函数不是类成员,只有类成员才能是虚函数。
- 如果派生类没有重定义函数,则会使用基类版本。
- 重新定义继承的方法若和基类的方法不同(协变除外),会将基类方法隐藏;如果基类声明方法被重载,则派生类也需要对重载的方法重新定义,否则调用的还是基类的方法。
注意:对象的虚函数表中存放的实际上并不是虚函数的入口地址,而是一个跳转指令(jmp)的地址,该跳转指令,转向虚函数的入口,为了叙述方便,我这里作出约定:我们就认为虚函数表中就存放的是虚函数的入口地址。
虚函数的存放顺序与函数的声明顺序是相同的。
2.派生类的对象的内存布局是:前四个字节依然存放虚表指针,虚表中首先存放父类的虚函数地址,注意,由于派生类中也可能有①自己的虚函数,同时派生类也可以②重写父类的虚函数,虚函数表的分布如何:
对于情况一而言,将派生类新增加的虚函数地址依次添加到虚表(虚表中已经有父类的虚函数地址)的后面。
对于情况二而言,如果派生类重写了父类的虚函数,则将重写后的虚函数地址替换掉父类原来的虚函数地址,如果没有重写,则按照父类的虚表顺序存放虚函数地址
接下来的内存中依次存放该对象的父类的数据成员(非静态的数据成员),然后再存放派生类自己的数据成员。(还有内存对齐的问题)
——抽象类
形式:virtual 函数原型=0;// =0表示没有函数体定义:在定义一个表达抽象概念的基类时,有时无法给出某些函数的具体实现方法,就可以将这些函数声明为纯虚函数。特点:无具体实现方法。
定义:声明了纯虚函数的类,都成为抽象类。主要特点:抽象类只能作为基类来派生新类,不能声明抽象类的对象。(既然都是一个抽象概念了,纯虚函数没有具体实现方法,故不能创造该类的实际的对象)但是可以声明抽象类的指针变量或引用变量,通过指针或引用,就可以指向并访问派生类对象,进而访问派生类的成员。(体现了多态性)作用:因为其特点,基类只是用来继承,可以作为一个接口,具体功能在派生类中实现(接口)
#include "stdafx.h" using namespace std; class Base1 { public: virtual void display()=0;//不需要再基类中给出函数的函数体 }; class Base2:public Base1 { public: virtual void display(); }; void Base2::display() { cout<<"Base2::display()"<<endl; } class derived:public Base2 { public: virtual void display(); }; void derived::display() { cout<<"derived::display()"<<endl; } void fun(Base1* pt) { pt->display(); } int main() { /*Base1 a;*/ Base2 b; derived c; //fun(&a); fun(&b); fun(&c); system("Pause"); return 0; }