C++多态二三事

C++多态二三事,第1张

C++多态二三事

多态:
  • 多态的概念
    • 1.多态的定义和实现
      • ①虚函数重写的三个例外
      • ②C++11: override 和 final
    • 2.重载、覆盖、隐藏对比
    • 3.抽象类
    • 4.接口继承和实现继承
  • 多态原理
    • 1.探究虚表存放的位置
    • 2.打印虚表
    • 3.菱形虚继承、虚函数

多态的概念

多态:多种形态;不同的对象完成同一件事情会发生不同的行为,产生不同的结果

多态包括
静态的多态:函数重载(静态绑定:静态指编译时)
动态的多态:父类指针或引用调用重写了的虚函数(动态绑定:是指运行时)

1.多态的定义和实现

构成多态还需要两个条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数:即被virtual修饰的类非静态成员函数称为虚函数

静态函数没有this指针,无法形成切片,那就无法调用派生类重写了的虚函数,无法形成多态

虚函数是为了形成多态

虚函数的重写(覆盖)
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)

注意与重定义区分:
如果函数名相同,不是重写就是重定义(隐藏)

①虚函数重写的三个例外

虚函数重写要求重写函数与基类完全相同

但也有例外:

①协变(基类与派生类虚函数返回值类型不同)
基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

#include
using namespace std;
class A{};
class B :public A{};
class C
{
public:
	virtual A* show()
	{
		cout << "C" << endl;
		return new A;
	}
};
class D :public C
{
public:
	virtual B* show()
	{
		cout << "D" << endl;
		return new B;
	}
};
int main()
{
	D d;
	C* c = &d;
	c->show();
	return 0;
}

运行结果:

②析构函数的重写
编译器会自动对基类,派生类的析构函数名统一处理成destructor,所以父子类析构函数自动构成重定义(不重写的话)

一般情况下,重不重写并没有影响:


无论重不重写并没有影响,都是先调用D的析构,D析构调用完后调用父类析构,接着C再析构

但是在特殊情况下,重写就十分必要

在这种情况下,我们是想通过delete分别调用它们的析构函数

但是c2的析构函数没重写,发生切片后,c2只能调用从父类继承下来的成员,所以也调用了父类的方法,并没有析构释放动态开辟的空间,这样就容易造成资源泄漏

重写后:

所以编译器为什么要对子类和父类的析构函数名进行处理
想必已经很明确了,就是让父子类的析构函数构成重写,让它们调用指向的对象的析构函数

③不写子类的virtual
子类的virtual不写,编译器也认为它是虚函数完成了重写

②C++11: override 和 final
  1. final:修饰虚函数,表示该虚函数不能再被重写

  2. override: 检查派生类虚函数是否重写

2.重载、覆盖、隐藏对比

3.抽象类

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


C类就是抽象类

1.抽象类能更好地表示没有实例对象对应的抽象类型 比如动物
2.体现了接口继承(跟Java的接口相似),强制子类重写虚函数

通过父类对象的指针或引用指向子类对象来使用

4.接口继承和实现继承

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

不用多态,就不要被函数定义成虚函数

多态原理

虚表指针(铺垫):

只要类中有虚函数就会有虚表指针

这个指针存放的是函数指针数组

#include
using namespace std;

class C
{
public:
	virtual void show() = 0;
	
	
	virtual ~C()
	{
		cout << "C" << endl;
	}
};
class D :public C
{
public:
	
	virtual void show( )override
	{
		cout << "hello" << endl;
	}

	 virtual ~D()
	{
		cout << "D" << endl;
	}
	 int* a;
};
int main()
{
	D d;
	return 0;
}


注意:
虚继承和这里完全是不同的两个东西,虽然都有virtual

虚继承的叫虚基表,存放的是偏移量
而这里叫虚表,存放的是函数指针

完成重写:

如果没完成重写:

可以看到c跟d里面的虚函数指针指向同一个函数,那这个函数一定是父类的

说明这个虚函数指针是来自父类的拷贝,如果子类重写了该虚函数,就把这个函数的地址拷贝到对应位置上

所以满足多态后,虚函数表被修改了,在运行时,再到指向的对象中的虚表中去找对应的虚函数调用。

所以指向父类就调用的是父类的虚函数,指向子类就调用的是子类的虚函数

如果不构成多态:

比如这样修改一下

void test(C c)
{
	c.show();
}

调用的就是父类的show函数,与传入的是什么对象无关

总结:
以我的理解,本质上就是在找这个虚函数指针,虚表里面函数指针存的是谁的地址,就调用的是谁

比如C c=d/c 这样根据切片是重新开辟了一份空间,此时c是一个对象了,虚表指针在C上(每个类实例化出的对象共用一个虚表指针),调用的就是C的虚函数

