de风——【从零开始学C++】(十四)继承:让代码复用不再难!

📌 专栏说明:本专栏「从零开始学C++」面向完全零基础的同学,每篇文章聚焦一个核心概念,用最通俗的语言 + 完整可运行的代码帮你把知识点吃透。本文主题:继承(Inheritance)。
C++相关专题链接🔗:
🧭 前言
在学会了类与对象之后,相信你已经能写出一些简单的程序了。但随着代码量的增加,你会发现:不同的类之间,往往有很多相似甚至相同的成员变量和成员函数。
比如你写了一个"动物"类,里面有名字、年龄、叫声等属性;之后你还要写"狗"类、"猫"类……难道每次都要把这些公共属性重新写一遍吗?
当然不用!继承 就是 C++ 给出的解决方案,让你可以复用已有的代码,在此基础上扩展新功能。
一、继承是什么?
简介与作用
继承(Inheritance) 是面向对象编程(OOP)三大特性之一。
- 父类(基类):被继承的类,提供公共的属性和行为。
- 子类(派生类):继承父类,在拥有父类所有"允许访问"成员的同时,还可以添加自己独有的成员。
继承的核心价值:
- 代码复用:公共代码写一次,多处使用。
- 扩展性强:子类可以在父类基础上添加新功能,不影响父类。
- 逻辑清晰:用"is-a"关系建模,狗 is-a 动物,学生 is-a 人……
💡 通俗来说:继承就像"继承家业",父亲留下的东西(属性+方法),儿子可以直接用,还能自己再添置新家产。
代码示例
示例一:最基础的继承写法
#include <iostream>
#include <string>
using namespace std;
// 父类(基类)
class Animal {
public:
string name;
int age;
void eat() {
cout << name << " 正在吃东西" << endl;
}
void sleep() {
cout << name << " 正在睡觉" << endl;
}
};
// 子类(派生类),Dog 继承自 Animal
class Dog : public Animal {
public:
// Dog 独有的方法
void bark() {
cout << name << " 汪汪汪!" << endl;
}
};
int main() {
Dog dog;
dog.name = "小黑"; // 继承自 Animal
dog.age = 3; // 继承自 Animal
dog.eat(); // 继承自 Animal
dog.bark(); // Dog 自己的方法
return 0;
}
输出结果:
小黑 正在吃东西
小黑 汪汪汪!
✅ 可以看到,
Dog类没有定义name、age、eat(),却可以直接使用,这就是继承的魔力。
二、继承的三种访问方式
简介与作用
继承时,冒号后面可以加 public、protected、private 三个关键字,它们决定了父类成员在子类中的可见范围。
| 父类成员权限 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
📌 日常使用最多的是
public继承,protected和private继承较少见,初学阶段掌握public继承即可。
记住一个关键规则:父类的 private 成员,子类永远无法直接访问(无论哪种继承方式)。
实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员 在派⽣类的访问⽅式==Min(成员在基类的访问限定符,继承⽅式),public >protected> private。
代码示例
示例二:三种继承方式的访问权限对比
#include <iostream>
using namespace std;
class Base {
public:
int pub = 1;
protected:
int pro = 2;
private:
int pri = 3; // 任何继承方式下,子类都无法直接访问
};
// public 继承:pub->public, pro->protected
class PublicDerived : public Base {
public:
void show() {
cout << "pub = " << pub << endl; // ✅ 可访问
cout << "pro = " << pro << endl; // ✅ 可访问
// cout << pri << endl; // ❌ 编译错误!private 不可访问
}
};
// protected 继承:pub->protected, pro->protected
class ProtectedDerived : protected Base {
public:
void show() {
cout << "pub = " << pub << endl; // ✅ 可访问(降为protected)
cout << "pro = " << pro << endl; // ✅ 可访问
}
};
int main() {
PublicDerived pd;
pd.show();
cout << "外部访问 pd.pub = " << pd.pub << endl; // ✅ public 继承,pub 还是 public
ProtectedDerived prd;
prd.show();
// cout << prd.pub << endl; // ❌ protected 继承后,pub 变成 protected,外部不可访问
return 0;
}
三、继承中的构造与析构顺序
简介与作用
当我们创建一个子类对象时,父类的构造函数和子类的构造函数都会被调用。同理,对象销毁时,析构函数也都会被调用。
调用顺序规则:
- 构造:先执行父类构造函数,再执行子类构造函数(从上到下)
- 析构:先执行子类析构函数,再执行父类析构函数(从下到上,与构造相反)
💡 这很好理解:儿子出生之前,父亲必须先存在;儿子去世之后,父亲才离开。
代码示例
示例三:观察构造与析构的调用顺序cpp
#include <iostream>
using namespace std;
class Father {
public:
Father() {
cout << "Father 构造函数被调用" << endl;
}
~Father() {
cout << "Father 析构函数被调用" << endl;
}
};
class Son : public Father {
public:
Son() {
cout << "Son 构造函数被调用" << endl;
}
~Son() {
cout << "Son 析构函数被调用" << endl;
}
};
int main() {
cout << "--- 创建 Son 对象 ---" << endl;
Son s;
cout << "--- 对象即将销毁 ---" << endl;
return 0; // s 离开作用域,析构函数自动调用
}
输出结果:
--- 创建 Son 对象 ---
Father 构造函数被调用
Son 构造函数被调用
--- 对象即将销毁 ---
Son 析构函数被调用
Father 析构函数被调用
示例四:子类构造函数中调用父类有参构造(初始化列表)
如果父类构造函数需要参数,子类必须在初始化列表中显式调用:
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
string name;
int age;
Person(string n, int a)
:name(n)
,age(a)
{
cout << "Person 构造:" << name << ", " << age << endl;
}
};
class Student : public Person {
public:
int studentID;
// 子类构造函数通过初始化列表调用父类有参构造
Student(string n, int a, int id)
: Person(n, a)//关键点
, studentID(id)
{
cout << "Student 构造:学号=" << studentID << endl;
}
void show() {
cout << "姓名:" << name << " 年龄:" << age
<< " 学号:" << studentID << endl;
}
};
int main()
{
Student stu("张三", 18, 1001);
stu.show();
return 0;
}
输出结果:
Person 构造:张三, 18
Student 构造:学号=1001
姓名:张三 年龄:18 学号:1001
四、函数重写(Override)
简介与作用
子类可以重新定义父类中已有的函数,这叫做函数重写(也叫覆盖)。重写后,通过子类对象调用该函数时,执行的是子类版本的函数。
关键点:
- 函数名、参数列表、返回值类型必须与父类完全一致。
- 建议加上
override关键字(C++11 起),让编译器帮你检查是否真的重写了父类函数,避免拼错函数名等低级错误。
💡 注意与**函数重载(Overload)**区分:重载是同一个类中同名不同参的多个函数;重写是子类对父类同名同参函数的重新定义。
代码示例
示例五:子类重写父类的方法
#include <iostream>
using namespace std;
class Shape {
public:
void draw()
{
cout << "画一个形状" << endl;
}
};
class Circle : public Shape
{
public:
// 重写父类的 draw()
void draw()
{
cout << "画一个圆形 " << endl;
}
};
class Rectangle : public Shape
{
public:
void draw()
{
cout << "画一个矩形 " << endl;
}
};
int main() {
Shape s;
s.draw(); // 调用父类版本
Circle c;
c.draw(); // 调用 Circle 的重写版本
Rectangle r;
r.draw(); // 调用 Rectangle 的重写版本
return 0;
}
输出结果:
画一个形状
画一个圆形 ⭕
画一个矩形 ▭
示例六:使用 override 关键字(推荐写法)
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() { // virtual 表示这是虚函数,子类可以重写
cout << "动物在叫..." << endl;
}
};
class Cat : public Animal {
public:
void speak() override { // override 明确告诉编译器:这是重写
cout << "猫:喵喵喵~" << endl;
}
};
class Dog : public Animal {
public:
void speak() override {
cout << "狗:汪汪汪!" << endl;
}
};
int main() {
Cat cat;
Dog dog;
cat.speak();
dog.speak();
return 0;
}
输出结果:
猫:喵喵喵~
狗:汪汪汪!
📌 关于
virtual虚函数与多态,将在后续文章中深入讲解,这里先有个印象即可。
⚠️ 经典 Bug:误以为重写但实际没有生效
class Base {
public:
void func(int x) {
cout << "Base::func(int)" << endl;
}
};
class Derived : public Base {
public:
// 注意:参数类型不同,这是【重载】而非【重写】!
void func(double x) {
cout << "Derived::func(double)" << endl;
}
};
int main() {
Derived d;
d.func(1); // 调用的是 Base::func(int),而非 Derived::func(double)!
return 0;
}
解决方法:使用 override 关键字,如果重写不正确编译器会直接报错,帮你发现问题。
五、子类访问父类的同名成员
简介与作用
当子类定义了与父类同名的成员变量或成员函数时,子类成员会遮蔽(隐藏)父类同名成员。如果子类仍然需要访问父类的同名成员,需要加上作用域限定符 父类名::。
代码示例
示例七:子类与父类同名成员的访问
#include <iostream>
using namespace std;
class Base {
public:
int value = 100;
void show() {
cout << "Base::show(),value = " << value << endl;
}
};
class Derived : public Base {
public:
int value = 999; // 与父类同名,会遮蔽父类的 value
void show() {
cout << "Derived::show(),value = " << value << endl;
}
void showAll() {
cout << "子类 value = " << value << endl;
cout << "父类 value = " << Base::value << endl; // 用作用域限定符访问父类成员
Base::show(); // 调用父类的 show()
}
};
int main() {
Derived d;
d.show(); // 调用子类的 show()
d.Base::show(); // 通过作用域限定符调用父类的 show()
d.showAll();
return 0;
}
输出结果:
Derived::show(),value = 999
Base::show(),value = 100
子类 value = 999
父类 value = 100
Base::show(),value = 100
六、多继承
简介与作用
C++ 支持多继承,即一个子类可以同时继承多个父类,获得多个父类的属性和方法。
语法:
class 子类 : public 父类A, public 父类B, ... { };
⚠️ 虽然 C++ 支持多继承,但实际工程中应谨慎使用,因为多继承容易引起"菱形继承"等复杂问题(见下文)。很多现代语言(如 Java、C#)甚至直接禁止了多继承。
代码示例
示例八:同时继承两个父类
#include <iostream>
using namespace std;
class Flyable {
public:
void fly() {
cout << "我会飞!" << endl;
}
};
class Swimmable {
public:
void swim() {
cout << "我会游泳!" << endl;
}
};
// Duck 同时继承 Flyable 和 Swimmable
class Duck : public Flyable, public Swimmable {
public:
void quack() {
cout << "嘎嘎嘎!" << endl;
}
};
int main() {
Duck duck;
duck.fly(); // 来自 Flyable
duck.swim(); // 来自 Swimmable
duck.quack(); // Duck 自己的方法
return 0;
}
输出结果:
我会飞!
我会游泳!
嘎嘎嘎!
⚠️ 经典 Bug:菱形继承的二义性问题
#include <iostream>
using namespace std;
class Animal {
public:
int age = 5;
};
class Dog : public Animal {};
class Cat : public Animal {};
// 同时继承 Dog 和 Cat,而两者都继承自 Animal
// Animal 的成员被继承了两份!
class Hybrid : public Dog, public Cat {};
int main() {
Hybrid h;
// h.age = 10; // ❌ 编译错误!age 有歧义,不知道是 Dog 的 age 还是 Cat 的 age
h.Dog::age = 10; // 必须指定作用域才能访问
h.Cat::age = 20;
cout << "Dog::age = " << h.Dog::age << endl;
cout << "Cat::age = " << h.Cat::age << endl;
return 0;
}
这种多条路径导致同一个基类被继承多次的情况,就叫做菱形继承(Diamond Inheritance)。解决方案是使用虚继承(见下文)。
七、虚继承
简介与作用
虚继承(Virtual Inheritance) 是解决菱形继承问题的专用手段。
使用虚继承后,无论通过多少条路径继承同一个基类,该基类的成员只保留一份,消除了二义性。
语法:在继承时加上 virtual 关键字:
class 中间类 : virtual public 基类 { };
代码示例
示例九:虚继承解决菱形继承问题
#include <iostream>
using namespace std;
class Animal {
public:
int age = 5;
void show() {
cout << "Animal::age = " << age << endl;
}
};
// 关键:Dog 和 Cat 使用虚继承
class Dog : virtual public Animal {};
class Cat : virtual public Animal {};
class Hybrid : public Dog, public Cat {};
int main() {
Hybrid h;
h.age = 10; // ✅ 现在没有歧义了!Animal 只有一份
h.show(); // ✅ 正常调用
cout << "age = " << h.age << endl;
return 0;
}
输出结果:
Animal::age = 10
age = 10
💡 加上
virtual关键字后,Animal的数据在Hybrid对象中只存储一份,Dog::age和Cat::age指向同一块内存,从根本上解决了二义性问题。
八、综合案例:员工管理系统
最后,用一个小案例把本文的知识点串联起来,加深理解。
#include <iostream>
#include <string>
using namespace std;
// 基类:员工
class Employee {
protected:
string name;
int id;
double baseSalary;
public:
Employee(string n, int i, double s) : name(n), id(i), baseSalary(s) {}
virtual void showInfo() {
cout << "员工姓名:" << name
<< " 工号:" << id
<< " 基本工资:" << baseSalary << endl;
}
// 虚函数:计算工资(子类各自实现)
virtual double calcSalary() {
return baseSalary;
}
virtual ~Employee() {} // 虚析构,良好习惯
};
// 派生类:普通员工(无奖金)
class NormalEmployee : public Employee {
public:
NormalEmployee(string n, int i, double s) : Employee(n, i, s) {}
void showInfo() override {
cout << "[普通员工] ";
Employee::showInfo();
cout << " 实发工资:" << calcSalary() << endl;
}
double calcSalary() override {
return baseSalary;
}
};
// 派生类:销售员(有提成)
class SalesPerson : public Employee {
private:
double commission; // 提成
public:
SalesPerson(string n, int i, double s, double c)
: Employee(n, i, s), commission(c) {}
void showInfo() override {
cout << "[销售员工] ";
Employee::showInfo();
cout << " 提成:" << commission
<< " 实发工资:" << calcSalary() << endl;
}
double calcSalary() override {
return baseSalary + commission;
}
};
// 派生类:管理层(有年终奖)
class Manager : public Employee {
private:
double bonus; // 年终奖
public:
Manager(string n, int i, double s, double b)
: Employee(n, i, s), bonus(b) {}
void showInfo() override {
cout << "[管理人员] ";
Employee::showInfo();
cout << " 年终奖:" << bonus
<< " 实发工资:" << calcSalary() << endl;
}
double calcSalary() override {
return baseSalary + bonus;
}
};
int main() {
NormalEmployee e1("小李", 1001, 5000);
SalesPerson e2("小王", 1002, 3000, 2000);
Manager e3("张总", 1003, 10000, 50000);
cout << "======= 员工信息汇总 =======" << endl;
e1.showInfo();
cout << endl;
e2.showInfo();
cout << endl;
e3.showInfo();
return 0;
}
输出结果:
======= 员工信息汇总 =======
[普通员工] 员工姓名:小李 工号:1001 基本工资:5000
实发工资:5000
[销售员工] 员工姓名:小王 工号:1002 基本工资:3000
提成:2000 实发工资:5000
[管理人员] 员工姓名:张总 工号:1003 基本工资:10000
年终奖:50000 实发工资:60000
📝 总结
| 知识点 | 核心要点 |
|---|---|
| 继承基本语法 | class 子类 : public 父类 {} |
| 三种访问方式 | public / protected / private,日常用 public |
| 父类 private 成员 | 子类永远无法直接访问 |
| 构造/析构顺序 | 构造:父→子;析构:子→父 |
| 调用父类有参构造 | 在子类构造函数的初始化列表中调用 |
| 函数重写 | 子类重新定义父类函数,建议加 override |
| 同名成员访问 | 用 父类名::成员 指定作用域 |
| 多继承 | 一个子类继承多个父类,谨慎使用 |
| 菱形继承问题 | 同一基类被继承多次,产生二义性 |
| 虚继承 | 用 virtual 解决菱形继承,基类成员只保留一份 |
🚀 下期预告
掌握了继承,就为学习多态打下了坚实基础。下一篇文章,我们将深入讲解 C++ 中的多态机制:虚函数、纯虚函数、抽象类……让继承的威力真正发挥出来!
如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ 关注,你的支持是我继续创作的动力! 有任何问题欢迎在评论区留言,我会一一解答 😊
更多推荐

所有评论(0)