【对C++中的多态分析和总结】

【对C++中的多态分析和总结】,第1张

文章目录
  • 一、多态的概念
  • 二、多态的构成条件
    • 1. 虚函数
    • 2. 虚函数的重写(覆盖)
    • 3. 虚函数重写的两个例外
  • 三、final关键字
  • 四、override关键字
  • 五、重载、重写(覆盖)、重定义(隐藏)的区别
  • 六、抽象类
  • 七、多态的原理分析
    • 1. 虚函数表的相关概念
    • 2. 对多态原理的总结
    • 3. 对多态构成条件的理解
  • 七、单继承和多继承的虚函数表

一、多态的概念

多态,即多种形态,不同类型的对象去做同一件事,但是结果不同。(比如去商城买东西,没有会员的对象就不打折,有会员的才打折)。
多态还可以分为静态的多态和动态的多态。
静态的多态: 函数重载,看起来调用同一个函数有不同行为。
注意: 函数模板并没有实现出多态,是模板实例化后形成的函数重载,才实现的多态。
静态的多态:原理是编译时实现。
举例如下:

//静态的多态
int main()
{
	int a = 10;
	double d = 6.58;
	string s("hello world");
	
    //函数重载,调用同一样的函数有不同的行为
	cout << a << endl;   //cout.<
	cout << d << endl;   //cout.<
	cout << s << endl;   //cout.<
	return 0;
}

那什么是动态的多态呢?这个语法比较复杂,我们下面一步步来分析,下面所说的多态都是动态的多态。

二、多态的构成条件

在继承中要构成多态有两个条件:
1、必须通过基类的指针或者引用调用虚函数。(既可以接收父类也可以接收子类)
2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(覆盖)。
那什么是虚函数,什么是重写呢?我们再来看看它们的定义

1. 虚函数

虚函数: 即被virtual修饰的类成员函数称为虚函数。
举例如下:

class Person
{
public:
	virtual void Shopp()  //虚函数
	{
		cout << "购物----不打折" << endl;
	}
};
2. 虚函数的重写(覆盖)

重写是语法层的叫法,覆盖是原理层的叫法。
虚函数的重写(覆盖):
派生类中有一个跟基类完全相同的虚函数 (即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)(三同),称子类的虚函数重写了基类的虚函数。

举例如下:

class Person
{
public:
	virtual void Shopp()
	{
		cout << "购物---不打折" << endl;  //普通人购物不打折
	}
};

class Member :public Person
{
	virtual void Shopp()
	{
		cout << "会员购物---打9折" << endl;  //vip会员购物打9折
	}
};

//1.构成多态,跟p的类型没有关系,传的哪个类型的对象,
//调用的就是这个类型的虚函数 -- 跟对象有关
//2.不构成多态,调用就是p类型的函数 -- 跟类型有关
void Fun(Person& p)
{
	p.Shopp();
}

int main()
{
	Person p;
	Member m;

	Fun(p);
	Fun(m); //子类对象给父类对象,发生切片
	return 0;
}

注意:
在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性)。
(继承的是接口和属性,比如缺省值也会继承下来,也会把访问限定符继承下来)。
(重写的是接口,即函数内容重写)。

3. 虚函数重写的两个例外

1、重写要求返回值相同有一个例外:
协变:如果基类与派生类虚函数返回值类型不同,要求返回值是父子关系的指针或者引用。

class A {};
class B : public A {};

class Person 
{
public:
	virtual A* f() { return new A; }
};
class Student : public Person 
{
public:
	virtual B* f() { return new B; }
};
//虽然上面的两个虚函数的返回值不同,但是它们的返回值是父子关系的指针,所以是协变
//也是构成虚函数重写

2、析构函数的重写
如果我们不了解析构函数的重写,我们看到基类的析构函数和子类的析构函数名字肯定不同呀,肯定不能构成虚函数重写呀。这种说法是错误的!!!!!!!
因为虽然函数名不相同,看起来违背了重写的规则,其实不一样,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

所以有结论:
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。
那什么情况才需要对析构函数进行虚函数重写呢?????
我们只要记住一种情况就可以了:
动态申请的子类对象,如果给了父类指针管理,那么需要析构函数是虚函数。
举例如下:

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

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

int main()
{
	Person* p = new Member();  //operator new + 构造函数
	delete p; //析构函数p->destructor() + operator delete
	return 0;
}

理由如下:
如果动态申请的子类对象,如果给了父类指针管理,析构函数不实现多态,那么调用的析构函数就是父类的析构函数,而子类中的一些资源可能没有释放,造成内存泄漏。

三、final关键字

final关键字有两大用法:
1、直接修饰一个类,表示该类不能被继承
2、修饰虚函数,表示该虚函数不能再被重写

我们先来看看一个问题,如何设计一个不能被继承的类呢?
在C++98中是这样解决的:
把构造函数设置为私有的,这样在子类中就调用不了父类的构造函数。
举例如下:

class A
{
private:
	int _a;
	A(int a = 0)
		:_a(a)
	{}
public:
	//不是静态函数就不行,因为先有对象才能调用成员函数
	//要先有鸡才有蛋
	static A GreaterObj(int a = 0)  
	{
		return A(a);
	}
};

//间接限制,子类构成函数无法调用父类构造函数初始化成员,没办法实例化对象
class B :public A
{

};

int main()
{
	//B bb;  //不能被继承的类就做到了,因为你没办法实例化对象

	//但是父类可以实例化对象
	A aa = A::GreaterObj(6);
	return 0;
}

而在C++11中提供了final关键字,可以直接修饰一个类,表示该类不能被继承;
使用如下:

final关键字还可以修饰虚函数,表示该虚函数不能再被重写
使用举例如下:

四、override关键字

从上面的分析可以知道C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字写错而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来调试才能知道;这样就很麻烦,所以为了解决这个问题,在C++11中有了override关键字。

override关键字:
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
使用举例如下:

五、重载、重写(覆盖)、重定义(隐藏)的区别

六、抽象类

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

// 抽象--在现实世界中没有对应的实物
// 一个类型,如果一般在现实世界中,没有具体的对应实物就定义成抽象类比较好

class Person
{
public:
	//纯虚函数一般只声明,不实现;但是可以实现,
    //实现没有价值,因为没有成员或者指针能够调用到它
	virtual void Shopp() = 0;  //纯虚函数
	void fun()
	{
		cout << "void fun()" << endl;
	}
};


class Member :public Person
{
public:

};

int main()
{
	Person* ptr=nullptr;
	//ptr->Shopp();  //这里编译不会出错,但是运行就会崩溃
	
	ptr->fun();    
	//这里编译和运行都不出错,因为fun()是成员函数放在公共代码段
	//并且这里不是解引用,而是把ptr当做一个this指针传给fun()函数,就可以运行了
	return 0;
}

注意:纯虚函数的类,本质上是强制子类去完成虚函数重写。
而override关键字只是在语法上检查是否完成重写。
实现继承和接口继承:
1、普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
2、虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
所以如果不实现多态,不要把函数定义成虚函数。

七、多态的原理分析 1. 虚函数表的相关概念

什么是虚函数表呢?我们先来看看下面的代码,然后再分析对象模型。

class Person
{
public:
	virtual void Shopp1()
	{
		cout << "virtual void Shopp()1"<< endl;
	}
	virtual void Shopp2()
	{
		cout << "void fun()2" << endl;
	}
	int a = 0;
	char ch = 'd';
};

int main()
{
	Person p;
	return 0;
}

我们对这段代码进行调试,分析如下:

从上面的图中,我们了解到虚函数指针、虚函数表、虚函数的相关概念后,我们接着来分析分析多态的原理。我们还是对着对象模型进行拆分理解,代码如下:

class Person
{
public:
	virtual void Shopp()
	{
		cout << "购物--不打折" << endl;
	}
	int a = 1;
};

class Member :public Person
{
public:
	virtual void Shopp()
	{
		cout << "购物--打9折" << endl;
	}
	int b = 2;
};

void fun(Person& p)
{
	// 多态调用,在编译时,不能确定调用的是哪个函数
	// 运行时,去p指向对象的虚表中
	// 找到虚函数的地址。
	p.Shopp();
}

int main()
{
	Person Zhangsan;
	fun(Zhangsan);

	Member Lisi;
	fun(Lisi);

    //不是多态,编译时确定地址
	Zhangsan.Shopp();
	Lisi.Shopp();
	return 0;
}

我们还是对这段代码的执行过程进行分析:如下图所示

2. 对多态原理的总结

1、基类对象和派生类对象虚表是不一样的,这里我们发现Shopp完成了重写,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
2、虚函数表本质是一个存虚函数指针的指针数组。
3、虚函数和普通函数一样的,都是存在代码段的,虚表(函数指针数组)存放在代码段,
虚表指针存放在栈中,并且在对象调用构造函数在初始化列表阶段初始化的。
4、满足多态的函数调用,不是在编译时确定的,是运行起来以后到对象的中去找的。不满足多态的函数调用时编译时确认好的。

3. 对多态构成条件的理解

所以我们再回头过来看看,为什么必须通过基类的指针或者引用调用虚函数呢?
我们学习了多态的原理后就明白了:
因为如果把子类的对象赋值给父类对象,而不是引用的话,那么父类对象中存的虚表指针还是父类原来的虚表指针;所以调用的都是父类的虚函数,无法调用子类的虚函数。
(这里不能把子类对象的虚表指针拷贝给父类对象,因为这样会造成混淆,父类对象不确定调用的是父类的虚函数还是子类的虚函数。比如:父类对象调用到子类的析构函数那不就裂开了嘛。)

但是如果是引用的话,引用相当于取一个别名嘛,那么父类对象中存的虚表指针就是子类的虚表指针,所以就可以调用子类的虚函数。
总结:如果把子类对象给父类对象,如果是引用,那么父类对象中的虚表指针是子类对象的虚表指针。如果不是引用,那么父类对象中的虚表指针还是父类的虚表指针。

七、单继承和多继承的虚函数表

可以跳转到这篇博文看看:单继承和多继承的虚函数表。

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

原文地址: http://outofmemory.cn/langs/676114.html

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

发表评论

登录后才能评论

评论列表(0条)

保存