概述
继承是 C++ 面向对象编程的核心特性之一,它允许在保持原有类特性的基础上进行扩展,从而构建层次化的类结构。掌握继承机制对于编写高效、可维护的代码至关重要。
一、继承的概念及定义
继承通过派生类(子类)复用基类(父类)的成员。定义格式通常为 class Derived : public Base。其中 public 是继承方式,决定了基类成员在派生类中的访问权限。C++ 支持三种继承方式:public、protected 和 private。
- public 继承:基类的 public/protected 成员在派生类中保持原有的访问权限。
- protected 继承:基类的 public 成员变为 protected,protected 成员保持 protected。
- private 继承:基类的所有成员在派生类中都变为 private。
![图示:继承关系示意图]
例如,基类的 public 成员在 public 继承下仍是 public,但在 protected 继承下则变为 protected。
二、基类和派生类对象赋值转换
派生类对象可以赋值给基类对象、指针或引用,这被称为'向上转型'。由于派生类包含基类的所有成员,这种转换是安全的且自动完成。形象地说,这就像把派生类中属于基类的那部分'切'出来进行赋值,称为切片(Slicing)。
Student sobj;
Person pobj = sobj; // 对象切片
Person* pp = &sobj; // 指针向上转型
Person& rp = sobj; // 引用向上转型
反之,基类对象不能直接赋值给派生类对象。若需将基类指针转换为派生类指针(向下转型),必须谨慎处理。如果基类是多态类型,建议使用 dynamic_cast 进行运行时类型检查以确保安全。
向上转型永远安全,是配合虚函数实现多态的基础;向下转型存在风险,仅在确认对象实际类型为派生类时才应使用。
![图示:对象切片示意图]
三、继承中的作用域
在继承体系中,基类和派生类拥有各自独立的作用域。当子类和父类存在同名成员时,子类成员会屏蔽父类对同名成员的直接访问,这种现象称为隐藏(Hiding)。
class Person {
protected:
int _num = 111;
};
class Student : public Person {
protected:
int _num = 999;
public:
void Print() {
cout << "Person::_num: " << Person::_num << endl;
cout << "Student::_num: " << _num << endl;
}
};
在 Student 的 Print 函数中,通过作用域解析运算符 :: 明确访问父类的 _num 成员。实际编程中应尽量避免在继承体系里定义同名成员,以免造成代码理解和维护上的困难。
四、派生类的默认成员函数
编译器会自动为派生类生成一些默认成员函数,其执行顺序与初始化逻辑如下:
-
构造函数:派生类构造函数必须先调用基类构造函数来初始化基类部分。若基类没有默认构造函数,必须在派生类构造函数的初始化列表中显式调用合适的基类构造函数。
- 编译过程中,先初始化基类,再初始化派生类。派生类只初始化自己的成员。
-
拷贝构造函数:派生类的拷贝构造函数需要调用基类的拷贝构造函数,完成基类成员的拷贝初始化。如果基类无拷贝构造函数,默认调用默认构造函数。
-
赋值运算符函数:派生类的
operator=必须调用基类的operator=来完成基类成员的复制,避免隐藏基类逻辑。 -
析构函数:派生类的析构函数被调用完成后,会自动调用基类的析构函数。清理顺序遵循'先派生类后基类',确保资源正确释放。无需手动调用基类析构函数,否则会导致二次析构错误。
![图示:构造与析构顺序]
五、继承与友元
友元关系在继承体系中是不能自动继承的。基类的友元不能访问子类的私有和保护成员。简单来说,'父亲的朋友不是你的朋友',除非你单独声明其为朋友的友元。
class Student;
class Person {
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name;
};
class Student : public Person {
protected:
int _stunNum;
};
void Display(const Person& p, const Student& s) {
cout << p._name << endl;
cout << s._stunNum << endl;
}
这里 Display 函数作为 Person 类的友元,能访问 Person 的保护成员,但对于 Student 类,它并不具备天然访问其保护成员的权限。
六、继承与静态成员
若基类定义了 static 静态成员,在整个继承体系中,无论派生出多少个子类,都只会存在一个该静态成员的实例。所有派生类共享这份数据。
class Person {
public:
Person() { ++_count; }
public:
static int _count;
};
int Person::_count = 0;
class Student : public Person {};
Person 类中的 _count 静态成员,在 Student 类及其他派生类中都是共享的,可通过类名或对象来访问。
七、复杂的菱形继承及菱形虚拟继承
(一)单继承与多继承
单继承结构简单明了。多继承则允许一个子类有两个或以上直接父类,增加了灵活性但也引入了复杂性。
![图示:多继承关系]
(二)菱形继承
菱形继承是多继承的一种特殊情况。例如 Assistant 同时继承自 Student 和 Teacher,而这两者又都继承自 Person。此时 Assistant 中会出现两份 Person 成员的拷贝,导致数据冗余和二义性问题。访问 Person 成员时,编译器无法确定访问的是哪一个副本。
![图示:菱形继承问题]
(三)菱形虚拟继承
为解决上述问题,引入虚拟继承。在中间层继承时使用 virtual 关键字(如 class Student : virtual public Person),确保最终派生类对象中只存在一份 Person 成员的拷贝。
虚拟继承通过虚基表指针和虚基表来管理基类成员的存储和访问。虽然解决了冗余问题,但也增加了一定的实现复杂度。
![图示:菱形虚拟继承内存模型]
八、总结与反思
继承是一把双刃剑。它极大地促进了代码复用,构建了清晰的类层次结构,但也带来了复杂度和维护难度。这也是许多其他语言(如 Java)不采用多继承的原因。
在实际编程中,我们要谨慎选择继承和组合:
- 继承是'is-a'关系,适用于体现类之间层次关系或实现多态的场景。
- 组合是'has-a'关系,优先考虑对象组合,因为它耦合度低,代码维护性好,更符合'黑箱复用'原则。
![图示:组合优于继承]
代码示例:虚拟继承测试
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class A {
public:
A(const char* s) { cout << s << endl; }
~A() {}
};
class B : virtual public A {
public:
B(const char* sa, const char* sb) : A(sa) { cout << sb << endl; }
};
class C : virtual public A {
public:
C(const char* sa, const char* sb) : A(sa) { cout << sb << endl; }
};
class D : public B, public C {
public:
D(const char* sa, const char* sb, const char* sc, const char* sd)
: B(sa, sb), C(sa, sc), A(sa) { cout << sd << endl; }
};
int main() {
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
此示例展示了虚拟继承中基类构造函数的调用顺序,确保 A 类只被初始化一次。


