C++多态总结

news2024/11/24 3:55:59

多态的概念


多态的基本概念

多态支持不同对象完成不同的行为。
定义“人”为一种对象,那么不同的人对于一件事会有不同的行为。
比如:普通人买票时,票价为全价。
学生买票时,是半价买票。
军人买票时则可优先买票。同属于人这一大类,但是会有不同,多态的特征就是支持对一类中的某些不同做出不同的反映。

多态的构成条件

必须通过基类的指针或引用调用虚函数
虚函数在派生类中需要被重写

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};

class Soldier :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "优先买票" << endl;
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Soldier sd;
	Func(ps);
	Func(st);
	Func(sd);
	return 0;
}

ps,st,sd对象被指向到三个派生类的实现中。输出结果就是派生类中实现的结果。
在这里插入图片描述

虚函数

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

问题一:C++的类中,那些函数不能用virtual修饰,为什么?
不能是虚函数的成员函数有:静态成员函数,内联成员函数,构造函数,友元函数
编译器会为每一个含有virtual函数生成一个vtablel。构造函数调用时,Vtable没有建立,当然不能使用虚函数。构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类。
静态成员函数不能是virtual的,因为静态成员函数属于类而非单个具体对象,所有的对象共享一份代码,没有实现多态的必要。
inline成员函数可以声明为virtual,但是在编译时不会实际将代码直接在调用处展开。
友元函数也不能声明为virtual,因为友元关系是不能被继承的,编译会出错。
注:一般情况下,父类的析构函数需要定义为虚函数。
普通成员函数也不能是virtual的,否则不能通过编译,virtual只能出现在类声明中。

虚函数的重写

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

class Person {
public:
 	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
 	virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

不符合多态的条件

一:不是virtual函数

class Person
{
public:
	void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};

class Student :public Person
{
public:
	void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};

class Soldier :public Person
{
public:
	void BuyTicket()
	{
		cout << "优先买票" << endl;
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Soldier sd;
	Func(ps);
	Func(st);
	Func(sd);
	return 0;
}

ps,st,sd在Func中都被当做Person对象,不用virtual则没有vtable没法指向具体派生类中方法。所有都调用的是基类Person中的BuyTicket函数。
二:不是父类的指针或者引用调用

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};

class Soldier :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "优先买票" << endl;
	}
};
void Func(Person p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Soldier sd;
	Func(ps);
	Func(st);
	Func(sd);
	return 0;
}

这样在Func函数中传递的直接就是一个基类person对象,是可能调用派生类中的方法的。
三:派生类与基类函数参数不同

//(3)多态条件探究三:不符合重写 -- 参数不同
class Person
{
public:
	virtual void BuyTicket(int)
	{
		cout << "买票-全价" << endl;
	}
};

class Student :public Person
{
public:
	//Student中函数的参数类型与父类不同
	virtual void BuyTicket(char)
	{
		cout << "买票-半价" << endl;
	}
};

class Soldier :public Person
{
public:
	//Soldier中函数的参数类型与父类相同
	virtual void BuyTicket(int)
	{
		cout << "优先买票" << endl;
	}
};
void Func(Person& p)
{
	p.BuyTicket(1);
}

int main()
{
	Person ps;
	Student st;
	Soldier sd;
	Func(ps);
	Func(st);
	Func(sd);
	return 0;
}

这里Soldier的函数参数类型与基类相同,student中参数类型不同。无法构成重写。则,student与基类没有构成多态,soldier与基类构成多态。
在这里插入图片描述
四:返回值类型不同

class Person
{
public:
	virtual int BuyTicket()
	{
		cout << "买票-全价" << endl;
		return 0;
	}
};

class Student :public Person
{
public:
	//Student中函数的返回值类型与父类不同
	virtual double BuyTicket()
	{
		cout << "买票-半价" << endl;
		return 1.1;
	}
};

class Soldier :public Person
{
public:
	//Soldier中函数的返回值类型与父类相同
	virtual int BuyTicket()
	{
		cout << "优先买票" << endl;
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Soldier sd;
	Func(ps);
	Func(st);
	Func(sd);
	return 0;
}

在这里插入图片描述
返回值类型不同,必须是协变才可以,但是上边的例子也不是协变,所有就无法编译通过。
在这里插入图片描述

在这里插入图片描述
如果改成如上,那么就是重载。通过对象的方法才可调用。
st.BuyTicket(1)是对象的方法调用,调用的是返回值为double的方法。
st.Person::BuyTicket()调用的是父类中的方法。
Func(st)调用的是p.BuyTicket(),而student中没有重写BuyTicket()方法,所以,只能继承基类中的“全票”方法。

虚函数重写的两个例外

一:协变
基类与派生类虚函数的返回值类型不同
这个不同指的是基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或者引用。并不是随便一个虚函数的返回值类型不同。

class Person
{
public:
//返回父类的指针
	virtual Person* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return this;
	}
};

