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 友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友
元函数调用虚拟成员函数来解决友元的虚拟问题。
8 析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对
象,将调用子类的析构函数,然后自动调用基类的析构函数。
例如:
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它”。
分享到:
相关推荐
在C++编程语言中,虚基类、虚函数与纯虚函数是面向对象编程中用于实现多态性的重要概念。本文将深入解析这三个概念,并通过示例代码来具体阐述其用法与作用。 ### 虚基类 虚基类(Virtual Base Class)是用来解决...
在C++编程语言中,虚函数(Virtual Functions)和纯虚函数(Pure Virtual Functions)是面向对象编程的重要特性,它们都是实现多态性的方式,但各自有着不同的应用场景和功能特点。下面将详细解释这两个概念以及它们...
第三个:基类 派生类的关系 以及如何访问抽象类 纯虚函数 还应包含有对象成员(除了一般成员外) ----------------------------------------------------- 带有多对象成员的虚拟多继承 要求:1、虚函数 多态 多态表现...
Java与C++不同,它的所有非`final`方法默认都是虚函数。因此,Java中的多态性是内建的,无需显式声明`virtual`。这意味着在Java中,子类可以自由地覆盖父类的方法,即使父类没有声明`virtual`。 接下来,我们来看...
4. **接口与实现**:虚函数是既有接口又有默认实现,纯虚函数仅定义接口,不提供实现。 5. **继承关系**:子类继承虚函数后,可以不重写,而继承纯虚函数则必须重写,否则子类也将成为抽象类。 综上所述,虚函数和...
每个具有虚函数的类都有一个与之关联的虚函数表,其中包含了该类及其所有基类虚函数的地址。当通过指针调用虚函数时,编译器会查找该指针所指向对象的虚函数表,然后调用表中对应的函数地址。这个过程在运行时完成,...
Unity3d C# 虚函数 例子 虚函数是面向对象编程(OOP)语言中的一个重要概念,在 C# 语言中也广泛应用于实现多态性。虚函数顾名思义,是一个可以被重载的函数,它允许在继承关系中的子类重新实现父类中的函数,从而...
面向对象的编程中,虚函数和纯虚函数是实现多态性的重要机制。虚函数允许在运行时根据对象的实际类型动态地调用相应的函数版本,从而实现动态绑定。纯虚函数则是进一步强化这一特性的工具,它使得基类成为一个抽象类...
下面将详细介绍多态性的概念、虚函数以及纯虚函数与抽象类的概念和用法。 首先,多态性指的是相同的操作符或函数调用能够根据操作对象或调用上下文的不同而执行不同的行为。在C++中,多态性可以分为两类:静态多态...
总结一下,"纯虚函数接口 实现的dll动态链接库"是一种灵活的设计策略,它结合了C++的抽象类和DLL技术,实现了接口与实现的分离。抽象类定义了必须遵循的公共接口,而DLL提供了加载和调用这些接口的方法。客户端则...
纯虚函数是C++中一种特殊的虚函数,它在基类中声明但不提供具体的实现,其主要目的是为派生类提供一个必须重写的接口。通过在基类中声明纯虚函数,可以确保所有派生类都必须实现该函数,从而更好地支持多态性和接口...
在这个程序中,“利用虚函数计算各种图形面积”是核心功能,涉及到的主要知识点包括虚函数、基类与派生类、多态以及不同几何图形的面积计算。 首先,我们要理解虚函数的概念。在C++中,虚函数是基类声明的一种成员...
派生类的虚函数与基类中对应的虚函数具有相同的参数个数和类型。 5. 实现动态联编: 要实现动态联编,必须使用基类指针或对象引用来调用虚函数。 6. 抽象类的性质: 抽象类的性质是不能直接创建对象,属于抽象类...
1. 从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有...
1. **纯虚函数**:如果一个基类只声明不定义虚函数,那么这个函数就是纯虚函数,用`= 0`表示。例如: ```cpp class Base { public: virtual void print() = 0; }; ``` 这样的类不能被实例化,但可以作为其他...
1. **接口设计**:定义纯虚函数(`= 0`)的基类可以作为接口,允许不同的派生类实现具体功能。 2. **多态行为**:基类指针可以调用子类的虚函数,使得代码可以处理多种类型的对象而不需了解它们的具体类型。 3. **...
《C++中的虚函数与多态性》 在C++编程语言中,虚函数和多态性是面向对象设计的重要概念,它们允许程序员创建灵活且可扩展的代码结构。本章将深入探讨这两个关键特性。 4.1 静态联编 静态联编,又称为早期匹配,是...
在没有覆盖父类虚函数的情况下,子类的虚函数表会包含父类的虚函数指针,而子类新增的虚函数则添加到表的后面。如果有覆盖,子类的虚函数表将替换掉相应父类的虚函数指针,确保调用到的是子类的实现。 例如,如果有...
除了普通虚函数,C++还支持纯虚函数,通过在函数声明后加上`= 0`来声明。例如: ```cpp class Interface { public: virtual void doSomething() = 0; // 纯虚函数声明 }; ``` 纯虚函数使得一个类成为一个抽象基类...