学习整理——C++ virtual虚函数与多态

多态与动态绑定

多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。引用Charlie Calverts对多态的描述——多态性是允许你将父对象设置成为一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。

另一个与多态相关的一个名词是动态绑定:动态绑定是指在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。程序运行过程中,把函数(或过程)调用与响应调用所需要的代码相结合的过程称为动态绑定。Java是一个纯面向对象的语言,要理解动态绑定,可以简单写一段代码。

public class Poly {
	public static void main(String[] arg){
		Animal animal=getAnimal(System.currentTimeMillis());
		animal.shout();
	}
	
	private static Animal getAnimal(long tsp){
		if(tsp % 2 == 0){
			return new Cat();
		}else{
			return new Dog();
		}
	}
}

abstract class Animal{
	abstract public void shout();
}

class Cat extends Animal{
	@Override
	public void shout(){
		System.out.println("Miao");
	}
}

class Dog extends Animal{
	@Override
	public void shout(){
		System.out.println("Wang");
	}
}

上述代码中,在编译期我们无法确定main()函数中的animal.shout()会调用的是输出Miao的还是Wang的函数,只有在运行期通过对当前时间戳的判断才会唯一确定,这就是动态绑定。按照这种思路,或许我们很容易将这个Java代码写成C++的代码。

#include <iostream>

using namespace std;

class Animal{
	public:
		void shout(){
			cout << "Not implement" << endl;
		}
};

class Cat:public Animal{
	public:
		void shout(){
			cout << "Miao" << endl;
		}
};

class Dog:public Animal{
	public:
		void shout(){
			cout << "Wang" << endl;
		}
};

Animal* getAnimal(int flag){
	if(flag % 2){
		return new Cat();
	}else{
		return new Dog();
	}
}

int main(){
	int flag;
	cin >> flag;
	Animal* animal = getAnimal(flag);
	animal->shout();
	return 0;
}

但是,无论输入的flag是多少,输出都会是Not implement。这是因为C++是编译性语言,在编译期,由于声明的animal变量是Animal指针,所以编译器在编译animal->shout()时,直接引用了Animal类的shout()函数,运行时就调用了该函数,输出Not implement。由于Cat和Dog都是继承自Animal,所有两者都会包含Animal的shout()函数,只不过是被重写了而在一般情况下不可见。为了解决这种问题,做到上面Java代码的类似效果,C++引入了virtual关键字,正确的写法是:

#include <iostream>

using namespace std;

class Animal{
	public:
		virtual void shout(){
			cout << "Not implement" << endl;
		}
		virtual ~Animal(){}
};

class Cat:public Animal{
	public:
		void shout(){
			cout << "Miao" << endl;
		}
};

class Dog:public Animal{
	public:
		void shout(){
			cout << "Wang" << endl;
		}
};

Animal* getAnimal(int flag){
	if(flag % 2){
		return new Cat();
	}else{
		return new Dog();
	}
}

int main(){
	int flag;
	cin >> flag;
	Animal* animal = getAnimal(flag);
	animal->shout();
	return 0;
}


实现原理

参考自:http://blog.csdn.net/chosen0ne/article/details/10350305

C++

在C++中通过虚函数表的方式实现多态,每个包含虚函数的类都具有一个虚函数表(virtual table),在这个类对象的地址空间的最靠前的位置存有指向虚函数表的指针。在虚函数表中,按照声明顺序依次排列所有的虚函数。实例化一个对象时调用完构造函数后,便会初始化这些指针。由于C++在运行时并不维护类型信息,所以在编译时直接在子类的虚函数表中将被子类重写的方法替换掉。当程序中将要调用一个虚函数时,编译器会编译成从指针中找到具体的实现函数。因为虚函数指针在对象实例化时就被初始化了,所以即便是该对象被一个父类指针指向也能找到正确的函数。

另外,一日为虚终生为虚。一个函数被声明为虚时,其衍生类的该函数也将一直是虚函数,即使衍生类没有显式声明为virtual。


Java

而Java中,在运行时会维持类型信息以及类的继承体系。每一个类会在方法区中对应一个数据结构用于存放类的信息,可以通过Class对象访问这个数据结构。其中,类型信息具有superclass属性指示了其超类,以及这个类对应的方法表(其中只包含这个类定义的方法,不包括从超类继承来的)。而每一个在堆上创建的对象,都具有一个指向方法区类型信息数据结构的指针,通过这个指针可以确定对象的类型。

JVM中用于方法调用的指令包括:

invokevirtual:用于调用实例方法,会根据对象的实际类型进行调用。

invokespecial:需要特殊处理的实例方法,比如:public final方法、私有方法和父类方法等。调用的方法取决于引用的类型。

invokeinterface:调用接口的方法。

invokestatic:调用类方法。

按照上面描述,对于子类覆盖父类的方法,编译后,调用指令应该是invokevirtual,调用的方法取决于对象的类型。invokevirtual方法查找的实现方式是:

1. 通过对象中类指针找到其类信息,然后在方法表中根据方法签名找到该方法。

2. 如果不在当前类,则递归查找其父类的方法表直到Object类。

3. 如果找到Object类,也没有该方法,会抛出NoSuchMethodException异常。

与js、lua等动态语言类似,Java多态的实现方式依赖于内存中的类型体系信息,存在一个“原型链”,是一个完全动态的查找过程,相对于C++而言,效率会低一些,因为存在一个链表遍历查找的过程。之所以,Java中可以这样实现,本质上是因为它是一门虚拟机语言,虚拟机会维持所有的这些类型信息。


纯虚函数

虚函数为C++提供实现动态绑定的机制,但是拥有虚函数的类本身是可以被实例化的。要实现类似Java抽象类的机制,即类本身不能实例化,其子类需要实现其所有抽象方法才能被实例化,C++提供了纯虚函数。在C++中,拥有纯虚函数的类不能被实例化,其子类也需要实现其所有纯虚函数才能被实例化。纯虚函数的写法是:

		virtual void shout()=0;          //纯虚函数
		virtual void toShout(){};        //虚函数
只拥有纯虚函数可以被用作接口来使用,提供给其衍生类才实现,类似Java的interface。


问题

1.构造函数中可以调用虚函数吗?析构函数中可以调用虚函数吗?

如果在父类构造函数中调用虚函数,由于子类还没构造,所以可能会出错,但由于C++避免这种错误,所以会调用父类的实现,但这不是多态;在子类中调用虚函数,由于子类已经构造了,此时调用虚函数跟普通方法没有任何区别。

如果在子类析构函数中调用虚函数,情况如上,和调用普通方法没有任何区别;而在父类析构函数中调用虚函数,如果只是虚函数,情况也如上,C++会调用父类的实现,但如果是纯虚函数,编译期编译器就会报错,出现undefined reference。

所以不要在构造函数和析构函数里调用虚函数,即使没有语法错误,运行也不会如你所愿,就算有时会成功(不同编译器),到最后也会让人困惑不已。这种调用根本不是起到虚函数的作用,所以为了代码规范,可以说成是不可以调用。


2.构造函数可以是虚函数吗?

不可以。虚函数指针是存放在对象内存中,一个对象在实例化(构造)前是没有内存的。如果构造函数都是虚的时候,该虚构造函数指针能够放在哪里?该问题编译器能够查看出来,是语法错误。


3.析构函数可以是虚函数吗?

当一个类拥有虚函数的时候,析构函数应当也被声明为virtual。因为当使用多态时,父类的析构函数没有声明为virtual而调用delete时,会直接调用父类析构函数,子类没有被调用。为了程序的正确运行,防止只析构基类而不析构派生类的状况发生,应当记住这个规范。





  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值