您的位置:首页 > 编程语言 > C语言/C++

C++ Virtual基础知识讲解

2013-04-07 17:10 183 查看
Virtual是C++ OO机制中很重要的一个关键字。只要是学过C++的人都知道在类Base中加了Virtual关键字的函数就是虚拟函数(例如函数print),于是在Base的派生类Derived中就可以通过重写虚拟函数来实现对基类虚拟函数的覆盖。当基类Base的指针point指向派生类Derived的对象时,对point的print函数的调用实际上是调用了Derived的print函数而不是Base的print函数。这是面向对象中的多态性的体现。中国自学编程网,www.zxbc.cn (关于虚拟机制是如何实现的,参见Inside the C++ Object Model ,Addison Wesley 1996)

//---------------------------------------------------------

class Base

{

public:Base(){}

public:

virtual void print(){cout<<"Base";}

};

class Derived:public Base

{

public:Derived(){}

public:

void print(){cout<<"Derived";}

};

int main()

{

Base *point=new Derived();

point->print();

}

//---------------------------------------------------------

Output:

Derived

//---------------------------------------------------------

这也许会使人联想到函数的重载,但稍加对比就会发现两者是完全不同的:

(1) 重载的几个函数必须在同一个类中;

覆盖的函数必须在有继承关系的不同的类中

(2) 覆盖的几个函数必须函数名、参数、返回值都相同;

重载的函数必须函数名相同,参数不同。参数不同的目的就是为了在函数调用的时候编译器能够通过参数来判断程序是在调用的哪个函数。这也就很自然地解释了为什么函数不能通过返回值不同来重载,因为程序在调用函数时很有可能不关心返回值,编译器就无法从代码中看出程序在调用的是哪个函数了。

(3) 覆盖的函数前必须加关键字Virtual;

重载和Virtual没有任何瓜葛,加不加都不影响重载的运作。

关于C++的隐藏规则:

我曾经听说过C++的隐藏规则:

