C 虚函数和纯虚函数区别

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
这种同一代码可以产生不同效果的特点,被称为“多态”。
多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的 C++教 程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的用 途,我就不再重 复这个例子了,如果你不知道这个例子,随便找本书应该都有介绍。我试图从一个抽象的角 度描述一下,回头再结合那个画图的例子,也许你就更容 易理解。
多态可以使程序员脱离这种窘境。再回头看看1.1中的例子,bar()作为 A-B 这个类层次的 使用者,它并不知道这个类层次中有多少个类,每个类都叫什 么,但是一样可以很好的工 作,当有一个 C 类从 A 类派生出来后,bar()也不需要“知道”(修改)。这完全归功于多态-编译器针对虚函数产生了可以在运 行时刻确定被调用函数的代码。
元函数调用虚拟成员函数来解决友元的虚拟问题。
8 析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子 类对
象,将调用子类的析构函数,然后自动调用基类的析构函数。
例如:
Father *fp = new Son(); //合法的,无论父类是否为抽象类,即使是具体类,也可以指向子类。
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;} };
虽然实际情况远非这么简单,但是基本原理大致如此。
虚函数总是在派生类中被改写,这种改写被称为“override”。我经常混淆“overload”和 “override”这两个单词。但是随着各类 C++的书越来越多,后来的程序员也许不会再犯我犯 过的错误了。但是我打算澄清一下:
• override 是指派生类重写基类的虚函数,就象我们前面 B 类中重写了 A 类中的 foo() 函数。重写的函数必须有一致的参 数表和返回值(C++标准允许返回值不同的情况,这 个我会在“语法”部分简单介绍,但是很少编译器支持这个 feature)。这个单词好象一直 没有什么合 适的中文词汇来对应,有人译为 “覆盖”,还贴切一些。 • overload 约定成俗的被翻译为“重载”。是指编写一个与已有函数同名但是参数表不 同的函数。例如一个函数即可以接受整型数作为参数,也可以接受浮点数作为参数。
class A { public:
virtual ~A()=0; };
// 纯虚析构函数
当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:
class A { public:
A() { ptra_ = new char[10];} ~A() { delete[] ptra_;} private: char * ptra_; };
~Father()析构函数来释放由 Father 组件指向的内存。
这意味着:即使基类不需要显示析构函数提供服务,也不应该依赖于默认构造函数,而应 当提供虚拟析
构函数,即使他不执行任何操作: virtual ~BaseClass(){};
注:多态方式调用是指用父类的指针或引用来指向子类的实例后,用父类的指针或引用来调 用虚函数.
void foo(); };
// 从 B 继承,不是从 A 继承! // 也没有 virtual 关键字!
这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚
函数,在派生类中也是虚函数,即使不再使用 virtual 关键字。
如下声明表示一个函数为纯虚函数:
虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是 虚函数,但它不是多态的:
class A { public:
virtual void foo(); };
class B: public A {
virtual void foo(); };
void bar() {
A a; a.foo(); }
// 非虚析构函数
class B: public A { public:
B() { ptrb_ = new char[20];} ~B() { delete[] ptrb_;} private: char * ptrb_; };
void foo() {
A * a = new B; delete a; }
1.纯虚函数声明如下: virtual ReturnType FunctionName(Parameter)=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数
的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引 用。
2.虚函数声明如下:virtual ReturnType FunctionName(Parameter); 虚函数必须实现,如果不实现,编译器将报错,错误提示为:
在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构 成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需要基类的 时候写针对基 类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。 如果这个类层次有任何的改变(增加了新类),都需要使 用者“知道”(针对新类写代码)。 这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程序中的 “bad smell”之 一。
那么,在使用的时候,我们可以:
A * a = new B();
a->foo();
// 在这里,a 虽然是指向 A 的指针,但是被调用的函数(foo)却是 B 的!
这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。 它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是 在编译时刻被确 定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还 是哪个派生类的函数,所以被成为“虚”函数。
class A { public:
virtual void foo()=0; };
// =0标志一个虚函数为纯虚函数
一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚 函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有 这个函数。
析构函数也可以是虚的,甚至是纯虚的。例如:
这种写法的语意是:A 告诉 B,你最好 override 我的 bar()函数,但是你不要管它如何使
即:如果使用指向对象的引用或指针来调用虚拟方法,程序将使用为对象类型定义的方 法,而
不使用为引用或指针类型定义的方法。也称作动态联编或晚期绑定。
参考引用
<C++实践系列>C++中的虚函数(virtual function) 作者:张笑猛 原文出处:http://objects.nease.net/
假设我们有下面的类层次:
... delete *fp;
//是调用~Father()还是~Son() ???
如果使用默认的静态联编,则 delete 语句调用~Father()析构,这将释放由 Son 对象中的 Father 部分指
向的内存,但不会释放新的类成员指向的内存。
但如果析构函数是虚拟的, 则上面代码会先调用~Son()释放由 Son 组件指向的内容,然后 自动调用
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 不起作 用的情况。
纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类 (不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析 构函数来达到目的。
构造函数不ቤተ መጻሕፍቲ ባይዱ是虚的。
考虑下面的例子:
class A { public:
void foo() { bar();} private:
虚函数的标志是“virtual”关键字。
考虑下面的类层次:
class A { public:
virtual void foo(); };
class B: public A { public:
void foo(); };
// 没有 virtual 关键字!
class C: public B { public:
该虚函数,由多态方式调用的时候动态绑定。
5.虚函数是 C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类 定义的
函数
6 在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
7 友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以 通过让友
// A::foo()被调用
在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上面的类层次, 但是使用的方法变的复杂了一些:
void bar(A * a) {
a->foo(); // 被调用的是 A::foo() 还是 B::foo()? }
因为 foo()是个虚函数,所以在 bar 这个函数中,只根据这段代码,无从确定这里被调用的是 A::foo()还是 B::foo(),但是可以肯定的说:如果 a 指向的是 A 类的实例,则 A::foo()被调用, 如果 a 指向的是 B 类的实例,则 B::foo()被调用。
error LNK****: unresolved external symbol "public: virtual void __thiscall
ClassName::virtualFunctionName(void)"
3.对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
4.实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以 覆盖
void bar(A * a) {
a->foo(); }
会被改写为:
void bar(A * a) {
(a->vptr[1])(); }
因为派生类和基类的 foo()函数具有相同的 VTABLE 索引,而他们的 vptr 又指向不同的 VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个 foo()函数。
编译器是如何针对虚函数产生可以再运行时刻确定被调用函数的代码呢?也就是说,虚 函数实际上是如何被编译器处理的呢?Lippman 在深度探索 C++对象模型[1]中的不同章节 讲到了几种方式,这里把“标准的”方式简单介绍一下。
我所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为 virtual 的函数,就会为其搞一个虚函数表,也就是 VTABLE。VTABLE 实际上是一个函数 指针的数组,每个虚函数占用这个数组的一个 slot。一个类只有一个 VTABLE,不管它有多 少个实例。派生 类有自己的 VTABLE,但是派生类的 VTABLE 与基类的 VTABLE 有相同 的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的 时候,编 译器还会在每个实例的内存布局中增加一个 vptr 字段,该字段指向本类的 VTABLE。通过 这些手段,编译器在看到一个虚函数调用的时候,就会将 这个调用改写,针对1.1中的例子:
在这个例子中,程序也许不会象你想象的那样运行,在执行 delete a 的时候,实际上只 有 A::~A()被调用了,而 B 类的析构函数并没有被调用!这是否有点儿可怕?
如果将上面 A::~A()改为 virtual,就可以保证 B::~B()也在 delete a 的时候被调用了。因此 基类的析构函数都必须是 virtual 的。
相关文档
最新文档