C++中的虚函数(virtual function

c/c++ 专栏收录该内容
14 篇文章 0 订阅

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()

{

    Aa;

   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的函数,就会为其搞一个虚函数表,也就是VTABLEVTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个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.4overloadoverride 

    虚函数总是在派生类中被改写,这种改写被称为“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.1private的虚函数 

    考虑下面的例子:

 

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模式为例,CreatorfactoryMethod()就是虚函数,派生类override这个函数后,产生不同的Product类,被产生的Product类被基类的AnOperation()函数使用。基类的AnOperation()函数针对Product类进行操作,当然Product类一定也有多态(虚函数)。

 

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

 

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

 

4.参考资料 

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

 

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

 

 

C++ Delphi 的函数覆盖(Override)与重载(overload)

 

在面向对象编程中,当子类继承了来自基类的函数后,子类有可能需要对其中的一些函数作出与基类不同处理,比如: 

 

class CHuman 

public: 

void SayMyName()//打印出对象的姓名 

cout << "Hi, I am a human"<< endl; 

} 

 

那么很明显,假如他的子类有一个同名、同参数和返回值(一句话,一摸一样)的函数SayMyName,它会调用哪个函数呢?比如现在有一个class CMark 

 

class CMark: public CHuman 

public: 

void SayMyName() 

cout << "Hi, I am mark"<< endl; 

}; 

 

那么我们要问,下面的程序段:

 

CHuman *pH = new CMark; 

 

if (pH) 

pH->SayMyName(); 

else 

cout << "cast error! "<< endl; 

 

delete pH; 

pH = NULL; 

 

要打印出来的,真的是我们想要的Hi, I am mark 吗? 

 

不是。它输出了Hi, Iam a human。这很糟糕,当我们指着一个人要他说出自己的名字的时候,他却告诉我们他“是一个人”,而不是说出自己的名字。出现这样的问题原因在于,用基类的指针指向公有派生类,可以访问派生类从基类中继承的成员函数。但如果派生类中也有同名的函数,则结果仍然是访问基类的同名函数,而不是派生类本身的函数。而事实上,我们希望的是由一个对象的真实类型来决定到底该调用这些同名函数中的哪一个,就是说,这样的决议是动态(Dynamic)的。或者我们可以说,我们希望当一个对象是子类型时,它的同名函数在子类中的实现覆盖(override)掉基类的实现。 

 

我们先从C++对这个问题的处理说起。 

 

这是C++中比较典型的多态的例子,C++用虚函数来实现这样的多态。具体点说,就是使用virtual 关键字来将函数说明成虚函数,在上一个例子中就是应该声明成: 

 

class CHuman 

public: 

virtual void SayMyName()//打印出对象的姓名 

cout << "Hi, I am a human"<< endl; 

} 

 

这样,其他的代码还是那个老样子,但是我们的CMark 已经知道怎么说自己的名字了。CMarkSayMyName()函数是否加了virtual 关键字的说明并没有关系,因为根据C++语法的规定,因为它覆盖了CHuman 的同名函数,它自己也就成为virtual 的了。至于为什么一个virtual 关键字有那么神奇的效果呢?C++ FAQ Lite 对此是这样说明的:C++中,“虚成员函数是动态确定的(在运行时)。也就是说,成员函数(在运行时)被动态地选择,该选择基于对象的类型,而不是指向该对象的指针/引用的类型”。于是我们的pH就发现自己其实指向的是一个CMark类型的对象,而不是自己的类型所声明的CHuman,所以它聪明的调用了CMarkSayMyName 

 

 

Delphi 就是用override 关键字来说明函数覆盖的。被覆盖的函数必须是虚(virtual)的,或者是动态(dynamic)的,也就是说该函数在声明时应该包含这两个指示字中的一个,比如: 

 

procedure Draw; virtual; 

 

在需要覆盖的时候,只需要在子类中用override 指示字重新声明一下就可以了。 

 

procedure Draw; override 

 

