虚函数是动态绑定的基础,必须是非静态的成员函数

1、一般虚函数

1.1 引例

程序 运行结果&解释
#include <iostream>
using namespace std; class Base1
{
public:
void display() const;//这个不是虚函数
};
class Base2 : public Base1
{
public:
void display() const;
};
class Derived : public Base2
{
public:
void display() const;
};
void Base1::display() const
{
cout << "Base1::display()" << endl;
}
void Base2::display() const
{
cout << "Base2::display()" << endl;
}
void Derived::display() const
{
cout << "Derived::display()" << endl;
}
void fun(Base1 *p)
{
p->display();
}
int main()
{
Base1 b1;
Base2 b2;
Derived d1;
fun(&b1);
fun(&b2);
fun(&d1);
return 0;
}

运行结果:

Base1::display()
Base1::display()
Base1::display()

解释:

使用对象名,绑定发生在编译过程中。

根据类型兼容性规则,派生类对象的地址会被转换为指向基类的指针,fun中的p->display()会执行基类中的display()

/*
8-4 P316
虚函数成员
*/
#include <iostream>
using namespace std; class Base1
{
public:
virtual void display() const; //这个函数是虚函数,可多态
};
class Base2 : public Base1
{
public:
void display() const;
};
class Derived : public Base2
{
public:
void display() const;
};
void Base1::display() const
{
cout << "Base1::display()" << endl;
}
void Base2::display() const
{
cout << "Base2::display()" << endl;
}
void Derived::display() const
{
cout << "Derived::display()" << endl;
}
void fun(Base1 *p)
{
p->display();
}
int main()
{
Base1 b1;
Base2 b2;
Derived d1;
fun(&b1);
fun(&b2);
fun(&d1);
return 0;
}

运行结果:

Base1::display()
Base2::display()
Derived::display()

解释:

fun()中的实参p绑定到了不同子类的指针,程序知道这一点,故执行各子类的display(),实现了多态(运行时)

1.2 一般虚函数

1.2.1 什么是一般虚函数?

  1. 首先需要在基类中将这个同名函数声明为虚函数,这样通过基类类型的指针(引用)就可以使属于不同派生类的不同对象产生不同的行为,实现运行过程的多态
  2. 虚函数声明只能出现在类定义中的函数原型声明中,而不能出现在成员函数
    实现的时候!(一般不声明为内联函数)
  3. 虚函数成员语法:virtual 函数类型 函数名(形参表);

1.2.2 运行时多态需满足三个条件:

  1. 类之间满足兼容规则
  2. 声明虚函数
  3. 成员函数来调用,或者通过指针引用访问虚函数

1.2.3 系统如何判断派生类的函数成员是否为虚函数?

  1. 该函数是否与基类的虚函数有相同的名称
  2. 该函数是否与基类的虚函数有相同的参数个数及相同的对应数类型?(包括const)
  3. 该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型返回值

1.2.4 一般虚函数的补充说明

  1. 只有虚函数是动态绑定的(如果派生类需要重写与基类函数同名的函数时,应该在基类中将相应的函数声明为虚函数)
  2. 基类中声明的非虚函数,通常代表那些不希望被派生类修改的函数,是不能实现多态的。
  3. 在重写继承来的虚函数时,如果函数有缺省值,不要重新定义不同的值(虚函数是动态绑定的,但缺省形参值是静态绑定的,缺省形参值只能来自基类的定义
  4. 只有通过基类指针或者引用调用虚函数时,才会发生动态绑定!(基类的指针可以指向派生类的对象,基类的引用可以作为派生类对象的别名,但基类的对象不能表示派生类的对象)
    Derived d;
    Base *ptr=&d; //基类的指针ptr可以指向派生类的对象
    Base &ref=d; //基类的引用ref可以作为派生类对象的别名
    Base b=d; //调用Base1的复制构造函数用d构造b,b的类型是Base而非Derived
  5. 不能声明虚构造函数,但可以声明虚析构函数(保证使用基类类型的指针就能够调用适当的析构函数针对不同的对象进行清理工作)

2、虚析构函数

2.1 引例

程序 运行结果&解释
#include <iostream>
using namespace std;
class Base
{
public:
~Base(); //不是虚函数
};
Base::~Base()
{
cout << "Base destructor" << endl;
}
class Derived : public Base
{
public:
Derived();
~Derived(); //不是虚函数
private:
int *p;
}; Derived::Derived()
{
p = new int(0);
}
Derived::~Derived()
{
cout << "Derived destructor" << endl;
delete p;
}
void fun(Base *b)
{
delete b; //静态绑定,只会调用~Base()
}
int main()
{
Base *b = new Derived();
fun(b);
return 0;
}

运行结果:

Base destructor

解释:

此时通过基类指针删除派生类对象时调用的是基类的析构函数,派生类的析构函数没有被执行,因此派生类对象中动态分配的内存空间没有被释放,造成了内存泄露。

对于长期运行的程序来说,这是非常危险的!

/*
8-5 P320
虚析构函数举例
*/
#include <iostream>
using namespace std;
class Base
{
public:
// ~Base(); //不是虚函数
virtual ~Base(); //是虚函数
};
Base::~Base()
{
cout << "Base destructor" << endl;
}
class Derived : public Base
{
public:
Derived();
// ~Derived(); //不是虚函数
virtual ~Derived(); //是虚函数,基类虚函数要加virtual,派生类的函数不加virtual也行
private:
int *p;
}; Derived::Derived()
{
p = new int(0);
}
Derived::~Derived()
{
cout << "Derived destructor" << endl;
delete p;
}
void fun(Base *b)
{
delete b; //静态绑定,只会调用~Base()
}
int main()
{
Base *b = new Derived();
fun(b);
return 0;
}

运行结果:

Derived destructor
Base destructor

解释:

将基类析构析构函数声明为虚函数,在函数fun()中会执行派生类的析构函数,派生类中动态申请的内存空间被释放了。实现了多态。

2.2 虚析构函数的说明

  1. 如果你打算允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),就需要让基类的析构函数成为虚函数,否则执行delete的结果是不确定的。
  2. 派生类中如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的,但是该函数与虚函数是相互独立的,派生类中的函数并没有覆盖掉基类的版本