class Student :public Person
{
public:
//返回子类的指针
	virtual Student* BuyTicket()
	{
		cout << "买票-半价" << endl;
		return this;
	}
};

class Soldier :public Person
{
public:
//返回子类的指针
	virtual Soldier* BuyTicket()
	{
		cout << "优先买票" << endl;
		return this;
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Soldier sd;
	Func(ps);
	Func(st);
	Func(sd);
	return 0;
}

返回的是this指针。
注意协变要求的是返回指针或引用必须是父子关系。则,在一个基类中,可以返回类型可以另一个基类,同理,派生类中的返回类型也必须和基类返回类型有父子关系。

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

class Person
{
public:
	virtual A* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};

class Student :public Person
{
public:
	virtual B* BuyTicket()
	{
		cout << "买票-半价" << endl;
		return nullptr;
	}
};

class Soldier :public Person
{
public:
	virtual B* BuyTicket()
	{
		cout << "优先买票" << endl;
		return nullptr;
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Soldier sd;
	Func(ps);
	Func(st);
	Func(sd);
	return 0;
}

Person基类中返回类型是A基类,Student和Soldier中返回的是B派生类,他们都不是返回自己那个类对象。
二:析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor。
编辑器为什么会这样处理呢?
当基类指针指向派生类的时候,若基类析构函数不声明为虚函数,在析构时,只会调用基类而不会调用派生类的析构函数,从而导致内存泄露。
析构派生类对象为什么会首先调用基类的析构函数?
析构函数的主要作用,就是在该类实例对象不需要的时候,对这个对象进行一些清理作用(主要是内存的释放.)一个基类,它也是一个类,它也可以有对象, 所以基类的析构函数负责基类内部开辟的一些内存的清理工作.
那么, 对于派生类. 可能会产生一些内存的开销, 那么派生类也需要构造函数. 基类只了解基类内部的内存开销, 派生类只了解派生类自己的内存开销. 他们之间互不了解. 继承关系, 多数都是建立在扩充基类的关系上.
派生类丰富了基类, 如果派生类的实例对象在销毁的时候, 不去自动调用基类的析构函数, 那么请问? 基类的清理工作谁去做? 基类的内部, 有一些是派生类不了解的. 为了方便,为了安全,为了管理. 所以派生类的对象销毁的时候, 继承机制会分级调用各级的析构函数.
内存的清理工作必须严格要求. 谁开辟的,谁最后释放. 一人做事儿一人当. 孩子闯了祸, 不应该让老爹去认错. 同时继承关系, 基类有基类的责任, 派生类有派生类的责任. 分工明确了, 不能越俎代庖. 各部门相互协调, 程序才能顺畅运行.
谁开辟的必须谁清理,派生类中继承着基类中的部分,那就要基类的析构函数清理。

例子

class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};


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

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

在这里插入图片描述
在这里插入图片描述

C++11 override 和 final

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

class A
{
public:
	virtual void Func() final {}
};

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

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

class A
{
public:
	virtual void Func(){}
};

class B: public A
{
public:
	virtual void Func1() override {}
};

在这里插入图片描述

重载,重写,重定义

重载

函数名相同,参数列表不同(参数类型、参数顺序),不能用返回值区分。
特点:
作用域相同;
函数名相同;
参数列表必须不同,但返回值无要求;
特殊情况:若某一重载版本的函数前面有virtual关键字修饰,则表示它是虚函数,但它也是重载的一个版本。
作用效果:编译器根据函数不同的参数列表,将函数与函数调用进行早绑定,重载与多态无关,与面向对象无关,它只是一种语言特性。

重写

派生类重定义基类的虚函数,既会覆盖基类的虚函数(多态).
特点:
作用域不同;
函数名、参数列表、返回值相同;
基类函数是virtual;
特殊情况:若派生类重写函数是一个重载版本,那么基类的其他同名重载函数将在子类中隐藏。
作用效果:父类指针和引用指向子类的实例时,通过父类指针或引用可以调用子类的函数,这就是C++的多态。多态是是使用虚函数表(vtable)技术来实现的。

重定义

派生类对基类函数得重定义,派生类函数名与基类某函数同名。
特点:
作用域不同,既一个在基类一个在子类中;
函数名相同;
参数列表、函数返回值无要求;
特殊情况:若派生类定义的函数与基类的成员函数完全一样(名字、参数列表、返回值),且基类的该函数为virtual,则属于派生类重写基类的虚函数。


class Base
{
public:

