首页 > 编程语言 >从C向C++8——多态

从C向C++8——多态

时间:2024-02-05 12:00:39浏览次数:24  
标签:函数 int 基类 多态 C++ 派生类 指针

一.多态基础

面向对象程序设计语言有封装、继承和多态三种机制,这三种机制能够有效提高程序的可读性、可扩充性和可重用性。

“多态(polymorphism)”指的是同一名字的事物可以完成不同的功能。多态可以分为编译时的多态运行时的多态。前者主要是指函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;而后者则和继承、虚函数等概念有关。

1.多态

我们之前知道基类的指针可以指向派生类对象,但是其中存在一个问题:通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。大家可以自己写代码尝试确认,为了消除这种尴尬,让基类指针能够访问派生类的成员函数,C++增加了虚函数。使用虚函数非常简单,只需要在函数声明前面增加 virtual 关键字。

有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)

#include <iostream>
using namespace std;

//基类People
class People{
public:
    People(char *name, int age);
    virtual void display();  //声明为虚函数
protected:
    char *m_name;
    int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
    cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}

//派生类Teacher
class Teacher: public People{
public:
    Teacher(char *name, int age, int salary);
    virtual void display();  //声明为虚函数
private:
    int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
    cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}

int main(){
    People *p = new People("王志刚", 23);
    p -> display();

    p = new Teacher("赵宏佳", 45, 8200);
    p -> display();

    return 0;
}

上面的代码中,同样是p->display();这条语句,当 p 指向不同的对象时,它执行的操作是不一样的。同一条语句可以执行不同的操作,看起来有不同表现方式,这就是多态。多态是面向对象编程的主要特征之一,C++中虚函数的唯一用处就是构成多态。

C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。

多态满足条件:

  • 1、有继承关系
  • 2、子类重写父类中的虚函数

多态使用:父类指针引用指向子类对象

2.引用实现多态

引用在本质上是通过指针的方式实现的,既然借助指针可以实现多态,那么我们就有理由推断:借助引用也可以实现多态。

修改上例中 main() 函数内部的代码,用引用取代指针:

int main(){
    People p("王志刚", 23);
    Teacher t("赵宏佳", 45, 8200);
   
    People &rp = p;
    People &rt = t;
   
    rp.display();
    rt.display();
    return 0;
}

运行结果: 王志刚今年23岁了,是个无业游民。 赵宏佳今年45岁了,是一名教师,每月有8200元的收入。

由于引用类似于常量,只能在定义的同时初始化,并且以后也要从一而终,不能再引用其他数据,所以本例中必须要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。从运行结果可以看出,当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。

不过引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力,所以以后我们再谈及多态时一般是说指针。

3.多态剖析

我们知道一个空类在C++中的大小是1,如果一个类只含有成员函数,它近似于一个空类,如果使用sizeof()关键字计算其大小,其结果是1。

那么就上面的例子,现在我们计算People类的大小,应该为56字节,其中string为40字节,而隐含的vfptrx64环境下为8字节,分析以下Teachar的大小为64字节,注意VS的默认情况下是对齐方式的。

4.虚函数注意事项

C++虚函数对于运行时多态具有决定性的作用,有虚函数才能构成多态。

  1. 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
  2. 为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数。
  3. 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
  4. 只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为virtual void func();,派生类虚函数的原型为virtual void func(int);,那么当基类指针 p 指向派生类对象时,语句p -> func(100);将会出错,而语句p -> func();将调用基类的函数。
  5. **构造函数不能是虚函数。**对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。
  6. 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数。

二.虚函数

1.纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。 纯虚函数语法:

virtual 返回值类型 函数名 (函数参数) = 0;

当类中只要有了一个纯虚函数,这个类也称为抽象类。 抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

2.虚析构

问题:多态使用时,如果在子类中有属性开辟到堆区,那么父类指针在释放时无法调用子类的析构代码。

方法:将父类的析构函数改为虚析构或纯虚析构。

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

3.虚函数表

编译器之所以能通过基类指针指向的对象找到虚函数,是因为在创建对象时额外地增加了虚函数表。

如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。这里的数组就是虚函数表(Virtual function table),简写为vtable

其中某一多态的内存模型图,如图所示:

从C向C++8——多态_多态

图中左半部分是对象占用的内存,右半部分是虚函数表 vtable。在对象的开头位置有一个指针 vfptr,指向虚函数表,并且这个指针始终位于对象的开头位置。

