c++面向对象程序设计基础教程————多态性和虚函数(一)

简介: c++面向对象程序设计基础教程————多态性和虚函数(一)

前言


继承性反映的是类与类之间的层次关系,多态性则是考虑这种层次关系以及类自身特定成员函数之间的关系来解决行为的再抽象问题。多态性有两种表现形式一种是不同的对象在收到相同的消息时,产生不同的动作,主要通过虚函数来实现;另一种是同一对象收到相同的消息却产生不同的函数调用,主要通过函数重载来实现。本章将讨论多态性的主要内容:虚函数和动态联编;

一、静态联编与动态联编

多态性就是同一符号或名字在不同情况下具有不同解释的现象,既是指同一个函数的多种形态。c++可以支持两种多态性,编译时的多态性和运行时的多态性。

对一个函数的调用,要在编译时或在运行时确定将其

连接上相应的函数体的代码,这一过程称为函数联编(简称联编)。c++中两种联编形式:静态联编和动态联编。


静态联编


 静态联编是指在程序编译连接阶段进行的联编。编译器根据源代码调用固定的函数标识符,然后由连接器接管这些标识符,并用物理地址代替他们。这种联编又被称为早期联编,应为这种联编工作是在程序运行之前完成的。


静态联编所支持的多态性称为编译时的多态性,当调用重载函数时,编译器可以根据调用时所使用的实参在编译时就确定下来应调用哪个函数。下面来看在层次关系中一个静态联编的例子。


#include<iostream>
const double PI = 3.14;
using namespace std;
class Figure         //定义基类;
{
public:
  Figure() {};
  double area() const { return 0.0; }
};
class Circle :public Figure           //定义派生类,公有继承;
{
public:
  Circle(double myr) { R = myr; }
  double area() const { return PI * R * R; }
protected:
  double R;
};
class Rectangle :public Figure   //定义派生类,公有继承;
{
public:
  Rectangle(double myl, double myw) { L = myl; W = myw; }
  double area() const { return L * W; }
private:
  double L, W;
};
int main()
{
  Figure fig;                //基类Figure对象;
  double area;
  area = fig.area();
  cout << "Area of figure is" <<area<< endl;
  Circle c(3.0);            //派生类Circle对象;
  area = c.area();
  cout << "Area of Circle is" << area << endl;
  Rectangle rec(4.0, 5.0);
  area = rec.area();
  cout << "Area of Rectangle is" << area << endl;
  return 0 ;
}



上面的程序我们可以看出,在静态联编中,其实就是对重载函数的使用。定义不同的对象,通过对象来引用不同的函数从而实现我们要实现的功能。

静态联编的最大优点就是速度快,运行时的开销仅仅是传递参数,执行函数调用,清除等。不过,程序员必预测在每一种情况下所有的函数调用时,将要使用那些对象。这样,不仅有局限性,有时也是不可能实现的。

下面的程序就会说明这种情况:


#include<iostream>
using namespace std;
const double PI = 3.14;
class Figure
{
public:
  Figure() {};
  double area() const { return 0.0; }
};
class Circle :public Figure             //定义派生类,公有继承;
{
public:
  Circle(double myr) { R = myr; }
  double area()const { return PI*R*R; }
protected:
  double R;
};
class Rectangle :public Figure               //定义派生类,公有继承;
{                               
  public:
  Rectangle(double myl, double myw) { L = myl, W = myw; }
  double area()const { return L * W; }
private:
  double L, W;
};
void func(Figure &p)         //形参为基类的引用;
{
  cout << p.area() << endl;
}
int main()
{
  Figure fig;
  cout << "Area of figure is";
  func(fig);
  Circle c(3.0);   //Circle派生类对象;
  cout << "Area of Circle is";
  func(c);
  Rectangle rec(4.0, 5.0);
  cout << "Area of Rectangle is";
  func(rec);
  return 0;
}


bd11e1b8ab54d0b5af321323ff31d846_88b68aad4b3a481882991d410cde17f6.png


上面的程序没有报错,但是结果正确,那是为什么?

在编译时,编译器将函数中的形参p所执行的area()操作联编到Figure类的area()上,这样访问的知识从基类继承来的成员。

那有没有什么方法来改变这种局限性喃?


动态联编