2.3 override说明符

override与final都不是语言关键字(keyword),只有在特定的位置才有特别含意,其他地方仍旧可以作为一般标识符(identifier)使用。

  1. 用于声明显式函数覆盖。用于在编译期间发现未覆盖的错误
  2. 运用显式覆盖,编译器会检查派生类中声明overrid的函数,在基类中是否存在可被覆盖的虚函数,若不存在,则会报错。
  3. 作用:
    为了使派生类能覆盖基类的虚函数,但是容易弄错了参数列表,导致无法完成目标。
    想通过调试发现此类错误十分困难,故使用override(C++11),在编译时编译器就会发现此类错误

例:

class Base
{
public:
virtual void f1(int) const;
virtual void f2();
void f3();
};
class Derived1 : public Base
{
public:
void f1(int) const override; //正确,f1与基类中的f1匹配
void f2(int) override; //错误,基类中没有形如f2(int)的函数
void f3() override; //错误,f3不是虚函数
void f4() override; //错误,基类中没有名为f4的函数
};

2.4 final说明符

  1. 用来避免类被继承,或是避免基类的函数被覆盖
class Base1 final
{
};
class Derived1 : Base1// 编译错误:Base1为final,不允许被继承
{
};
class Base2
{
virtual void f() final;
};
class Derived2 : Base2
{
void f(); // 编译错误:Base2::f 为final,不允许被覆盖
};

3、纯虚函数,抽象类

3.1 纯虚函数(了解概念即可)

纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本

纯虚函数的声明格式为:

virtual 函数类型 函数名(参数表) = 0;

3.2 抽象类(不k)

带有纯虚函数的类称为抽象类

class 类名
{
virtual 函数类型 函数名(参数表) = 0;
//其他成员……
};

作用:

  1. 抽象类为抽象设计的目的而声明
  2. 将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为
  3. 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现

注意:

  1. 抽象类只能作为基类来使用
  2. 不能定义抽象类的对象
  3. 可以定义抽象类的指针或者引用(从而运用虚函数多态)

3.3 例8-6

/*
8-6 P323
抽象类举例
*/
#include <iostream>
using namespace std; class Base1 //基类Base1定义
{
public:
virtual void display() const = 0; //纯虚函数
}; class Base2 : public Base1 //公有派生类Base2定义
{
public:
void display() const; //覆盖基类纯虚函数
};
void Base2::display() const
{
cout << "Base2::display" << endl;
} class Derived : public Base2 //公有派生类Derived定义
{
public:
void display() const;
};
void Derived::display() const
{
cout << "Derived::display" << endl;
}
void fun(Base1 *ptr)
{
ptr->display();
}
int main()
{
// Base1 b1;//错误,声明基类对象
Base1 *p; //正确,声明基类指针
Base2 b2; //声明派生类对象
Derived d1; //声明派生类对象 p = &b2;
fun(p); //调用派生类Base2函数成员
p = &d1;
fun(p); //调用派生类Derived函数成员 return 0;
}

运行结果:

Base2::display
Derived::display

参考:

C++语言程序设计(第5版),郑莉,清华大学

