C++多态

1. 多态的实现

  • 静态的多态:编译时实现。如template 定义的模板函数传入不同的变量,编译时处理成不同的函数,然后根据传入变量的类型进行调用,这是由函数重载实现的。静态的多态有:函数重载、重定义、运算符重载。

  • 动态的多态:运行时实现。假设父类有许多派生类,那么对于父类和派生类中的某个function可以有多种实现方式。在外部调用时,可以设计一个函数,当传递入不同的对象可以调用不同类中的function。这种通过父类的指针或引用保存子类空间的地址,在运行时传递不同的对象时调用不同的函数的方式称为多态。实现动态的多态通过虚函数。虚成员函数有时称为方法。实现动态的多态需具有三个条件:

    • 有对父类继承的子类
    • 子类重写父类的虚函数
    • 通过父类指针(或引用)指向子类空间调用虚函数

1.1 虚函数

  • 在父类中某非静态成员函数前加virtual关键字修饰的成员函数称为虚函数。子类中该函数的virtual可加可不加,因为继承保留了父类虚函数的属性(父类的访问限定符也会被继承,虚函数表是可以公开访问的,即若父类中函数是公有的、子类是私有,但是可被外部访问——通过虚表里的函数指针)。被虚函数修饰的函数会产生虚函数指针(在构造函数初始化列表初始化),该指针指向虚函数表,该表中记录的是当前类的虚函数的入口地址。

  • 子类完成函数重写(覆盖)需满足三个条件:子类中的虚函数与父类虚函数(函数名、参数、返回值)相同。若任意一个条件不满足就构成隐藏。对于返回值不同的,有一个特殊情况也构成重写——协变(返回值是父子关系的指针或引用)。

class Base
{
public:
	virtual void greeting()
	{
		std::cout << "hello, I am Base!" << std::endl;
	}
};

class DerivedA : public Base
{
public:
	virtual void greeting()  //子类重写父类的虚函数:函数名、参数、返回值相同
	{
		std::cout << "hello, I am DerivedA!" << std::endl;
	}
};
class DerivedB : public Base
{
public:
	virtual void greeting(int a) //不满足构成多态的条件,为隐藏
	{
		std::cout << "hello, I am DerivedB!" << std::endl;
	}
};

void Func(Base& b)   //若不构成多态,最终的调用结果和形参b的类型有关;考虑参数传参(拷贝构造)
{
	b.greeting();
}

//----------- 协变 - 返回值是父子关系的指针或引用-----------
class A {};
class B :public A {};

class BaseTest {
public:
	virtual A* test()
	{
		std::cout << "virtual A* test()" << std::endl;
		return nullptr;
	}
};

class DerivedTest : public BaseTest {
public:
	virtual B* test()   //返回值与父类不同,但是构成重写
	{
		std::cout << "virtual B* test()" << std::endl;
		return nullptr;
	}
};

//----------------------------------------------------------

int main()
{
	Base* pB = new Base;
	Base* pDA = new DerivedA;
	pB->greeting();
	pDA->greeting();

	DerivedB pDB;
	Func(pDB);

	BaseTest* pBT = new DerivedTest;
	pBT->test();
	return 0;
}

运行结果

hello, I am Base!
hello, I am DerivedA!
hello, I am Base!
virtual B* test()

1.2 纯虚函数

  • 虚函数后加 = 0 构成纯虚函数。含有纯虚函数的类称为抽象类,抽象类不能单独实例化对象。抽象类必须继承且被子类重写,否则子类也是抽象类。纯虚函数使用前提:①不会单独调用父类;②父类一定会派生出子类,且子类一定会重写虚函数(此时父类的虚函数是没有意义的)。
class A
{
public: 
	virtual void f() = 0;  //纯虚函数只声明不实现——实现没有价值,虚函数放在虚函数表中,而不是静态存储区
};

class B: public A
{
public:    //派生类继承纯虚函数也不能实例化对象,只有重写纯虚函数才可
    virtual void f() 
    {
        std::cout << "hello world" << std::endl;
    }
};
int main()
{
    A* p = new B;
    p->f();
    return 0;
}

1.3 虚析构

  • 对于抽象类的析构需要使用虚析构函数(析构函数前加virtual),否则根据父类指针维护的子类对象在释放时,只会析构父类的空间而不会调用子类的析构。使用虚析构可以通过父类指针释放子类的所有空间。(先析构子类,再析构父类)
  • 若父类的析构函数是虚函数,则子类的析构构成重写——虽然析构的名字不同,但是编译器会将析构函数类名特殊处理成destructor这个统一的单词。
class Base {
public:
    virtual ~Base()
    {
        std::cout << "~Base()" << std::endl;
    }
};

class DerivedA : public Base {
public:
    virtual ~DerivedA()  // 重写父类的析构函数
    {
        std::cout << "~DerivedA()" << std::endl;
    }
};

