为了实现C++的多态,C++使用了一种动态绑定的技术,而这个技术的核心是虚函数表。每个包含了虚函数的类都包含一个虚函数表,同一个类的对象共同使用同一张虚表。
类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。
注意的是,编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚成员占据虚函数表中的一行。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。
虚函数(Virtual Function)是通过一张虚函数表来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表
,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中分配了指向这个表的指针的内存,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
稍微总结一下,虚函数在虚函数表中的顺序与定义时的顺序相同,后继有虚函数,顺次写入,因为虚函数表是一块连续的内存,所以虚函数表中虚函数的位置也是连续的;
虚函数表是和虚函数关联的,有虚函数则有虚函数表,否测没有。
虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。
在C++中,upcast是指将派生类对象的指针或引用转换为基类指针或引用的过程。这是C++中实现多态的一种方式。通过upcast,我们可以使用基类指针或引用访问派生类对象的成员,同时保留了多态性。
当我们将一个派生类对象的指针或引用转换为基类指针或引用时,这个过程被称为upcast。upcast可以在运行时动态地进行,因为编译器不知道实际指向的对象是什么类型。因此,upcast允许我们使用基类指针或引用访问派生类对象的成员,同时保留了多态性。
需要注意的是,upcast不会改变实际指向的对象,只是改变了指针或引用的类型。因此,在使用upcast时需要小心处理对象的实际类型和指针或引用的类型之间的关系,以避免出现类型不匹配的问题。
upcast使用例子:
#include
class Animal {
public:
virtual void sound() {
std::cout << "Animal makes a sound" << std::endl;
}
};
class Dog : public Animal {
public:
void sound() override {
std::cout << "Dog barks" << std::endl;
}
};
class Cat : public Animal {
public:
void sound() override {
std::cout << "Cat meows" << std::endl;
}
};
int main() {
Dog dog;
Cat cat;
Animal* animalPtr1 = &dog; // upcast from Dog to Animal
Animal* animalPtr2 = &cat; // upcast from Cat to Animal
animalPtr1->sound(); // output: Dog barks
animalPtr2->sound(); // output: Cat meows
return 0;
}
在上面的例子中,我们定义了一个基类Animal和两个派生类Dog和Cat。Dog和Cat都重写了sound()函数,以实现不同的声音输出。在main()函数中,我们创建了一个Dog对象和一个Cat对象,并将它们的地址分别存储在animalPtr1和animalPtr2指针变量中。这两个指针变量都是Animal类型的指针,因此我们可以使用它们来调用sound()函数。在调用时,由于animalPtr1指向的是Dog对象,而animalPtr2指向的是Cat对象,所以它们分别输出了"Dog barks"和"Cat meows"。这个过程就是通过upcast实现的。我们将派生类对象的指针转换为基类指针,然后使用基类指针来访问派生类对象的成员函数,实现了多态性。
总之,upcast是C++中实现多态的一种方式,它可以将派生类对象的指针或引用转换为基类指针或引用,以便在运行时动态地访问派生类对象的成员。
虚函数表在实现C++多态中起着至关重要的作用。多态是面向对象编程的一个核心特性,它允许我们使用基类指针或引用操作派生类对象,并根据对象的实际类型动态调用适当的成员函数。这种运行时的动态调用机制使得代码更加灵活和可扩展,从而有助于创建更具可维护性和可重用性的程序。
虚函数表(Virtual Function Table,简称V-Table)正是支持多态的关键所在。当一个类包含虚函数时,编译器会为该类生成一个虚函数表。虚函数表是一个包含指向虚函数地址的指针数组,其中每个指针都对应一个虚函数。同时,编译器还会在类的对象中添加一个指向虚函数表的指针,称为虚表指针。
在运行时,当我们使用基类指针或引用来调用虚函数时,程序会通过虚表指针找到正确的虚函数表。接着,它会根据虚函数在虚函数表中的索引定位到相应的虚函数地址,并调用该函数。这个过程称为动态绑定,它使得程序能够根据对象的实际类型来决定调用哪个类的成员函数。
举个简单的例子,假设我们有一个基类Shape和两个派生类Circle和Rectangle。假设Shape类中有一个虚函数area(),Circle和Rectangle类都重写了这个函数。当我们使用一个Shape指针或引用来调用area()函数时,虚函数表使得程序能够根据指针或引用指向的实际对象类型来调用Circle或Rectangle类的area()函数。这样一来,我们就可以在不了解具体对象类型的情况下,编写处理不同类型对象的通用代码。
虚析构函数在面向对象编程中起到了关键作用,特别是在涉及继承关系的类中。虚析构函数允许基类析构函数在删除派生类对象时正确调用,这有助于防止资源泄漏和其他潜在问题。以下是虚析构函数的重要性的详细解释:
class Base {
public:
// 没有使用virtual关键字
~Base() { cout << "Base destructor called." << endl; }
};
class Derived : public Base {
public:
~Derived() {
cout << "Derived destructor called." << endl;
// 释放派生类分配的资源
}
};
int main() {
Base* basePtr = new Derived();
delete basePtr; // 只会调用基类的析构函数,导致资源泄漏
return 0;
}
class Base {
public:
// 使用virtual关键字
virtual ~Base() { cout << "Base destructor called." << endl; }
};
class Derived : public Base {
public:
~Derived() {
cout << "Derived destructor called." << endl;
// 释放派生类分配的资源
}
};
int main() {
Base* basePtr = new Derived();
delete basePtr; // 调用派生类的析构函数,然后调用基类的析构函数
return 0;
}
总之,虚析构函数是一个重要的概念,可以确保在删除派生类对象时正确地调用基类和派生类的析构函数,从而避免资源泄漏。在设计涉及继承关系的类时,如果预计基类可能被作为接口使用,那么将基类的析构函数声明为虚函数是一种良好的编程实践。
说到对程序性能的影响,这是相对于模板来说的。虽然虚函数表实现了多态,但是因为虚函数表本身是一块连续的内存,在存储与运行时就会有多余的开销。
虚函数表并不是唯一的选择,也可以考虑使用模板来实现多态。模板在编译时实现多态,不需要在运行时运行时查找虚函数表,以下是模板实现多态的一些优势。
模板也有一些缺点,如代码膨胀(每个模板实例都会生成一份代码),编译时间增加。因此在选择时要权衡利弊。
虚函数表确实会对程序性能产生一定的影响,但是在大多数情况下,这种开销是负担得起的。在对性能要求比较苛刻的情况下,可以考虑使用其他技术,例如模板。
#include
// 基类 Shape
class Shape {
public:
virtual void area() const {
std::cout << "This is the area method in the base class Shape." << std::endl;
}
};
// 派生类 Circle
class Circle : public Shape {
public:
void area() const override {
std::cout << "This is the area method in the derived class Circle." << std::endl;
}
};
// 派生类 Rectangle
class Rectangle : public Shape {
public:
void area() const override {
std::cout << "This is the area method in the derived class Rectangle." << std::endl;
}
};
void printArea(const Shape& shape) {
shape.area(); // 调用虚函数,实现动态绑定
}
int main() {
Shape shape;
Circle circle;
Rectangle rectangle;
printArea(shape); // 输出:This is the area method in the base class Shape.
printArea(circle); // 输出:This is the area method in the derived class Circle.
printArea(rectangle); // 输出:This is the area method in the derived class Rectangle.
return 0;
}
在上面的示例中,我们有一个基类Shape和两个派生类Circle和Rectangle。基类Shape中定义了一个虚函数area(),而派生类Circle和Rectangle都重写了这个函数。
我们定义了一个printArea函数,该函数接受一个Shape引用作为参数。在printArea函数中,我们调用了area()函数,由于area()是虚函数,这里的调用会实现动态绑定。
在main函数中,我们创建了Shape、Circle和Rectangle对象,并分别将它们传递给printArea函数。虽然printArea函数接受的参数类型是Shape引用,但由于多态的存在,我们可以传递Circle和Rectangle对象。
当我们调用printArea函数时,程序会根据传入对象的实际类型,通过虚函数表来动态调用合适的成员函数。因此,printArea(shape)会调用基类Shape的area()函数,printArea(circle)会调用派生类Circle的area()函数,而printArea(rectangle)会调用派生类Rectangle的area()函数。
在C++中,多重继承允许一个派生类继承多个基类。在这种情况下,虚函数表的结构和实现将更加复杂。本段将深入探讨多重继承下虚函数表的结构和实现,以及这对程序员在设计类层次结构时的影响。
在多重继承的情况下,派生类需要维护一个虚函数表数组。数组中的每个元素都指向一个虚函数表,这些虚函数表分别对应每个基类。由于派生类需要处理多个基类的虚函数,虚函数表数组的顺序与基类的声明顺序相同。
以下是一个简单的多重继承示例,说明虚函数表数组的结构:
class Base1 {
public:
virtual void func1() {}
virtual void func2() {}
};
class Base2 {
public:
virtual void func3() {}
virtual void func4() {}
};
class Derived : public Base1, public Base2 {
public:
virtual void func1() override {}
virtual void func4() override {}
};
在这个例子中,Derived 类继承了 Base1 和 Base2。Derived 类将有两个虚函数表,一个来自 Base1,另一个来自 Base2。Derived 类覆盖了基类的部分虚函数。最终,Derived 类的虚函数表数组如下:
Derived::vftable[0]:
Derived::func1()
Base1::func2()
Derived::vftable[1]:
Base2::func3()
Derived::func4()
由于多重继承引入了额外的虚函数表指针和数组,这可能会导致额外的内存开销和间接访问成本。因此,程序员需要在性能和设计灵活性之间做出权衡。
在多重继承的情况下,代码的维护可能变得更加复杂。程序员需要仔细跟踪每个基类的虚函数,以确保正确覆盖和调用它们。此外,修改基类可能会影响多个派生类,需要谨慎处理。
在多重继承中,可能会遇到菱形继承问题,即一个类从两个或多个类继承,而这些类又从同一个基类继承。这会导致二义性和资源浪费。为解决这个问题,C++在多重继承下,派生类可能继承多个基类,这样就需要为每个基类创建一个虚函数表。因此,派生类的虚函数表数量取决于它继承的基类数量。
书山有路勤为径,学海无涯苦作舟。
11.1【C/C++ 多态核心 】C++虚函数表:让多态成为可能的关键
11.2 虚函数表详解