(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual

关键字,基类的函数将被隐藏(注意别与重载混淆)。

(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual

关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

----------引用自《高质量C++/C 编程指南》林锐 2001

这里,林锐博士好像犯了个错误。C++并没有隐藏规则,林锐博士所总结的隐藏规则是他错误地理解C++多态性所致。下面请看林锐博士给出的隐藏规则的例证:

//------------------------------------------------------

#include <iostream.h>

class Base

{

public:

virtual void f(float x){ cout << "Base::f(float) " << x << endl; }

void g(float x){ cout << "Base::g(float) " << x << endl; }

void h(float x){ cout << "Base::h(float) " << x << endl; }

};

class Derived : public Base

{

public:

virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }

void g(int x){ cout << "Derived::g(int) " << x << endl; }

void h(float x){ cout << "Derived::h(float) " << x << endl; }

};

void main(void)

{

Derived d;

Base *pb = &d;

Derived *pd = &d;

// Good : behavior depends solely on type of the object

pb->f(3.14f); // Derived::f(float) 3.14

pd->f(3.14f); // Derived::f(float) 3.14

// Bad : behavior depends on type of the pointer

pb->g(3.14f); // Base::g(float) 3.14

pd->g(3.14f); // Derived::g(int) 3 (surprise!)

// Bad : behavior depends on type of the pointer

pb->h(3.14f); // Base::h(float) 3.14 (surprise!)

pd->h(3.14f); // Derived::h(float) 3.14

}

//---------------------------------------------------------

林锐博士认为bp 和dp 指向同一地址,按理说运行结果应该是相同的,而事实上运行结果不同,所以他把原因归结为C++的隐藏规则,其实这一观点是错的。决定bp和dp调用函数运行结果的不是他们指向的地址,而是他们的指针类型。“只有在通过基类指针或引用间接指向派生类子类型时多态性才会起作用”(C++ Primer 3rd Edition)。pb是基类指针,pd是派生类指针,pd的所有函数调用都只是调用自己的函数,和多态性无关,所以pd的所有函数调用的结果都输出Derived::是完全正常的;pb的函数调用如果有virtual则根据多态性调用派生类的,如果没有virtual则是正常的静态函数调用,还是调用基类的,所以有virtual的f函数调用输出Derived::,其它两个没有virtual则还是输出Base::很正常啊,nothing surprise!

所以并没有所谓的隐藏规则,虽然《高质量C++/C 编程指南》是本很不错的书,可大家不要迷信哦。记住“只有在通过基类指针或引用间接指向派生类子类型时多态性才会起作用”。

纯虚函数:

C++语言为我们提供了一种语法结构,通过它可以指明,一个虚拟函数只是提供了一个可被子类型改写的接口。但是,它本身并不能通过虚拟机制被调用。这就是纯虚拟函数(pure

virtual function)。 纯虚拟函数的声明如下所示:

class Query {

public:

// 声明纯虚拟函数

virtual ostream& print( ostream&=cout ) const = 0;

// ...

};

这里函数声明后面紧跟赋值0。

包含(或继承)一个或多个纯虚拟函数的类被编译器识别为抽象基类。试图创建一个抽象基类的独立类对象会导致编译时刻错误。(类似地通过虚拟机制调用纯虚拟函数也是错误的例如)

// Query 声明了纯虚拟函数

// 所以, 程序员不能创建独立的 Query 类对象

// ok: NameQuery 中的 Query 子对象

Query *pq = new NameQuery( "Nostromo" );

// 错误: new 表达式分配 Query 对象

Query *pq2 = new Query;

抽象基类只能作为子对象出现在后续的派生类中。

如果只知道virtual加在函数前,那对virtual只了解了一半,virtual还有一个重要用法是virtual public,就是虚拟继承。虚拟继承在C++ Primer中有详细的描述,下面稍作修改的阐释一下:

在缺省情况下C++中的继承是“按值组合”的一种特殊情况。当我们写

class Bear : public ZooAnimal { ... };

每个Bear 类对象都含有其ZooAnimal 基类子对象的所有非静态数据成员以及在Bear中声明的非静态数据成员类似地当派生类自己也作为一个基类对象时如:

class PolarBear : public Bear { ... };

则PolarBear 类对象含有在PolarBear 中声明的所有非静态数据成员以及其Bear 子对象的所有非静态数据成员和ZooAnimal 子对象的所有非静态数据成员。在单继承下这种由继承支持的特殊形式的按值组合提供了最有效的最紧凑的对象表示。在多继承下当一个基类在派生层次中出现多次时就会有问题最主要的实际例子是iostream 类层次结构。ostream 和istream 类都从抽象ios 基类派生而来,而iostream 类又是从ostream 和istream 派生

class iostream :public istream, public ostream { ... };

缺省情况下,每个iostream 类对象含有两个ios 子对象:在istream 子对象中的实例以及在ostream 子对象中的实例。这为什么不好?从效率上而言,存储ios 子对象的两个复本,浪费了存储区,因为iostream 只需要一个实例。而且,ios 构造函数被调用了两次每个子对象一次。更严重的问题是由于两个实例引起的二义性。例如,任何未限定修饰地访问ios 的成员都将导致编译时刻错误:到底访问哪个实例?如果ostream 和istream 对其ios 子对象的初始化稍稍不同,会怎样呢?怎样通过iostream 类保证这一对ios 值的一致性?在缺省的按值组合机制下,真的没有好办法可以保证这一点。

C++语言的解决方案是,提供另一种可替代按“引用组合”的继承机制虚拟继承(virtual inheritance )在虚拟继承下只有一个共享的基类子对象被继承而无论该基类在派生层次

中出现多少次共享的基类子对象被称为虚拟基类。

通过用关键字virtual 修政一个基类的声明可以将它指定为被虚拟派生。例如,下列声明使得ZooAnimal 成为Bear 和Raccoon 的虚拟基类:

// 关键字 public 和 virtual

// 的顺序不重要

class Bear : public virtual ZooAnimal { ... };

class Raccoon : virtual public ZooAnimal { ... };

虚拟派生不是基类本身的一个显式特性,而是它与派生类的关系如前面所说明的,虚拟继承提供了“按引用组合”。也就是说,对于子对象及其非静态成员的访问是间接进行的。这使得在多继承情况下,把多个虚拟基类子对象组合成派生类中的一个共享实例,从而提供了必要的灵活性。同时,即使一个基类是虚拟的,我们仍然可以通过该基类类型的指针或引用,来操纵派生类的对象。

c++ virtual function 虚函数

这几天一直在笔试,有人遇到这类题了。翻了翻标准,总结一下

当决定调用哪个函数时,

如果是虚函数,那么取决于指针所指向的对象的类型。

如果是非虚函数,那么取决于指针的类型

ISO/IEC 14882:2003 page 174

[Note: the interpretation of the call of a virtual function depends on the type of the object for which it is

called (the dynamic type), whereas the interpretation of a call of a nonvirtual member function depends

only on the type of the pointer or reference denoting that object (the static type) (5.2.2). ]

当用父类指针(引用)指向子类对象时判断调用哪个函数总结起来就是:

先看指针(或引用)是什么类型的,再看调用的函数是否是虚函数。

如果是虚函数再看是否被子类override,如果override了,那么调用的是子类中的函数。

如果不是虚函数,那么就在指针(引用)所指的类中查找对应的函数,调用的是父类中的函数。

22:49 2008-10-29 补充:

了解了上面的内容后,就能明白为什么要将基类的析构函数声明为虚函数。

因为,如果父类析构函数不是虚函数。而此时你有一个指向子类对象的父类指针,那么当你delete这个指针的时候,父类的析构函数被调用,但是子类的析构函数没有被调用!就是情况C。如果父类的析构函数声明为虚函数那么子类的析构函数也会是虚函数。这样二者的虚函数都会被调用,也就可以避免潜在的资源未释放的问题。

这也是Effective C++第七款的意思:Declare destructor virtual in polymorphic base class

多态提供了与具体实现相隔离的另一类接口,即把“what”从“how”中分离出来,多态性提高了代码的组织性和可读性。多态性是一种对消息的处理方式可以随接手消息的处理对象而变的一种机制。

1.向上映射



将派生类对象通过引用或者指针变成基类对象,引用或者指针的活动称为向上映射。向上映射总是安全的,因为是从更专门的类型到更一般的类型

eg:

derived d;

base* b1 = &d;//发生向上映射

base& b2 = d;//发生向上映射

void fun(base&){}

fun(d);//发生向上映射

2.函数调用与捆绑



捆绑:把函数体与函数调用相联系

早捆绑:由编译器连接器完成,捆绑在程序运行之前

晚捆绑:捆绑在运行时发生,实现晚捆绑事必须有一种机制在运行时确定对象的类型和合适的调用函数

c++中晚捆绑的实现:

关键字virtual告诉编译器它应该实行晚捆绑

编译器对每个包含虚函数的类创建VTABLE,放置类的虚函数地址

编译器秘密放置vpointer(VPTR)

当多态调用时,使用VPTR在VTABLES表中查找函数地址

3.虚函数



为了引起晚捆绑的特定的函数

虚函数提供了一种分辨对象执行不同功能的能力

但是从效率角度出发时,虚函数不是高效的,它需要VPTR指针的压栈出栈操作等

4.纯虚函数



纯虚函数是一种特殊的虚函数,它的一般格式如下:

class <类名>

{

virtual <类型><函数名>(<参数表>)=0;



};

在许多情况下,在基类中不能对虚函数给出有意义有实现,而把它说明为纯虚函数,它的实现留给该基类的派生类去做

纯虚函数和虚函数的区别:

1)虚函数和纯虚函数可以定义在同一个类(class)中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类(class)不能被称为抽象类(abstract class)。

