在C++面向对象编程中,继承是最强大的特性之一。它允许我们基于已有类创建新类,新类将自动获得父类的属性和方法。这种机制就像生物学中的遗传——子代会自然获得父代的特征。
从技术实现角度看,继承通过派生类(Derived Class)和基类(Base Class)的关系来实现。派生类继承基类的成员变量和成员函数,同时可以添加自己特有的成员。这种关系形成了类之间的层次结构,是代码复用的重要手段。
注意:虽然继承能减少代码重复,但过度使用会导致类之间耦合度过高。建议遵循"组合优于继承"的原则,只在确实存在is-a关系时使用继承。
C++提供了三种继承方式,通过访问说明符控制基类成员在派生类中的可见性:
cpp复制class Base {
public:
int public_member;
protected:
int protected_member;
private:
int private_member;
};
class Derived : public Base {
// public_member保持public
// protected_member保持protected
// private_member不可访问
};
public继承表示"是一个"的关系,派生类对象可以被当作基类对象使用。这是最符合Liskov替换原则的继承方式。
cpp复制class Derived : protected Base {
// public_member变为protected
// protected_member保持protected
// private_member不可访问
};
protected继承下,基类的public成员在派生类中变为protected。这种继承方式较少使用,通常只在特定设计模式中出现。
cpp复制class Derived : private Base { // 默认继承方式
// public_member变为private
// protected_member变为private
// private_member不可访问
};
private继承表示"根据...实现"的关系,它只是实现细节,不反映接口上的is-a关系。从设计角度考虑,这种情况下使用组合(composition)通常更合适。
当创建派生类对象时,构造函数的调用顺序为:
cpp复制class Base {
public:
Base() { cout << "Base constructor" << endl; }
};
class Member {
public:
Member() { cout << "Member constructor" << endl; }
};
class Derived : public Base {
Member m;
public:
Derived() { cout << "Derived constructor" << endl; }
};
// 输出顺序:
// Base constructor
// Member constructor
// Derived constructor
析构函数的调用顺序与构造函数完全相反:
cpp复制class Base {
public:
Base(int x) { /*...*/ }
};
class Derived : public Base {
public:
Derived(int x) : Base(x) { /*...*/ }
};
派生类构造函数只能初始化自己的直接基类,不能初始化基类的基类。
虚基类的构造函数由最派生类直接调用。
C++支持一个类同时继承多个基类:
cpp复制class InputDevice { /*...*/ };
class OutputDevice { /*...*/ };
class IODevice : public InputDevice, public OutputDevice {
// 同时继承两个基类
};
多重继承虽然强大,但容易导致设计复杂化,应谨慎使用。
当多个基类继承自同一个祖先类时,会产生菱形继承结构:
code复制 Base
/ \
Derived1 Derived2
\ /
MostDerived
这种情况下,MostDerived类会包含多份Base类的子对象,可能导致数据冗余和二义性。
使用虚继承可以解决菱形继承问题:
cpp复制class Base { /*...*/ };
class Derived1 : virtual public Base { /*...*/ };
class Derived2 : virtual public Base { /*...*/ };
class MostDerived : public Derived1, public Derived2 { /*...*/ };
虚继承确保在菱形继承结构中,最派生类只包含一个共享的基类子对象。
提示:虚继承会增加对象大小和访问开销,只在确实需要解决菱形继承问题时使用。
当派生类中使用一个名称时,编译器按以下顺序查找:
派生类中定义的名称会隐藏基类中的同名名称,即使参数列表不同:
cpp复制class Base {
public:
void func(int) { /*...*/ }
};
class Derived : public Base {
public:
void func(double) { /*...*/ } // 隐藏了Base::func(int)
};
Derived d;
d.func(1); // 调用Derived::func(double)
要访问被隐藏的基类成员,可以使用作用域解析运算符:
cpp复制d.Base::func(1); // 调用Base::func(int)
如果希望在派生类中保留基类的函数重载,可以使用using声明:
cpp复制class Derived : public Base {
public:
using Base::func; // 引入Base中的所有func重载
void func(double) { /*...*/ }
};
Derived d;
d.func(1); // 调用Base::func(int)
d.func(1.0); // 调用Derived::func(double)
将派生类指针/引用转换为基类指针/引用是安全的,编译器会自动完成:
cpp复制Derived d;
Base* pb = &d; // 向上转型
Base& rb = d; // 向上转型
将基类指针/引用转换为派生类指针/引用需要使用dynamic_cast(需要多态支持):
cpp复制Base* pb = new Derived;
Derived* pd = dynamic_cast<Derived*>(pb);
if (pd) { /* 转换成功 */ }
警告:应尽量避免向下转型,这通常是设计存在问题的信号。如果必须使用,优先选择dynamic_cast而非static_cast,因为前者会进行运行时类型检查。
typeid可以在运行时获取对象的实际类型信息(需要包含
cpp复制#include <typeinfo>
Base* pb = new Derived;
cout << typeid(*pb).name() << endl; // 输出实际类型名
注意:要使typeid返回派生类类型,基类必须有虚函数(多态类型)。
派生类对象应该能够替换基类对象使用,而不破坏程序的正确性。这意味着:
在以下情况考虑使用组合而非继承:
虽然本文聚焦于继承,但继承的真正威力在与多态结合时才能完全展现。多态允许我们通过基类接口操作派生类对象,这是面向对象设计的核心概念之一。
当基类函数声明为virtual时,派生类可以重写(override)这些函数,实现运行时多态:
cpp复制class Shape {
public:
virtual void draw() const = 0; // 纯虚函数
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() const override { /* 实现圆形的绘制 */ }
};
class Square : public Shape {
public:
void draw() const override { /* 实现正方形的绘制 */ }
};
void drawAll(const vector<Shape*>& shapes) {
for (auto s : shapes) {
s->draw(); // 多态调用
}
}
这种设计使得我们可以添加新的Shape派生类而不需要修改drawAll函数,符合开闭原则(对扩展开放,对修改关闭)。