动态联编是指在程序运行时进行的联编。只有向具有多态性的函数传递一个实际对象的时候,该函数才能与多种可能的函数中的一种来联系起来。这种联编又被称为晚期联编。

动态联编所支持的多态性被称为运行时多态性。在程序代码中要知名某个成员函数具有多态性需要进行动态联编,且需要关键字virtual来标记。这种用virtual关键字标记的函数称为虚函数.


动态联编的优点 动态联编的缺点

增强了编程灵活性,问题抽象性和程序易维护性 函数调用顺序慢


虚函数


虚函数的作用


虚函数是一个成员函数,该成员函在基类内部声明并且被派生类重新定义。为了创建虚函数,应在基类中该函数生命的前面加上关键字virtual。

virtual<返回值类型><函数名>(<形式参数>)

{

《函数体》

}

如果某类中一个成员函数被说明为虚函数,这便意味着该成员函数在派生类中可能存在不同的实现方式。当继承包含虚函数的类时,派生类将重新定义该虚函数衣服和自身的需要。从本质上讲,虚函数实现了“相同界面,多种实现”的理念。而这种理念时运行时的多态性的基础,既是动态联编的基础。


动态联编需要满足的条件:


(1).类之间满足类型兼容规则;

(2).要声明虚函数;

(3).要由成员函数来调用或者是通过指针,引用来访问虚函数。


#include<iostream>
using namespace std;
const double PI = 3.14;
class Figure
{
public:
  Figure() {};
  virtual double area()const { return 0.0; }        //定义为虚函数;
};
class Circle :public Figure                          //定义派生类,公有继承;
{                               
public:
  Circle(double myr) { R = myr; }
  virtual double area()const { return PI * R * R; }
protected:
  double R;
};
class Rectangle :public Figure           //定义派生类,公有继承方式;
{
public:
  Rectangle(double myl, double myw) { L = myl, W = myw; }
  virtual double area()const { return L * W; }
private:
  double L , W;
};
void fun(Figure& p)                 //形参为积基类的引用;
{
  cout << p.area() << endl;
}
int main()
{
  Figure fig;
  cout << "Figure of area is";
  fun(fig);
  Circle c(3.0);
  cout << "Area of Circle is";
  fun(c);
  Rectangle rec(4.0, 5.0);
  cout << "Area of Retangle is";
  fun(rec);
  return 0;
}


8104e034099d5b5542c8bd653325343d_d4805936eb1e4fcdb4132ae5edc588e5.png


这个时候我们发现,答案正确了。看到这里,有没有一种熟悉的感觉,和我们前面学的虚基是不是相似。所以这里我们以可以理解为动态联编之所以能够不断地联编,就是因为成员函数产生了副本。


虚函数与一般重载函数的区别


乍一看,上面的程序,虚函数类似于重载函数。但它不属于重载函数,虚函数与一般的重载函数的区别:


虚函数 重载函数


虚函数不仅要求函数名相同,而且要求函数的签名,返回类型也相同。也就是说函数原型必须完全相同,而且虚函数的特性必须是体现在基类和派生类的类层次结构中 重载函数只要求函数有相同的函数名,并且重载函数 是在相同作用域定义的名字相同的不同函数

虚函数只能是非静态成员函数 重载函数可以是成员函数或友元函数

构造函数不能定义为虚函数,析构函数能定义为虚函数 构造函数可以重载,析构函数不可以

虚函数是根据对象来调用的 重载函数调用是以传递参数 序列的差别 来调用

虚函数是在运行时联编 重载函数是在编译时联编

继承虚属性

基类中说明的虚函数具有自动向下传给他的派生类的性质。不管经历多少派生类层,所有界面相同的函数都熬吃虚特性。因为派生类也是基类。

在派生类中重新定义虚函数时,要求与基类中说明的虚函数原型完全相同,这时对派生类的虚函数中virtual说明可以省略,但是为了提高程序的可读性。为了区分重载函数,而把一个派生类中重定义基类的虚函数称为覆盖。

示例:


#include<iostream>
using namespace std;
class Base
{
public:
  virtual int func(int x)          //虚函数;
  {
  cout << "This is Base class";
  return x;
  }
};
class Subclass :public Base          //派生类,公有继承;
{
public:
 int func(int x)                     //没有使用virtual关键字,但是依旧为虚函数;
  {
  cout << "This is Subclass class";
  return x;
  }
};
void fun(Base& x)
{
  cout << "x=" << x.func(5) << endl;
}
int main()
{
  Base bc;
  fun(bc);
  Subclass be;
  fun(be);
  return 0;
}


