c++多态的一些细节

1、多态的构成条件

        多态是不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如student继承了person,person对象买票全价,student买票半价。代码和执行结果如下。

class person
{
public:
	virtual void BuyTicket()
	{
		cout << "全价" << endl;
	}
};

class student: public person
{
public:
	virtual void BuyTicket()
	{
		cout << "半价" << endl;
	}
};

void Func(person& p)
{
	p.BuyTicket();
}

int main()
{
	person p;
	student s;
	Func(p);
	Func(s);
	return 0;
}

        在继承中构成多态有两个条件:(1)必须通过基类的指针或者引用调用虚函数,如上,就是通过将子类对象传参给父类引用调用子类的虚函数。(2)被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

        虚函数:被virtual修饰的类成员函数为虚函数。注意,这里的virtual关键字和之前在菱形继承中所讲的virtual关键字(虚继承)没有一点关系。

        虚函数的重写(也称覆盖):在派生类中有一个跟父类完全相同的虚函数(即派生类虚函数与基类虚函数的函数名、返回值类型、参数列表完全相同),称子类的虚函数重写了基类的虚函数。注意,在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,派生类会从基类继承virtual关键字,也可以构成多态(但是不规范),如果基类的成员函数不加virtual,那么就算派生类的成员函数加了也不构成多态,这时候构成重定义(隐藏)。

        注意上述代码的Func函数,在函数体内通过一个person&类型(也可以是指针)来调用成员函数,如果构成多态,则调用的函数与person&指向的对象有关。如果不构成多态,那么调用的函数由类型来决定,即只会调用person类型的成员函数。子类在重写父类虚函数时,也会继承父类函数的缺省参数,并且无法修改。

        

        虚函数重写有两个例外:

        (1)协变(基类与派生类虚函数的返回值类型不同),基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。注意(两个返回值的对象必须具有继承关系)。如下这段代码,两个虚函数依旧构成多态。

class A
{};

class B:public A
{};

class person
{
public:
	virtual A* BuyTicket()
	{
		cout << "全价" << endl;
		return nullptr;
	}
};

class student: public person
{
public:
	virtual B* BuyTicket()
	{
		cout << "全价" << endl;
		return nullptr;
	}
};

               (2)析构函数的重写(基类与派生类析构函数的名字不同),用户需要将基类和派生类的析构函数定义为虚函数。先看下面这段代码及其执行结果。

class person
{
public:
	~person()
	{
		cout << "~person" << endl;
	}
};

class student :public person
{
public:
	~student()
	{
		cout << "~student" << endl;
	}
};

int main()
{
	person* pp = new student;
	delete pp;
	return 0;
}

        为什么只执行了基类的析构函数?在之前讲过的派生类地址赋值给基类指针里有讲,这个基类指针只能访问基类有的成员,自然不能调用派生类的析构函数,这本质是派生类对象对子类对象赋值时的切片行为。一旦student类中有其他成员变量,delete时,这部分成员变量就不会释放掉,造成内存泄漏。

        所以在继承中,析构函数也需要用virtual来修饰,构成多态,这样在delete时,会根据指向的对象是父类还是子类来选择调用析构函数。有人会有疑问,父类和子类的析构函数不是不同名吗,为什么会构成多态?这是因为编译器将类的析构函数都修改为destructor(),变成了同名函数,所以可以构成多态。

2、override和final

        final修饰虚函数(放在参数列表后方修饰),则该虚函数不能被重写。如果修饰类则该类不能被继承。override用于检查函数是否重写,放在参数列表后方修饰,如果没有完成重写会发生报错。

3、纯虚函数和抽象类

        在虚函数后面加上 =0,这个函数为纯虚函数(纯虚函数只有声明即可,不需要实现),包含纯虚函数的类叫做抽象类(也叫接口类), 抽象类不能实例化出对象。派生类继承之后如果不重写纯虚函数,那么派生类也是一个抽象类,也不能实例化出对象。只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数也更加体现了接口继承。

class Car//抽象类
{
	virtual void drive() = 0;
};

class Benz :public Car//没有完成重写,还是一个抽象类
{};

class Bmw :public Car//完成了重写,可以实例化出对象
{
	virtual void drive()
	{}
};

int main()
{
	Bmw b;
	return 0;
}

4、虚表与多态的原理

        观察下面这段典型的构成重写的继承关系的代码和运行结果。(x86环境下,指针的大小为4字节)

class Person
{
public:
	virtual void BuyTicket() { cout << "全价" << endl; }
	int _p;
};

class Student :public Person
{
public:
	virtual void BuyTicket() { cout << "半价" << endl; }
	int _s;
};