【C++复习】第八章 多态性(2)(虚函数,纯虚函数)的更多相关文章

  1. 虚函数&纯虚函数&抽象类&虚继承

    C++ 虚函数&纯虚函数&抽象类&接口&虚基类   1. 多态 在面向对象语言中,接口的多种不同实现方式即为多态.多态是指,用父类的指针指向子类的实例(对象),然后通过 ...

  2. 【转】C++ 虚函数&纯虚函数&抽象类&接口&虚基类

    1. 动态多态 在面向对象语言中,接口的多种不同实现方式即为多态.多态是指,用父类的指针指向子类的实例(对象),然后通过父类的指针调用实际子类的成员函数. 多态性就是允许将子类类型的指针赋值给父类类型 ...

  3. C++ 虚函数&纯虚函数&抽象类&接口&虚基类(转)

    http://www.cnblogs.com/fly1988happy/archive/2012/09/25/2701237.html 1. 多态 在面向对象语言中,接口的多种不同实现方式即为多态.多 ...

  4. C++基础知识 基类指针、虚函数、多态性、纯虚函数、虚析构

    一.基类指针.派生类指针 父类指针可以new一个子类对象 二.虚函数 有没有一个解决方法,使我们只定义一个对象指针,就可以调用父类,以及各个子类的同名函数? 有解决方案,这个对象指针必须是一个父类类型 ...

  5. C++学习基础十二——纯虚函数与抽象类

    一.C++中纯虚函数与抽象类: 1.含有一个或多个纯虚函数的类成为抽象类,注意此处是纯虚函数,而不是虚函数. 2.如果一个子类继承抽象类,则必须实现父类中的纯虚函数,否则该类也为抽象类. 3.如果一个 ...

  6. c++虚函数,纯虚函数,抽象类,覆盖,重载,隐藏

    C++虚函数表解析(转) ——写的真不错,忍不住转了  http://blog.csdn.net/hairetz/article/details/4137000 浅谈C++多态性  http://bl ...

  7. C++学习笔记(十二):类继承、虚函数、纯虚函数、抽象类和嵌套类

    类继承 在C++类继承中,一个派生类可以从一个基类派生,也可以从多个基类派生. 从一个基类派生的继承称为单继承:从多个基类派生的继承称为多继承. //单继承的定义 class B:public A { ...

  8. C++ Primer--虚函数与纯虚函数的区别

    首先:强调一个概念 定义一个函数为虚函数,不代表函数为不被实现的函数. 定义他为虚函数是为了允许用基类的指针来调用子类的这个函数. 定义一个函数为纯虚函数,才代表函数没有被实现. 定义纯虚函数是为了实 ...

  9. C++:抽象基类和纯虚函数的理解

    转载地址:http://blog.csdn.net/acs713/article/details/7352440 抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层. ...

  10. c++ 多态,虚函数、重载函数、模版函数

    c++三大特性:封装.继承.多态.封装使代码模块化,继承扩展已存在的代码,多态的目的是为了接口重用 虚函数实现:虚函数表:指针放到虚函数表 多态:同名函数对应到不同的实现 构造父类指针指向子类的对象 ...

随机推荐

  1. R Works with Google Earth Engine - Installation 【rgee - 安装问题解决集锦】

    Date : 2022/04/24 Intallation Tutorial - Reference : Introduction to rgee (r-project.org) Prerequisi ...

  2. C++程序设计实验二 数组、指针与C++标准库

    Info.hpp文件源码 #include<iostream> #include<string> #include<iomanip> using namespace ...

  3. ubuntu18 电脑重启后登录后无法进入桌面

    ubuntu18 电脑重启后登录后无法进入桌面 应该是ubuntu桌面管理器gdm3和nvidia驱动冲突导致的 解决办法: 首先卸载已有的nvidia驱动 注意:在下载完驱动后,此时电脑没有驱动文件 ...

  4. 在windows服务中托管asp.net.core

    参考:https://docs.microsoft.com/zh-cn/aspnet/core/host-and-deploy/windows-service?view=aspnetcore-3.1& ...

  5. AWG含义及尺寸电流对照表-转载

    AWG含义及尺寸电流对照表 - 麦穗鱼~ - 博客园 (cnblogs.com) AWG(American wire gauge)美国线规,是一种区分导线直径的标准,又被称为 Brown & ...

  6. JS 开始时间/结束时间和当前时间进行比较

    项目需求:到截止日期一些功能不可以再使用,那么需要判断当前时间与截止时间进行比较,记录一下吧 注意: 1.橙色字体的代码换成你自己的变量 2. .valueOf()其实就是将中国时间转为时间戳 3.截 ...

  7. vxWidgets(一):初识

    wxWidgets 和 QT 之间的选择 跨平台的C++ GUI工具库很多,可是应用广泛的也就那么几个,Qt.wxWidgets便是其中的翘楚这里把GTK+排除在外,以C实现面向对象,上手相当困难,而 ...

  8. 备份docker mysql数据库

    1.查看容器 docker ps 2.进入容器 docker exec -it 容器id /bin/bash 3.备份数据 mysqldump 数据库名称 -uroot -p >/root/** ...

  9. OS-lab3

    OS-lab3 lab2之后,我们能够通过MMU访问内存了,不过操作系统最重要的是能够让进程运行. include env.h 定义了进程控制相关的变量,如进程数量NENV.进程状态ENV_FREE等 ...

  10. Vue: 单页面应用如何保持登录状态

    这篇文章写的还可以 https://www.xiabingbao.com/post/vue/vue-keep-logininfo.html