动态联编和静态联编

一、动态联编和静态联编的基本概念

将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在C语言中这个步骤更简单,因为C语言的函数名不允许重复,即没有函数重载这一说,每个函数名都对应着一个函数代码块。但是在C++中要复杂,因为C++中允许函数重载,必须得根据函数名和形参列表来对应一个函数代码块。C/C++编译器在编译过程就完成了这种联编。在编译过程中进行联编被称为静态联编早期联编)。
但是虚函数的产生使得静态联编变的困难,因为父类的虚函数允许被子类重写。当我们用一个父类指针指向一个子类对象的时候,编译器编译阶段可以知道父类指针的类型,但是它不能够直接用父类指针的类型中的虚函数作为本次调用的函数代码块。因为可能子类对虚函数进行重写了,这种情况下用户明显是想要调用重写后的函数。那么可能要说了,那编译器通过对象类型调用该类型中的重写后的函数不就可以实现编译阶段早期绑定了?然而,通常情况下,只有在运行时才能确定对象的模型。对于虚函数,编译器要通过动态联编晚期联编)的方式确定对应的函数代码块。即在运行时,通过对象类型确定调用的虚函数的函数代码块。重写了,就调用在对象类型中重写的函数对应的函数代码块,没有重写,那么就调用父类虚函数对应的函数代码块。
总结:
对于普通成员函数,如果通过对象调用普通成员函数,编译器直接调用该对象类型中的该函数对应的函数代码块;如果通过指针调用普通成员函数,那么编译器会直接根据指针类型调用该类型中的该函数对应的函数代码块。这些都是在编译阶段确定的,都是静态联编
对于虚函数,如果通过对象调用虚函数,编译器会直接通过对象的类型如果是通过指针或者引用的方式调用虚函数,那么编译器将无法确定该指针类型中的虚函数对应的代码块是否是用户想要调用的。因为如果是父类指针指向子类对象的话,当子类没有对父类虚函数重写,我们意思肯定是调用父类的虚函数,如果重写了,我们意思肯定是调用子类重写后的函数,这个时候编译器不能直接说因为是父类指针,就直接去调用父类中的虚函数对应的代码块。所以有了虚函数指针和虚函数表的概念,通过运行时查虚函数表的方式,确定要调用的函数的代码块的地址,因为如果子类没有重写这个虚函数的话,虚函数表中放的是父类的虚函数,如果子类对父类的虚函数重写了,那么重写后的函数的地址会覆盖掉父类虚函数的地址,调用的就是重写后的函数了。(对于虚函数指针和虚函数表,请点击此处

有一种说法,说对于调用普通成员函数来说,都是静态联编,这个没问题。但是说对于虚函数来说,如果是通过对象调用虚函数,不会经过查虚函数表,是静态联编;如果是通过指针调用虚函数,就会经过查虚函数表,是动态联编

二、指针和引用类型的兼容性

父类指针可以指向子类对象。指向子类对象的父类指针的使用请点击此处。将子类对象的类型转化为父类对象,为向上强制转换,编译器可以直接隐式转换,而父类对象的类型转换为子类对象的类型,为向下强制转换,必须显示转换。
隐式向上转换是基类指针或者基类引用可以指向基类对象或派生类对象,因此需要动态联编。

class A 
{
public:
	virtual void fun() { cout << "A fun()" << endl; }
};

class B :public A
{
public:
	void fun() { cout << "B fun()" << endl; }
};

void test1(A* a)
{
	a->fun();
}

void test2(A& a)
{
	a.fun();
}

void test3(A a)
{
	a.fun();
}

int main()
{
	B b;
	test1(&b);
	test2(b);
	test3(b);
	cout << "/***********************************/" << endl;
	A a;
	test1(&a);
	test2(a);
	test3(a);
	
	return 0;
}

运行结果:
在这里插入图片描述
对于test1和test2很好理解。参考指向子类对象的父类指针的使用
对于test3而言,形参是值拷贝的临时对象,这个值拷贝不针对虚函数指针,也就是说用哪个类创建的对象,这个对象的虚函数指针就指向哪个类的虚函数表,所以对于test3不论是传A的对象还是B的对象,利用拷贝构造函数创建出来的形参是临时对象,且对象隐藏的虚函数指针指向的都是A类的虚函数表。所以调用的都是A类的虚函数中的fun函数,而不是B类的虚函数表中被重写的fun函数。

三、静态联编和动态联编的效率问题

既然动态联编这么好用,为什么还要存在静态联编呢?
(1)效率方面
静态联编是在编译期间就执行好的,而动态联编是运行期间才开始。不仅如此,动态联编还需要通过查虚函数表,找到虚函数地址,再去这个地址里找虚函数。而且动态联编需要生成虚函数指针(存在对象中),还需要生成虚函数表。所以说动态联编的步骤比静态联编的步骤复杂,而且还需要生成虚函数指针虚函数表。时间和空间都比静态联编消耗的多。
(2)概念模型方面
虚函数一般是为了子类涉设计的,预期子类需要重写这个函数,所以将这个函数写成虚函数。然而,有些函数并不需要被子类重新定义,那么父类就没必要将这个函数写成虚函数。效率提高了,还告诉子类我没有把这个设置成虚函数,就是为了告诉你不要重新定义这个函数。

四、有关虚函数的注意事项

1.编译器不允许将构造函数设置成虚函数

构造函数不能是虚函数。因为如果构造函数放到虚函数表中,那么子类创建对象会调用父类的构造函数,然而事实上是子类先调用父类的构造函数,再用自己的构造函数。所以这个不符合继承构造函数调用的逻辑。

2.有继承关系时,析构函数尽量设置成虚函数

有继承关系时,特别是如果子类有指针成员变量,我们需要在子类的析构函数中判断指针是否指向堆空间,是的话,我们需要在子类的析构函数中对堆空间进行释放。那么如果不将父类析构函数设置成虚析构函数的话,那么如果用父类指针指向子类对象,父类指针将无法调用子类的析构函数,无法将申请的堆空间释放。(对于为什么父类析构函数写成虚函数,指向子类对象的父类指针就可以调用子类的析构函数,而不把父类析构函数写成虚函数,指向子类对象的父类指针不可以调用子类的析构函数,请点击此处查看指向父类对象的父类指针的使用)
所以有继承关系时,尽量将析构函数写成虚函数,哪怕这个类不需要用析构函数做什么。

3.友元不能是虚函数

虚函数必须是对类的成员函数而言的,不可以将友元函数设置成虚函数。

4.重新定义问题

如果派生类重新定义父类的成员函数,那么父类的所用同名的成员函数都被隐藏。包括虚函数。如下代码:

class A 
{
public:
	virtual void fun(int a) {}
	void fun() {}
};

class B :public A
{
public:
	void fun() {}
};

int main()
{
	B b;

	//b.fun(10);//error
	b.fun();//调用的是子类的成员函数
	return 0;
}

子类写了一个fun函数,父类的两个fun函数读背隐藏了(注意只要是函数同名,就被隐藏,和函数重载无关)。
如果再调用父类的fun函数,需要显示调用。
如下:
b.A::fun();
b.A::fun(10);
如果重新定义继承的方法,应该确保与原来的原型完全相同(函数名,形参列表)。当然,可以将函数返回类型修改。
(1)只要子类的函数名和形参列表与父类的虚函数相同,编译器就会认为这个是对父函数的虚函数的重写,这个时候如果返回类型和父类不一样,编译器会报错。(除去唯一一个例外:返回类型可以协变,比如父类虚函数返回类型是父类指针(引用)形式,子类重写父类的虚函数,允许将函数的返回类型改为子类指针(引用)形式)。
(2)如果子类重写父类的虚函数,那么子类的其他重名函数(不论是不是虚函数)也会被隐藏。
总的来说,只要父类的函数名和子类的函数名相同,都会被隐藏。
如下示例:

class A 
{
public:
	virtual void fun() { cout << "A fun()" << endl; }
	void fun(int a) { cout << "A fun(int)" << endl; }
};

class B :public A 
{
public:
	int fun(int a, int b) { cout << "B fun(int)" << endl; return 0; }
	
	//下面语句错误,由于函数名和形参列表与父类的虚函数一样,
	// 编译器会认为子类是对父类的虚函数重写
	// 但是由于返回类型不同,编译器会报错:重写后的函数的返回类型与被重写的虚函数的返回类型不同,而且也不是协变
	//int fun() {}
};

int main()
{
	B b;
	b.fun(10,20);
	//以下两种语句错误
	// 本来想使用父类的fun函数,但是由于子类有同名函数,父类的所有同名函数都被隐藏,所以找不到,编译器报错
	//b.fun();//想要调用父类的fun()函数
	// b.fun(10);想要调用父类的fun(int)函数
	//调用父类被隐藏的函数,需要告诉编译器这些函数在哪里,如下
	b.A::fun();    //告诉编译器是调用A类的被隐藏fun()函数
	b.A::fun(10);//告诉编译器是调用A类的被隐藏的fun(int)函数

	A* aptr = &b; //父类指针指向子类对象,调用的函数都是父类的(还有子类对父类的虚函数重写的函数)
	aptr->fun();    //A类的fun()
	aptr->fun(10);//A类的fun(int)
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孟小胖_H

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值