在语法上来说,声明为virtualdynamic是等价的。它们的差别在于,前者在实现上对速度进行了优化,而后者对代码大小进行了优化。 

 

假如基类和子类都含有同一个函数名和参数,并且在子类中不加override 指示字呢?这在语法上也是正确的。这意味着子类的函数同名实现把基类的实现隐藏(hide)掉了,尽管这二者在派生类中都存在。那么就回到了本文开头的第一个例子说明的情况:当我们指着一个人要他说出自己的名字的时候,他却告诉我们他“是一个人”,而不是说出自己的名字。 

 

值得注意的是,与我们在C++中常常不加区分的把覆盖一个函数和重载一个函数通称为重载不同,在Delphi 中,只有重载(overload) 才是我们平时所说的重载,被重载的函数依然存在,依靠参数来决定到底调用那个实现。当然,当overload掉的函数和基类的函数参数相同时,基类的实现就被hide掉了,就像上面提到的一样。而覆盖(override)则是把让被覆盖的函数不可见了,确确实实的"覆盖"掉了,原来的实现就不见了。基于这样的原因,许多文章甚至一些书都错误的把override翻译成重载,笔者认为并不合适。

 

 

多态(POLYMORPHISM),覆盖(OVERRIDE),重载(OVERLOAD)

 

这几个概念还真是容易混淆。

 

多态(polymorphism),覆盖(Override),重载(overload)

也有把override译为重载的。

关于overrideoverload的翻译,好像不是很统一。

更多的应该是:

