【C++】多态(上)


个人主页~


多态

一、多态的概念

用大白话讲就是完成某个行为,不同对象去完成会产生不同状态,C++多态就是在不同继承关系的类对象,去调用同一函数,产生了不同的行为

二、多态的定义以及实现

1、多态的构成条件

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

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

2、虚函数

被virtual修饰的类成员函数被称为虚函数

cpp 复制代码
class A
{
public:
	virtual void B()
	{
		cout << "virtual void B()" << endl;
	}
};

B函数是一个虚函数,这里的virtual与虚拟继承的virtual没有关系,只是它们表示相同的意思,关键字用在不同的对象上有不同的效果

3、虚函数的重写

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

cpp 复制代码
class A 
{
public:
	virtual void C()
	{
		cout << "A : virtual void C()" << endl;
	}
};

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

void test(A& a)
{
	a.C();
}

int main()
{
	A a;
	B b;
	test(a);
	test(b);

	return 0;
}

虚函数重写的两个特殊情况

①协变

所谓协变就是基类与派生类虚函数返回值类型不同

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

cpp 复制代码
class A 
{};
class B : public A 
{};

class C 
{
public:
	virtual A* f() 
	{
		return new A; 
	}
};
class D : public C 
{
public:
	virtual B* f() 
	{
		return new B; 
	}
};
② 析构函数的重写

析构函数的重写的特征是基类与派生类析构函数的名字不同

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同,虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor()

cpp 复制代码
class A 
{
public:
	virtual ~A() 
	{
	 cout << "~A()" << endl; 
	}
};
class B : public A 
{
public:
	virtual ~B() 
	{
	 cout << "~B()" << endl; 
	}
};

int main()
{
	A* pa = new A;
	B* pb = new B;

	delete pa;
	cout << endl;
	delete pb;
	return 0;
}

这样的设计巧妙的将父子类的析构函数关系在一起,当然这其实是一个设计失误,这个多态或者说继承的概念是在析构函数的概念之后产生的,所以作为第一个面向对象的语言,就只能通过打补丁,也就是编译器将两个析构函数转化为destructor()保证它们同名

4、C++11的override和final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

(1)final

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

(2)override

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

5、重载、重写、隐藏的对比

重写也叫覆盖,隐藏也叫重定义

三、抽象类

1、概念

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

cpp 复制代码
class A
{
public:
	virtual void D() = 0;
};
class B :public A
{
public:
	virtual void D()
	{
		cout << "BBBBBBBBBB" << endl;
	}
};
class C :public A
{
public:
	virtual void D()
	{
		cout << "CCCCCCCCCC" << endl;
	}
};
void Test()
{
	A* pb = new B;
	A* pc = new C;
	pb->D();
	pc->D();
}

2、接口继承和实现继承

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

派生类继承基类虚函数接口的一个实例

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

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

int main(int argc, char* argv[])
{
    B* p = new B;
    p->test();
    return 0;
}

这里就是因为派生类继承基类接口,重写函数,但是接口还是基类的,函数是被重写的,所以这里给的缺省值应该取基类A类func函数中的

四、多态的原理

1、虚函数表

cpp 复制代码
class A
{
public:
	virtual void func1()
	{
		cout << "virtual void func1()" << endl;
	}
private:
	int _a;
};

int main()
{
	cout << sizeof(A) << endl;
	return 0;
}

我们发现A类的大小为8bytes,除了私有成员_a以外还有一个_vtfptr的指针,我们把它叫做虚函数表指针,一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表简称虚表

cpp 复制代码
class A
{
public:
	virtual void Func1()
	{
		cout << "A::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "A::Func2()" << endl;
	}
	void Func3()
	{
		cout << "A::Func3()" << endl;
	}
private:
	int _a = 1;
};

class B : public A
{
public:
	virtual void Func1()
	{
		cout << "B::Func1()" << endl;
	}
private:
	int _b = 2;
};

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

派生类对象b中也有一个虚表指针,b对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员

基类a对象和派生类b对象虚表是不一样的,这里我们发现Func1完成了重写,所以b的虚表中存的是重写的B::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖,重写是语法的叫法,覆盖是原理层的叫法

另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表

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

总结一下派生类的虚表生成:

a.先将基类中的虚表内容拷贝一份到派生类虚表中

b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的 ,只是他的指针又存到了虚表中,另外对象中存的不是虚表,存的是虚表指针,虚表在VS下存在于代码段


今日分享就到这里了~

相关推荐
程序猿进阶1 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
qq_433618443 分钟前
shell 编程(二)
开发语言·bash·shell
闻缺陷则喜何志丹6 分钟前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
charlie11451419118 分钟前
C++ STL CookBook
开发语言·c++·stl·c++20
袁袁袁袁满18 分钟前
100天精通Python(爬虫篇)——第113天:‌爬虫基础模块之urllib详细教程大全
开发语言·爬虫·python·网络爬虫·爬虫实战·urllib·urllib模块教程
ELI_He99924 分钟前
PHP中替换某个包或某个类
开发语言·php
Laofanqie66626 分钟前
电脑丢失bcrypt.dll文件是什么原因?找不到bcrypt.dll文件修复办法来啦!
经验分享·电脑
小林熬夜学编程29 分钟前
【Linux网络编程】第十四弹---构建功能丰富的HTTP服务器:从状态码处理到服务函数扩展
linux·运维·服务器·c语言·网络·c++·http
亦世凡华、30 分钟前
MySQL--》如何在MySQL中打造高效优化索引
数据库·经验分享·mysql·索引·性能分析
m0_7482361132 分钟前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust