多态内容:虚函数、纯虚函数、抽象类、虚析构函数、虚拟继承

什么是多态?

多态性( polymorphism )是面向对象设计语言的基本特征之一。仅仅是将数据和函数捆绑在一起,进行类的封装,使用一些简单的继承,还不能算是真正应用了面向对象的设计思想。多态性是面向对象的精髓。多态性可以简单地概括为“一个接口,多种方法”

通常是指对于同一个消息、同一种调用,在不同的场合,不同的情况下,执行不同的行为 。

为什么使用多态?

我们知道,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类)。它们的目的都是为了代码重用。而多态除了代码的复用性外,还可以解决项目中紧偶合的问题,提高程序的可扩展性。

如果项目耦合度很高的情况下,维护代码时修改一个地方会牵连到很多地方,会无休止的增加开发成本。而降低耦合度,可以保证程序的扩展性。而多态对代码具有很好的可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。

C++支持两种多态性:编译时多态和运行时多态。

编译时多态:也称为静态多态,我们之前学习过的函数重载、运算符重载就是采用的静态多态,C++编译器根据传递给函数的参数和函数名决定具体要使用哪一个函数,又称为先期联编(early binding)。

运行时多态:在一些场合下,编译器无法在编译过程中完成联编,必须在程序运行时完成选择,因此编译器必须提供这么一套称为“动态联编”(dynamic binding)的机制,也叫晚期联编(late binding),C++通过虚函数来实现动态联编

虚函数的定义

虚函数就是在基类中被声明为virtual,并在一个或多个派生类中被重新定义的成员函数。其形式如下:

// 类内部
class 类名 {
	virtual 返回类型 函数名(参数表)
	{
		//...
	}
};
//类之外
virtual 返回类型 类名::函数名(参数表)
{
	//...
}

如果一个基类的成员函数定义为虚函数,那么它在所有派生类中也保持为虚函数,即时在派生类中忽略了virtual关键字,也仍然是虚函数。派生类对虚函数可根据需要重定义,重定义的格式有一定的要求:
1、与基类的虚函数有相同的参数个数
2、与基类的虚函数有相同的参数类型
3、与基类的虚函数有相同的返回类型

class Base {
public:
	virtual void display()
	{   cout << "Base::display()" << endl;  }
	virtual void print()
	{   cout << "Base::print()" << endl;    }
};

class Derived
: public Base {
public:
	virtual void display()
	{ cout << "Derived::display()" << endl; }
};

void test(Base * pbase)
{
	pbase->display();
	pbase->print();
}

int main(int argc,char **argv)
{
    Base base;
    Derived derived;
    test(&base);
    test(&derived);
    return 0;
}

程序执行结果:
Base::display()
Base::print()
Derived::display()
Base::print()

上面的例子中,对于test() 函数,如果不管测试的结果,从其实现来看,通过类Base 的指针pbase 只能调用到Base 类型的display 函数;但是最终结果test(&derived)调用,会调用到Derived 类的display 函数,这里就体现出虚函数的作用了,这是怎么做到的呢,或者说虚函数底层是怎么实现的呢?

虚函数的实现机制

虚函数就是通过一张虚函数表(Virtual Function Table)实现的。具体地讲,当类中定义了一个虚函数后,会在该类创建的对象的存储布局的开始位置多一个虚函数指针(vfptr),该虚函数指针指向了一张虚函数表,而该虚函数表就像一个数组,表中存放的就是各虚函数的入口地址。如下图:

当一个基类中设有虚函数,而一个派生类继承了该基类,并对虚函数进行了重定义,我们称之为覆盖(override),这里的覆盖指的是派生类的虚函数表中相应虚函数的入口地址被覆盖。

在这里插入图片描述
虚函数机制是如何被激活的呢,或者说动态多态是怎样表现出来的呢?

1、基类定义虚函数,派生类重定义虚函数(需要有继承关系)
2、创建派生类对象
3、基类指针或引用指向派生类对象
4、基类指针调用虚函数

哪些函数不能被设置为虚函数?

1、普通函数(非成员函数):定义虚函数的主要目的是为了重写达到多态,所以普通函数声明为虚函数没有意义,因此编译器在编译时就绑定了它。

2、静态成员函数:静态成员函数对于每个类都只有一份代码,所有对象都可以共享这份代码,他不归某一个对象所有,所以它也没有动态绑定的必要。