    // 三个重载函数
    void fun()
    {
        std::cout << "base fun()" << std::endl;
    }
    void fun(int)
    {
        std::cout << "base fun(int)" << std::endl;
    }

    virtual void fun(int, double)
    {
        std::cout << "base fun(int,double)" << std::endl;
    }

};

class Derive : public Base
{
public:
    // 重定义基类fun函数,隐藏了基类的三个重载函数
    void fun(double)
    {
        std::cout << "Drive fun(int)" << std::endl;
    }
};

class Derive1:public Base
{
public:
    // 重写基类的fun(int,double)函数,同时隐藏了基类的两个重载函数
    void fun(int, double)
    {
        std::cout << "Derive1 fun(int,double)" << std::endl;
    }
};

抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
为什么会有抽象类?
1.最重要的原因是,可以将接口与实现分离。接口是软件产品最有价值的资源,设计接口比实现接口需要耗费更昂贵的成本。因此,要将接口保护起来,以免在针对客户需求修改实现的时候,程序员不小心把接口破坏掉。
2.引入抽象基类和纯虚函数方便实现C++的多态特性。可以用抽象基类的指针去调用子类对象的方法。
3.很多时候,许多基类被实例化是不合理的。例如“形状”这个基类,被实例化之后反而会让人相当费解,所以干脆将“形状”这个类定义为抽象类,由它派生出正方形,三角形等子类。

注:抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。

接口继承与实现继承

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

多态实现原理

在实际中,基类与派生类对于virtual函数都会建立一个虚表。子类中的函数进行重写,所以地址发生变化。当调用一个已经重写过的函数时,底层会寻找不同的虚表指针。对于不是普通成员函数,未加virtual修饰的的函数,就不会进入虚表中,也就不会有多态的特性。
在这里插入图片描述
(1)虚表是一个指针(void**),存的也是指针,图中的void*。
(2)同一个类型的对象共用一张虚表,vs下,不管是否完成重写,子类虚表跟父类虚表都不是同一个。
(3)派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/46374.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java项目-第165期ssm咨询交流论坛_ssm毕业设计_计算机毕业设计

java项目-第165期ssm咨询交流论坛_ssm毕业设计_计算机毕业设计 【源码请到资源专栏下载】 今天分享的项目是《ssm咨询交流论坛》 该项目分为2个角色&#xff0c;管理员和用户。 用户可以浏览前台,包含功能有&#xff1a; 首页、学校风采、师资力量、投稿信息 、论坛交流、系统公…

认识vue3以及语法运用简介

Vue3支持2的大多数特性 性能提升: 打包大小减少41%, 初次渲染快55%,更新快133%, 内存使用减少54% 推出一系列组合型API ref与reactive watch与computed 新的声明周期函数。 一、组合API------reactive、ref 与 toRefs 在Vue2.x中&#xff0c;定义数据都是在data中&#xff0c;但…

LeetCode——Weekly Contest 320(附动态规划解题思路)

LeetCode周赛第320场记录 质量还不错的一场周赛&#xff0c;也可以学到不少知识。 2475. 数组中不等三元组的数目(排序荷兰国旗问题) 这道题非常简单&#xff0c;就是从头向后一一找出不含重复数字的三元组。我在比赛时直接写了一个三重循环暴力来解&#xff0c;这道题的数据…

WebDAV之葫芦儿·派盘+元思笔记

元思笔记 支持webdav方式连接葫芦儿派盘。 卡片笔记不仅是笔记爱好者,学生、医生、投资等各行各业的人都在不约而同的夸赞元思笔记的好。这是一款面向大众的卡片笔记软件,解决了笔记类软件的一个痛点:绝大多数人都很难坚持每天记一点东西。任何笔记工具,不论是纸笔还是电…

时序特征提取工具

在选择了需要提取的特征&#xff0c;确定了时序数据特征提取数据集的长度并对先验知识建模之后&#xff0c;就需要利用工具搭建特征提取系统。科研机构围绕不同问题域搭建的开源时序数据特征提取工具已经不少&#xff0c;我们可以利用这些工具快速实现希望达成的算法效果。下面…

[附源码]Python计算机毕业设计Django的残障人士社交平台

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

相控阵天线(十):波束跃度、虚位技术、幅度相位误差分析(含代码)

目录简介波束跃度不同移相器位数对方向图的影响不同移相器位数对波束跃度的影响虚位技术不同虚位位数对指向精度的影响不同虚位位数对副瓣电平的影响幅度相位误差分析随机误差周期误差Python代码示例简介 阵列天线的成本、批量和可制造性等实际问题的解决方案的选择直接影响阵…

[msyql]实战:关于回表的一次查询优化实战

起因与前置环境思考与解决方案 第一个理解与方法——分块分页第二个理解与方法——拆分子查询第三个理解与方法——拆分子查询分块分页 原理浅析与总结 回表和索引覆盖的浅解 原理简单说明MYSQL中回表的实现 总结与收获 起因与前置环境 目前在职的公司是已经运转挺久的电商类…

leetcode 343. 整数拆分(动态规划)

题目链接&#xff1a;343. 整数拆分 动态规划 (1) 确定 dpdpdp 数组下标含义&#xff1a; dp[i]dp[i]dp[i]: 将 iii 拆分为至少两个正整数之后的最大乘积&#xff1b; (2) 确定递推公式&#xff1a; 当 i≥2i \ge 2i≥2 时, 设 jjj 是 iii 拆分出来的第一个正整数&#xff0c…

1990-2021年全国30省城镇登记失业率

1、时间&#xff1a;1990-2021年 2、来源&#xff1a;整理自统计NJ 3、数据说明&#xff1a; 包括全国30个省份&#xff0c;不包括西藏&#xff0c;其中北京、天津、辽宁、吉林、江苏、浙江、湖南、四川、新疆2021年数据存在缺失&#xff0c; 内含原始数据&#xff0c;线性…

猿如意开发工具|python3.7

文章目录 一、猿如意是什么&#xff1f;一、猿如意的下载安装使用二、使用猿如意下载安装python3.7总结前言 对于程序猿来说&#xff0c;辅助开发工具箱是非常重要的&#xff0c;可以方便广大的开发者们。今天我就介绍一款非常好用的开发工具箱-猿如意。 一、猿如意是什么&…

大数据必学Java基础(一百零八):过滤器的生命周期

文章目录 过滤器的生命周期 一、构造方法 二、初始化方法 三、拦截请求方法

用R语言实现神经网络预测股票实例

神经网络是一种基于现有数据创建预测的计算系统。最近我们被客户要求撰写关于神经网络的研究报告&#xff0c;包括一些图形和统计输出。 如何构建神经网络&#xff1f; 神经网络包括&#xff1a; 输入层&#xff1a;根据现有数据获取输入的层隐藏层&#xff1a;使用反向传播…

基于PHP+MySQL动漫周边商城销售网站的开发与设计

随着时代的发展,人们对动漫周边产品的关注度越来越高,尤其是对当代的年轻人来说,对一些动漫的手办和玩具等商品都非常的热爱。但是当下时长上的动漫周边产品销售网站还很少,这对钟爱动漫周边产品的来说是一件很痛苦的事情,明明知道一件出现了这些相关产品,但是没有渠道能够购买…

【简单、实用】kubernetes的etcd备份与恢复实现恢复集群配置

学习目标 内容 提示:由于牵涉概念过多,本章主要讲解具体的备份恢复,其他概述 官网:https://kubernetes.io/zh-cn/docs/tasks/administer-cluster/configure-upgrade-etcd/#backing-up-an-etcd-cluster 一. etcd的工作原理 可将其分成两层次:Http层请求、接收消息;剩下的…

家电专用降压DC-DC方案PL8310

PL8310是一个单片36V, 1A降压开关监管机构。PL8310集成了一个36V 250mΩ高侧和一个36V, 140mΩ低侧mosfet提供1A持续负载电流超过4.5V至36V宽工作输入电压带33V输入过电压保护。峰值电流模式控制速度快瞬态响应和逐周电流限制。PL8310具有可配置的线路下降补偿&#xff0c;可配…

CenterNet算法代码剖析

目录 一、图片预处理 1、cv读取原始图片 2、读取图片的中心点 3、计算仿射变化2*3的矩阵 4、基于双线性插值的仿射变化&#xff0c;将原始图片映射到dst图片 5、将原始图片的值归一化到0~1之间 6、使用样本集的mean和std再进行z-score归一化 7、计算特征图的大小&#…

linux mailxdingding机器人报警

前言&#xff1a;采用devops的思想来确认做本文内容目的 作为 <用户角色> 我想要 <结果> 以便于 <目的> 作为运维人员&#xff0c;我想要服务器故障时候能够进行报警&#xff0c;以便于即使处理服务器故障、保障服务器稳定运行 两种方式 邮箱 客户端授权码 …

Kafka - 10 Kafka副本 | 分区副本分配 | 手动调整分区副本 | Leader Partition 负载平衡 | 增加副本因子

文章目录1. 分区副本分配2. 手动调整分区副本3. Leader Partition 负载平衡4. 增加副本因子1. 分区副本分配 如果 kafka 服务器只有 4 个节点&#xff0c;那么设置 kafka 的分区数大于服务器台数&#xff0c;在 kafka底层如何分配存储副本呢&#xff1f; ① 创建 16 分区&…

[附源码]计算机毕业设计springboot高校学生宿舍管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…