C++父类虚函数的继承规则
C++ 父类虚函数的继承规则
虚函数基础概念
在 C++ 中,虚函数(virtual function)是一种特殊的成员函数,它允许在派生类中被重新定义。虚函数主要用于实现多态性(polymorphism),这是面向对象编程的重要特性之一。
当一个函数被声明为虚函数时,它在基类和派生类中都可以有不同的实现。通过基类指针或引用调用虚函数时,会根据指针或引用实际指向的对象类型来决定调用哪个类的函数版本,这一过程被称为动态绑定(dynamic binding)。
例如,假设有一个基类 Animal
,其中包含一个虚函数 speak
:
class Animal {
public:
virtual void speak() {
std::cout << "I am an animal" << std::endl;
}
};
这里,speak
函数被声明为虚函数。现在我们可以定义一个派生类 Dog
并重新定义 speak
函数:
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
在 Dog
类中,speak
函数重写(override)了基类 Animal
的 speak
函数。这里使用了 override
关键字,它是 C++11 引入的,用于显式表明该函数是重写基类的虚函数,有助于编译器检查重写的正确性。如果不小心拼写错误函数名,编译器会报错,而不是创建一个新的函数。
虚函数的继承规则
- 自动继承虚属性
- 当派生类从基类继承时,基类中的虚函数会自动成为派生类的虚函数,即使在派生类中没有再次使用
virtual
关键字声明。例如:
- 当派生类从基类继承时,基类中的虚函数会自动成为派生类的虚函数,即使在派生类中没有再次使用
class Base {
public:
virtual void print() {
std::cout << "Base::print" << std::endl;
}
};
class Derived : public Base {
public:
void print() {
std::cout << "Derived::print" << std::endl;
}
};
在上述代码中,Derived
类的 print
函数虽然没有再次声明为 virtual
,但它依然是虚函数,因为它继承自 Base
类的虚函数 print
。这是 C++ 虚函数继承的一个重要特性,使得在派生类层次结构中,虚函数的多态行为能够自然延续。
- 重写的规则
- 函数签名必须一致:派生类中重写虚函数时,函数签名(函数名、参数列表、常量性)必须与基类中的虚函数完全一致。例如:
class Shape {
public:
virtual double area() const {
return 0.0;
}
};
class Circle : public Shape {
public:
double area() const override {
// 假设半径为 5
return 3.14 * 5 * 5;
}
};
这里,Circle
类的 area
函数重写了 Shape
类的 area
函数。注意,两个函数都是 const
成员函数,且参数列表和函数名完全相同。如果在 Circle
类的 area
函数中不小心遗漏了 const
,编译器会将其视为一个新的函数,而不是重写 Shape
类的 area
函数。
- 返回类型的协变:在 C++ 中,重写虚函数时,返回类型可以是基类虚函数返回类型的派生类型,这被称为返回类型的协变(covariant return types)。例如:
class BaseObject {
};
class DerivedObject : public BaseObject {
};
class Base {
public:
virtual BaseObject* createObject() {
return new BaseObject();
}
};
class Derived : public Base {
public:
DerivedObject* createObject() override {
return new DerivedObject();
}
};
在这个例子中,Derived
类的 createObject
函数返回 DerivedObject*
,它是 Base
类 createObject
函数返回类型 BaseObject*
的派生类型。这种返回类型的协变特性增强了多态性的灵活性,使得派生类能够返回更具体类型的对象。
- 访问控制与虚函数继承
- 基类虚函数的访问控制属性在派生类中重写时会有所影响。例如,如果基类的虚函数是
public
的,派生类重写的虚函数可以是public
或protected
,但不能是private
。因为如果派生类将重写的虚函数设为private
,那么通过基类指针或引用就无法访问到该函数,这与多态性的初衷相违背。例如:
- 基类虚函数的访问控制属性在派生类中重写时会有所影响。例如,如果基类的虚函数是
class A {
public:
virtual void func() {
std::cout << "A::func" << std::endl;
}
};
class B : public A {
protected:
void func() override {
std::cout << "B::func" << std::endl;
}
};
在上述代码中,B
类重写的 func
函数是 protected
的。虽然通过 B
类的对象不能直接调用 func
,但通过 A
类指针或引用指向 B
类对象时,仍然可以调用 B
类重写的 func
函数。
- 相反,如果基类虚函数是
protected
,派生类重写的虚函数可以是protected
或private
,但不能是public
。因为派生类不能扩大基类成员的访问权限。例如:
class C {
protected:
virtual void anotherFunc() {
std::cout << "C::anotherFunc" << std::endl;
}
};
class D : public C {
private:
void anotherFunc() override {
std::cout << "D::anotherFunc" << std::endl;
}
};
在这个例子中,D
类重写的 anotherFunc
函数是 private
的,这是符合访问控制规则的。
- 虚函数与多重继承 在多重继承的情况下,虚函数的继承规则会变得稍微复杂一些。假设有多个基类,每个基类都有虚函数,派生类继承自这些基类并可能重写虚函数。例如:
class Base1 {
public:
virtual void print1() {
std::cout << "Base1::print1" << std::endl;
}
};
class Base2 {
public:
virtual void print2() {
std::cout << "Base2::print2" << std::endl;
}
};
class Derived : public Base1, public Base2 {
public:
void print1() override {
std::cout << "Derived::print1" << std::endl;
}
void print2() override {
std::cout << "Derived::print2" << std::endl;
}
};
在这个多重继承的例子中,Derived
类继承自 Base1
和 Base2
两个基类,并分别重写了它们的虚函数 print1
和 print2
。通过 Derived
类对象或指向 Derived
类对象的 Base1
或 Base2
指针/引用,可以调用相应的重写函数。
然而,如果不同基类中有相同签名的虚函数,可能会导致命名冲突。例如:
class BaseA {
public:
virtual void commonFunc() {
std::cout << "BaseA::commonFunc" << std::endl;
}
};
class BaseB {
public:
virtual void commonFunc() {
std::cout << "BaseB::commonFunc" << std::endl;
}
};
class DerivedAB : public BaseA, public BaseB {
public:
void commonFunc() override {
std::cout << "DerivedAB::commonFunc" << std::endl;
}
};
在这种情况下,DerivedAB
类重写了两个基类中相同签名的 commonFunc
函数。通过 DerivedAB
类对象调用 commonFunc
不会有问题,但如果通过 BaseA
或 BaseB
指针/引用调用,就需要明确指定使用哪个基类的版本,例如:
DerivedAB obj;
BaseA* ptrA = &obj;
ptrA->BaseA::commonFunc(); // 调用 BaseA 的 commonFunc
BaseB* ptrB = &obj;
ptrB->BaseB::commonFunc(); // 调用 BaseB 的 commonFunc
- 虚函数与菱形继承 菱形继承是多重继承中的一种特殊情况,它可能会导致数据冗余和歧义问题。当涉及虚函数时,同样需要注意一些规则。例如:
class GrandParent {
public:
virtual void print() {
std::cout << "GrandParent::print" << std::endl;
}
};
class Parent1 : public GrandParent {
public:
void print() override {
std::cout << "Parent1::print" << std::endl;
}
};
class Parent2 : public GrandParent {
public:
void print() override {
std::cout << "Parent2::print" << std::endl;
}
};
class Child : public Parent1, public Parent2 {
public:
void print() override {
std::cout << "Child::print" << std::endl;
}
};
在这个菱形继承结构中,Child
类从 Parent1
和 Parent2
继承,而 Parent1
和 Parent2
又都从 GrandParent
继承。Child
类重写了 GrandParent
类的 print
虚函数。
为了避免数据冗余和歧义,通常使用虚继承(virtual inheritance)。例如:
class GrandParent {
public:
virtual void print() {
std::cout << "GrandParent::print" << std::endl;
}
};
class Parent1 : virtual public GrandParent {
public:
void print() override {
std::cout << "Parent1::print" << std::endl;
}
};
class Parent2 : virtual public GrandParent {
public:
void print() override {
std::cout << "Parent2::print" << std::endl;
}
};
class Child : public Parent1, public Parent2 {
public:
void print() override {
std::cout << "Child::print" << std::endl;
}
};
在这种情况下,通过虚继承,GrandParent
的数据成员在 Child
类中只会有一份拷贝,并且虚函数的调用也能正确解析,确保多态行为的一致性。
- 纯虚函数与抽象类
纯虚函数(pure virtual function)是一种特殊的虚函数,它在基类中没有实现,仅提供函数声明,并要求派生类必须重写它。纯虚函数通过在函数声明后加上
= 0
来定义。例如:
class Shape {
public:
virtual double area() const = 0;
};
包含纯虚函数的类被称为抽象类(abstract class)。抽象类不能实例化对象,它主要作为其他派生类的基类,为派生类提供一个通用的接口。例如,Shape
类是一个抽象类,因为它包含纯虚函数 area
。
派生类必须重写基类中的纯虚函数,否则派生类也会成为抽象类。例如:
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
};
在 Rectangle
类中,重写了 Shape
类的纯虚函数 area
,因此 Rectangle
类不再是抽象类,可以实例化对象。
虚函数表与动态绑定
- 虚函数表(vtable) C++ 实现虚函数的动态绑定是通过虚函数表(vtable)机制。每个包含虚函数的类都有一个虚函数表,该表是一个函数指针数组,其中每个元素指向类中的一个虚函数。
当创建一个包含虚函数的类对象时,对象的内存布局中会有一个指向虚函数表的指针(vptr)。例如,对于前面定义的 Animal
类:
class Animal {
public:
virtual void speak() {
std::cout << "I am an animal" << std::endl;
}
};
当创建 Animal
类的对象时,对象的内存布局大致如下:
[ vptr | other data members ]
vptr
指向 Animal
类的虚函数表,该虚函数表中包含 speak
函数的地址。
当派生类继承自包含虚函数的基类时,派生类会继承基类的虚函数表,并根据需要修改虚函数表中的指针。例如,对于 Dog
类:
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
Dog
类对象的内存布局为:
[ vptr | other data members ]
Dog
类的虚函数表与 Animal
类的虚函数表结构相似,但 speak
函数的指针指向 Dog
类重写的 speak
函数版本。
- 动态绑定过程 当通过基类指针或引用调用虚函数时,动态绑定就会发生。例如:
Animal* animalPtr = new Dog();
animalPtr->speak();
在上述代码中,animalPtr
是一个 Animal
类指针,但实际指向一个 Dog
类对象。在运行时,首先通过 animalPtr
找到 Dog
类对象的 vptr
,然后通过 vptr
找到 Dog
类的虚函数表,最后在虚函数表中找到 speak
函数的地址并调用它,从而实现了动态绑定,调用的是 Dog
类重写的 speak
函数。
虚函数继承规则的应用场景
- 图形绘制系统
在一个图形绘制系统中,可以定义一个基类
Shape
,并将draw
函数定义为虚函数:
class Shape {
public:
virtual void draw() const = 0;
};
class Circle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a circle" << std::endl;
}
};
class Rectangle : public Shape {
public:
void draw() const override {
std::cout << "Drawing a rectangle" << std::endl;
}
};
然后可以通过一个 Shape
指针数组来存储不同类型的图形对象,并调用它们的 draw
函数:
Shape* shapes[2];
shapes[0] = new Circle();
shapes[1] = new Rectangle();
for (int i = 0; i < 2; ++i) {
shapes[i]->draw();
}
这样,通过虚函数的继承和动态绑定,实现了不同图形对象的统一绘制接口,使得系统具有良好的扩展性和灵活性。
- 游戏开发中的角色行为
在游戏开发中,假设有一个基类
Character
,包含虚函数performAction
:
class Character {
public:
virtual void performAction() {
std::cout << "Character is performing a default action" << std::endl;
}
};
class Warrior : public Character {
public:
void performAction() override {
std::cout << "Warrior is attacking" << std::endl;
}
};
class Mage : public Character {
public:
void performAction() override {
std::cout << "Mage is casting a spell" << std::endl;
}
};
在游戏逻辑中,可以通过 Character
指针或引用来调用不同角色的 performAction
函数,实现游戏角色行为的多态性。
总结虚函数继承规则的要点
- 基类虚函数自动成为派生类虚函数,派生类重写虚函数时函数签名(除返回类型协变外)必须一致。
- 访问控制在重写虚函数时需遵循一定规则,不能随意扩大或缩小访问权限。
- 多重继承和菱形继承中虚函数的继承需要注意命名冲突和数据冗余问题,可通过适当的方式解决。
- 纯虚函数使基类成为抽象类,派生类必须重写纯虚函数才能实例化。
- 虚函数的动态绑定通过虚函数表机制实现,理解这一机制有助于深入掌握虚函数的工作原理。
虚函数的继承规则是 C++ 面向对象编程中实现多态性的关键,熟练掌握这些规则对于编写高效、可维护和可扩展的 C++ 程序至关重要。无论是在大型系统开发还是小型项目中,合理运用虚函数继承规则都能提升代码的质量和灵活性。