3、内联成员函数:内联函数本就是为了减少函数调用的代价,所以在代码中直接展开。但虚函数一定要创建虚函数表,这两者不可能统一。另外,内联函数在编译时被展开,而虚函数在运行时才动态绑定。

4、构造函数:这个原因很简单,主要从语义上考虑。因为构造函数本来是为了初始化对象成员才产生的,然而虚函数的目的是为了在完全不了解细节的情况下也能正确处理对象,两者根本不能“ 好好相处 ”。因为虚函数要对不同类型的对象产生不同的动作,如果将构造函数定义成虚函数,那么对象都没有产生,怎么完成想要的动作呢。

5、友元函数:当我们把一个函数声明为一个类的友元函数时,它只是一个可以访问类内成员的普通函数,并不是这个类的成员函数,自然也不能在自己的类内将它声明为虚函数。

虚函数的访问

指针访问

使用指针访问非虚函数时,编译器根据指针本身的类型决定要调用哪个函数,而不是根据指针指向的对象类型;使用指针访问虚函数时,编译器根据指针所指对象的类型决定要调用哪个函数(动态联编),而与指针本身的类型无关。

引用访问

使用引用访问虚函数,与使用指针访问虚函数类似,表现出动态多态特性。不同的是,引用一经声明后,引用变量本身无论如何改变,其调用的函数就不会再改变,始终指向其开始定义时的函数。因此在使用上有一定限制,但这在一定程度上提高了代码的安全性,特别体现在函数参数传递等场合中,可以将引用理解成一种“受限制的指针” 。

对象访问

和普通函数一样,虚函数一样可以通过对象名来调用,此时编译器采用的是静态联编。通过对象名访问虚函数时,调用哪个类的函数取决于定义对象名的类型。对象类型是基类时,就调用基类的函数;对象类型是子类时,就调用子类的函数。

成员函数中的访问

在类内的成员函数中访问该类层次中的虚函数,采用动态联编,要使用this指针。

构造函数和析构函数中访问

构造函数和析构函数是特殊的成员函数,在其中访问虚函数时,C++采用静态联编,即在构造函数或析构函数内,即使是使用"this->虚函数名"的形式来调用,编译器仍将其解释为静态联编的“本类名::虚函数名”。即它们所调用的虚函数是自己类中定义的函数,如果在自己的类中没有实现该函数,则调用的基类中的虚函数。但绝对不会调用任何在派生类中重定义的虚函数。

纯虚函数

纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的是实现留给该基类的派生类去做。这就是纯虚函数的作用。纯虚函数的格式如下:

class 类名 {
public:
	virtual 返回类型 函数名(参数包) = 0;
}

设置纯虚函数的意义,就是让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它。

class Base { 
public:
	virtual void display() = 0;
};

class Child
: public Base {
public:
	virtual void display()
	{ cout << "Child::display()" << endl;} 
}

声明纯虚函数的目的在于,提供一个与派生类一致的接口。
注:成员函数后加const后我们称这个函数为常函数,常函数内不可以修改成员属性,成员属性声明时加关键字mutable后,在常函数中依然可以修改。

class Figure {
public:
	virtual void display() const = 0;
	virtual double area() const = 0;
};

class Circle
: public Figure {
public:
	explicit Circle(double radius)
	:_radius(radius)
	{}
	void display() const {cout << "Circle";}
	double area() const
	{return 3.14159 * _radius * _radius;}
private:
	double _radius;
}

class Rectangle
: public Figure {
public:
	Rectangle(double length, double width)
	: _length(length)
	, _width(width)
	{}
	void display() const { cout << "Rectangle"; }
	double area() const { return _length * _width; }
private:
	double _length;
	double _width;
};

class Triangle
: public Figure {
public:
	Triangle(double a, double b, double c)
	: _a(a)
	, _b(b)
	, _c(c)
	{}
	void display() const { cout << "Triangle"; }
	double area() const {
		double p = (_a + _b + _c) / 2;
		return sqrt(p * (p - _a) * (p - _b) * (p - _c));
	}
private:
	double _a;
	double _b;
	double _c;
};

抽象类

一个类可以包含多个纯虚函数。只要类中含有一个纯虚函数,该类便为抽象类。一个抽象类只能作为基类来派生新类,不能创建抽象类的对象。

