C++中重载与重写函数区别及虚函数(转载)

2023-05-16

C++中重载与重写函数区别及虚函数 

C++中的虚函数(virtual function)
1.简介
虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次:

class A
{
public:
virtual void foo() { cout << "A::foo() is called" << endl;}
};

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

那么,在使用的时候,我们可以:

A * a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!

这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。

虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是多态的:

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

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

void bar()
{
A a;
a.foo(); // A::foo()被调用
}

1.1 多态
在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上面的类层次,但是使用的方法变的复杂了一些:

void bar(A * a)
{
a->foo(); // 被调用的是A::foo() 还是B::foo()?
}

因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则B::foo()被调用。

这种同一代码可以产生不同效果的特点,被称为“多态”。

1.2 多态有什么用?
多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的C++教程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的用途,我就不再重复这个例子了,如果你不知道这个例子,随便找本书应该都有介绍。我试图从一个抽象的角度描述一下,回头再结合那个画图的例子,也许你就更容易理解。

在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类),都需要使用者“知道”(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程序中的“bad smell”之一。

多态可以使程序员脱离这种窘境。再回头看看1.1中的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要“知道”(修改)。这完全归功于多态--编译器针对虚函数产生了可以在运行时刻确定被调用函数的代码。

1.3 如何“动态联编”
编译器是如何针对虚函数产生可以再运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在深度探索C++对象模型[1]中的不同章节讲到了几种方式,这里把“标准的”方式简单介绍一下。

我所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写,针对1.1中的例子:

void bar(A * a)
{
a->foo();
}

会被改写为:

void bar(A * a)
{
(a->vptr[1])();
}

因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。

虽然实际情况远非这么简单,但是基本原理大致如此。

1.4 overload和override
虚函数总是在派生类中被改写,这种改写被称为“override”。我经常混淆“overload”和“override”这两个单词。但是随着各类C++的书越来越多,后来的程序员也许不会再犯我犯过的错误了。但是我打算澄清一下:

override是指派生类重写基类的虚函数,就象我们前面B类中重写了A类中的foo()函数。重写的函数必须有一致的参数表和返回值(C++标准允许返回值不同的情况,这个我会在“语法”部分简单介绍,但是很少编译器支持这个feature)。这个单词好象一直没有什么合适的中文词汇来对应,有人译为“覆盖”,还贴切一些。
overload约定成俗的被翻译为“重载”。是指编写一个与已有函数同名但是参数表不同的函数。例如一个函数即可以接受整型数作为参数,也可以接受浮点数作为参数。
2. 虚函数的语法
虚函数的标志是“virtual”关键字。

2.1 使用virtual关键字
考虑下面的类层次:

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

class B: public A
{
public:
void foo(); // 没有virtual关键字!
};

class C: public B // 从B继承,不是从A继承!
{
public:
void foo(); // 也没有virtual关键字!
};

这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。

2.2 纯虚函数
如下声明表示一个函数为纯虚函数:

class A
{
public:
virtual void foo()=0; // =0标志一个虚函数为纯虚函数
};

一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。

2.3 虚析构函数
析构函数也可以是虚的,甚至是纯虚的。例如:

class A
{
public:
virtual ~A()=0; // 纯虚析构函数
};

当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:

class A
{
public:
A() { ptra_ = new char[10];}
~A() { delete[] ptra_;} // 非虚析构函数
private:
char * ptra_;
};

class B: public A
{
public:
B() { ptrb_ = new char[20];}
~B() { delete[] ptrb_;}
private:
char * ptrb_;
};

void foo()
{
A * a = new B;
delete a;
}

在这个例子中,程序也许不会象你想象的那样运行,在执行delete a的时候,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用!这是否有点儿可怕?

如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。

纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。

2.4 虚构造函数?
构造函数不能是虚的。

3. 虚函数使用技巧 3.1 private的虚函数
考虑下面的例子:

class A
{
public:
void foo() { bar();}
private:
virtual void bar() { ...}
};

class B: public A
{
private:
virtual void bar() { ...}
};

在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发生B::bar()对A::bar()的override不起作用的情况。

这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。

3.2 构造函数和析构函数中的虚函数调用
一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。例如:

class A
{
public:
A() { foo();} // 在这里,无论如何都是A::foo()被调用!
~A() { foo();} // 同上
virtual void foo();
};

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

void bar()
{
A * a = new B;
delete a;
}

如果你希望delete a的时候,会导致B::foo()被调用,那么你就错了。同样,在new B的时候,A的构造函数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()。

3.3 多继承中的虚函数 3.4 什么时候使用虚函数
在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。

以设计模式[2]中Factory Method模式为例,Creator的factoryMethod()就是虚函数,派生类override这个函数后,产生不同的Product类,被产生的Product类被基类的AnOperation()函数使用。基类的AnOperation()函数针对Product类进行操作,当然Product类一定也有多态(虚函数)。

另外一个例子就是集合操作,假设你有一个以A类为基类的类层次,又用了一个std::vector<A *>来保存这个类层次中不同类的实例指针,那么你一定希望在对这个集合中的类进行操作的时候,不要把每个指针再cast回到它原来的类型(派生类),而是希望对他们进行同样的操作。那么就应该将这个“一样的操作”声明为virtual。

现实中,远不只我举的这两个例子,但是大的原则都是我前面说到的“如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的”。这句话也可以反过来说:“如果你发现基类提供了虚函数,那么你最好override它”。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++中重载与重写函数区别及虚函数(转载) 的相关文章

随机推荐

  • c++全局变量的正确声明

    C 43 43 中全局变量声明错误情况 造成 LNK2005 错误主要有以下几种情况 xff1a 1 xff0e 重复定义全局变量 可能存在两种情况 xff1a A 对于一些初学编程的程序员 xff0c 有时候会以为需要使用全局变量的地方就
  • C++函数声明和定义

    在上一节 xff0c 我们已经学会了如何阅读函数原型和如何调用一个函数 然而 xff0c 仅靠系统给出的标准库函数是不够用的 我们有时候要根据我们的实际要求 xff0c 写出一个合适自己使用的函数 那么 xff0c 我们如何来自己动手编写一
  • cpp之间函数引用和类引用的方法

    除了 include外 xff0c cpp文件之间怎么引用函数 file C h ifndef FOO H define FOO H extern void sayHello endif FOO H file B cpp include 3
  • 在C++中调用DLL中的函数

    1 dll 的优点 代码复用是提高软件开发效率的重要途径 一般而言 xff0c 只要某部分代码具有通用性 xff0c 就可将它构造成相对独立的功能模块并在之后的项目中重复使用 比较常见的例子是各种应用程序框架 xff0c ATL MFC 等
  • HC-SR04超级简单教程(快速入门)

    目录 一 模块介绍 xff08 个人理解 xff09 1 简单理解 2 该模块的参数 二 HC SR04的操作 三 代码 1 代码前的注意事项 2 关键代码 四 代码实战效果图 五 结束 一 模块介绍 xff08 个人理解 xff09 1
  • C++中对于类来说头文件(.h)和源文件(.cpp)都应该写些什么 (类的常规创建)

    头文件 h xff1a 写类的声明 xff08 包括类里面的成员和方法的声明 xff09 函数原型 define常数等 xff0c 但一般来说不写出具体的实现 在写头文件时需要注意 xff0c 在开头和结尾处必须按照如下样式加上预编译语句
  • c#的接口详解

    什么是接口 xff1f 其实 xff0c 接口简单理解就是一种约定 xff0c 使得实现接口的类或结构在形式上保持一致 个人觉得 xff0c 使用接口可以使程序更加清晰和条理化 xff0c 这就是接口的好处 xff0c 但并不是所有的编程语
  • C语言中的全局变量定义与使用

    1 书中对 全局变量 的定义 在函数内定义的变量是局部变量 xff0c 而在函数外定义的变量叫做外部变量 xff0c 外部变量是全局变量 全局变量可以为本文件中其他函数所共用它的有效范围从定义变量的位置开始到本源文件结束 建立全局变量的作用
  • C,C++,C#note

    1 c 43 43 中的类的定义和声明可以都写在头文件中 xff0c 然后cpp文件include头文件 xff1b 也可以声明在头文件 xff0c 定义在cpp文件 xff1b 或者所有声明和定义都放在cpp文件 xff1b 混写定义与声
  • 在C++中可以用3种方法访问一个字符串

    用字符数组存放一个字符串 例6 10 定义一个字符数组并初始化 xff0c 然后输出其中的字符串 ol class snippet num style margin top 0px margin bottom 0px margin left
  • 动态存储和静态存储区域区别

    动态存储方式 所谓动态存储方式是指在程序运行期间根据需要进行动态的分配存储空间的方式 动态存储变量是在程序执行过程中 xff0c 使用它时才分配存储单元 xff0c 使用完毕立即释放 典型的例子是函数的形式参数 xff0c 在函数定义时并不
  • c++中默认32位int类型转换截取高位部分

    int类型的变量固定为4个字节 xff0c 共32位 int类型变量初始值一般为0 xff0c 范围 2 31 2 31 1 即 2147483648 xff0c 2147483647 16 位系统下 xff0c int 是 2 个字节 x
  • c#-接口

    c 中的接口中必须是未实现的方法 xff0c 例如属性 xff0c 事件 xff0c 索引器 xff0c 一般函数等 除此之外不能有其他的成员 xff0c 类可以继承两个或两个以上的接口 xff0c 派生类中必须实现接口中的所有方法
  • C# SerialPort 读写三菱FX系列PLC

    1 xff1a 串口初始化 com 61 new SerialPort 34 COM3 34 9600 Parity Even 7 StopBits One 2 xff1a 打开关闭串口 if com IsOpen com Close co
  • c++中用new和不用new创建对象的本质区别

    1 xff1a 作用域不同 不用new xff1a 作用域限制在定义类对象的方法中 xff0c 当方法结束时 xff0c 类对象也被系统释放了 xff0c xff08 安全不会造成内存系统泄漏 xff09 用new xff1a 创建的是指向
  • ubuntu系统通过ifconfig查看eth0只有ipv6没有ipv4的解决方案

    只有inet6 addr 配置eth0 3 重新启动sudo etc init d networking restart IP地址出来了 看起来就正常 4 看其他资料 xff0c linux 43 QT4 0 xff0c 在这个开发板上的l
  • Action<T> 无参数委托详解

    C 委托Action Action lt T gt Func lt T gt Predicate lt T gt CLR环境中给我们内置了几个常用委托Action Action lt T gt Func lt T gt Predicate
  • 在C++里,有两种方法创建对象:

    方法一 xff1a ClassName object param 这样就声明了一个ClassName类型的object对象 xff0c C 43 43 会为它分配足够的存放对象所有成员的存储空间 注意 xff1a 为节省存储空间 xff0c
  • C++中引用(&)的用法和应用实例

    对于习惯使用C进行开发的朋友们 xff0c 在看到c 43 43 中出现的 amp 符号 xff0c 可能会犯迷糊 xff0c 因为在C语言中这个符号表示了取地址符 xff0c 但是在C 43 43 中它却有着不同的用途 xff0c 掌握C
  • C++中重载与重写函数区别及虚函数(转载)

    C 43 43 中重载与重写函数区别及虚函数 C 43 43 中的虚函数 virtual function 1 简介 虚函数是C 43 43 中用于实现多态 polymorphism 的机制 核心理念就是通过基类访问派生类定义的函数 假设我