C++进阶:多态(笔记)

1. 多态相关概念

1.1 简述:多态

1. 什么是多态

  1. 不同的对象去做同一个行为时,得到的结果不同。反应到编程语言中,即为不同类型的对象调用同一个函数,得到的返回值不同。
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "全票" << endl;
	}
};

class Children : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "半票" << endl;
	}
};

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

int main()
{
	Person p1;
	Children p2;
	
	BuyTicket(p1);
	BuyTicket(p2);

	return 0;
}

2. 虚函数

  1. 虚函数:被关键字virtual修饰的函数
virtual void func1()
{
	//...
}

3. 构成多态的条件

  1. 父类与子类的虚函数必须构成重写关系(三同:函数名,函数参数,函数返回值相同)
  2. 必须用父类的指针或者引用去调用虚函数。

4. 多态的特殊情况

  1. 协变:父类与子类的虚函数返回值不同,也可以构成重写,只是返回值类型必须为父类/子类(不是同一父类/子类也可以)的指针或者引用。
  2. 析构函数即使函数名不同也构成重写,这时因为编译在编译时会将析构函数的名字统一处理为destructor
  3. 特殊的,父类的虚函数加virtual,子类的虚函数不加virtual也构成重写,子类的虚函数被视作实现重写(建议不要省略)。
class A
{
public:
	virtual void func(int a = 1)
	{
		cout << "A->" << a << endl;
	}

	virtual void test()
	{
		func();
	}
};

class B : public A
{
public:
	virtual void func(int a = 0)
	{
		cout << "B->" << a << endl;
	}
};

int main()
{
	B().test();

	return 0;
}

在这里插入图片描述

  1. <1> B类因为本身没有进行test函数的重写,所以调用test时会调用从A类继承而来的test。
    <2> 调用继承而来的test的时需要使用父类指针进行调用,会发生赋值兼容转换从B类中截断出A类的部分。
    <3> 调用的func函数为B类重写A类后的func函数,又因为虚函数的重写是实现重现,会直接继承父类的函数框架,只重写内部的实现。

5. 多态调用与普通调用

  1. 普通调用:根据指针/引用的类型,调用指针/引用类型的函数
  2. 多态调用:使用父类指针指向子类对象,根据指针/引用指向对象的类型,调用指向对象的函数。
  3. 即使父类与子类构成多态,但不采用多态调用的方式,也不会达到多态的效果。
  4. 普通函数会在编译时就将函数的地址写入符号表中,而重写的虚函数其地址存储在对象的虚表中,当我们使用多态调用对其进行调用时,则是在运行时从对象的虚表中获得对应虚函数的地址。

6. 补充语法

  1. 如何定义实现一个不能被继承的父类:
    <1> 使用private私有化构造函数
    <2> 使用关键字final修饰父类
class A final
{
	//...
};
  1. 关键字override,检查子类是否重写了父类的虚函数,如果没有,会发生报错
class Person()
{
public:
	virtual void func1()
	{
		//...
	}
};

class Student : public Person
{
public:
	virtual void func1() override
	{
		//...
	}
}

7. 含有虚函数的类的大小

class Base
{
public:
	virtual void func1()
	{
		cout << "hello world" << endl;
	}
private:
	int _b = 1;
	char _c = 'a';
};
  1. 内存对齐:(VS最大对齐数为8)
    <1> int(对齐数4),大小 + 4,地址相对位置起始处:0(小于8,取自己;大于8,取8)
    <2> char(对齐数1),大小 + 1,地址相对位置起始处:4
    <3> 最后空间大小取最大对齐数的整数倍处(取8),8字节
  2. 虚函数表指针(虚表指针):指向虚函数表的指针,虚函数表内存储序函数的地址,虚函数表可以视作一个函数指针数组
  3. 含有虚函数的类内部,除开本身包含的成员变量外,还有额外包含一个虚表指针,虚表内存储着这个类所有虚函数的地址。
int main()
{
	Base a;

	return 0;
}

在这里插入图片描述

