C++ 多态

文章详细介绍了多态的概念,强调了动态多态(继承中的基类与派生类关系)和静态多态(函数重载)的区别。接着,讨论了虚函数的定义、重写、以及构造和析构函数的特殊情况。文章还提到了抽象类与纯虚函数的作用,以及虚函数表在单继承和多继承情况下的表现。最后,讲解了`override`和`final`关键字在控制虚函数重写中的作用。
摘要由CSDN通过智能技术生成

目录

一.   概念

二.   定义与实现

1.构成条件

2.虚函数

3.虚函数的重写

4.协变

5.override 和 final

三.   抽象类

四.   虚函数表

1.单继承虚函数表

2.多继承的虚函数表


一.   概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

而不同的对象,我们可以使用函数重载来实现,也可以使用继承中的基类和派生类来表示

静态,主要是包括静态的和动态的,函数重载是静态的,是在编译的过程中实现的,而继承是动态的多态,是在运行的时候实现的,我们主要来看的是动态的多态


二.   定义与实现

1.构成条件

在继承中要构成多态有两个条件:

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.虚函数

虚函数是指被virtual修饰的非静态的类成员函数

class A {
public:
	virtual void Name() { cout << 'A' << endl; }
};

3.虚函数的重写

想要实现虚函数的重写,需要让基类与派生类的函数名、返回值类型、参数(个数、类型)都相同

class A {
public:
	virtual void Name() { cout << 'A' << endl; }
};

class B : public A {
public:
	virtual void Name() { cout << 'B' << endl; }
};

而在虚函数重写时,派生类的虚函数也可以不加virtual关键字,依然可以实现虚函数的重写,这是因为,B类的Name函数继承了A类Name函数的属性(包括virtual关键字、访问限定符)

因此,也可以写作这样

class A {
public:
	virtual void Name() { cout << 'A' << endl; }
};

class B : public A {
private:
	void Name() { cout << 'B' << endl; }
};

实践一下 

class A {
public:
	virtual void Name() { cout << 'A' << endl; }
};

class B : public A {
private:
	void Name() { cout << 'B' << endl; }
};

void Func(A& n)
{
	n.Name();
}
int main()
{
	A a;
	B b;
	Func(a);
	Func(b);
	return 0;
}

可以看到,依旧能够完成虚函数的重写,而同样,即使B类的Name函数的访问限定符为private,依旧可以在类外的Func函数中被调用。

而对于构造函数和析构函数来说,首先构造函数不能是虚函数,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的,至于什么是虚函数表后面再说。

而对于析构函数来说,首先我们在继承中提到过,析构函数会被处理成destrutor(),因此基类和派生类的析构函数函数名是相同的,因此析构函数可以是虚函数,而我们也尽量将析构函数写作虚函数。

class A {
public:
	~A(){ cout << "~A()" << endl; }
};

class B : public A {
public:
	~B() { cout << "~B()" << endl; }
};

int main()
{
	A* a =new A;
	delete a;
	B* b = new B;
	delete b;
	return 0;
}

首先如果我们正常使用new和delete,可以看到,在delete对象b时调用了派生类和基类的析构函数

但如果我们这样去使用的话

class A {
public:
	~A(){ cout << "~A()" << endl; }
};

class B : public A {
public:
	~B() { cout << "~B()" << endl; }
};

int main()
{
	A* a =new A;
	delete a;
	A* b = new B;
	delete b;
	return 0;
}

会发现delete对象b只会去调用基类的析构函数,引起内存泄漏

因此我们可以将析构函数写作虚函数来解决这个问题

class A {
public:
	virtual ~A(){ cout << "~A()" << endl; }
};

class B : public A {
public:
	virtual ~B() { cout << "~B()" << endl; }
};

int main()
{
	A* a =new A;
	delete a;
	A* b = new B;
	delete b;
	return 0;
}

 

4.协变

我们上面说到,想要实现重写,其中一个条件是返回值相同,但当返回值为该类的指针或引用时,也可以实现重写

class A {
public:
	virtual A* Name() 
	{
		cout << 'A' << endl; 
		return this;
	}
private:
	int _a = 10;
};

class B : public A {
public:
	virtual B* Name()
	{ 
		cout << 'B' << endl; 
		return this;
	}
private:
	int _b = 20;
};

5.override 和 final

final:修饰虚函数,表示该虚函数不能再被重写

class A {
public:
	virtual void Name() final { cout << 'A' << endl; }
private:
	int _a = 10;
};

class B : public A {
public:
	virtual void Name() { cout << 'B' << endl; }
private:
	int _b = 20;
};

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

class A {
public:
	virtual void Name() { cout << 'A' << endl; }
private:
	int _a = 10;
};

class B : public A {
public:
	virtual void Name(int i) override{ cout << 'B' << endl; }
private:
	int _b = 20;
};