和普通的虚函数不同,在派生类中一般要对基类中纯虚函数进行重定义。如果该派生类没有对所有的纯虚函数进行重定义,则该派生类也会成为抽象类。这说明只有在派生类中给出了基类中所有纯虚函数的实现时,该派生类便不再是抽象类。

除此以外,还有另外一种形式的抽象类。对一个类来说,如果只定义了protected型的构造函数而没有提供public构造函数,无论是在外部还是在派生类中作为其对象成员都不能创建该类的对象,但可以由其派生出新的类,这种能派生新类,却不能创建自己对象的类是另一种形式的抽象类。

class Base {
protected:
	Base(int base): _base(base) { cout << "Base()" << endl;}
public:
	int _base;
};

class Derived
: public Base {
public:
	Derived(int base, int derived)
	: Base(base)
	, _derived(derived)
	{ cout << "Derived(int,int)" << endl; }
	void print() const
	{
		cout << "_base:" << _base
		<< ", _derived:" << _derived << endl;
	}
private:
	int _derived;
};
void test()
{
	Base base(1);//error
	Derived derived(1, 2);
}

虚析构函数

虽然构造函数不能被定义成虚函数,但析构函数可以定义为虚函数,一般来说,如果类中定义了虚函数,析构函数也应被定义为虚析构函数,尤其是类内有申请的动态内存,需要清理和释放的时候。

class Base {
public: 
	Base(const char * pstr)
	: _pstr(new char[strlen(pstr)+1]())
	{ cout << "Base(const char *)" << endl;}

	~Base() {
		delete [] _pstr;
		cout << "~Base()" << endl;
	}
private:
	char * _pstr;
}

class Derived
: public Base {
public:
	Derived(const char * pstr, const char * pstr2)
	: Base(pstr)
	, _pstr2(new char[strlen(pstr2)+1]())
	{ cout << "Derived(const char *, const char *)" << endl;}

	~Derived(){
		delete [] _pstr2;
		cout << "~Derived()" << endl;
	}
private:
	char * _pstr2;
}

void test()
{
	Base * pbase = new Derived("hello","wuhan");
	delete pbase;
}

如上,在例子中,如果基类Base的析构函数没有设置成虚函数,则在执行delete pbase;语句时,不会调用派生类Derived的析构函数,这样就造成内存泄漏。此时,将基类Base的析构函数设置为虚函数,就可以解决该问题。

如果有一个基类的指针指向派生类的对象,并且想通过该指针delete派生类对象,系统只会执行基类的析构函数,而不会执行派生类的析构函数。为避免这种情况的发生,往往把基类的析构函数声明为虚的,此时,系统将先执行派生类对象的析构函数,然后再执行基类的析构函数。

如果基类的析构函数声明为虚的,派生类的析构函数也将自动成为虚析构函数,无论派生类析构函数声明中是否加virtual关键字。

重载、隐藏、覆盖

重载:发生在同一个类中,函数名称相同,但是参数的类型、个数、顺序不同。

覆盖:发生在父子类中,同名虚函数,参数亦完全相同。

隐藏:发生在父子类中,指的是在某些情况下,派生类中的函数屏蔽了基类中的同名函数。当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数。

菱形继承

菱形继承概念:两个派生类继承同一个基类,又有某个类同时继承两个派生类。
设定基类为Base,两个派生类为Derived1和Derived2,该类为MyClass
则Derived1和Derived2都继承了Base的数据,当MyClass使用Base中的数据时,会产生二义性。
(多继承引发的二义性,我们可以通过作用域加以区分)。
并且,在MyClass类中,Base中的数据我们只需要一份就可以了。

class Base {
public:
	int _data;
};

class Derived1 : virtual public Base {

};

class Derived2 : virtual public Base {

};

class MyClass : public Derived1, public Derived2 {


};