当通过指针调用虚函数时,先根据指针找到 vfptr,再根据 vfptr 找到虚函数的入口地址。

三.多态案例练习1

1.要求

写一个简单计算器。

2.实验代码

#include <iostream>
#include <string>
using namespace std;

//基类
class Calculate {
public:
    int m_num1;
    int m_num2;
    //虚函数
    virtual int getresult() {
        return 0;
    }
};

//加法类计算器
class AddCalculate :public Calculate {
    int getresult() {
        return m_num1 + m_num2;
    }
};

//减法计算器
class SubCalculate :public Calculate {
    int getresult() {
        return m_num1 - m_num2;
    }
};


//乘法计算器
class MulCalculate :public Calculate {
    int getresult() {
        return m_num1 * m_num2;
    }
};


//除法计算器
class DivCalculate :public Calculate {
    int getresult() {
        return m_num1 / m_num2;
    }
};
int main() {

    Calculate* p = new AddCalculate;
    p->m_num1 = 20;
    p->m_num2 = 10;
    cout << p->m_num1 << "+" << p->m_num2 << "=" << p->getresult() << endl;

    return 0;
}

3.总结

多态的优点

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

四.多态案例练习2

1.题目

案例描述:

电脑主要组成部件为 CPU (用于计算) ,显卡 (用于显示) ,内存条 (用于存储)。将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如intel厂商和Lenovo厂商,创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口。测试时组装三台不同的电脑进行工作 。

从C向C++8——多态_派生类_02

2.实验代码

#include<iostream>
using namespace std;

//抽象CPU类
class CPU
{
public:
	//抽象的计算函数
	virtual void calculate() = 0;
};

//抽象显卡类
class VideoCard
{
public:
	//抽象的显示函数
	virtual void display() = 0;
};

//抽象内存条类
class Memory
{
public:
	//抽象的存储函数
	virtual void storage() = 0;
};

//电脑类
class Computer
{
public:
	Computer(CPU * cpu, VideoCard * vc, Memory * mem)
	{
		m_cpu = cpu;
		m_vc = vc;
		m_mem = mem;
	}

	//提供工作的函数
	void work()
	{
		//让零件工作起来,调用接口
		m_cpu->calculate();

		m_vc->display();

		m_mem->storage();
	}

	//提供析构函数 释放3个电脑零件
	~Computer()
	{

		//释放CPU零件
		if (m_cpu != NULL)
		{
			delete m_cpu;
			m_cpu = NULL;
		}

		//释放显卡零件
		if (m_vc != NULL)
		{
			delete m_vc;
			m_vc = NULL;
		}

		//释放内存条零件
		if (m_mem != NULL)
		{
			delete m_mem;
			m_mem = NULL;
		}
	}

private:

	CPU * m_cpu; //CPU的零件指针
	VideoCard * m_vc; //显卡零件指针
	Memory * m_mem; //内存条零件指针
};

//具体厂商
//Intel厂商
class IntelCPU :public CPU
{
public:
	virtual void calculate()
	{
		cout << "Intel的CPU开始计算了!" << endl;
	}
};

class IntelVideoCard :public VideoCard
{
public:
	virtual void display()
	{
		cout << "Intel的显卡开始显示了!" << endl;
	}
};

class IntelMemory :public Memory
{
public:
	virtual void storage()
	{
		cout << "Intel的内存条开始存储了!" << endl;
	}
};

//Lenovo厂商
class LenovoCPU :public CPU
{
public:
	virtual void calculate()
	{
		cout << "Lenovo的CPU开始计算了!" << endl;
	}
};

class LenovoVideoCard :public VideoCard
{
public:
	virtual void display()
	{
		cout << "Lenovo的显卡开始显示了!" << endl;
	}
};

class LenovoMemory :public Memory
{
public:
	virtual void storage()
	{
		cout << "Lenovo的内存条开始存储了!" << endl;
	}
};