如果形成了多态,这个c是指向传入对象的,c就会去传入对象中找到虚函数指针,调用的是该对象的虚函数(如果重写)

例如:

#include
using namespace std;
class C
{
public:
	virtual void show()
	{
		cout << "C" << endl;
	}
	int c;
};
class D :public C
{
public:
	
	virtual void show(int )
	{
		cout << "D" << endl;
	}
	int d;
};
void test(C c)
{
	c.show();
}
int main()
{
	D d;
	test(d);
	C c;
	test(c);
	C c1;
	return 0;
}

所以要形成多态必须父类的指针或引用指向子类对象


在拷贝构造时并不会拷贝vfptr,容易造成紊乱,vfptr只与所属类有关

注意:
1.对象中的虚表指针是在构造函数初始化列表初始化的,虚表是在编译时就生成好的

2.虚函数表存放的是类中所有的虚函数的地址,虚函数跟普通函数一样,编译完成后放在代码段

3.虚函数的重写,也叫覆盖,子类会先调用父类的构造函数,父类会将虚函数表内容拷贝给子类,子类重写了再逐一修改


C类的虚函数表

1.探究虚表存放的位置

对象的前四个字节就是虚表的地址

#include
using namespace std;
class C
{
public:
	C()
	{

	}
	virtual void show()
	{
		cout << "C" << endl;
	}
	virtual void show1()
	{
		
	}
	int c;
};

int a = 0;
int main()
{
	C* c = new C;
	printf("C的虚表:%pn", *((int*)c));//取前四个字节
	
	int i;
	printf("栈上地址:%pn", &i);
	printf("数据段地址:%pn", &a);
	
	int* arr = new int;
	printf("堆地址:%pn", arr);
	
	const char* str = "hello world";
	printf("代码段地址:%pn", str);
	
	return 0;
}

所以虚函数在代码段

2.打印虚表
include
using namespace std;
class A
{
public:
	virtual void test1()
	{
		cout << "A1" << endl;
	}
	virtual void test2()
	{
		cout << "A2" << endl;
	}
	int _a;
};
class B:public A
{
public:
	virtual void test1()
	{
		cout << "B1" << endl;
	}
	virtual void test3()
	{
		cout << "B3" << endl;
	}
	int _b;
};
int main()
{
	A a;
	B b;
	return 0;
}


可以看到VS编译器监视做了特殊的处理只能看到从父类继承下来的函数指针

但内存窗口里面真实说明了虚函数表里面有三个函数指针
打印:

多态虚表就非常清晰了

多继承:

#include
using namespace std;
class A
{
public:
	virtual void test1()
	{
		cout << "A1" << endl;
	}
	virtual void test2()
	{
		cout << "A2" << endl;
	}
	int _a;
};
class B
{
public:
	virtual void test1()
	{
		cout << "B1" << endl;
	}
	virtual void test2()
	{
		cout << "B2" << endl;
	}
	int _b;
};
class C :public A, public B
{
public:
	virtual void test1()
	{
		cout << "C1" << endl;
	}
	virtual void test3()
	{
		cout << "C3" << endl;
	}
	int _c;

};
typedef void (*VFunc)();//定义函数指针VFunc
void PrintVF(VFunc* ptr)//函数指针的数组指针
{
	printf("虚表指针:%pn", ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		printf("VF[%d]:%pn",i, ptr[i]);
		ptr[i]();
	}
	printf("n");
}
int main()
{
	C c;
	PrintVF((VFunc*)(*(int*)&c));//打印A继承下来的虚表
	PrintVF((VFunc*)(*(int*)((char*)&c+sizeof(A))));//打印B继承下来的虚表
	return 0;
}


可以看到test3只放第一张虚表

3.菱形虚继承、虚函数
#include
using namespace std;
class A
{
public:
	virtual void show()
	{
		cout << "A" << endl;
	}
	int _a;
};
class B :virtual public A
{
public:
	virtual void show()
	{
		cout << "B" << endl;
	}
	int _b;
};
class C :virtual public A
{
public:
	virtual void show()
	{
		cout << "C" << endl;
	}
	int _c;
};
class D :public B, public C
{
public:
	virtual void show()
	{
		cout << "D" << endl;
	}
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

作如下修改,B,C各增加一个虚函数,A并不修改

修改后 修改前
此时虚基表的第一行就存了到B作用域的虚函数表的偏移量

而虚基表的18 00 00 00是存的到作用域公共基类A的偏移量

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/4950458.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-13
下一篇 2022-11-13

发表评论

登录后才能评论

评论列表(0条)

保存