int main()
{
	Person p;
	Student s;
	cout << sizeof(p) << endl;
	cout << sizeof(s) << endl;
	return 0;
}

        我们知道成员函数都是存储在代码段的,不会存储在对象中,实际对象大小怎么比我们预想的多了四个字节?这是因为基类如果有虚函数,则基类对象中会有一个指针vfptr(virtual function table pointer),指向一张虚函数表(简称虚表)本质上是一个函数指针数组。这张虚函数表中保存基类中虚函数的地址。派生类继承基类的所有成员,自然将这个指针vfptr也继承下来了。注意如果一个派生类与两个父类时多继承的关系,而两个父类中都有vfptr,那么自然要继承两个vfptr,观察下面这段代码以及运行结果和内存分布。同理,之前虚继承中讲的虚基表也是存在代码段的,同一类型的对象都共有同一张虚基表。

class Person
{
public:
	virtual void BuyTicket() { cout << "全价" << endl; }
	int _p;
};

class Man
{
public:
	virtual void man(){}
};

class Student :public Person, public Man
{
public:
	virtual void BuyTicket() { cout << "半价" << endl; }
	int _s;
};

int main()
{
	Person p;
	Student s;
	cout << sizeof(p) << endl;
	cout << sizeof(s) << endl;
	return 0;
}

        s对象继承了Person和Man类型的两张虚函数表vfptr,显然大小为16字节。

        派生类对基类的虚函数重写,本质上就是将虚表中所存储的函数地址替换了。

        观察下面这段构成多态的代码。在上面的基础上多了一个Func函数。

class Person
{
public:
	virtual void BuyTicket() { cout << "全价" << endl; }
	int _p;
};

class Man
{
public:
	virtual void man(){}
};

class Student :public Person, public Man
{
public:
	virtual void BuyTicket() { cout << "半价" << endl; }
	int _s;
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person p;
	Student s;
	Func(p);
	Func(s);
	return 0;
}

        运行结果很显然如下。

        如果将Func函数的参数类型由Person&改为Person,自然不构成多态。(构成多态的两个原则1、完成虚函数的重写2、由类的引用或指针调用虚函数)运行结果如下。

        

        对比两次的运行结果,多态之所以能够实现,在虚函数重写的基础上,通过调用Func函数,传参时发生派生类对基类赋值的切片行为,调用虚函数需要查询虚表中保存的函数,在派生类中的虚表保存的虚函数是派生类重写之后的虚函数,调用的自然是派生类的虚函数,如果传参传入的是父类的对象,对象中的虚表保存的是没有重写的虚函数,调用的自然是基类的虚函数。注意构成多态时,是在调用函数传参完成派生类对基类赋值时切片行为,这才使得调用的函数由引用或者指针指向的对象决定的,将Func函数的参数类型由Person&改为Person后,不构成多态,这时候函数体内对象调用的函数在编译阶段就已经确定了,编译时,编译器就根据Person对象确定了调用的时基类的函数,而多态是在程序运行时确定调用哪个函数的。

        众所周知,不论是普通函数还是虚函数,都是存储在代码段的,那么虚表存储在哪里?也是存储在代码段的,这样,不论类实例化出多少对象,虚表都只有一个,但是对象的vfptr是各自独有的,不过都指向一张虚表。注意,因为虚表是一个函数指针数组,存储类中虚函数的地址,内联函数(inline)是没有地址的,所以内联函数不能重写,之前讲过类中定义的函数默认是inline函数,如果加virtual关键字修饰,那么编译器会将函数处理为非内联函数。

5、虚函数和多继承

        在单继承关系下,如果父类和子类都有虚函数,且父类和子类的虚函数不构成重写,那么子类的虚函数会放在父类的虚表中,与父类的虚函数同时存储在虚表中,不发生覆盖。子类不会新生成一张虚表来存放自己的虚函数。子类中只有一个vfptr。

        在多继承关系下,如果两个父类和子类都有虚函数,且父类和子类的虚函数不构成重写,那么子类中会有两个父类的两张虚表,子类的虚函数存放在第一个父类的虚表中。子类中有两个vfptr。

6、一些其他细节

        静态成员可以是虚函数吗?不能,因为静态成员函数的参数列表没有this指针,访问虚函数本质是通过this指针访问对象里的vfptr来访问虚函数,而通过类名::成员函数的方式并不需要实例化出对象,因此静态成员不能是虚函数。

        构造函数可以是虚函数吗?不能,因为对象中的虚函数表指针是在构造函数初始化初始化列表阶段才初始化的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值