多态的学习

目录

1.多态的概念

2.多态的定义及实现

2.1多态的构成条件

2.1.1实现多态的两个必要条件

2.1.2虚函数

2.1.3虚函数的重写/覆盖

[2.1.4 一个题目](#2.1.4 一个题目)

2.1.5虚函数的一些其他问题

析构函数的重写

[2.1.6 override和final关键字](#2.1.6 override和final关键字)

override:

final

[2.1.7 重载,重写,隐藏](#2.1.7 重载,重写,隐藏)

3.纯虚函数和抽象类

3.1包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象

3.1.1包含纯虚函数的类都叫抽象类

3.1.2重写就不是纯虚函数,不是纯虚函数就不是抽象类,就能实例化出对象

3.1.3//不能实例化出对象,但是能定义指针

4.多态的原理

4.1虚函数表指针

4.2多态的原理

[4.2.2 动态绑定与静态绑定](#4.2.2 动态绑定与静态绑定)

[4.2.3 虚函数表(函数指针数组)](#4.2.3 虚函数表(函数指针数组))

打印虚表的地址:


1.多态的概念

多态:通俗来说就是多种形态

多态分为编译时多态 --- 静态多态

主要是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时归为静态

运行时多态 --- 动态多态

完成某个函数,可以传不同的对象就会完成不同的行为,就达到多种形态,我们把运行时动态

2.多态的定义及实现

2.1多态的构成条件

多态是一个继承关系下的类对象去调用同一函数产生了不同的行为

2.1.1实现多态的两个必要条件

1)必须指针或者引用调用虚函数

因为只有基类的指针或引用才能既指向基类对象又指向派生类对象

2)被调用的函数必须是虚函数

派生类必须对基类的虚函数重写或者覆盖,重写或覆盖了派生类才能有不同的函数,多态的不同形态效果才能达到

cpp 复制代码
class person
{
public:
	virtual void buyticket() { cout << "买票-全价" << endl; }
};

class student : public person 
{
public:
	virtual void buyticket() { cout << "买票-打折" << endl; }
};
void Func(Person* ptr)
{
	// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket
	// 但是跟ptr没关系,而是由ptr指向的对象决定的。
	ptr->BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
	//指向谁调用谁
	//ptr父类指针可以指向父类调用父类的
	//ptr父类指针可以指向子类调用子类的
}

2.1.2虚函数

类成员函数(非静态)前面加virtual修饰,这个成员函数被称为虚函数

非成员函数不能加virtual修饰

cpp 复制代码
virtual void func()
{}

2.1.3虚函数的重写/覆盖

派生类中有一个跟基类完全相同的虚函数

派生类的虚函数重写了基类的虚函数:派生类虚函数与基类虚函数的返回值函数,函数名字,参数列表完全相同

cpp 复制代码
class person
{
public:
	virtual void buyticket() { cout << "买票-全价" << endl; }
};

class student : public person 
{
public:
	virtual void buyticket() { cout << "买票-打折" << endl; }
};

在重写基类函数时,派生类的虚函数在不加virtual时,还是构成重写的,因为继承下来了。但是基类不能不加virtual

//这里的virtual和继承那里的virtual没有关系,只是一个关键字用在了两个地方

cpp 复制代码
class person
{
public:
	virtual void buyticket() { cout << "买票-全价" << endl; }
};

class student : public person 
{
public:
    void buyticket() { cout << "买票-打折" << endl; }
};

2.1.4 一个题目

cpp 复制代码
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()调用的是父类的,因为子类里面继承了父类的

调用test()之后要去调用func()

要用this去调用func this->func()

是否构成多态? 构成

这里的this是A* 是一个父类的指针

是虚函数

指向谁调用谁

A* 实际上是p传给this的,这里的p指向的是一个派生类对象B,所以指向的应该是一个派生类对象

所以是:B->1 没有参数,用缺省参数

声明用的属性是

满足多态的情况下,调用的是子类重写的这个虚函数

cpp 复制代码
virtual void func(int val = 1){ std::cout<<"B->"<< val <<std::endl; }

2.1.5虚函数的一些其他问题

析构函数的重写

基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtusl关键字,都与基类的析构函数构成重写

编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了virtual修饰,派生类的析构函数就构成重写

cpp 复制代码
class A
{
public:
	 ~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
	return 0;
}
bash 复制代码
~A()
~A()

D:\code\41-4-codes\11_9\x64\Debug\11_9.exe (进程 3788)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .

如果调用的是子类的不加virtual是不会有问题的

cpp 复制代码
int main()
{
	A* p1 = new A;
	B* p2 = new B;
	delete p1;
	delete p2;
	return 0;
}
bash 复制代码
~A()
~B()->delete:000002CF29E401D0
~A()

D:\code\41-4-codes\11_9\x64\Debug\11_9.exe (进程 33652)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .

原因是没有调用合适的析构函数,造成内存的泄漏

delete p2时只调用A的析构函数,没有调用B的析构函数,~B()中再释放资源,就会导致内存泄漏

正确调用才能正确析构

delete

调用p2的析构

p1->destructor();

operator delete(p1);//free

p2->destructor();

operator delete(p2);//free

构成多态才能正确释放,才能指向谁调用谁的析构函数,正确释放资源

而有没有形成多态和当前指针类型有关 1)A*---基类的指针

2)虚函数的重写

这样才能析构构成多态

cpp 复制代码
// 只有派⽣类Student的析构函数重写了Person的析构函数,
//下⾯的delete对象调⽤析构函数才能构成多态,   
//才能保证p1和p2指向的对象正确的调⽤析构函数
  
class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
	virtual ~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
	return 0;
}
bash 复制代码
~A()
~B()->delete:0000027C92845450
~A()