void test01()
{
	//第一台电脑零件
	CPU * intelCpu = new IntelCPU;
	VideoCard * intelCard = new IntelVideoCard;
	Memory * intelMem = new IntelMemory;

	cout << "第一台电脑开始工作:" << endl;
	//创建第一台电脑
	Computer * computer1 = new Computer(intelCpu, intelCard, intelMem);
	computer1->work();
	delete computer1;

	cout << "-----------------------" << endl;
	cout << "第二台电脑开始工作:" << endl;
	//第二台电脑组装
	Computer * computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);;
	computer2->work();
	delete computer2;

	cout << "-----------------------" << endl;
	cout << "第三台电脑开始工作:" << endl;
	//第三台电脑组装
	Computer * computer3 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);;
	computer3->work();
	delete computer3;

标签:函数,int,基类,多态,C++,派生类,指针
From: https://blog.51cto.com/u_16150223/9604336

相关文章

  • c++11的左值 右值的笔记
    在C++11的程序中,所有的值必须属于左值,将亡值,纯右值之一。将忘值则是c++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(以为他用),比如返回右值引用T&&的函数返回值,std::move的返回值,或者转换为T&&的类型的转换函数的返回值。而剩余的,可以标识函数、对象的值都属......
  • C++类和对象
    1.类和对象1.1声明类classPerson{private:stringname;intage;};1.2作为类实例的对象1.3使用句点运算符访问成员1.4使用指针运算符(->)访问成员2.关键字public与private3.构造函数3.1构造函数重载3.2默认构造函数:只要不需要......
  • C++CLI 析构函数和终结器理解
    测试类:#pragmaoncerefclassHello{public:Hello();~Hello();!Hello();};/***************/#include"Hello.h"Hello::Hello(){System::Console::WriteLine("构造函数!");}Hello::~Hello(){System::Console::WriteLi......
  • C++ Primer 学习笔记 PartI C++基础
    Ch1开始略这一章包含控制流,条件,循环,注释,标准IO等内容。对于C语言/ACMC+STL中常见数值的内容不再赘述,仅总结较为不熟悉的内容。PartIC++基础CH2变量和基本类型2.1基本内置类型2.1.1算术类型2.1.1类型转换向unsigned赋超出范围的值,结果取余,对于signed,结果未定义。......
  • C++中promise和future初认识
    future/promisefuture提供了一个基于数据(future模板类型)的异步概念:对于一个类型T,可以在以后通过get接口获得这个类型T的变量。或者打个不太恰当的比方,当你获得一个future对象时,就获得了一个消费券(consumer):拿着这张券可以兑换(get)一个T类型的结果(如果数据未就绪的话会阻塞等......
  • C++多线程 第三章 在线程间共享数据
    第三章在线程间共享数据共享数据基本问题如果所有共享数据都只读,那就没有问题.不变量(invariants):对特定数据结构总为真的语句.例如:"该变量表示线程数量."修改线程之间共享数据的一个常见潜在问题就是破坏不变量.竞争条件(racecondition):线程竞争执行各自的操作,导......
  • C++之INI配置文件读写/注释库 inicpp 介绍【简单易用-包含inicpp.hpp头文件即可】
    一个头文件(header-file-only)搞定INI文件读写、甚至进行注释。跨平台,并且用法极其简单。MITlicense,从此配置INI文件就像喝水。【注:对您有帮助的话,Star或Issues为项目维护提供动力,感谢。】-byofficalofJN-inicppproject.一、库下载https://github.com/dujingning/inicpp......
  • 14. C++函数的编译
    C++函数的编译C++和C语言的编译方式不同。C语言中的函数在编译时名字不变,或者只是简单的加一个下划线_(不同的编译器有不同的实现),例如,func()编译后为func()或_func()。而C++中的函数在编译时会根据它所在的命名空间、它所属的类、以及它的参数列表(也叫参数签名)等信息进行重新......
  • 创建大量栅格文件并分别写入像元数据:C++ GDAL代码实现
      本文介绍基于C++语言GDAL库,批量创建大量栅格遥感影像文件,并将数据批量写入其中的方法。  首先,我们来明确一下本文所需实现的需求。已知我们对大量遥感影像进行了批量读取与数据处理操作——具体过程可以参考文章C++GDAL提取多时相遥感影像中像素随时间变化的数值数组;而随......
  • A Knight's JourneyC++
    题目看半天看不懂。題目把我恶心坏了。看网上说按字典顺序输出,到底是什么意思半天没搞懂。#include<iostream>#include<string>usingnamespacestd;intd[8][2]={{-1,-2},{1,-2},{-2,-1},{2,-1},{-2,1},{2,1},{-1,2},{1,2}};intvisit[8][8]={0};boolDFS(i......