Virtual

概述

定义一个函数为虚函数,不代表函数为不被实现的函数。

定义为虚函数是为了允许用基类的指针来调用子类的这个函数。

定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

class Base 
{
public:
	virtual void fun1() { cout << "Base::fun1" << endl; }
	virtual void fun2() = 0;
};

class A : public Base
{
public:
	void fun1() { cout << "A::fun1" << endl; }
};

class B: public Base
{
public:
	void fun1() { cout << "B::fun1" << endl; }
	void fun2() { cout << "B::fun2" << endl; }
};

class C : public Base
{
public:
	void fun2() { cout << "C:fun2" << endl; }
};

int main()
{
	//Base* baseA = new A(); // ERROR

	Base* baseB = new B();
	baseB->fun1(); // B::fun1
	baseB->fun2(); // B::fun2

	Base* baseC = new C();
	baseC->fun1(); // Base::fun1
	baseC->fun2(); // C:fun2

	delete baseB;
	delete baseC;
}

虚表

每个包含了虚函数的类都包含一个虚表。

实例化一个对象,输出对象的地址和对象成员v的地址,如果对象的地址和x的地址相同,那么就意味着编译器把虚函数表放在了末尾,如果两个地址不同,那么就意味着虚函数表是放在最前面的。

class Base 
{
public:
	int v;
	virtual void fun1() { cout << "Base::fun1" << endl; }
};

正好相差了4个字节(x86),因此编译器把生成的虚函数表放在了最前面

Base* base = new Base();
cout << base << endl;		// 0104F328
cout << &(base->v) << endl; // 0104F32C

获取虚表

class Base 
{
public:
	virtual void fun1() { cout << "Base::fun1" << endl; }
	virtual void fun2() { cout << "Base::fun2" << endl; }
	virtual void fun3() { cout << "Base::fun3" << endl; }
};

class A : public Base
{
public:
	virtual void fun2() { cout << "A::fun2" << endl; }
};
Base* base = new Base();
long* p1 = (long*)base;// 转换
long* p2 = (long*)(*p2);
for (int i = 0; i < 3; ++i) {
	cout << p2[i] << ' ';
}
cout << endl;// 10424985 10424980 10425435

A* a = new A();
long* p3 = (long*)a;
long* p4 = (long*)(*p3);
for (int i = 0; i < 3; ++i) {
	cout << p4[i] << ' ';
}
cout << endl; // 10424985 10425585 10425435

可见第一个和第三个地址相同,而第二个不同。

通过函数指针调用函数

typedef void (*F)();

F f1 = (F)p2[0];
F f2 = (F)p2[1];
F f3 = (F)p2[2];
f1(); //Base::fun1
f2(); //Base::fun2
f3(); //Base::fun3

A* a = new A();
long* p3 = (long*)a;
long* p4 = (long*)(*p3);
F f4 = (F)p4[0];
F f5 = (F)p4[1];
F f6 = (F)p4[2];
f4(); //Base::fun1
f5(); //A::fun2`
f6(); //Base::fun3

虚表函数顺序

class Base 
{
public:
	virtual void fun1() { cout << "Base::fun1" << endl; }
	virtual void fun2() { cout << "Base::fun2" << endl; }
	virtual void fun3() { cout << "Base::fun3" << endl; }
};

class A : public Base
{
public:
	virtual void funA() { cout << "A::funA" << endl; }
	virtual void fun2() { cout << "A::fun2" << endl; }
};

虚函数表的顺序就是基类的虚函数表中的虚函数的顺序+自己的虚函数的顺序

Base* a = new A();
long* p3 = (long*)a;
long* p4 = (long*)(*p3);
F f0 = (F)p4[0];
F f1 = (F)p4[1];
F f2 = (F)p4[2];
F f3 = (F)p4[3];
f0(); //Base::fun1
f1(); //A::fun2`
f2(); //Base::fun3
f3(); //A::funA

基类虚构

看下面的例子:

class Base1
{
public:
    ~Base1(){cout << "delete Base1\n";}
};

class Base2
{
public:
    virtual ~Base2(){cout << "delete Base2\n";}
};

class A1:public Base1
{
public:        
    ~A1(){cout << "delete A1\n";}
};

class A2:public Base2
{
public:    
    ~A2(){cout << "delete A2\n";}
};

int main()
{
    Base1* b1 = new A1;
    A1* a1 = new A1;
    Base2* b2 = new A2;
    A2* a2 = new A2;
    cout << "b1:\n";
    delete b1;
    cout << "a1:\n";
    delete a1;
    cout << "b2:\n";
    delete b2;
    cout << "a2:\n";
    delete a2;
    return 0;
}

输出如下:

b1:
delete Base1
a1:
delete A1
delete Base1
b2:
delete A2
delete Base2
a2:
delete A2
delete Base2

在实现多态时,要让基类虚构函数为虚函数,保证当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,防止内存泄漏

多态实现

要实现虚函数和多态,要通过指针或者引用的方式

  • 一个类实例只会存放非静态成员变量和指向虚表的指针 vptr(如果存在),并且一般 vptr 在头部。
  • 普通成员函数在内存某一位置,编译器都会对函数的名字进行修饰映射,让其变成唯一的函数名。

下面用一个例子演示这样的内存布局:

class Base
{
public:
    Base() { cout << "create Base\n"; }
    virtual ~Base() { cout << "delete Base\n"; }
    virtual void virtualShow() { cout << "virtual show Base\n"; }
    void showBase() { cout << "show Base\n"; }
    char ch{ 'b' };
};

class A :public Base
{
public:
    A() { cout << "create A\n"; }
    ~A() { cout << "delete A\n"; }
    virtual void virtualShow() { cout << "virtual show A\n"; }
    void showA() { cout << "show A\n"; }
    char ch{ 'a' };
    int integer{ 0 };
};
vptr
vptr

用指针或者引用可以实现多态,是因为可以让基类实例的 vptr 指向子类实例的 vptr 。

Base* pb = new A();
pb->virtualShow();
cout << pb->ch << endl;
delete pb;

输出:

create Base
create A
virtual show A
b
delete A
delete Base

引用类似:

A a;
Base& rb = a;
cout << rb.ch << endl;
rb.virtualShow();

输出:

create Base
create A
b
virtual show A
delete A
delete Base

但对于实际的对象:

Base b = A();
b.virtualShow();
cout << b.ch << endl;

输出:

create Base
create A
delete A
delete Base
virtual show Base
b
delete Base

可见生成了临时的 A 的实例,然后被删除了,并且 Base 的实例被删除了两次。

总结

  • 每一个基类都会有自己的虚函数表,派生类的虚函数表的数量根据继承的基类的数量来定。
  • 派生类的虚函数表的顺序,和继承时的顺序相同。
  • 派生类自己的虚函数放在第一个虚函数表的后面,顺序也是和定义时顺序相同。
  • 对于派生类如果要覆盖父类中的虚函数,那么会在虚函数表中代替其位置。

在编译的过程中编译器就为含有虚函数的类创建了虚函数表,并且编译器会在构造函数中插入一段代码,用来给虚函数指针赋值,虚函数表是在编译的过程中创建

虚函数指针是基于对象的,对象在实例化的时候,虚函数指针就会创建。虚函数指针是在运行时创建。在实例化对象的时候会调用到构造函数,会执行虚函数指针的赋值代码,从而将虚函数表的地址赋值给虚函数指针。

普通函数是静态编译的,没有运行时多态,只会根据指针或引用的字面值类对象,调用自己的普通函数,是父类为子类提供的强制实现,在继承关系中,子类一般不重写父类的普通函数。