2)虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子类(sub class)中实现该函数才可以使用,因为纯虚函数在基类(base class)只有声明而没有定义。

纯虚函数是非常有用的,因为它们使得类具有明显的抽象性

5.抽象类



含有纯虚函数的类称为抽象类

任何对抽象类的实例化操作都将导致错误的产生,因为抽象类是不能直接被调用的,必须被子类继承重载后,根据要求调用其子类的方法

6.对象切片



C++提供了继承机制和虚拟,并通过(且只能通过)指向同一类族的指针或者引用来实现多态,否则多态不起作用。原因之一著名的对象切片(Object slicing)问题。

1)无虚拟机制继承的对象切片

//:objcut.cpp

#include <iostream.h>

class MyBase

{

public:

void Get(){};

void Set(){};

public:

int b;

};

class DerivedMyBase: public MyBase

{

public:

void Print(){};

void GetD(){};

};

main()

{

DerivedMyBase aDMB;

MyBase aMB = aDMB; // MyBase* aMB = &aDMB;

aMB->Print();

}

编译将出错,这是因为在将aDMB拷贝给aMB时发生了对象切片,在aMB对象中只有MyBase的信息,所有的关于DerivedMyBase类的信息都被切片了。

注释后面那个语句也将导致同样的错误,但是并没有发生对象切片,发生错误的原因是: 因为DerivedMyBase是一个MyBase,所以“MyBase * pMB = &aDMB;”是合法的。而pMB仅仅是一个指针,通过该指针引用的是aDMB,但编译器对于该指针应用对象的了解仅限于MyBase,对于DerivedMyBase类的信息一无所知——这也就是在实践中通常将基类作为抽象类来实现多态的原因,此时派生类中的所有不属于基类的信息都无法通过基类指针或引用来获取,因为编译器在解析该指针或引用指向的内存区时是按照基类的信息来解释的。

