c++纯虚函数与虚函数的区别

作者总结:

1.纯虚函数声明如下: virtual ReturnType FunctionName(Parameter)=0;

 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数

 的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。

2.虚函数声明如下:virtual ReturnType FunctionName(Parameter);

 虚函数必须实现,如果不实现,编译器将报错,错误提示为:

 error LNK****: unresolved external symbol "public: virtual void __thiscall

 ClassName::virtualFunctionName(void)"

3.对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。

4.实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖

 该虚函数,由多态方式调用的时候动态绑定。

5.虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的

 函数

6 在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。

7 友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友

 元函数调用虚拟成员函数来解决友元的虚拟问题。

析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对

 象,将调用子类的析构函数,然后自动调用基类的析构函数。

 例如:

 Father *fp = new Son(); //合法的,无论父类是否为抽象类,即使是具体类,也可以指向子类。

 ...

 delete *fp; //是调用~Father()还是~Son() ???

 如果使用默认的静态联编,则delete语句调用~Father()析构,这将释放由Son对象中的Father部分指

 向的内存,但不会释放新的类成员指向的内存。

 但如果析构函数是虚拟的, 则上面代码会先调用~Son()释放由Son组件指向的内容,然后自动调用

 ~Father()析构函数来释放由Father组件指向的内存。

 这意味着:即使基类不需要显示析构函数提供服务,也不应该依赖于默认构造函数,而应当提供虚拟析

 构函数,即使他不执行任何操作: virtual ~BaseClass(){};

注:多态方式调用是指用父类的指针或引用来指向子类的实例后,用父类的指针或引用来调用虚函数.

 即:如果使用指向对象的引用或指针来调用虚拟方法,程序将使用为对象类型定义的方法,而

 不使用为引用或指针类型定义的方法。也称作动态联编或晚期绑定。

参考引用

<C++实践系列>C++中的虚函数(virtual function)
作者:张笑猛
原文出处:http://objects.nease.net/


1.简介

假设我们有下面的类层次:

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它”。

4.参考资料

[1] 深度探索C++对象模型,Stanley B.Lippman,侯捷译

[2] Design Patterns, Elements of Reusable Object-Oriented Software, GOF

<think>好的,我现在需要详细介绍一下C++中的纯虚函数虚函数。首先,我得回忆一下这两个概念的基本定义和区别虚函数(Virtual Function)是在基类中使用virtual关键字声明的成员函数,允许在派生类中被重写(override)。当通过基类指针或引用调用虚函数时,实际调用的是对象的动态类型对应的函数版本,这就是多态性的体现。例如,基类Animal有一个虚函数speak(),派生类Dog和Cat分别重写这个函数,那么通过Animal指针调用speak()会根据实际对象类型执行不同的实现。 然后,纯虚函数(Pure Virtual Function)是在基类中声明但没有实现的虚函数,通过语法`virtual 返回类型 函数名(参数列表) = 0;`来定义。包含纯虚函数的类称为抽象类(Abstract Class),不能实例化,只能作为接口供派生类继承。派生类必须实现所有的纯虚函数,否则它们也会成为抽象类。例如,基类Shape有一个纯虚函数draw(),派生类Circle和Rectangle必须实现draw()才能被实例化。 接下来,我需要比较两者的区别虚函数可以有默认实现,而纯虚函数没有,必须由派生类实现。含有纯虚函数的类是抽象类,不能创建对象,而含有虚函数的类仍然可以实例化,除非虚函数被声明为纯虚函数。 还要考虑使用场景。虚函数用于实现运行时多态,允许基类提供默认行为,派生类可以选择是否覆盖。纯虚函数则用于定义接口,强制派生类实现特定方法,确保所有子类都有该功能。 可能需要注意的细节包括:构造函数不能是虚函数,而析构函数通常应该是虚函数,尤其是在有继承关系的情况下,以确保正确释放资源。此外,C++11引入了override和final关键字,增强了对虚函数重写的安全性和控制。 需要验证一下纯虚函数是否有例外情况。比如,纯虚函数在基类中可以有定义吗?虽然语法上可以提供一个实现,但派生类仍然必须重写它,调用时需要明确指定基类作用域。这可能在某些设计模式中有用,但比较少见。 最后,总结两者的作用和重要性,强调它们在实现多态和接口设计中的关键角色。可能需要给出简单的代码示例,比如基类定义虚函数纯虚函数,派生类继承并实现它们,展示多态的行为。</think>以下是C++虚函数纯虚函数的对比详解: 一、核心概念 1. 虚函数(Virtual Function) - 声明方式:基类中用 virtual 关键字修饰 - 特性:允许派生类重写(override)成员函数 - 调用机制:运行时根据对象实际类型动态绑定(动态多态) - 可实例化性:包含虚函数的类可以实例化 2. 纯虚函数(Pure Virtual Function) - 声明方式:在虚函数声明后加 = 0 - 特性:强制派生类必须实现的接口规范 - 类特性:使所在类成为抽象类(不能实例化) - 语法示例:virtual void draw() = 0; 二、核心差异对比表 | 特性 | 虚函数 | 纯虚函数 | |---------------------|-----------------------|--------------------------| | 实现要求 | 基类可提供默认实现 | 基类无实现,必须由派生类实现 | | 类实例化能力 | 允许实例化 | 使类变为抽象类,禁止实例化 | | 派生类强制实现要求 | 可选择性重写 | 必须实现所有纯虚函数 | | 典型用途 | 提供可扩展的默认行为 | 定义接口规范 | 三、使用场景分析 1. 虚函数典型应用场景: ```cpp class Animal { public: virtual void speak() { // 可被子类覆盖的默认实现 std::cout << "Animal sound" << std::endl; } }; class Dog : public Animal { public: void speak() override { // 选择性覆盖基类实现 std::cout << "Woof!" << std::endl; } }; ``` 2. 纯虚函数典型应用场景: ```cpp class Shape { // 抽象基类 public: virtual void draw() = 0; // 强制接口规范 }; class Circle : public Shape { public: void draw() override { // 必须实现的接口 std::cout << "Drawing circle" << std::endl; } }; ``` 四、关键注意事项 1. 虚析构函数原则:当类存在虚函数时,应声明虚析构函数 ```cpp class Base { public: virtual ~Base() = default; // 虚析构函数确保正确释放资源 }; ``` 2. 现代C++特性(C++11+): - 使用override显式标记重写(提高代码安全性) ```cpp class Derived : public Base { public: void func() override; // 明确表示重写基类虚函数 }; ``` 3. 特殊用法:纯虚函数可提供实现(需通过基类限定符调用) ```cpp class Interface { public: virtual void api() = 0; }; // 纯虚函数的实现(不常见但合法) void Interface::api() { std::cout << "Default implementation" << std::endl; } class Impl : public Interface { public: void api() override { Interface::api(); // 显式调用基类实现 // 添加具体实现... } }; ``` 五、设计模式中的应用 1. 虚函数:模板方法模式(基类定义算法框架,子类重写具体步骤) 2. 纯虚函数:策略模式(定义统一接口,具体策略强制实现) 理解这两个概念的区别应用场景,是掌握C++面向对象设计和多态编程的关键基础。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值