8. 抽象类

  1. 包含纯虚函数的类,这种类无法实例化出对象。
  2. 继承抽象类的子类必须要对纯虚函数进行重写,重写后子类才能够实例化出对象。
class Car
{
public:
	virtual void Drive() = 0
	{}
};

class Benz : public Car
{
public:
	virtual void Drive()
	{
		cout << "舒适" << endl;
	}
};

class BMW : public Car
{
public:
	virtual void Drive()
	{
		cout << "操控" << endl;
	}
};

1.2 概念汇总与补充

1. 重载,隐藏与重写

  1. 重载:会函数的重载,构成重载的函数必须在一个作用域中,且函数名相同,参数类型不同。
  2. 隐藏(覆盖):在父类与子类中的同名函数,在不同的作用域中,参数类型可以不同。
  3. 重写(重定义):在父类与子类中函数名相同,参数类型相同,返回值相同的虚函数(协变例外)。
  4. 注:分别在父类作用域与子类作用域的同名函数,不是重写就是隐藏。

2. 内联函数可以作为虚函数

  1. 想要能够作为一个虚函数的前提为必须是一个函数,拥有函数地址,可是内联函数在一般情况下不会创建栈帧,会直接在原地展开没有函数地址。
  2. 但,特殊的,内联函数在多态调用是不会展开,其也就不再具备内联属性,这里体现了内联函数的双向属性。

3. 静态成员函数与构造函数

  1. 静态成员函数没有this指针,而虚函数的调用需要通过父类指针来实现多态调用,运行时也就无法通过虚表指针进行调用,所以静态成员函数不能时虚函数。
  2. 构造函数只有在创建对象时的初始化列表中初始化对象,而虚表指针需要在编译阶段就完成初始化,所以构造函数不能是虚函数。

4. 虚函数的调用速度与抽象类

  1. 虚函数在进行普通调用时与普通函数的调用方式相同,只有在进行多态调用时,因为在运行时要通过虚表指针去搜索虚函数地址,所以会比普通函数的调用慢。
  2. 抽象类强制其子类进行虚函数重写,是一种接口继承的体现。

2. 多态重写的底层原理

2.1 虚函数存储的结构与位置

1. 虚函数的存储结构
在这里插入图片描述

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

	virtual void func2()
	{
		cout << "func2" << endl;
	}

	virtual void func3()
	{
		cout << "func3" << endl;
	}

private:
	int _b = 1;
	char _c = 'a';
};

typedef void(*PTR)();

void Print(PTR* p)
{
	for (int i = 0; p[i]; i++)
	{
		printf("p[%d] = %p\n", i, p[i]);
	}
	p[0]();
	p[1]();
	p[2]();
}

int main()
{
	Base* p = new Base;

	//相近类型才可以发生类型转换
	Print((PTR*)(*((int*)p)));

	return 0;
}

2. 虚函数与虚表指针在内存中的位置

class A
{
public:
	virtual func()
	{}
}

int main()
{
	//栈
	int a = 10;
	//堆
	int* b = new int;
	//静态区
	static int c = 0;
	//常量区
	const char* str = "hello world";
	
	printf("栈:%p\n", &a);
	printf("堆:%p\n", b);
	printf("静态区:%p\n", &c);
	printf("常量区:%p\n", str);
	
	//函数指针
	typedef void (*PTR)();
	
	A d;
	PTR* pd = (PTR*)(*((int*)(&d)));
	printf("虚表指针:%p\n", pd);
	printf("虚函数地址:%p\n", pd[0]);

	return 0;
}

在这里插入图片描述
3. 补充

  1. 当子类拥有独属于自己的虚函数时,也会将此虚函数的函数指针记录至虚表中,但,监视窗口无法查看。

在这里插入图片描述

2.2 重写覆盖

class A
{
public:
	virtual void func1()
	{}
};

class B : public A
{
public:
	//重写/未重写
	//virtual void func1()
	//{}
};

int main()
{
	A* p1 = new A;
	B* p2 = new B;

	return 0;
}

在这里插入图片描述

  1. 语法上的重写,反映到底层实现上就是函数指针的覆盖。
  • 17
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值