2)对象切片的原理

对象切片产生的原因是bitwise的copy构造函数的调用。在“MyBase aMB = aDMB;”中由编译器生成的拷贝构造函数不需要对虚拟机制进行额外的处理,此时依照bitwise copy,所有属于DerivedMyBase的信息都丢掉了。而在“ MyBase * pMB = &aDMB;”中,根本就不需要调用copy ctor,所以切片不会发生。

3)虚拟机制与拷贝方式

当类中没有虚拟机制、没有其他类对象的成员时(只包含built-in类型、指针或者数组),默认copy ctor进行的是bitwise copy,这会导致对象切片的发生。然而,当类中有虚拟机制,或者有其他类对象成员时,默认copy ctor采用的是memberwise copy,并且会对虚拟机制进行正确的拷贝。

因为包含虚拟机制的类在定义一个对象时,编译器会向ctor中添加初始化vtable和vbaseclasstable(依赖于具体编译器)的代码,这样可以保证vtable中的内容与类型完全匹配。也就是说MyBase和DerivedMyBase有这相似的VTABLE,但不是完全相同——例如DerivedMyBase中还可以定义自己的virtual函数,这样它的VTABLE就会有更多表项。

而多态的实现是通过将函数调用解析为VTABLE中的偏移量来实现。pMB->Get()可能会被编译器解析成:

(*pMB->__vtable[Offset_of_Get])();

而当MyBase作为虚基类时,访问其中的数据成员可能就是:

pMB->__vBaseClassMyBase->b;

那么,当“aMB = aDMB;”,copy ctor会执行memberwise copy,正确的初始化aMB的VTABLE,而不是仅仅将aDMB的VTABLE拷贝过来。如果是bitwise copy,aMB对象中的VTABLE将是aDMB的,aMB.Get()调用的将是DervieMyBase定义的Get(),这显然是不符合语义和逻辑的。

7.虚函数和构造函数



当创建一个包含有虚函数的对象时,必须初始化它的VPTR以及指向相应的VTABLE,这必须有关虚函数的调用之前完成。编译器在构造函数的开头部分秘密的插入了能初始化VPTR的代码,这个相当与一个小小的内联函数调用

8.虚拟析构函数



如果一个指针是指向基类的,编译器只能知道delete期间调用这个析构函数的基类版本,当把虚构函数声明为虚函数时则可以解决这个问题。

eg:

//:pvdest.cpp

#include <iostream.h>

class base

{

public:

~base()

{

cout << "~base()" << endl;

}

};

class derived : public base

{

public:

~derived()

{

cout << "~derived()" << endl;

}

};

main()

{

base* bp = new derived;

delete bp;

system("pause");

}

输出:~base()

如果改为 virtual ~base()

输出:~derived()

~base()

出处:http://blog.sina.com.cn/s/blog_6ae7d6b00100pb4v.html

virtual ReturnType Function_1(); //虚函数声明

virtual ReturnType Function_2() = 0; //纯虚函数声明

先讲示例吧,再总结结论。

2、示例:

class Animail{

public:

void Function_1() {cout <<"Animail::Function_1()"<<endl;};

};

class Dog: public
Animail{

public:

void Function_1() {cout <<"Dog::Function_1()"<<endl;};

};

int main(int argc, char* argv[])

