C++中Virtual的作用和应用

发布时间:2023-05-22

在C++中,Virtual是一种非常重要的概念,它允许程序员在类的继承关系中实现多态,也就是说,不同的对象可以有不同的行为,即方法。本文将从多个角度详细阐述Virtual的作用和应用。

一、Virtual的基本概念

Virtual是一个C++中的关键字,用于声明一个方法为虚函数。虚函数是可以在派生类中重新定义的基类函数。不同于非虚函数,虚函数在运行时被决定调用哪个版本,而不是在编译时被决定。这种行为被称为动态绑定(Dynamic Binding)或者后期绑定(Late Binding)。

下面是一个简单的Virtual函数的代码示例:

#include <iostream>

using namespace std;

class Shape {
    public:
        virtual void draw() {
            cout << "Drawing Shape";
        }
};

class Circle : public Shape {
    public:
        void draw() {
            cout << "Drawing Circle";
        }
};

int main() {
    Shape* shape = new Circle();
    shape->draw(); //输出 "Drawing Circle"
    return 0;
}

在这个示例中,我们定义了一个Shape类,它包含了一个draw()方法,这个方法在派生类中可以被重新定义。我们又定义了一个Circle类,它继承了Shape类,并且重新定义了draw()方法。在main函数中,我们实例化了一个Circle对象,并将其指针赋值给一个Shape类型的指针。最终,我们通过指针调用了Shape的虚函数draw(),由于我们使用了动态绑定,程序在运行时会调用Circle中重新定义的draw()方法,输出 "Drawing Circle"。

二、Virtual的作用

1. 实现多态性

Virtual的最重要的作用就是实现多态性。在上面的示例中,我们可以看到,通过继承和重写基类的虚函数,我们可以在运行时实现不同的行为,使得不同类的对象可以有不同的行为。这种行为在面向对象编程中非常有用,可以用于实现更加灵活的代码和高效的设计。

2. 实现动态绑定

Virtual的另一个作用就是实现动态绑定,也就是在运行时才决定调用哪个版本的函数。这种行为非常重要,因为它允许我们编写更加灵活的代码,同时也是面向对象编程中非常基本的概念。

3. 防止对象切割

在C++中,如果调用一个函数并将一个派生类的对象传递给一个基类的引用或指针,通常会发生对象切割的情况。对象切割的含义是,派生类对象被转换为基类对象,从而丢失了派生类的信息。但是,如果我们将基类函数声明为虚函数,那么在运行时会调用正确的版本,这样就不会发生对象切割。这是Virtual最常见的应用之一。

三、Virtual的应用

1. 纯虚函数和抽象类

一个纯虚函数(Pure Virtual Function)是在基类中定义的一个虚函数,它没有实现。纯虚函数的定义形式是在函数声明后加上“= 0”。纯虚函数以及包含一个或多个纯虚函数的类,被称作抽象类。抽象类不能被实例化,只能用作其他类的基类。可以将抽象类的指针或引用用作参数,而不必知道实际的对象类型。

以下是一个使用纯虚函数和抽象类的示例:

#include <iostream>

using namespace std;

class Shape {
    public:
        virtual void draw() = 0; //纯虚函数
        void print() {
            cout << "This is a Shape";
        }
};

class Circle : public Shape {
    public:
        void draw() {
            cout << "Drawing Circle";
        }
};

int main() {
    Shape* shape = new Circle();
    shape->draw(); //输出 "Drawing Circle"
    shape->print(); //输出 "This is a Shape"
    return 0;
}

2. 虚析构函数

在C++中,如果一个类有指向堆内存的指针成员,那么这个类应该有一个虚析构函数(Virtual Destructor)。如果不使用虚析构函数,那么在释放一个派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这样就可能造成内存泄漏。

以下是一个使用虚析构函数的示例:

#include <iostream>

using namespace std;

class Shape {
    public:
        Shape() {
            cout << "Shape Constructed" << endl;
        }
        virtual ~Shape() {
            cout << "Shape Destructed" << endl;
        }
};

class Circle : public Shape {
    public:
        Circle() {
            cout << "Circle Constructed" << endl;
        }
        ~Circle() {
            cout << "Circle Destructed" << endl;
        }
};

int main() {
    Shape* s = new Circle();
    delete s;
    return 0;
}

在这个示例中,Shape类有一个虚析构函数,在派生类Circle中定义了自己的析构函数。在main函数中,我们实例化了一个Circle对象,并将其指针赋值给Shape类型的指针。在程序结束之前,我们使用delete操作符删除了这个对象。通过使用虚析构函数,程序在运行的时候可以正确地调用派生类的析构函数,从而释放内存。

3. 虚函数表

在类中使用虚函数后,C++编译器会创建一个虚函数表(Vtable),用于存储虚函数的地址。虚函数表是在编译时创建的,每个类只有一个虚函数表,存储了这个类所有的虚函数。

以下是一个使用虚函数表的示例:

#include <iostream>

using namespace std;

class Shape {
    public:
        virtual void draw() {
            cout << "Drawing Shape";
        }
};

class Circle : public Shape {
    public:
        void draw() {
            cout << "Drawing Circle";
        }
};

int main() {
    Shape* shape = new Circle();
    shape->draw(); //输出 "Drawing Circle"

    //输出指向虚函数表的指针
    long* vTablePointer = (long*)(*(long*)shape);
    cout << "Vtable address: " << vTablePointer << endl;

    //输出虚函数的地址
    long* functionPointer = (long*)(*(long*)vTablePointer);
    cout << "Draw function address: " << functionPointer << endl;

    return 0;
}

在这个示例中,我们实例化了一个Circle对象,并将其指针赋值给一个Shape类型的指针。在程序中,我们通过指针输出了指向虚函数表和draw函数的指针的地址。

4. 虚函数和非虚函数之间的调用

虚函数和非虚函数之间可以互相调用,虚函数在运行时被动态绑定,而非虚函数在编译时被决定。如果在虚函数中调用基类的非虚函数,那么基类的非虚函数将被静态地绑定。

以下是一个虚函数和非虚函数之间的调用的示例:

#include <iostream>

using namespace std;

class Shape {
    public:
        virtual void draw() {
            cout << "Drawing Shape, ";
            print(); //调用基类的非虚函数
        }
        void print() {
            cout << "This is a Shape";
        }
};

class Circle : public Shape {
    public:
        void draw() {
            cout << "Drawing Circle, ";
            print(); //调用基类的非虚函数
        }
        void print() {
            cout << "This is a Circle";
        }
};

int main() {
    Shape* shape = new Circle();
    shape->draw(); //输出 "Drawing Circle, This is a Shape"

    return 0;
}

在这个示例中,我们在Shape类中定义了一个虚函数draw()和一个非虚函数print(),在Circle类中重新定义了这两个函数。在main函数中,我们实例化了一个Circle对象,并将其指针赋值给一个Shape类型的指针。在程序中,我们通过指针调用了Shape的draw()方法。由于我们使用了动态绑定,程序在运行时会调用Circle中重新定义的draw()方法,输出 "Drawing Circle, This is a Shape"。

四、总结

本文详细地阐述了Virtual的基本概念、作用和应用,从多角度深入地剖析了Virtual这个重要的C++语言特性。在实际编程中,Virtual是非常重要的,可以帮助我们编写出更加灵活、高效和可维护的代码。我们希望本文能够帮助读者深入地理解Virtual,在实践中正确地应用它。