C++中的虚函数(virtual function)

限时加码!20+主流AI编程工具免费用 购周边加赠Coding Plan Lite,Claude Code、Cursor等即刻畅享,学习进阶更高效! 阅读详情
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它”。

C++中重载(overload)、重写(override,也叫做“覆盖”)和重定义(redefine,也叫作“隐藏”)的区别? 指派生类的函数屏蔽了与其同名的基类函数。特点:①函数名称相同,但是返回值可以不同。②作用域分别位于基类和派生类之中;①如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。②如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。所以,此处只要求是同名函数,不管参数列表是否相同,基类函数都会被隐藏。 阅读详情

相关推荐

基函数析构函数virtual

第一种代码:基函数析构函数无virtual #include<iostream> class A { public: A() { printf(" 创建A \n"); } ~A() { printf(" 销毁A \n"); } // 增加了虚拟关键字 }; class B : public A { public: B() { printf("创建B \...

算法攻城狮 444

C++中重载和重写的区别

重载 是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。 #include<bits/stdc++.h> using namespace std; class A { void fun() {}; void fun(int i) {}; void fun(int i, int j) {}...

菜鸡工坊 1万+

C++重写和重载的区别

重写就是 C:\Program Files\MSBuild\Microsoft.Cpp\v4.0\Microsoft.CppCommon.targets(393,5): error MSB6006: “rc.exe”已退出,代码为 1。

喜欢吃一口烤肉的啵啵 2107

C++ 虚函数 (virtual function)

C++ 虚函数 (virtual function). 虚拟函数是什么, 虚拟函数的使用方法.

我是小白呀的博客 8660

C++ Virtual Function 虚函数

C++ Virtual Function 虚函数 虚函数允许我们重写(override)子类中的方法。 如果我们有两个类 A 和 B, 其中 B 是 A 的子类。如果我们在 A 中创建一个方法,并将其标记为 virtual, 则我们可以在 B 中重写该方法来让它完成一些别的操作。 举例说明: #include <iostream> #include <string> cl...

weixin_44387066的博客 389

C++ 虚函数 (virtual function) 介绍

C++ 虚函数和纯虚函数介绍

qq_43799400的博客 7099

C++虚函数 Virtual Function

显式标记派生类中对基类虚函数的重写,强制编译器检查函数签名是否完全匹配,避免因拼写错误或参数不匹配导致意外定义新函数。修饰虚函数:禁止派生类进一步重写该函数(如关键算法或接口)。修饰类:禁止类被继承(如工具类或不可变类)。结尾,强制派生类必须实现。

aningxiaoxixi的博客 477

C++ Pure Virtual Function虚函数

C++ Virtual Function虚函数虚函数的特例。 抽象函数和接口都是纯虚函数。 纯虚函数允许我们在基类里定义一个方法,该方法没有任何的实现,而继承该基类的子类则会具体实现这个方法。 对比于虚函数而言,虚函数在基类中也拥有实现过程,如果子类并没有重写该方法,则依然可以调用基类的方法。而纯虚函数由于基类方法并没有实现过程,所以子类必须要重写该方法并为其添加具体的实现过程。 接口类仅仅...

weixin_44387066的博客 916

C++ 虚函数 (virtual function) - 纯虚函数 (pure virtual function)

C++ 虚函数 (virtual function) - 纯虚函数 (pure virtual function) C++ 三大特性:封装、继承和多态。 1. 虚函数 C++ 虚函数的主要作用是运行时多态,父类中提供虚函数的实现,为子类提供默认的函数实现。子类可以重写父类的虚函数实现子类的特殊化,也可以不重写父类的虚函数。 2. 纯虚函数 C++ 中包含纯虚函数的类,被称为是抽象类。抽象类不能使用 new 实例化对象,只有实现了纯虚函数的子类才能 new 实例化对象。C++ 中的虚函数“只提供申明,没有实

程永强 1172

C++虚函数 Virtual Function

virtual有两个用处。  一个是最常见的: virtual关键字修饰类的成员函数。另一个是虚基类机制,用于多重继承。在定义派生类时,要在基类描述前加关键字virtual。避免内存中父虚基类成员的重复放置。 虚函数: 定义:虚函数必须是类的 非静态成员函数 且 非构造函数,其访问权限是public(可以定义为private or proteceted,但是对于多

LUCKYOJ 1231
上一篇: [转载]Const用法小结
下一篇: [转载]编程入门:浅谈C语言的可变参数
csdboy
博客等级 码龄25年 0粉丝 19原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值