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

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. VUE学习-自定义指令

    自定义指令 有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令. <div id="directive-demo"> <input ...

  2. js区分图片加载中和加载完成状态

    var _ent = document.getElementById("test"); if (_ent.complete) { //图片已经加载完成 _ent.stop(); } ...

  3. 卸载K8s集群及k8s命令自动补全

    一.配置命令自动补全 yum install -y bash-completion source /usr/share/bash-completion/bash_completion source & ...

  4. linux端口探测

    一.常用命令 1.测试端口是否能通(已有服务) 命令:nc -vz -w 2 10.0.1.161 9999 说明:-v可视化,-z扫描时不发送数据,-w超时几秒,后面跟数字 2.测试端口是否能通(没 ...

  5. StunServer

    Stun 服务器 npm下载stun包 npm i stun -s google stun服务器 google的stun的服务器一般国内访问较慢,所以一般自己搭建一个服务器 const stun = ...

  6. springMVC学习day02

    了解springMVC 1. 了解官网 1.  首先到spring地址去,然后选择项目下面的任何一个子项目,我选择spring framework https://spring.io/ 2.选择spr ...

  7. vue 简单原理

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  8. Nginx教程由浅入深

    Nginx   一.安装Nginx 1.准备工作 (1)打开虚拟机,使用远程连接工具连接 linux 操作系统 (2)到 nginx 官网下载软件 http://nginx.org/ 2.开始进行 n ...

  9. Postman中添加多个Cookie

    在接口测试中,很多接口都是需要登录后才能获取到数据的.如何标识登录状态呢?有些app用token,有些app用Cookie.通过Fiddler抓包看到,我涯使用的是Cookie的方式,而且是有多个Co ...

  10. 解决Google翻译不能用的问题

    解决Google翻译不能用的问题   1.打开C:\Windows\System32\drivers\etc\hosts 2.在hosts后面加入 203.208.40.66 translate.go ...