D:\code\41-4-codes\11_9\x64\Debug\11_9.exe (进程 6564)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .

2.1.6 override和final关键字

override:

放在派生类里面,没有完成重写就会报错

cpp 复制代码
class person
{
public:
    void buyticket() { cout << "买票-全价" << endl; }
};

class student : public person 
{
public:
	virtual void buyticket() override { cout << "买票-打折" << endl; }
};
final

如果我们不想让派生类重写这个虚函数,就可以用final去修饰

cpp 复制代码
class person
{
public:
    void buyticket() final { cout << "买票-全价" << endl; }
};

class student : public person 
{
public:
	virtual void buyticket() { cout << "买票-打折" << endl; }
};

2.1.7 重载,重写,隐藏

3.纯虚函数和抽象类

纯虚函数:在虚函数的后面写上=0 ,纯虚函数不需要定义实现,只要声明即可

3.1包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象

cpp 复制代码
class Car
{
public:
	virtual void Drive() = 0;
};
int main()
{
	Car c;
	return 0;
}

3.1.1包含纯虚函数的类都叫抽象类

派生类继承后不重写纯虚函数,那派生类也是抽象类

cpp 复制代码
class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
};
int main()
{
	//Car c;
	Benz b;
	return 0;
}

纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象

3.1.2重写就不是纯虚函数,不是纯虚函数就不是抽象类,就能实例化出对象

cpp 复制代码
class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
int main()
{
	//Car c;
	Benz b;
	return 0;
}

3.1.3//不能实例化出对象,但是能定义指针

cpp 复制代码
class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
int main()
{
	//Car c;

	//不能实例化出对象,但是能定义指针
	Car* pBenz = new Benz;
	pBenz->Drive();

	Car* pBMW = new BMW;
	pBMW->Drive();
	return 0;
}

4.多态的原理

4.1虚函数表指针

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}
//12

对象中的这个指针我们叫做虚函数表指针,虚函数表指针指向一张表,是一个指针数组,指针数组存一个一个的指针,这个指针是虚函数的指针

虚函数表存储虚函数的地址

所以说虚函数重写是多态的条件之一

4.2多态的原理

多态是如何做到指向父类调用父类,指向子类调用子类的?----

指向父类,运行时到指向父类对象的虚函数表中找到对应的虚函数进行调用

指向子类,运行时到指向子类对象切片出的父类对象的虚函数表中找到对应的虚函数进行调用

--- 达到指向父类调用父类,指向子类调用子类的效果

动态多态:运行时到指向对象虚表中找到对应虚函数进行调用

当你是虚函数,会把虚函数放到虚表里面;当完成了虚函数的重写,会把派生类当中虚表对应的虚函数覆盖成重写的虚函数