覆盖(override)和重载(overload

1。覆盖override

 

Overriding 也许叫做overwriting更合适, 

OVERLOAD覆盖是指在子类(c++中的派生类)中重新定义父类的函数,其函数名、参数列、返回值类型必须同父类中的相对应被覆盖的函数严格一致,覆盖函数和被覆盖函数只有函数体(花括号中的部分)不同,当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本。

比如

#ruby code

class Base

def m1

puts "in base"

end

end

 

class Sub < Base

def m1

puts "in sub"

end

 

end

我们说Sub类覆盖了父类的m1方法

 

2。重载(overload):

在同一个类中,出现多个同名的方法的现象就是Overload

重载事发生在同一个类中,不同方法之间的现象。

 

c++或者java中,方法一般为

返回类型方法名(参数1,参数2)

判断2个方法是不是overload,主要指方法名一样,参数不一样,

参数不一样指的是参数的个数,相同位置的参数的类型是否一样,而与参数(型参)的名称无关(参数类型/个数/顺序,不同),

与返回类型也无关。程序会根据不同的参数列来确定需要调用的函数

比如c++或者java中,这都是overload

void m1();

void m1(int arg);

void m1(int arg, char* x);

 

ruby中,不存在这样的overload

#ruby code

def method_a(a)

puts "method_a(a) "+a.to_s

end

 

def method_a(a,b)

puts "method_a(a,b)"+a.to_s+" "+b.to_s

end

 

method_a("a") #这句会出错,因为这个方法的定义已经被method_a(a,b)重定义给覆盖了

method_a("a","b")

 

 

method_a("a") 这句会出错,因为这个方法的定义已经被method_a(a,b)重定义给覆盖了

因为ruby的弱类型和可变参数列表,使得overload不是很明显。

 

 

3。多态(polymorphism)

至于多态,我还没有见过一个看一眼就能明白的定义。

有的说是允许将子类类型的指针赋值给父类类型的指针,当然java中没有指针的概念。

多态有时候也被称为动态绑定或者晚绑定或运行时绑定,意思是编译的时候不必关心,运行的时候才决定调用哪个对象的哪个方法。

我觉得多态的用途之一就是在父类提供一个接口(服务),然后调用的时候用的却是子类的具体实现。这个结合java中的interface应该是比较形象的,但是我懒得再去写几个例子了。

先来看一个java中不用interface的例子:

//java code

public class Base{

 

void m1(){

System.out.println("in base,m1");

}

 

void m2(){

System.out.println("in base,m2");

}

 

public static void main(String[] argv){

Base b=new Sub();

b.m1();

b.m2(); 

 

}

}

 

class Sub extends Base {

void m1(){

System.out.println("in sub,m1");

 

}

main方法中b声明为Base类型,而实例化的时候是一个Sub类的实例,Sub中没有实现m2方法,所以,将调用Basem2方法,而Sub overwirte了父类的m1方法,所以,b.m2()将调用Sub类的m1方法。

 

c++中可能复杂点:

//c++ code

#include 

 

class Base{

public:

void m1(){

cout<<"i am in base n";

}

virtual void m2(){

cout<<"i am in base ,virtualn";

};

 

class Sub:public Base{

public:

void m1(){

cout<<"i am in sub n";

virtual void m2(){

cout<<"i am in sub ,virtualn";

};

 

void fnm1(Base& b ){

b.m1(); 

}

void fnm2(Base& b ){

b.m2(); 

}

 

 

 

int main(void)

{

 

Sub s;

s.m1();

s.m2();

Base b;

b.m1();

b.m2();

Base* bs=new Sub();

bs->m1();

bs->m2();

delete bs;

 

Sub s;

fnm1(s);

fnm2(s);

return(0);

}

 

c++中,需要virtual关键字(当然Sub::m2()virtual是可以省略的)

运行结果如下:

i am in sub

i am in sub ,virtual

i am in base

i am in base ,virtual

i am in base

i am in sub ,virtual

i am in base

i am in sub ,virtual

 

fnm2(Base& b )中,b是一个Sub类型,如果这个方法是virtual的,则调用Subm2,否则,调用Basem2

Ruby的弱类型性,也不怎么存在这个问题。

#ruby code

class Base

def m1

puts "in base"

end

end

 

class Sub < Base

def m1

puts "in sub"

end

 

end

 

def fn(a)

a.m1

end

 

a=Sub.new

fn(a)

b=Base.new

fn(b)

 

总之,多态性是面向对象的基本特性,而overload应该不算是面向对象的特性吧。

 

 

[C++基础]重载、覆盖、多态与函数隐藏

 

经常看到C++的一些初学者对于重载、覆盖、多态与函数隐藏的模糊理解。在这里写一点自己的见解,希望能够C++初学者解惑。

要弄清楚重载、覆盖、多态与函数隐藏之间的复杂且微妙关系之前,我们首先要来回顾一下重载覆盖等基本概念。

首先,我们来看一个非常简单的例子,理解一下什么叫函数隐藏hide

#include <iostream>

using namespace std;

class Base{

public:

   void fun() { cout << "Base::fun()" << endl; }

};

class Derive : public Base{

public:

   void fun(int i) { cout << "Derive::fun()" << endl;}

};

int main()

{

   Derive d;

       //下面一句错误,故屏蔽掉

   //d.fun();error C2660: 'fun' : function does not take 0 parameters

   d.fun(1);

       Derive*pd =new Derive();

       //下面一句错误,故屏蔽掉

       //pd->fun();errorC2660: 'fun' : function does not take 0 parameters

       pd->fun(1);

       deletepd;

   return 0;

}

/*在不同的非命名空间作用域里的函数不构成重载,子类和父类是不同的两个作用域。

在本例中,两个函数在不同作用域中,故不够成重载,除非这个作用域是命名空间作用域。*/

在这个例子中,函数不是重载overload,也不是覆盖override,而是隐藏hide

接下来的5个例子具体说明一下什么叫隐藏 

1

#include <iostream>

using namespace std;

class Basic{

public:

       voidfun(){cout << "Base::fun()" << endl;}//overload

       voidfun(int i){cout << "Base::fun(int i)" << endl;}//overload

};

class Derive :public Basic{

public:

       voidfun2(){cout << "Derive::fun2()" << endl;}

}; 

int main()

{

       Derived;

       d.fun();//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。

       d.fun(1);//正确,派生类没有与基类同名函数声明,则基类中的所有同名重载函数都会作为候选函数。

       return0;

2

#include <iostream>

using namespace std;

class Basic{

public:

       voidfun(){cout << "Base::fun()" << endl;}//overload

       voidfun(int i){cout << "Base::fun(int i)" << endl;}//overload

};

class Derive :public Basic{

public:

       //新的函数版本,基类所有的重载版本都被屏蔽,在这里,我们称之为函数隐藏hide

   //派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。

       voidfun(int i,int j){cout << "Derive::fun(int i,int j)" <<endl;}

       voidfun2(){cout << "Derive::fun2()" << endl;}

};

 

int main()

{

       Derived;

       d.fun(1,2);

       //下面一句错误,故屏蔽掉

       //d.fun();errorC2660: 'fun' : function does not take 0 parameters

       return0;

3

#include <iostream>

using namespace std;

class Basic{

public:

       voidfun(){cout << "Base::fun()" << endl;}//overload

       voidfun(int i){cout << "Base::fun(int i)" << endl;}//overload

};

class Derive :public Basic{

public:

       //覆盖override基类的其中一个函数版本,同样基类所有的重载版本都被隐藏hide

   //派生类中有基类的同名函数的声明,则基类中的同名函数不会作为候选函数,即使基类有不同的参数表的多个版本的重载函数。

       voidfun(){cout << "Derive::fun()" << endl;}

       voidfun2(){cout << "Derive::fun2()" << endl;}

}; 

int main()

{

       Derived;

       d.fun();

       //下面一句错误,故屏蔽掉

       //d.fun(1);errorC2660: 'fun' : function does not take 1 parameters

       return0;

4

#include <iostream>

using namespace std;

class Basic{

public:

       voidfun(){cout << "Base::fun()" << endl;}//overload

       voidfun(int i){cout << "Base::fun(int i)" << endl;}//overload

};

class Derive :public Basic{

public:

       usingBasic::fun;

       voidfun(){cout << "Derive::fun()" << endl;}

       voidfun2(){cout << "Derive::fun2()" << endl;}

};

 

int main()

{

       Derived;

       d.fun();//正确

       d.fun(1);//正确

       return0;

}

/* 

输出结果

Derive::fun()

Base::fun(int i)

Press any key to continue

*/ 

5

#include <iostream>

using namespace std;

class Basic{

public:

       voidfun(){cout << "Base::fun()" << endl;}//overload

       voidfun(int i){cout << "Base::fun(int i)" << endl;}//overload

};

class Derive :public Basic{

public:

       usingBasic::fun;

       voidfun(int i,int j){cout << "Derive::fun(int i,int j)" <<endl;}

       voidfun2(){cout << "Derive::fun2()" << endl;}

}; 

int main()

{

       Derived;

       d.fun();//正确

       d.fun(1);//正确

       d.fun(1,2);//正确

       return0;

}

/* 

输出结果

Base::fun()

Base::fun(int i)

Derive::fun(int i,int j)

Press any key to continue*/ 

 

好了,我们先来一个小小的总结重载与覆盖两者之间的特征 

 重载overload的特征:

         相同的范围(在同一个类中)

         函数名相同参数不同;

         virtual 关键字可有可无。 

覆盖override是指派生类函数覆盖基类函数,覆盖的特征是:

         不同的范围(分别位于派生类与基类)

         函数名和参数都相同; 

         基类函数必须有virtual关键字。(若没有virtual 关键字则称之为隐藏hide)

如果基类有某个函数的多个重载(overload)版本,而你在派生类中重写(override)了基类中的一个或多个函数版本,或是在派生类中重新添加了新的函数版本(函数名相同,参数不同),则所有基类的重载版本都被屏蔽,在这里我们称之为隐藏hide。所以,在一般情况下,你想在派生类中使用新的函数版本又想使用基类的函数版本时,你应该在派生类中重写基类中的所有重载版本。你若是不想重写基类的重载的函数版本,则你应该使用例4或例5方式,显式声明基类名字空间作用域。 

事实上,C++编译器认为,相同函数名不同参数的函数之间根本没有什么关系,它们根本就是两个毫不相关的函数。只是C++语言为了模拟现实世界,为了让程序员更直观的思维处理现实世界中的问题,才引入了重载和覆盖的概念。重载是在相同名字空间作用域下,而覆盖则是在不同的名字空间作用域下,比如基类和派生类即为两个不同的名字空间作用域。在继承过程中,若发生派生类与基类函数同名问题时,便会发生基类函数的隐藏。当然,这里讨论的情况是基类函数前面没有virtual 关键字。在有virtual 关键字关键字时的情形我们另做讨论。 

继承类重写了基类的某一函数版本,以产生自己功能的接口。此时C++编绎器认为,你现在既然要使用派生类的自己重新改写的接口,那我基类的接口就不提供给你了(当然你可以用显式声明名字空间作用域的方法,见[C++基础]重载、覆盖、多态与函数隐藏(1))。而不会理会你基类的接口是有重载特性的。若是你要在派生类里继续保持重载的特性,那你就自己再给出接口重载的特性吧。所以在派生类里,只要函数名一样,基类的函数版本就会被无情地屏蔽。在编绎器中,屏蔽是通过名字空间作用域实现的。 

 所以,在派生类中要保持基类的函数重载版本,就应该重写所有基类的重载版本。重载只在当前类中有效,继承会失去函数重载的特性。也就是说,要把基类的重载函数放在继承的派生类里,就必须重写。   

 这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,具体规则我们也来做一小结:

         如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类无virtual关键字,基类的函数将被隐藏。(注意别与重载混淆,虽然函数名相同参数不同应称之为重载,但这里不能理解为重载,因为派生类和基类不在同一名字空间作用域内。这里理解为隐藏)

         如果派生类的函数与基类的函数同名,但是参数不同。此时,若基类有virtual关键字,基类的函数将被隐式继承到派生类的vtable中。此时派生类vtable中的函数指向基类版本的函数地址。同时这个新的函数版本添加到派生类中,作为派生类的重载版本。但在基类指针实现多态调用函数方法时,这个新的派生类函数版本将会被隐藏。

         如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。(注意别与覆盖混淆,这里理解为隐藏)

         如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数有virtual关键字。此时,基类的函数不会被“隐藏”。(在这里,你要理解为覆盖哦^_^)

 插曲:基类函数前没有virtual关键字时,我们要重写更为顺口些,在有virtual关键字时,我们叫覆盖更为合理些,戒此,我也希望大家能够更好的理解C++中一些微妙的东西。费话少说,我们举例说明吧。 

6

#include <iostream>

using namespace std; 

class Base{

public:

       virtualvoid fun() { cout << "Base::fun()" << endl; }//overload

   virtual void fun(int i) { cout << "Base::fun(int i)"<< endl; }//overload

}; 

class Derive : public Base{

public:

       voidfun() { cout << "Derive::fun()" << endl; }//override

   void fun(int i) { cout << "Derive::fun(int i)" <<endl; }//override

       voidfun(int i,int j){ cout<< "Derive::fun(int i,int j)"<<endl;}//overload

}; 

 intmain()

{

 Base *pb  = new Derive();

 pb->fun();

 pb->fun(1); 

  //下面一句错误,故屏蔽掉

 //pb->fun(1,2);virtual函数不能进行overload,error C2661: 'fun' : no overloaded function takes 2parameters

 cout << endl;

 Derive *pd  = new Derive();

 pd->fun();

 pd->fun(1);

 pd->fun(1,2);//overload 

  delete pb;

 delete pd;

 return 0;

}/* 

输出结果 

 Derive::fun()

Derive::fun(int i)

Derive::fun()

Derive::fun(int i)

Derive::fun(int i,int j)

Press any key to continue

*/ 

 7-1

#include <iostream> 

using namespace std; 

class Base{

public:

       virtual void fun(int i){ cout <<"Base::fun(int i)"<< endl; }

}; 

 


  • 2
    点赞
  • 0
    评论
  • 3
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值