{

Animail*p = NULL; //定义基类的指针

Animail cAnimail;

Dog cDog;

p = &cAnimail;

p->Function_1();

p =&cDog;

p->Function_1();

return 0;

}

这段代码的输出结果是什么呢?起初我认为是:Animail::Function_1()与Dog::Function_1(), 因为第一次输出是引用基类Animail的实例,第二次输出是引用子类Dog的实例。事实上答案是Animail::Function_1()与 Animail::Function_1(),为什么呢?

这里我们需要明白:你就记住,不管引用的实例是哪个类的,当你调用的时候,系统会调用左值那个对象所属类的方法。比如说 上面的代码类Animail和 Dog都有一个Function_1函数,因为p是一个Animail类的指针,所以不管你将p指针指向类Animail或是类Dog,最终调用的函数都是类Animail的Function_1函数。这就是静态联篇,编译器在编译的时候就已经确定好了。可是如果我想实现跟据实例的不同来动态决定调用哪个函数呢?这就须要用到虚函数(也就是动态联篇)。

class Animail{

public:

virtual void Function_1() {cout <<"Animail::Function_1()"<<endl;};

};

class Dog: public Animail{

public:

virtual void Function_1() {cout <<"Dog::Function_1()"<<endl;};

};

在基类的成员函数前加关键字virtual,则表示这个函数是一个虚函数。

所谓虚函数就是在编译的时候不确定要调用哪个函数,而是动态决定将要调用哪个函数。它的作用就是为了能让这个函数在它的子类里面可以被重载,这样的话,编译器就可以使用后期绑定来达到多态了,也就是:用基类的指针来调用子类的这个函数。

要实现虚函数必须保证派生类的函数名与基类相同,参数名参数类型等也要与基类相同。但派生类中的virtual关键字可以省略,也表示这是一个虚函数。

下面来分析一下代码,声明一个基类的指针(必须是基类,反之则不行)p,把p指向类Animail的实例cAnimail,调用Function_1函数,这时系统会判断p所指向的实例的类型,如果是Animail类的实例就调用Animail类的Function_1函数,如果是Dog类的实例cDog就调用Dog类的Function_1函数。

下面来讲一下纯虚函数,包含纯虚函数的类也可叫虚基类或者抽象类

class Animail{

public:

virtual void GetColor() = 0;

};

class Dog : public
Animail{

public:

virtual void GetColor() {cout <<"Yellow"endl;};

};

class Pig : public
Animail{

public:

virtual void GetColor() {cout <<"White"<<endl;};

};

如上代码,定义了一个动物类(Animail),类中有一函数GetColor可取得动物颜色,但动物有很多很多种,颜色自然无法确定,所以就把它声明为纯虚函数,也就是光声明函数名不去定义(实现)它,类Dog继承了Animail并实现了里面的代码,返回黄色。Bike类同样道理。有一点须要注意一下,纯虚函数不能实例化,但可以声明指针,所以上面的main函数中: Animail cAnimail; 编译器会告诉你:由于它的成员的原因,无法instantiate 抽象类Animail,并且警告你GetColor() 没有定义。

虚函数

1, 虚函数是非静态的、非内联的成员函数。

2, 若类中一个成员函数被说明为虚函数,则该成员函数在派生类中可能有不同的实现。当使用该成员函数操作指针或引用所标识的对象时,对该成员函数调用可采用动态联编。

5, 定义了虚函数后,程序中声明的指向基类的指针就可以指向其派生类。在执行过程中,该函数可以不断改变它所指向的对象,调用不同版本的成员函数,而且这些动作都是在运行时动态实现的。虚函数充分体现了面向对象程序设计的动态多态性。虚函数充分体现了面向对象程序设计的动态多态性。

纯虚函数

1, 当在基类中不能为虚函数给出一个有意义的实现时,可以将其声明为纯虚函数,其实现留待派生类完成。

2, 纯虚函数的作用是为派生类提供一个一致的接口,它只是个函数的声明而已,它告诉编译器,在这个类中的这个纯虚函数是没有函数定义的,该类不能创建对象(即不能实例化),但可以声明指针,该类的派生类负责给出这个虚函数的重载定义。

出处:http://blog.sina.com.cn/s/blog_624ca8080100f7th.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: