分享

C++中virtual

 迎风初开 2015-01-23
C++中virtual的三种用法 (2012-09-24 13:11:05)转载▼
标签: 杂谈 分类: 编程语言相关
virtual用法一
#include 
using namespace std;

class A{
public:
     virtual  void  display(){  cout<<"A"<<ENDL; }
     };

class B :  public A{
public:
            void  display(){ cout<<"B"<<ENDL; }
     };

void doDisplay(A *p)
{
p->display();
delete p;
}

int main(int argc,char* argv[])
{
doDisplay(new B());
return 0;
}

这段代码打印出的结果为B,但是当把A类中的virtual去掉之后打印出的就为A。
当基类中没有virtual的时候,编译器在编译的时候把p看做A类的对象,调用的自然就是A类的方法。
但是加上virtual之后,将dispaly方法变成了虚方法,这样调用的时候编译器会看调用的究竟是谁的实例化对象,
这样就实现了多态的效果。
也就是说,当基类的派生类中有重写过基类的虚方法的时候,使用基类

的指针指向派生类的对象,调用这个方法实际上调用的会是派生类最后实现的方法

virtual用法二
#include 
using namespace std;
class Person{
   public:    Person(){ cout<<"Person构造"<<ENDL; }
               ~Person(){ cout<<"Person析构"<<ENDL; }
};

class Teacher : virtual public Person{
   public:    Teacher(){ cout<<"Teacher构造"<<ENDL; }
               ~Teacher(){ out<<"Teacher析构"<<ENDL; }
};

class Student : virtual public Person{
  public:      Student(){ cout<<"Student构造"<<ENDL; }
               ~Student(){ cout<<"Student析构"<<ENDL; }
};

class TS : public Teacher,  public Student{
public:            TS(){ cout<<"TS构造"<<ENDL; }
                    ~TS(){ cout<<"TS析构"<<ENDL; }
};

int main(int argc,char* argv[])
{
TS ts;
return 0;
}

这段代码的终端输出结果为:
Person构造
Teacher构造
Student构造
TS构造
TS析构
Student析构
Teacher析构
Person析构

当Teacher类和Student类没有虚继承Person类的时候,也就是把virtual去掉时候终端输出的结果为:
Person构造
Teacher构造
Person构造
Student构造
TS构造
TS析构
Student析构
Person析构
Teacher析构
Person析构

     大家可以很清楚的看到这个结果明显不是我们所期望的。
我们在构造TS的时候需要先构造他的基类,也就是Teacher类和

Student类。
而Teacher类和Student类由都继承于Person类。这样就导致了构造TS的时候实例化了两个Person类。同样的道理,析构

的时候也是析构了两次Person类,这是非常危险的,也就引发出了virtual的第三种用法,虚析构。


virtual用法三
#include 
using namespace std;
class Person{
 public:        Person()  {name = new char[16];cout<<"Person构造"<<ENDL;}
      virtual  ~Person()  {delete []name;cout<<"Person析构"<<ENDL;}
 private:
         char *name;
         };
class Teacher :virtual public Person{
public:         Teacher(){ cout<<"Teacher构造"<<ENDL; }
              ~Teacher(){ cout<<"Teacher析构"<<ENDL; }
};
class Student :virtual public Person{
public:         Student(){ cout<<"Student构造"<<ENDL; }
              ~Student(){ cout<<"Student析构"<<ENDL; }
};
class TS : public Teacher,public Student{
public:             TS(){ cout<<"TS构造"<<ENDL; }
                 ~TS(){ cout<<"TS析构"<<ENDL; }
};
int main(int argc,char* argv[])
{
Person *p = new TS();
delete p;
return 0;
}
这段代码的运行结果为:
Person构造
Teacher构造
Student构造
TS构造
TS析构
Student析构
Teacher析构
Person析构

但是当我们把Person类中析构前面的virtual去掉之后的运行结果为:
Person构造
Teacher构造
Student构造
TS构造
Person析构
程序崩溃

     很明显这个结果不是我们想要的程序,崩溃造成的后果是不可预计的,所以我们一定要注意在基类的析构函数前面加上virtual

,使其变成虚析构在C++程序中使用虚函数,虚继承和虚析构是很好的习惯 可以避免许多的问题。
     

//////
The concept of the virtual function solves the following problem:
In object-oriented programming, when a derived class inherits from a base class, an object of the derived class may be referred to via a 

pointer or reference of the base class type instead of the derived class type. If there are base class methods overridden by the derived 

class, the method actually called by such a reference or pointer can be bound either 'early' (by the compiler), according to the declared 

type of the pointer or reference, or 'late' (i.e. by the runtime system of the language), according to the actual type of the object referred to.
Virtual functions are resolved 'late'. If the function in question is 'virtual' in the base class, the most-derived class's implementation of the 

function is called according to the actual type of the object referred to, regardless of the declared type of the pointer or reference. If it is not 

'virtual', the method is resolved 'early' and the function called is selected according to the declared type of the pointer or reference.
Virtual functions allow a program to call methods that don't necessarily even exist at the moment the code is compiled.
In C++, virtual methods are declared by prepending the virtual keyword to the function's declaration in the base class. This modifier is 

inherited by all implementations of that method in derived classes, meaning that they can continue to over-ride each other and be late-bound.
Example[edit]

Class Diagram of Animal
For example, a base class Animal could have a virtual function eat. Subclass Fish would implement eat() differently than subclass Wolf, but 

one can invoke eat() on any class instance referred to as Animal, and get the eat() behavior of the specific subclass.
class Animal {
    void /*non-virtual*/ move(void) {
        std::cout << "This animal moves in some way" << std::endl;
    }
    virtual void eat(void) {}
};

// The class "Animal" may possess a definition for eat() if desired.
class Llama : public Animal {
    // The non virtual function move() is inherited but cannot be overridden
    void eat(void) {
        std::cout << "Llamas eat grass!" << std::endl;
    }
};
This allows a programmer to process a list of objects of class Animal, telling each in turn to eat (by calling eat()), without needing to know 

what kind of animal may be in the list, how each animal eats, or what the complete set of possible animal types might be.
Abstract classes and pure virtual functions[edit]
A pure virtual function or pure virtual method is a virtual function that is required to be implemented by a derived class if that class is not 

abstract. Classes containing pure virtual methods are termed "abstract" and they cannot be instantiated directly. A subclass of an abstract 

class can only be instantiated directly if all inherited pure virtual methods have been implemented by that class or a parent class. Pure virtual 

methods typically have a declaration (signature) and no definition (implementation).
As an example, an abstract base class MathSymbol may provide a pure virtual function doOperation(), and derived classes Plus and Minus 

implement doOperation() to provide concrete implementations. Implementing doOperation() would not make sense in the MathSymbol class, 

as MathSymbol is an abstract concept whose behaviour is defined solely for each given kind (subclass) of MathSymbol. Similarly, a given 

subclass of MathSymbol would not be complete without an implementation of doOperation().
Although pure virtual methods typically have no implementation in the class that declares them, pure virtual methods in C++ are permitted to 

contain an implementation in their declaring class, providing fallback or default behaviour that a derived class can delegate to, if appropriate.
Pure virtual functions can also be used where the method declarations are being used to define an interface - similar to what the interface 

keyword in Java explicitly specifies. In such a use, derived classes will supply all implementations. In such a design pattern, the abstract 

class which serves as an interface will contain only pure virtual functions, but no data members or ordinary methods. In C++, using such 

purely abstract classes as interfaces works because C++ supports multiple inheritance. However, because many OOP languages do not 

support multiple inheritance, they often provide a separate interface mechanism. An example is the Java programming language.
Behavior during construction and destruction[edit]
Languages differ in their behavior while the constructor or destructor of an object is running. For some languages, notably C++, the virtual 

dispatching mechanism has different semantics during construction and destruction of an object. While it is recommended that virtual 

function calls in constructors should be avoided for C++,[1] in some other languages, for example C# and Java, the derived implementation 

can be called during construction and design patterns such as the Abstract Factory Pattern actively promote this usage in languages 

supporting the ability.
Virtual destructors[edit]
Object-oriented languages typically manage memory allocation and de-allocation automatically when objects are created and destroyed. 

However, some object-oriented languages allow a custom destructor method to be implemented, if desired. If the language in question uses 

automatic memory management, the custom destructor (generally called a finalizer in this context) that is called is certain to be the 

appropriate one for the object in question. For example, if an object of type Wolf that inherits Animal is created, and both have custom 

destructors, the one called will be the one declared in Wolf.
In manual memory management contexts, the situation can be more complex, particularly as relates to static dispatch. If an object of type 

Wolf is created but pointed to by an Animal pointer, and it is this Animal pointer type that is deleted, the destructor called may actually be the 

one defined for Animal and not the one for Wolf, unless the destructor is virtual. This is particularly the case with C++, where the behavior is 

a common source of programming errors.


////
 Virtual是C++ OO机制中很重要的一个关键字。只要是学过C++的人都知道在类Base中加了Virtual关键字的函数就是虚拟函数(例如

下面例子中的函数print),于是在Base的派生类Derived中就可以通过重写虚拟函数来实现对基类虚拟函数的覆盖。当基类Base的

指针point指向派生类Derived的对象时,对point的print函数的调用实际上是调用了Derived的print函数而不是Base的print函数。这是面

向对象中的多态性的体现。(关于虚拟机制是如何实现的,参见Inside the C++ Object Model ,Addison Wesley 1996)

[cpp] view plaincopy
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++多态性所致。下面请看林

锐博士给出的隐藏规则的例证:
[cpp] view plaincopy
#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)。 纯虚拟函数的声明如下所示:
[cpp] view plaincopy
class Query {  
public:  
// 声明纯虚拟函数  
virtual ostream& print( ostream&=cout ) const = 0;  
// ...  
};  
这里函数声明后面紧跟赋值0。
包含(或继承)一个或多个纯虚拟函数的类被编译器识别为抽象基类。试图创建一个抽象基类的独立类对象会导致编译时刻错误

。(类似地通过虚拟机制调用纯虚函数也是错误的)
[cpp] view plaincopy
// Query 声明了纯虚拟函数, 我们不能创建独立的 Query 类对象  
// 正确: NameQuery 是 Query 的派生类  
Query *pq = new NameQuery( "Nostromo" );  
// 错误: new 表达式分配 Query 对象  
Query *pq2 = new Query();  

虚析构:
如果一个类用作基类,我们通常需要virtual来修饰它的析构函数,这点很重要。如果基类的析构函数不是虚析构,当我们用delete

来释放基类指针(它其实指向的是派生类的对象实例)占用的内存的时候,只有基类的析构函数被调用,而派生类的析构函数不会被

调用,这就可能引起内存泄露。如果基类的析构函数是虚析构,那么在delete基类指针时,继承树上的析构函数会被自低向上依次

调用,即最底层派生类的析构函数会被首先调用,然后一层一层向上直到该指针声明的类型。

虚继承:
如果只知道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 子对象中的实例。这为什么不好

?从效率上而言,iostream只需要一个实例,但我们存储了ios 子对象的两个复本,浪费了存储区。此外,在这一过程中,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 { ... };
虚拟派生不是基类本身的一个显式特性,而是它与派生类的关系。如前面所说明的,虚拟继承提供了“按引用组合”。也就是说,对

于子对象及其非静态成员的访问是间接进行的。这使得在多继承情况下,把多个虚拟基类子对象组合成派生类中的一个共享实例

,从而提供了必要的灵活性。同时,即使一个基类是虚拟的,我们仍然可以通过该基类类型的指针或引用,来操纵派生类的对象

 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多