4.2.2 动态绑定与静态绑定

静态绑定:编译时确定调用函数的地址

动态绑定:运行时到指向对象的虚函数表中找到调用函数的地址

4.2.3 虚函数表(函数指针数组)

1)基类对象的虚函数表中存放基类所有虚函数的地址

2)派生类由两部分构成,继承下来的基类和自己的成员

继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针

继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void func1(){ cout << "func1" << endl; }
    void func2() { cout << "func2" << endl; }
	int _a1 = 1;
	int _a2 = 0;
};
class Student:public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
	virtual void func1() { cout << "func1" << endl; }
	virtual void func3() { cout << "func3" << endl; }
	int _a3 = 3;
};
int main()
{
	Person p;
	Student s;
	return 0;
}

哪怕没有重写还是不一样的虚函数表

不同的父类和子类用的是不同的虚函数表

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void func1(){ cout << "func1" << endl; }
    void func2() { cout << "func2" << endl; }
	int _a1 = 1;
	int _a2 = 0;
};
class Student:public Person {
public:
	int _a3 = 3;
};
int main()
{
	Person p;
	Student s;
	return 0;
}

同类型的对象共享相同的虚函数表

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void func1(){ cout << "func1" << endl; }
    void func2() { cout << "func2" << endl; }
	int _a1 = 1;
	int _a2 = 0;
};
class Student:public Person {
public:
	int _a3 = 3;
};
int main()
{
	Person p1;
	Person p2;
	Person p3;

	Student s;
	return 0;
}

3)派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址

4)派生类的虚函数表中包含:基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数的地址

5)虚函数和普通函数一样编译好是一段指令,都是存在代码段的,只是虚函数的地址存到了虚表中

6)vs下虚函数表是存在代码段的(常量区)

打印虚表的地址:

//指针前面这个类型的意义是->指针解引用的时候看多大的内存空间

//先取到指针,指针都是地址,地址决定了解引用看多大空间的内容,通过指针的强转解引用来进行控制

//函数的地址就是这一段指令第一句指令的地址

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};
class Derive : public Base
{
public:
	// 重写基类的func1
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func1" << endl; }
	void func4() { cout << "Derive::func4" << endl; }
protected:
	int b = 2;
};
int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);

	Base b;
	Derive d;
	Base* p3 = &b;
	Derive* p4 = &d;

	printf("Base虚表地址:%p\n", *(int*)p3);
	printf("Derive虚表地址:%p\n", *(int*)p4);
	printf("虚函数地址:%p\n", &Base::func1);
	//函数名就可以代表函数的地址,但我们在类域里面,要指定类域
	//要取成员函数的地址,要加&
	printf("普通函数地址:%p\n", &Base::func5);
	return 0;
}
bash 复制代码
栈:0000005869D8FBE4
静态区:00007FF7173CF000
堆:00000184F6CA6B60
常量区:00007FF7173CBE48
Base虚表地址:00000000173CBDC8
Derive虚表地址:00000000173CBE18
虚函数地址:00007FF7173C1492
普通函数地址:00007FF7173C135C

D:\code\41-4-codes\11_12\x64\Debug\11_12.exe (进程 37232)已退出,代码为 0 (0x0)。
要在调试停止时自动关闭控制台,请启用"工具"->"选项"->"调试"->"调试停止时自动关闭控制台"。
按任意键关闭此窗口. . .
相关推荐
‘’林花谢了春红‘’4 小时前
C++ list (链表)容器
c++·链表·list
机器视觉知识推荐、就业指导6 小时前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
朝九晚五ฺ6 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
猫爪笔记7 小时前
前端:HTML (学习笔记)【1】
前端·笔记·学习·html
Yang.997 小时前
基于Windows系统用C++做一个点名工具
c++·windows·sql·visual studio code·sqlite3
熬夜学编程的小王8 小时前
【初阶数据结构篇】双向链表的实现(赋源码)
数据结构·c++·链表·双向链表
zz40_8 小时前
C++自己写类 和 运算符重载函数
c++
六月的翅膀8 小时前
C++:实例访问静态成员函数和类访问静态成员函数有什么区别
开发语言·c++
pq113_68 小时前
ftdi_sio应用学习笔记 3 - GPIO
笔记·学习·ftdi_sio