8fedc6b5936768c826751db8b73969b0_b4356eef2ee24bb9971ef9ce9d5dc10b.png


下面我们来分析一下虚函数的错误使用


#include<iostream>
using namespace std;
class Base
{
public:
  virtual int func(int x)          //虚函数;
  {
  cout << "This is Base class";
  return x;
  }
};
class Subclass :public Base          //派生类,公有继承;
{
public:
 virtual float func(int x)                     //函数返回类型不同
  {
  cout << "This is Subclass class";
  return x;
  }
};
void fun(Base& x)
{
  cout << "x=" << x.func(5) << endl;
}
int main()
{
  Base bc;
  fun(bc);
  Subclass be;
  fun(be);
  return 0;
}


ab76f23f164a3672e4ba697be83981b7_aa3431a021254543a0d1a7b87bb9a383.png


这里派生类中的虚函数,仅仅只是使用了不同的返回类型,但是就报错了,这是用为在c++中,只靠返回类型不同的信息,进行函数匹配是模糊的。


#include<iostream>
using namespace std;
class Base
{
public:
  virtual int func(int x)          //虚函数;
  {
  cout << "This is Base class";
  return x;
  }
};
class Subclass :public Base          //派生类,公有继承;
{
public:
 virtual int func(float x)                     //函数形参不同
  {
  cout << "This is Subclass class";
  return x;
  }
};
void fun(Base& x)
{
  cout << "x=" << x.func(5) << endl;
}
int main()
{
  Base bc;
  fun(bc);
  Subclass be;
  fun(be);
  return 0;
}



如果派生类与基类的虚函数仅函数名相同,其他不同,则c++认为是重定义函数,是隐藏的,丢失了虚特性。

一个类中的虚函数说明只对派生类中重定义的函数有影响,对他的基类并没有影响。


示例虚函数对他的基类的函数没有影响


#include<iostream>
using namespace std;
class Base
{
public:
  int func(int x)             //不是虚函数;
  {
  cout << "This is Base class";
  return x;
  }
};
class Subclass :public Base
{
public:
  virtual int func(int x)              //虚函数;
  {
  cout << "This is Subclass class";
  return x;
  }
};
class Subclass2 :public Subclass
{
public:
  int func(int x)                //自动成为虚函数;
  {
  cout << "This is Baseclass2 class";
  return x;
  }
};
int main()
{
  Subclass2 sc2;
  Base& bc = sc2;
  cout << "x=" << bc.func(5) << endl;
  Subclass& sc = sc2;
  cout << "x=" << sc.func(5) << endl;
  return 0;
}


0f15a4b97a90b422143c42434a79c638_fe5418727bbd40a9a1e10b5dfeff3e63.png


从结果看出,func()的操作贝莱你白难道Subclasses类中,显然进行的是动态联编。

相关文章
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
68 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
50 13
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
50 5
|
1月前
|
存储 C++
【C++面向对象——输入输出流】处理二进制文件(头歌实践教学平台习题)【合集】
本任务要求使用C++读取二进制文件并在每行前添加行号后输出到控制台。主要内容包括: 1. **任务描述**:用二进制方式打开指定文件,为每一行添加行号并输出。 2. **相关知识**: - 流类库中常用的类及其成员函数(如`iostream`、`fstream`等)。 - 标准输入输出及格式控制(如`cin`、`cout`和`iomanip`中的格式化函数)。 - 文件的应用方法(文本文件和二进制文件的读写操作)。 3. **编程要求**:编写程序,通过命令行参数传递文件名,使用`getline`读取数据并用`cout`输出带行号的内容。 4. **实验步骤**:参考实验指
38 5
|
1月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
40 5
|
1月前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
48 4
|
1月前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
32 3
|
4月前
|
算法 数据挖掘 Shell
「毅硕|生信教程」 micromamba:mamba的C++实现,超越conda
还在为生信软件的安装配置而烦恼?micromamba(micromamba是mamba包管理器的小型版本,采用C++实现,具有mamba的核心功能,且体积更小,可以脱离conda独立运行,更易于部署)帮你解决!
121 1
|
4月前
|
安全 程序员 编译器
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则
112 11
|
4月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
125 1