C++之 “虚函数” 详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Zhang_1218/article/details/78700587

C++之 “虚函数” 详解

原文见:www.louhang.xin

虚函数在C++中有着十分重要的作用,通过虚函数可以实现多态(polymorphism)机制

在看《C++ primer plus》时,发现作者将虚函数放在类继承的一章之中,和动态/静态联编一起进行了讲解。我也就顺着复习了继承,便再剖析一下虚函数。

虚函数--类的成员函数前面加virtual关键字,则这个成员函数称为虚函数。

在之前的博客《多态及其对象模型》中我剖析虚函数在其中的作用。

底层原理:

当存在虚函数时,编译器会给每个对象添加一个隐藏成员,此隐藏成员保存的是指向虚函数地址数组的指针。而这个数组就叫虚函数表(virtual function table)。

虚函数表中存放的正是为类对象进行声明的虚函数的地址。

需要注意的是,在继承之中,如果基类存在虚表,那么基类对象将存在一个指向其虚表的指针;而派生类对象中将包含的是一个指向独立虚表的指针。这个独立虚表中不仅包含从父类继承的虚函数地址,还包括派生类定义的新的虚函数的地址。

当调用虚函数时,程序将查看存储在对象中的虚表的地址,进而去虚表中查找相应的虚函数。

代码如下:

#include<iostream>
using namespace std;

//单继承模型
class Father
{
public:
	virtual void fun1()
	{
		cout << "Father::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Father::fun2()" << endl;
	}
protected:
	int _a = 10;
};
class Son : public Father
{
public:
	virtual void fun2()
	{
		cout << "Son::fun2()" << endl;
	}
	virtual void fun3()
	{
		cout << "Son::fun3()" << endl;
	}
protected:
	int _b = 20;
};

void Fun(Father& f)
{
	f.fun2();
}
void test()
{
	Father a;
	Son b;
	Fun(a);
	Fun(b);
}
int main()
{
	test();
	system("pause");
	return 0;
}

在监视窗口查看:

image.png

我们可以看到在基类内部存在着一个地址,而这个地址指向了一张表,既虚表,虚表内部存储的正是虚函数。但是我们会发现派生类虚函数fun3却不在此表内部。实际上,fun3也在此表内部,只不过编译器做了优化,没有在监视窗口显示出来而已。

我们可以在内存中观察,也可以通过书写函数将虚表内的地址打印出来。

内存中观察:

image.png


通过函数来将虚表及其内存放的地址打印出来,如下:

#include<iostream>
using namespace std;
class Father
{
public:
	virtual void fun1()
	{
		cout << "Father::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Father::fun2()" << endl;
	}
private:
	int _a = 10;
};
class Son : public Father
{
public:
	virtual void fun2()
	{
		cout << "Son::fun2()" << endl;
	}
	virtual void fun3()
	{
		cout << "Son::fun3()" << endl;
	}
protected:
	int _b = 20;
};
typedef void(*FUNC) ();
void PrintVTable(int* VTable)
{
	cout << " 虚表地址>" << VTable << endl;
	for (int i = 0; VTable[i] != 0; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]);
		FUNC f = (FUNC)VTable[i];
		f();
	}
	cout << endl;
}
void test()
{
	Father a;
	Son b;
	int* VTable1 = (int*)(*(int*)&a);
	int* VTable2 = (int*)(*(int*)&b);
	PrintVTable(VTable1);
	PrintVTable(VTable2);
}
int main()
{
	test();
	system("pause");
	return 0;
}

TIM截图20171203105545.png

image.png

可以看到在创建的两个对象中都存在着虚表。这也不可避免的出现了一些问题:

  1. 每个对象占据内存都变大了,增加内存来存放虚表的地址。

  2. 针对每个类,编译器都要创建一个虚函数表。

  3. 每次的函数调用,都要额外执行操作,要去虚表中查找虚函数的地址。

虽然非虚函数比虚函数的效率稍高一点,但是起不具备动态联编,不能构成多态。

虚函数应用:

  • 构造函数

构造函数不能为虚构函数。虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数,因为容易使用时容易引 起混淆。

  • 析构函数

析构函数定义为虚函数,除非类不用做基类。

class A
{
public:
    A() 
    {     
		_ptra = new char[10];
    }
    
    ~A() 
    { 
        delete[] _ptra;
    }       
	
private:
    char* _ptra;
};

class B: public A
{
public:
    B()
	{ 
		_ptrb = new char[20];
	}
    ~B() 
	{ 
		delete[] _ptrb;
	}
	
private:
    char * _ptrb;
};

void test()
{
    A * a = new B;
    delete a;
}

上面的程序存在内存泄漏,因为其是静态联编,在释放对象a时仅仅调用了A的·析构函数调用了,B的析构函数并未调用,这就造成了一个很危险的漏洞。

但如果将A类的析构函数定义为虚析构函数,那么执行的将是动态联编,则会将内存全部成功的释放掉。

  • 纯虚函数

纯虚函数只进行声明,而不定义。如下:

class Test
{
public:
    virtual void fun()=0;   // =0 标志一个虚函数为纯虚函数
};

包含有纯虚函数的类是抽象类,而抽象类不能进行实例化。只能被其他派生类继承,而纯虚函数就是一个公共的接口,所有继承了抽象类的派生类内部都包含纯虚函数。

纯虚函数不定义,其定义交给派生类来完成,继承了抽象类的派生类必须对纯虚函数进行定义

  • 友元函数

友元函数不能定义为虚函数。因为友元不是类的成员,只有类的成员才能定义为虚函数。

总结:

        1. 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外) 

        2. 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。 

        3. 只有类的成员函数才能定义为虚函数。 

        4. 静态成员函数不能定义为虚函数。 

        5. 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。 

        6. 构造函数不能为虚函数,最好也不要将operator=定义为虚函数,因为容易使用时容易引起混淆。

        7. 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。 

        8. 最好把基类的析构函数声明为虚函数。


参考资料:

    《C++ primer plus》Stephen Prata,张海龙,袁国忠译

    《深度探索C++对象模型》 Stanley B.Lippman,侯捷译




猜你喜欢

转载自blog.csdn.net/Zhang_1218/article/details/78700587