int main()
{
    //DerivedA s;  //无论是否是虚函数,都能正确调用其析构函数

    Base* p1 = new DerivedA;  // operator new + 构造
    delete p1; // 构造 + operator delete  若不写虚析构,无法调用子类析构函数。
    return 0;
}
# 结果
~DerivedA()
~Base()

1.4 纯虚析构

  • 纯虚析构:必须在类外实现。
#include <iostream>
#include <string>

class A
{
public:
	virtual ~A() = 0;
};
A::~A()  //必须在类外实现
{
	std::cout << "virtual ~A() = 0" << std::endl;
}

class B :public A
{
	~B()
	{
		std::cout << "~B()" << std::endl;
	}
};
int main()
{
	A* p = new B;
	delete p;
	return 0;
}

2. 多态实现的原理

首先看一段代码:

#include <iostream>
#include <string>

class Base
{
public:
	virtual void greeting()
	{
		std::cout << "hello, I am Base!" << std::endl;
	}

	virtual void hello()
	{
		std::cout << "hello world!" << std::endl;
	}
};


class DerivedA : public Base
{
public:
	virtual void greeting()  //子类重写父类的虚函数
	{
		std::cout << "hello, I am DerivedA!" << std::endl;
	}
};


int main()
{
	Base b;
	std::cout << sizeof(b) << std::endl;

    Base *pb = new	Base;
	Base* pDa1 = new DerivedA;
	Base* pDa2 = new DerivedA;
	return 0;
}

输出结果

4 # 包含了一个虚函数表指针

在这里插入图片描述

可以看到:

  • 父类和子类的虚函数表指针指向的是不同的虚函数表,虚函数表中指向的是各自类中的虚函数;若重写了,就指向重写的新函数地址。
    在这里插入图片描述

  • 这里可以解释为什么需要父类指针(或引用)指向子类空间调用虚函数才可以实现多态:指针和引用可以获取对象的虚表指针,而使用Base b = DerivedA调用拷贝构造,无法拷贝子类虚函数表指针(否则父类对象的虚函数表指针会出现指向子类虚表的情况)。

  • 同一个类构造的不同对象共有一份虚函数表,虚函数指针表指向同一个地址。

  • 没有重写的虚函数使用的是同一地址。

  • 补充:普通函数和虚函数(非纯虚)存储位置一样,都在代码段。虚函数表存放在常量区。

  • 对于多继承的子类对象,其中可能含有多份来自父类的虚表指针:

#include <iostream>
#include <string>

class Base1
{
public:
	virtual void Fun1()
	{
		std::cout << "Base1::Fun1()!" << std::endl;
	}

	virtual void Fun2()
	{
		std::cout << "Base1::Fun2()!" << std::endl;
	}
};

class Base2
{
public:
	virtual void Fun1()
	{
		std::cout << "Base2::Fun1()!" << std::endl;
	}

	virtual void Fun2()
	{
		std::cout << "Base2::Fun2()!" << std::endl;
	}

};

class DerivedA : public Base1, public Base2
{
public:
	virtual void Fun1()  //重写继承的函数
	{
		std::cout << "DerivedA::Fun1()!" << std::endl;
	}

private:
	virtual void Fun3()  //私有的函数
	{
		std::cout << "DerivedA::Fun3()!" << std::endl;
	}

	void Fun4()  // 普通函数不存在虚表里
	{
		std::cout << "DerivedA::Fun4()!" << std::endl;
	}
};

typedef void (*vfptr)();  //定义一个虚函数指针类型
void PrintVFTable(vfptr* pf)  //传入虚函数表指针,打印虚表函数
{
	for (int i = 0; pf[i]; i++)  //虚函数表以nullptr结尾
	{
		printf("table[%d] -> %p ", i,pf[i]);
		pf[i]();
	}
}

int main()
{
	DerivedA da;
	PrintVFTable((vfptr*)*((void**)&da));
	std::cout << std::endl;

	PrintVFTable((vfptr*)*((void**)( (char *)&da + sizeof(Base1) ) ) );
	Base2* pda = &da;
	std::cout << std::endl << "切片的结果一样,Base2* pda = &da->:" << std::endl;
	PrintVFTable((vfptr*)*((void**)(pda)));

	return 0;
}

输出结果

table[0] -> 00401262 DerivedA::Fun1()!
table[1] -> 0040129E Base1::Fun2()!
table[2] -> 0040114A DerivedA::Fun3()!

table[0] -> 004013CA DerivedA::Fun1()!
table[1] -> 00401087 Base2::Fun2()!

切片的结果一样,Base2* pda = &da->:
table[0] -> 004013CA DerivedA::Fun1()!
table[1] -> 00401087 Base2::Fun2()!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值