void test() {
	MyClass m;
	//m._data = 20;   产生不明确的数据问题
	m.Derived1::_data = 20;
	m.Derived2::_data = 10;
	//当菱形继承,两个父类拥有相同数据,需要加以作用域区分
	cout << "m.Derived1::_data = " << m.Derived1::_data << endl;
	cout << "m.Derived2::_data = " << m.Derived2::_data << endl;

	//菱形继承导致了最初的基类数据有两份,但我们只需要一份,浪费资源
	//利用虚继承 解决菱形继承问题
    //继承之前 加上关键字virtual变为虚继承,则Base类称为虚基类
	//当我们发生虚继承之后,该份_data数据就只有一个了

	cout << "m._data = " << m._data << endl;

	//采用virtual继承时,MyClass从Derived1和Derived2继承下来的不再是两份_data,而是两个vbptr
	//vbptr 虚基类指针 指向了 vbtable 虚基类表
	//两个vbptr分别指向两个不同父类的vbtable,表中存有_data数据的地址,
	//这个地址只有一份,通过加上不同的偏移可以得到同一块_data数据内存的地址
}

虚拟继承

C++ 中被virtual关键字所修饰的事物或现象在本质上是存在的,但是没有直观的形式表现,无法直接描述或定义,需要通过其他的间接方式或手段才能够体现出其实际上的效果。关键就在于存在、间接和共享这三种特征:

1、虚函数是存在的
2、虚函数必须通过一种间接的运行时(而不是编译时)机制才能够激活(调用)的函数。
3、共享性表现在基类会共享被派生类重定义后的虚函数

那虚拟继承又是如何表现这三种特征的呢?

1、存在即表示虚继承体系和虚基类确实存在
2、间接性表现在当访问虚基类的成员时同样也必须通过某种间接机制来完成(通过虚基表来完成)
3、共享性表现在虚基类会在虚继承体系中被共享,而不会出现多份拷贝

虚拟继承是指在继承定义中包含了virtual关键字的继承关系。虚基类是指在虚继承体系中的通过virtual继承而来的基类。语法格式如下:

class Baseclass;
class Subclass
: public/private/protected virtual Baseclass
{
public:    //...
private:   //...
protected: //...
};
//其中Baseclass称之为Subclass的虚基类,而不是说Baseclass就是虚基类
#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;

class A {
public:
	A() : _ia(10) {}
	virtual void f() { cout << "A::f()" << endl; }
private:
	int _ia;
};
class B
: virtual public A {
public:
	B() : _ib(20) {}
	void fb() { cout << "A::fb()" << endl; }
	/*virtual*/ void f() { cout << "B::f()" << endl; }
	virtual void fb2() { cout << "B::fb2()" << endl; }
private:
	int _ib;
};
void test(void) {
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
	B b;
	return 0;
}
// 结论一:单个虚继承,不带虚函数
// 虚继承与继承的区别
// 1. 多了一个虚基指针
// 2. 虚基类位于派生类存储空间的最末尾
// 结论二:单个虚继承,带虚函数
// 1. 如果派生类没有自己的虚函数,此时派生类对象不会产生虚函数指针
// 2. 如果派生类拥有自己的虚函数,此时派生类对象就会产生自己本身的虚函数指针,并且该虚函数指针位于派生类对象存储空间的开始位置

虚拟继承时派生类对象的构造和析构

在普通的继承体系中,比如A派生出B,B派生出C,则创建C对象时,在C类构造函数的初始化列表中调用B类构造函数,然后在B类构造函数初始化列表中调用A类的构造函数,即可完成对象的创建操作。但在虚拟继承中,则有所不同。

#include <iostream>

using namespace std;

class A{
public:
    A(){ cout << "A()" << endl; }
    
    A(int ia):_ia(ia) 
    {cout << "A(int)" << endl;}
    
    void f() { cout << "A::f()" << endl; }
protected:
    int _ia;
};

class B
: virtual public A{
public:
    
    B(){ cout << "B()" << endl; }
    
    B(int ia, int ib)
    :A(ia)
    ,_ib(ib)
    { cout << "B(int,int)" << endl;}

protected:
    int _ib;
};

class C
: public B {
public:
    
    C(int ia, int ib, int ic)
    : B(ia, ib)
    , _ic(ic)
    { cout << "C(int,int,int)" << endl; }

    void show() const {
        cout << "_ia: " << _ia << endl
             << "_ib: " << _ib << endl
             << "_ic: " << _ic << endl;
    }
private:
    int _ic;
};

int main(int argc,char **argv)
{
    C c(10,20,30);
    c.show();
    return 0;
}

该程序的执行结果为:

A()
B(int,int)
C(int,int,int)
_ia: 190518752
_ib: 20
_ic: 30

