lesson2 - 多态

1. 多态的概念

多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果。 

  • 比如买车票这件事
    • 普通人买票->正常买票
    • 学生买票->半价买票
    • 军人买票->优先买票

2. 多态定义及实现 

多态: 指向谁调用谁 

2.1 多态构成的两个条件:

  1. 虚函数重写  
  2. 父类指针或者引用去调用虚函数

2.2 虚函数的理解

class Person
{
public:
	//被virtual修饰的类成员函数
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
  •  只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual

  • 虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何关系。虚函数这里的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性

2.2 虚函数重写的两个条件:

//父类
class Person
{
public:
	//父类的虚函数
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
//子类
class Student : public Person
{
public:
	//子类的虚函数重写了父类的虚函数
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
//子类
class Soldier : public Person
{
public:
	//子类的虚函数重写了父类的虚函数
	virtual void BuyTicket()
	{
		cout << "优先-买票" << endl;
	}
};
  1. 这个函数是虚函数,
  2. 三同:函数名,参数,返回值都要相同

 2.3 满足虚函数重写的两大特例

#include <iostream>
using namespace std;
class Person
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
	// 虚函数重写/覆盖条件 : 虚函数 + 三同(函数名、参数、返回值)
	
	// 特例1:子类虚函数不加virtual,依旧构成重写 (实际最好加上)
	//void BuyTicket() { cout << "买票-半价" << endl; }
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

    //重写的协变:返回值可以不同, 但是要求返回值必须是父子关系的指针或者引用
	//.....
};

class Soldier : public Person {
public:
	virtual void BuyTicket() { cout << "优先买票" << endl; }
};

// 多态两个条件:
// 1、虚函数重写
// 2、父类指针或者引用去调用虚函数

// void Func(Person* p) // 父类指针或者引用去调用虚函数
void Func(Person& p)// 父类指针或者引用去调用虚函数
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Soldier sd;

	Func(ps);
	Func(st);
	Func(sd);

	return 0;
}

  1. 子类虚函数不加virtual,依旧构成重写(实际中最好加上)
  2. 重写的协变:返回值可以不同,但是要求返回值必须是父子关系的指针或者引用

3. 经典面试题: 下面程序的输出结果是?

#include <iostream>
using namespace std;

class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};

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

int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}
  • 首先test不构成多态,func构成多态
  • p的类型是B*,指向的是B,p->test(A*this),这里会发生切片:子类指针传给父类指针
  • 父类里的test中的this->func,会去调用子类B的func,程序的结果是B->0吗
  • 这里有两点需要注意:
    • 虚函数func重写的是接口继承,重写实现
    • 普通函数继承实现继承
  •  虚函数func重写,这里的子类B中func会去调用自己的func实现,会继承父类的接口
    virtual void func(int val = 1)接口继承
    { std::cout << "B->" << val << std::endl; };重写实现
  • 这段程序的运行结果是B->1

4. 经典面试题: 下面程序的输出结果是?

#include <iostream>
using namespace std;

class A
{
public:
	virtual void func(int val) { std::cout << "A->" << val << std::endl; }
	void test() { func(1); }
};

class B : public A
{
public:
	void func(int val) { std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{
	A*p = new B;

	p->test();
	return 0;
}

 

  • 这段代码中的p虽然是A*的指针,但是这里发生了切片操作,p指向的是B的那一大段空间 
  • 发生了多态,与指针类型无关

5. 多态的原理

#include <iostream>
using namespace std;

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }

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

	int _a = 0;
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

	int _b = 0;
};

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

int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);

	return 0;
}

  •  vfptr是一张虚函数的表,本质就是一个函数指针数组
  • 多态调用:运行时是去指向对象的虚表中找到函数地址,并进行调用(在符合多态的两个条件时,)
  • 普通调用:编译链接时就确认了函数地址,运行时直接调用

5.1 动态绑定和静态绑定

静态绑定: 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载

动态绑定: 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

//父类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
//子类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};

 6. 在继承中把析构函数定义成虚函数

#include <iostream>
using namespace std;

 //建议在继承中析构函数定义成虚函数
class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }

	//int* _ptr;
};

class Student : public Person {
public:
	// 析构函数名会被处理成destructor,所以这里析构函数完成虚函数重写
	virtual ~Student() { cout << "~Student()" << endl; }
};

int main()
{
	Person* ptr1 = new Person;
	delete ptr1;

	Person* ptr2 = new Student;
	delete ptr2;

	return 0;
}

 

  • 这段代码在不同的编译器中,结果可能不一样,
  • 析构函数名会被处理成destructor,所以这里析构函数完成虚函数重写
  • 发生了多态,与指针类型无关

7. 关于重载.覆盖(重写),隐藏(重定义)的对比

  • 重载 : a. 两个函数在同一作用域,b.函数名相同,参数不同(类型,顺序,个数)
  • 重写(覆盖):
    两个函数分别在基类派生类的作用域
    函数名/参数/返回值都必须相同(协变除外) -> 简称三同
    特殊点1: 两个函数都必须是虚函数(其实子类可以不用加virtual)
    特殊点2: 如果返回值不同,则必须是父类的指针或引用
  •  重定义(隐藏)
    两个函数分别在基类派生类的作用域
    函数名相同
    两个基类和派生类的同名函数不构成重写,那么就是重定义(隐藏)

8.  C++11 override final(使该类不可被子类继承)

8.1  final(使该类不可被子类继承) -> 加在父类上

  •  在一个父类的成员函数的后面+final使其变成不可被子类继承

