【C++】多态 (下)

下面我们引入一个抽象类的概念,所谓抽象,就是它并不实际存在。同样,抽象类也是实例不出对象的,那它有什么用呢?比如在实际生活中,我们说不同品牌的车给人的驾驶体验感使不同的,比如假设奔驰侧重于内饰,宝马侧重于运动性能,那我们就抽象出来一个车的概念,单说车是没有意义的,要加上品牌才可以。

那么什么样的类才算是抽象类呢?含有纯虚函数的类就是抽象类,下面举一个简单的例子

class car {
public:
	virtual void thechara() = 0;
};

class Benz :public car {
public:
	virtual void thechara() {
		cout << "侧重于内饰" << endl;
	}
};

class BMW :public car {
public:
	virtual void thechara() {
		cout << "侧重于运动性能" << endl;
	}
};

car是抽象类,无法实例化出对象,这也是符合实际的。下面的类是继承的car的,如果子类不想是抽象类,那么就必须重写函数,这也是符合现实需求的。所以说抽象类的现实意义还是很大的

下面我们来看一下多态实现的原理,它是怎么实现同一个类的指针或引用调到不同的函数的呢?下面先看一个例子

这个类的大小为什么会是8呢?他不就一个int的一个成员吗?这就要看看对象中到底存着什么

我们看到不仅又一个int成员,还存着一个指针,32位下指针大小为4,确实总共为8。其实这个指针叫做虚函数表指针(virtual function pointer),也就是说它存的是虚函数的地址。也就是说,所有的虚函数的指针汇集到一起形成一个函数指针数组,而_vfptr中存的就是这个函数指针数组的指针。为了更加深入的理解,我们在多写几个函数,加上继承的关系来研究一下

class A {
public:
	virtual void func1() {
		cout << "A::func1()" << endl;
	}
	virtual void func2() {
		cout << "A::func2()" << endl;
	}
private:
	int _a = 0;
};

class B:public A {
public:
	virtual void func2() {
		cout << "B::func2()" << endl;
	}
	virtual void func3() {
		cout << "B::func3()" << endl;
	}
private:
	int _b=0;
};
class C :public B {
public:
	virtual void func3() {
		cout << "C::func3()" << endl;
	}
private:
	int _c = 0;
};

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

先是有三个类的关系如上,我们分别创建三个类的对象,我们通过监视窗口来看一下它们中存着什么

我们可以看到,对于func1来说,B和C都是直接继承,并没有完成重写,所以A,B,C中func1的地址是相同的;对于func2来说,B重写了,C直接继承B的,所以B和C的func2地址是一样的;对于func3来说,它是B中新创建的一个新虚函数,它跟其他虚函数都是一样的,但是VS的监视窗口并没有显示地址,其实它就存在于虚函数表两个函数的下边,我们可以来验证一下,我们找一下b的虚函数表的地址,然后通过内存窗口看一下

可以看到,确实有三个函数的地址,那么我通过多态调用再看一下汇编代码验证一下

看jmp前边的地址,确实跟虚函数表中的地址确实是一样的

通过前边的验证,我们就大概知道了多态的原理,虽然是同一类型的指针,但是它指向了不同的虚函数表,于是就调到的不同的函数。其实这个虚函数表也是继承的父类的,所以我们不重写函数,函数的指针是相同的,我们重写之后就相当于把这个指针就覆盖了。所以虚函数的重写也叫做覆盖,覆盖是原理层上的概念,重写是语法层上的概念

我们上面说的虚函数表是在编译阶段就生成了,运行的时候加载到内存中;至于说对象中存的虚函数表指针,它存放在成员变量的前面,所以是在初始化列表之前生成的

接口继承和实现继承

我们先学的继承,那时候普通函数是会继承下来的,我们可以使用那个函数,所以继承的是它的实现,当然我们可以进行重定义(隐藏),重定义的也是它的实现;我们在上一篇说过,重写是声明并不重写,所以继承的是接口,目的是为了重写。所以如果不实现多态,不要把函数定义为虚函数。

动态绑定和静态绑定

静态绑定又称为前期绑定(早绑定),是在程序编译期间就确定了程序的行为,也称为静态多态。这也是一种多态,就是对于不同的对象有着不同的行为,并且是在编译期间确定的。比如函数重载:对于不同的对象调用不同的函数。

动态绑定又称为后期绑定(晚绑定),就是在程序运行期间,根据拿到的类型确定程序的具体行为,调用具体的函数,也成为动态多态。我们这两篇博客讲的都是动态多态。

下面我们探索一下虚函数表存在哪个区域(栈区,堆区,静态区,常量区)。我们既然想看它存在哪个区域,就要看它的地址离那个区域最近

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

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

int main() {
	A a;
	B b;
	int i = 0;
	int* j = new int;
	static int k = 10;
	const char* p = "xxxxxxxx";
	printf("栈区:%p\n", &i);
	printf("堆区:%p\n", j);
	printf("静态区:%p\n", &k);
	printf("常量区:%p\n", p);

	printf("A:%p\n", *(int*)&a);
	printf("B:%p\n", *(int*)&b);

	return 0;
}

我们可以看到,它们两个的虚函数表的指针距离常量区是最近的,所以虚函数表就存在常量区

我们说过函数重写后虚函数表中的指针会被覆盖,所以同类型对象虚函数表指针相同

我们也可以试着来打印一下虚函数表,从前边我们可以看到,虚函数表是以空为结尾的,这可以作为我们的限制条件

这分别是内存中和我们打印出来的,可以看到它们是一样的

我们多继承的话,如果两个父类都继承下来了虚函数,那么子类的话是有两个虚函数表的

下面我们来看一下,各种特殊函数能不能构成多态

内联函数可以是虚函数,虽然说虚函数要存个地址到虚函数表,而内联函数是直接展开没有地址,看似是矛盾的,但是多态调用的时候编译器就忽略内联这个特性,而普通调用就有内联特性,这属于是多重属性

静态成员函数不能是虚函数,因为静态成员函数没有this指针,它是无法放进虚函数表的

构造函数不能是虚函数,因为虚函数表指针是在初始化列表之前完成的

析构函数可以是虚函数,并且最好把基类的析构函数定义为虚函数,因为之前我们讲到一个内存泄露的问题,这就是因为不满足多态调用,为了让它满足,我们就写成虚函数

相关推荐

  1. <span style='color:red;'>C</span>++<span style='color:red;'>多</span><span style='color:red;'>态</span>

    C++

    2024-03-25 09:00:04      48 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-25 09:00:04       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-25 09:00:04       100 阅读
  3. 在Django里面运行非项目文件

    2024-03-25 09:00:04       82 阅读
  4. Python语言-面向对象

    2024-03-25 09:00:04       91 阅读

热门阅读

  1. 第三十二章 配置服务器访问

    2024-03-25 09:00:04       40 阅读
  2. 大数据实时计算的Windows功能?

    2024-03-25 09:00:04       37 阅读
  3. 【生产力】VSCode 插件 Draw.io Integration

    2024-03-25 09:00:04       44 阅读
  4. 面试(一)

    2024-03-25 09:00:04       32 阅读
  5. 商业技术成功案例

    2024-03-25 09:00:04       32 阅读
  6. Spring Boot 加载配置文件的优先级

    2024-03-25 09:00:04       36 阅读
  7. 网络安全简答题

    2024-03-25 09:00:04       35 阅读