三.   抽象类

在虚函数后面写上=0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数


四.   虚函数表

1.单继承虚函数表

class A {
public:
	virtual void Func1(){}
private:
	int _a = 10;
};

class B : public A {
public:
	virtual void Func1(){}
private:
	int _b = 20;
};

int main()
{
	A a;
	B b;
	return 0;
}

 在调试的过程中,我们可以看到在对象中除了类成员变量,还存在一个名为_vfptr的指针,对应的地址中存放的是一个数组,而这个数组就被称作是虚函数表,而这个数组,实质上是一个函数指针数组。

而我们也可以通过内存来观察一下,其中虚函数表最后会放一个nullptr

 而若是我们多添加几个虚函数

class A {
public:
	virtual void Func1(){}
	virtual void Func2(){}
private:
	int _a = 10;
};

class B : public A {
public:
	virtual void Func1() {}
	virtual void Func3() {}
private:
	int _b = 20;
};

int main()
{
	A a;
	B b;
	return 0;
}

而我们可以看到,虚函数表中并没有显示有Func3,但其实是存在的,我们可以通过内存来看 

 

可以看到,在nullptr之前,有三个地址,最后一个代表的就是Func3

但当我们观察Func3的地址时发现和虚基表中的地址并不相同,就连Func1和Func2的也不相同,这是因为,在我使用的vs中,进行了一些处理,我们可以调用一下Func1来通过反汇编观察

class A {
public:
	virtual void Func1(){}
	virtual void Func2() {}
private:
	int _a = 10;
};

class B : public A {
public:
	virtual void Func1(){}
	virtual void Func3() {};
private:
	int _b = 20;
};

int main()
{
	A a;
	B b;
	b.Func1();
	return 0;
}

 

 

 

可以看到,通过call,我们调用到了jmp,它的地址就是存在虚函数表里的地址 

而通过jmp我们才找到Func1函数。

当然,我们也可以通过打印虚函数表来观察

首先整体思路就是通过类型转换来使对象的地址所指向的内容为单个指针大小(32位4字节,64位8字节),之后通过遍历知道结尾处的nullptr

首先我们可以先将函数的实现写出来

class A
{
public:
	virtual void Func1() { cout << "Func1" << endl; }
	virtual void Func2() { cout << "Func2" << endl; }
};

class B:public A
{
public:
	virtual void Func1() { cout << "Func1" << endl; }
	virtual void Func3() { cout << "Func3" << endl; }
};

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :%p,->", i+1, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}

重点在于应该如何进行传参

首先我们需要强制类型转换为4字节(8字节),这里我们可以使用int*(long long*)来实现,不过这样需要进行条件编译来判断应该使用Int*还是long long*,还有一种方式,我们可以直接使用void**来完成。之后我们就需要进行解引用操作,最后由于类型不匹配,我们还需要进行一次强转为VFPTR*,这样我们就可以传参了

class A
{
public:
	virtual void Func1() { cout << "Func1" << endl; }
	virtual void Func2() { cout << "Func2" << endl; }
};

class B:public A
{
public:
	virtual void Func1() { cout << "Func1" << endl; }
	virtual void Func3() { cout << "Func3" << endl; }
};

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :%p,->", i+1, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}

int main()
{
	B b;
	PrintVTable((VFPTR*)(*(int*)&b));
	return 0;
}

 

总结一下单继承的虚函数表,我们可以得到以下结论

1.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。另外Func2继承下来后是虚函数,所以也放进了虚表

2.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

3.派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

4.和普通函数相同,虚函数也是存在代码段的,只是需要通过对象中的虚函数表来进行调用

2.多继承的虚函数表

class A {
public:
	virtual void Func1() {  }
	virtual void Func2() {  }
private:
	int _a = 10;
};

class B
{
public:
	virtual void Func1() {  }
	virtual void Func3() {  }
private:
	int _b = 20;
};

class C : public A, public B {
public:
	virtual void Func1(){  }
private:
	int _c = 30;
};

int main()
{
    A a;
    B b;
	C c;
	return 0;
}



我们可以看到,派生类中的两个虚函数表中,代表Func1的指针并不相同,我们在上面说到过,虚函数表中存的其实是jmp指令,而这两个jmp指令会指向同一个Func1。

class A {
public:
	virtual void Func1() {  }
	virtual void Func2() {  }
private:
	int _a = 10;
};

class B
{
public:
	virtual void Func1() {  }
	virtual void Func3() {  }
private:
	int _b = 20;
};

class C : public A, public B {
public:
	virtual void Func1() {  }
private:
	int _c = 30;
};

int main()
{
	C c;
	A& a = c;
	B& b = c;
	a.Func1();
	b.Func1();
	return 0;
}

我们可以用上述的代码通过汇编来看,这里就不多做演示


 

 

评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

finish_speech

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值