8.2 override(判读子类是否发生了虚函数的重写) ->加在子类上

#include <iostream>
using namespace std;

class Car{
public:
	virtual void Drive1() {}
	void Drive2(){}
};

class Benz :public Car {
public:
	// 检查子类虚函数是否完成重写
	virtual void Drive1() override { cout << "Benz-舒适" << endl; }
	virtual void Drive2() override { cout << "Benz-舒适" << endl; }
};

int main()
{
	Benz A;
	return 0;
}

  •  在子类后面+override,判断这个子类的虚函数是否发生了虚函数的重写

9. 抽象类 && 纯虚函数

  •  在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
    类),抽象类 不能实例化出对象

10. 多继承中的多态

#include <iostream>
using namespace std;

class Car
{
public:
	virtual void Drive() = 0;
};

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

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

int main()
{
	//Car c;
	//BMW b;
	Car* ptr = new BMW;
	ptr->Drive();

	ptr = new Benz;
	ptr->Drive();

	return 0;
}

 

  •  其中如果不需要用到父类的虚函数,仅仅是为了实现多态,
  • 将父类的虚函数写成纯虚函数,将父类定义成抽象类

11. 理解虚表

#include <iostream>
using namespace std;

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person::买票-全价" << endl;
	}

	virtual void Func1()
	{
		cout << "Person::Func1()" << endl;
	}
};

class Student : public Person {
public:
	// 这里只用BuyTicket()完成了虚函数的重写
	virtual void BuyTicket()
	{
		cout << "Student::买票-半价" << endl;
	}

	virtual void Func2()
	{
		cout << "Student::Func2()" << endl;
	}
};

int main()
{
	// 同一个类型的对象共用一个虚表
	Person p1;
	Person p2;

	// vs下 不管是否完成重写,子类虚表跟父类虚表都不是同一个
	Student s1;
	Student s2;
	return 0;
}

  •  同一个类型的对象共用一个虚表
  •  vs下 不管是否完成重写,子类虚表跟父类虚表都不是同一个

11. 1父类的虚表

#include <iostream>
using namespace std;

typedef void(*VFPTR)();

// 打印虚函数表中的虚函数地址,并且调用虚函数
void PrintVFTable(VFPTR* table)
{
	for (size_t i = 0; table[i] != nullptr; ++i)
	{
		printf("vft[%d]:%p->", i, table[i]);
		VFPTR pf = table[i];
		pf();
	}
	cout << endl;
}

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1 = 1;
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2 = 2;
};

class Derive : public Base1, public Base2 {
public:
	//这里只重写了 func1
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d = 3;
};

int main()
{
	Derive d;

	PrintVFTable((VFPTR*)(*(int*)&d));
	PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));

	return 0;
}

 

  • 这里子类的虚函数func3没有发生重写的,也没有发生隐藏,就是一个普通的虚函数,
  • 虽然在监视窗口中没有看到func3,但是在打印出父类的虚表Base1和Base2中,发现这个普通的虚函数存在第一个父类的虚表中

12. 问答题

12.1 什么是多态? 

  • 就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会
    产生出不同的状态

12.2 什么是重载、重写(覆盖)、重定义(隐藏)? 

  • 重载 : a. 两个函数在同一作用域,b.函数名相同,参数不同(类型,顺序,个数)
  • 重写(覆盖):
    两个函数分别在基类派生类的作用域
    函数名/参数/返回值都必须相同(协变除外) -> 简称三同
    特殊点1: 两个函数都必须是虚函数(其实子类可以不用加virtual)
    特殊点2: 如果返回值不同,则必须是父类的指针或引用
  •  重定义(隐藏)
    两个函数分别在基类派生类的作用域
    函数名相同
    两个基类和派生类的同名函数不构成重写,那么就是重定义(隐藏)

 12.3 多态的实现原理? 

  •  每一个类中都有一个vfptr,它是一张虚函数的表,本质就是一个函数指针数组
  • 多态调用:运行时去指向对象的虚表中找到函数地址,并进行调用(在符合多态的两个条件时,)

12.4 inline函数可以是虚函数吗?

  • 不可以,但是inline只是一个建议,当一个函数是虚函数以后,多态调用中,inline会直接失效

12.5 静态成员可以是虚函数吗? 

  • 不可以 ,因为 静态成员函数没有this指针 ,使用类型::成员函数
    的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

12.6 构造函数可以是虚函数吗

  • 不可以,virtual函数是为了实现多态,运行时去虚表中找对应虚函数进行调用
  • 对象中虚表指针都是构造函数初始化列表阶段才初始化的
  • 构造函数的虚函数是没有意义的

 12.7 析构函数可以是虚函数吗

  • 可以,建议基类的析构函数定义成虚函数
  • 析构函数名都会被处理成destructor,所以这里析构函数完成了虚函数重写
  • 构成多态与指针类型就无关了

12.8 拷贝构造 和 operator=可以是虚函数?

  •  拷贝构造不可以,拷贝构造也是构造函数,同上

  • operator=可以但是没有什么实际价值

12.9  对象访问普通函数快还是虚函数更快?

  • 不构成多态, 是一样快的。
  • 构成多态,则调用的普通函数快
    因为构成多态,运行时调用虚函数需要到虚函数表中去查找

12.10 虚函数是在什么阶段生成的,存在哪的?

  • 编译阶段就生成好的,存在代码段(常量区)

12. 11什么是抽象类?抽象类的作用? 

  • 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类
  • 抽象类强制重写了虚函数,另外抽象类体现出了 接口继承关系
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值