从最终的打印结果来看,在创建对象c的过程中,我们看到C带三个参数的构造函数执行了,同时B带两个参数的构造函数也执行了,但A带一个参数的构造函数没有执行,而是执行了A的默认构造函数。这与我们的预期是有差别的。如果想要得到预期的结果,我们还需要在C的构造函数初始化列表最后,显式调用A的相应构造函数。那为什么需要这样做呢?

在 C++ 中,如果继承链上存在虚继承的基类,则最底层的子类要负责完成该虚基类部分成员的构造。即我们需要显式调用虚基类的构造函数来完成初始化,如果不显式调用,则编译器会调用虚基类的缺省构造函数,不管初始化列表中次序如何,对虚基类构造函数的调用总是先于普通基类的构造函数。如果虚基类中没有定义的缺省构造函数,则会编译错误。因为如果不这样做,虚基类部分会在存在的多个继承链上被多次初始化。很多时候,对于继承链上的中间类,我们也会在其构造函数中显式调用虚基类的构造函数,因为一旦有人要创建这些中间类的对象,我们要保证它们能够得到正确的初始化。这种情况在菱形继承中非常明显, 我们接下来看看这种情况。

#include <iostream>
using std::cout;
using std::endl;
class B {
public:
	B() : _ib(10), _cb('B') { cout << "B()" << endl; }
	B(int ib, char cb)
	: _ib(ib), _cb(cb){ cout << "B(int,char)" << endl; }
	//virtual
	void f() { cout << "B::f()" << endl; }
	//virtual
	void Bf() { cout << "B::Bf()" << endl; }
private:
	int _ib;
	char _cb;
};
class B1 : virtual public B {
public:
	B1() : _ib1(100), _cb1('1') {}
	B1(int ib, char ic, int ib1, char cb1)
	: B(ib, ic)
	, _ib1(ib1)
	, _cb1(cb1)
	{ cout << "B1(int,char,int,char)" << endl; }
	//virtual
	void f() { cout << "B1::f()" << endl; }
	//virtual
	void f1() { cout << "B1::f1()" << endl; }
	//virtual
	void Bf1() { cout << "B1::Bf1()" << endl; }
private:
	int _ib1;
	char _cb1;
};

class B2 : virtual public B {
public:
	B2() : _ib2(1000), _cb2('2') {}
	B2(int ib, char ic, int ib2, char cb2)
	: B(ib, ic)
	, _ib2(ib2)
	, _cb2(cb2)
	{ cout << "B2(int,char,int,char)" << endl; }
	//virtual
	void f() { cout << "B2::f()" << endl; }
	//virtual
	void f2() { cout << "B2::f2()" << endl; }
	//virtual
	void Bf2() { cout << "B2::Bf2()" << endl; }
private:
	int _ib2;
	char _cb2;
};
class D
: public B1
, public B2 {
public:
	D() : _id(10000), _cd('3') {}
	D(int ib1, char ib1,
	int ib2, char cb2,
	int id, char cd)
	: B1(ib1, ib1)
	, B2(ib2, cb2)
	, _id(id)
	, _cd(cd)
	{ cout << "D(...)" << endl; }
	//virtual
	void f() { cout << "D::f()" << endl; }
	//virtual
	void f1() { cout << "D::f1()" << endl; }
	//virtual
	void f2() { cout << "D::f2()" << endl; }
	//virtual
	void Df() { cout << "D::Df()" << endl; }
private:
	int _id;
	char _cd;
};
void test(void) {
	D d;
	cout << sizeof(d) << endl;
	return 0;
}
//结论:虚基指针所指向的虚基表的内容
// 1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
// 2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移

如果是在若干类层次中,从虚基类直接或间接派生出来的派生类的构造函数初始化列表均有对该虚基类构造函数的调用,**那么创建一个派生类对象的时候只有该派生类列出的虚基类的构造函数被调用,其他类列出的将被忽略,**这样就保证虚基类的唯一副本只被初始化一次。即虚基类的构造函数只被执行一次。

对于虚继承的派生类对象的析构,析构函数的调用顺序为:

1、先调用派生类的析构函数;
2、然后调用派生类中成员对象的析构函数;
3、再调用普通基类的析构函数;
4、最后调用虚基类的析构函数。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值