继承基础
定义与作用
继承允许一个类(派生类)复用另一个类(基类)的成员,是面向对象编程中实现代码复用和"is-a"关系的基础机制:
class Person { /* 姓名、年龄 */ };
class Student : public Person { /* 学号、GPA */ }; // Student is-a Person
继承的核心价值:代码复用 + 多态接口。
核心原理
三种继承方式下的访问权限变化
派生类对象的构造与析构过程
构造顺序:基类 → 派生类成员(声明顺序) → 派生类构造函数体 析构顺序:完全相反
完整示例
示例一:学生信息系统的继承层次
场景说明:设计 Person → Student → GraduateStudent 的继承链,展示属性和行为的复用。
#include <iostream>
#include <string>
#include <vector>
class Person {
public:
Person(std::string name, int age)
: name_(std::move(name)), age_(age) {}
const std::string& getName() const { return name_; }
int getAge() const { return age_; }
virtual void introduce() const {
std::cout << "我叫 " << name_ << ",今年 " << age_ << " 岁。" << std::endl;
}
virtual ~Person() = default;
protected:
std::string name_;
int age_;
};
class Student : public Person {
public:
Student(std::string name, int age, std::string student_id, std::string major)
: Person(std::move(name), age), // 调用基类构造函数
student_id_(std::move(student_id)),
major_(std::move(major)) {}
const std::string& getStudentID() const { return student_id_; }
const std::string& getMajor() const { return major_; }
void introduce() const override {
std::cout << "我是学生 " << name_ << ",学号 " << student_id_
<< ",专业 " << major_ << "。" << std::endl;
}
protected:
std::string student_id_;
std::string major_;
};
class GraduateStudent : public Student {
public:
GraduateStudent(std::string name, int age, std::string student_id,
std::string major, std::string advisor)
: Student(std::move(name), age, std::move(student_id), std::move(major)),
advisor_(std::move(advisor)) {}
void introduce() const override {
std::cout << "我是研究生 " << name_ << ",学号 " << student_id_
<< ",专业 " << major_ << ",导师 " << advisor_ << "。" << std::endl;
}
void doResearch() const {
std::cout << name_ << " 正在导师 " << advisor_ << " 指导下做研究。" << std::endl;
}
private:
std::string advisor_;
};
int main() {
std::cout << "===== 继承层次演示 =====" << std::endl;
Person person("张老师", 45);
person.introduce();
Student student("张三", 20, "2024001", "计算机科学");
student.introduce();
GraduateStudent grad("李四", 24, "2024002", "人工智能", "刘教授");
grad.introduce();
grad.doResearch();
// 基类指针指向派生类对象
std::cout << "\n===== 多态调用 =====" << std::endl;
const Person& ref = grad;
ref.introduce(); // 动态绑定:调用 GraduateStudent::introduce
return 0;
}
预期输出:
===== 继承层次演示 =====
我叫 张老师,今年 45 岁。
我是学生 张三,学号 2024001,专业 计算机科学。
我是研究生 李四,学号 2024002,专业 人工智能,导师 刘教授。
李四 正在导师 刘教授 指导下做研究。
===== 多态调用 =====
我是研究生 李四,学号 2024002,专业 人工智能,导师 刘教授。
逐段分析:
Student通过public Person继承,复用name_、age_、getName()、getAge()- 派生类构造函数必须在初始化列表中调用基类构造函数:
Person(std::move(name), age) introduce()是虚函数——const Person& ref = grad; ref.introduce();调用的是GraduateStudent的版本(动态绑定)name_、student_id_、major_声明为protected——派生类可以访问,外部不可访问- 继承层次:
Person→Student→GraduateStudent,每层增加新的属性和行为
示例二:继承构造函数(C++11)
场景说明:使用 C++11 的 using 声明继承基类的构造函数,避免手写转发构造。
#include <iostream>
#include <string>
class Employee {
public:
Employee(std::string name) : name_(std::move(name)) {
std::cout << "Employee 构造: " << name_ << std::endl;
}
Employee(std::string name, int id)
: name_(std::move(name)), id_(id) {
std::cout << "Employee 构造: " << name_ << " (ID:" << id_ << ")" << std::endl;
}
virtual ~Employee() = default;
const std::string& getName() const { return name_; }
protected:
std::string name_;
int id_ = 0;
};
class Engineer : public Employee {
public:
using Employee::Employee; // C++11 继承构造函数
void code() const {
std::cout << name_ << " 正在写代码..." << std::endl;
}
};
int main() {
std::cout << "===== 继承构造函数 =====" << std::endl;
// 直接使用继承来的基类构造函数
Engineer e1("张三");
e1.code();
Engineer e2("李四", 10086);
e2.code();
return 0;
}
预期输出:
===== 继承构造函数 =====
Employee 构造: 张三
张三 正在写代码...
Employee 构造: 李四 (ID:10086)
李四 正在写代码...
逐段分析:
using Employee::Employee;将基类的所有构造函数"提升"到派生类——不必手写转发- 编译器为每个基类构造函数生成一个对应的派生类构造函数,参数完全一致
- 派生类自己的成员变量(如果有)会使用其类内初始化器
- 这是 C++11 引入的语法糖,替代了 C++98 中手写
Engineer(string name) : Employee(name) {}的样板代码
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 派生类构造函未调用基类构造函数 | 基类部分未初始化 | 在初始化列表中显式调用基类构造函数 |
| private 继承后外部无法访问基类接口 | 编译错误 | 用 public 继承(默认 is-a 关系)或 using 声明 |
| 派生类中定义了与基类同名的非虚函数 | 隐藏(非覆盖) | 用 override 标记虚函数覆盖 |
| 继承构造函数的参数与派生类成员初始化冲突 | 派生类成员使用默认值 | 明确 NSDMI 或重写构造函数 |
| 基类没有默认构造函数时派生类省略基类初始化 | 编译错误 | 必须显式调用基类的有参构造函数 |
常见面试问题
三种继承方式的区别?各自的使用场景?——public 继承保持基类访问权限不变(最常用,表达 is-a)。protected 继承将 public 降为 protected(表达"实现复用但非 is-a")。private 继承将所有基类成员变为 private(表达"用...实现",优先用组合替代)。
派生类对象的构造和析构顺序?——构造:基类 → 成员(声明顺序) → 构造函数体。析构:完全相反。这保证了派生类析构时可以安全使用基类资源。
继承构造函数做了什么?什么场景使用?——
using Base::Base(C++11)让编译器自动为派生类生成转发到基类的构造函数。用于派生类没有新增需要初始化参数的成员时。派生类中定义与基类同名的非虚函数会怎样?——隐藏基类同名函数,即使是不同参数签名。要覆盖虚函数必须用
override标记。"is-a" 和 "has-a" 的区别?分别用什么表达?——is-a 是继承关系(
Student is-a Person→ public 继承)。has-a 是组合关系(Car has-a Engine→ 成员对象)。优先组合而非继承。
小结
- public 继承表达 is-a 关系,是最常用的继承方式
- 派生类构造时必须先在初始化列表调用基类构造函数
- 构造顺序:基类 → 成员 → 函数体;析构顺序完全相反
using Base::Base(C++11)继承构造函数,消除转发样板代码- 优先组合而非继承——继承是 C++ 中最强的耦合关系