在面向对象编程的世界里,“避免重复” 与 “灵活扩展” 是开发者始终追求的目标,而 C++ 的继承机制正是实现这两个目标的核心工具。它让我们能够从已有的类(基类)中 “继承” 成熟的成员变量与成员函数,无需重新编写重复代码;同时又能在新类(派生类)中添加专属成员、重写原有函数,让类的功能随需求自然延伸。无论是模拟现实世界中 “动物与猫、狗” 的层级关系,还是开发中 “基础组件与定制组件” 的复用场景,继承都为代码的组织与维护提供了清晰的逻辑框架。理解继承,便是掌握 C++ 面向对象编程的关键一步

继承(inheritance)本质是类设计层次的复用
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
假如我们现在要设计一套图书管理系统/学生管理系统,甚至于说学校的门禁系统(这里插一句题外话,),我们需要做的第一步就是设计定义出有哪些类(如学生、老师、保安、食堂阿姨)
有些信息/方法是独有的,有些则是公共的(大家都有)。同理,有些成员是公共的,每个都写出来就实在是太恶心了,为了方便,我们就要用到接下来我们要介绍的继承——复用——直接用别人的,只不过以前复用的是函数,现在的继承变成了类设计层面的复用

公共的特性单独放到一个类里面——这个类就叫做:基类/父类

没有继承之前我们设计了两个类Student和Teacher,而这两个类里面都有姓名/地址电话/年龄等成员变量等,设计两个类的话,存在代码冗余,也不简洁,但Student类有自己单独的成员——学号,成员函数——学习,Teacher类有自己单独的成员——职称,成员函数——授课
class Student
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 学习
void study()
{
// ...
}
protected:
string _name = "peter"; // 姓名
string _address; // 地址
string _tel; // 电话
int _age = 18; // 年龄
int _stuid; // 学号
};
class Teacher
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 授课
void teaching()
{
//...
}
protected:
string _name = "Cx330"; // 姓名 我们就可以把公共的成员放到一个类里面,这样就可以复用这个类,可以省去很多代码,使代码变得简洁

代码演示:
class Person
{
public:
//进入校园/图书馆/实验室刷二维码等身份认证
void identity()
{
cout << "void indetity" << _name << endl;
}
void func()
{
cout << _age << endl;
}
protected:
string _name = "Cx330";
string _address;//地址
string _tel;//电话
private:
int _age = 18;
};
class Student :public Person
{
public:
void study()
{
//...学习
//基类私有成员,派生类不可见,语法上限制不能直接使用
//cout << _age << endl;
//但是可以间接使用
func();
}
protected:
int _stuid;//学号
};
class Teacher : public Person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 授课
void teaching()
{
//...
}
protected:
string _title; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity();
s.study();
return 0;
}运行结果:

下面我们看到Person是基类,也称作父类;Student是派生类,也称作子类


正是因为有三种继承方式和三种访问,所以才有了继承基类成员访问方式的九种变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
|---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |

namespace Cx330
{
template<class T>
class stack : public vector<T>
{
public:
void push(const T& x)
{
// 但是模版是按需实例化的,调用了哪个成员函数,就实例化哪个
// push_back等成员函数未实例化,所以找不到
vector<T>::push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
const T& top()
{
return vector<T>::back();
}
bool empty()
{
return vector<T>::empty();
}
};
}
int main()
{
Cx330::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << endl;
st.pop();
}
//// 但是模版是按需实例化的,调用了哪个成员函数,就实例化哪个
//// 构造/析构/push_back会实例化,其他成员函数就不会实例化
//vector<int> v;
//v.push_back(1);
return 0;
}模版是按需实例化的,调用了哪个成员函数,就实例化哪个,这里构造/析构/push_back会实例化,其他成员函数就不会实例化

1、通常情况下我们把一个类型的对象赋值给另一个类型的指针或者引用时,存在类型转换,中间会产生临时对象,所以需要加const,例如:
int i=l;
const double&d = i;public继承中,就是一个特殊处理的例外,派生类对象可以赋值给基类的指针/基类的引用,而不需要加const,这里的指针和引用绑定是派生类对象中的基类部分,如下图所示。也就意味着一个基类的指针或者引用,可能指向基类对象,也可能指向派生类对象
2、派生类对象赋值给基类对象是通过基类的拷贝构造函数或者赋值重载函数完成的(这两个函数的细节后面小节会细讲),这个过程就像派生类自己定义部分成员切掉了一样,所以也被叫做切割或者切片,如下图
3、基类对象不能赋值给派生类对象
4、基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI(Run-TimeTypeInformation)的dynamic_cast来进行识别后进行安全转换

class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
int i = 1;
double d = i;
const double& rd = i;
string s1 = "11111";
const string& rs = "11111";
Student s;
Person p = s;
//特殊处理
Person& rp = s;
Person* ptr = &s;
//不支持(p = s)父不能传子
//s = (Student)p;
return 0;
}运行结果:

注意:参数是否相同不重要
结局方案:
注意在实际中在继承体系里面最好不要定义同名的成员
Student的_num和Person的_num构成隐藏关系,这样代码虽然能跑,但是非常容易混淆
//派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问
class Person
{
protected:
string _name = "⼩李⼦"; // 姓名
int _num = 111; // ⾝份证号
};
class Student : public Person
{
public:
void Print()
{
cout << _num << endl;
cout <<Person:: _num << endl;
}
protected:
int _num = 999; // 学号
};1、A和B类中的两个func构成什么关系()
A. 重载 B. 隐藏 C. 没关系
2、下面程序的编译运行结果是什么()
A. 编译报错 B. 运行报错 C. 正常运行

代码演示:
//隐藏
//需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏,参数是否相同不重要
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)" << i << endl;
}
};
int main()
{
//Student s;
//s.Print();//999
B b;
b.fun(10);
//b.fun();//error
b.A::fun();//解决方案
return 0;
}运行:

1、同名函数,只看函数名,不看参数,参数是否相同不重要; 2、b.fun(); // 报错;解决方案:b.A::fun(); 3、实践中不建议定义同名函数,完全是坑自个儿

6个默认成员函数,我们之前在类和对象就已经见识过了,默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢

首先我们思考两个问题:
1、我们不写,默认生成的函数行为是什么?是否符合需求 2、不符合,我们要自己实现,如何实现?
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};本质:可以把派生类当做多一个的自定义类型成员变量(基类)的普通类
class Student : public Person
{
public:
Student(const char* name = "张三", int num = 18, string address = "郑州")
:Person(name)
,_num(num)
,_address(address)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
,_num(s._num)
,_address(s._address)
{
//深拷贝,需要自己实现
}
//隐藏基类的赋值重载
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
_address = s._address;
}
return *this;
//深拷贝,需要自己实现
}
~Student()
{
//因为多态中⼀些场景析构函数需要构成重写,
//那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),
// 所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系
//不要显示调用基类析构,编译器会在派生类析构结束后自动调用基类析构
//Person::~Person();
cout << "~Student()" << endl;
}
//构造先父后子,析构先子后父,如果显式调用,无法保证先子后父的析构顺序
protected:
int _num; //学号
string _address;//地址
};继承的基类成员变量(整体对象)+ 自己的成员变量(遵循普通的规则,跟类和对象部分一样)默认生成的构造,派生类自己的成员,内置类型不确定,自定义类型调用默认构造,基类部分调用默认构造本质上可以把派生类当做多了自定义类型成员变量,(基类)的普通类总,跟普通类原则一样。派生类一般要自己的实现构造,不显示写析构、拷贝析构、赋值重载,除非派生类有深拷贝的资源


运行:

思考:为什么Person会析构两次呢?

(1)核心的原因就是因为不用显示调用基类析构,编译器会在派生类析构结束之后自动调用析构,如果显示调用父类析构,无法保障先子后父的析构顺序。
(2)隐式的原因就是为了安全性,如下图所示

再来个main函数,我们测试一下构造和析构
运行结果:

1、跟普通类的原则基本上一样; 2、派生类一般要自己实现构造,不需要写拷贝构造、析构、赋值重载; 3、除非派生类有深拷贝的资源
#include<iostream>
#include<vector>
#include<list>
using namespace std;
//class Person
//{
//public:
// //进入校园/图书馆/实验室刷二维码等身份认证
// void identity()
// {
// cout << "void indetity" << _name << endl;
// }
// void func()
// {
// cout << _age << endl;
// }
//protected:
// string _name = "Cx330";
// string _address;//地址
// string _tel;//电话
//private:
// int _age = 18;
//};
//
//class Student :public Person
//{
//public:
// void study()
// {
// //...学习
// //基类私有成员,派生类不可见,语法上限制不能直接使用
// //cout << _age << endl;
//
// //但是可以间接使用
// func();
// }
//protected:
// int _stuid;//学号
//};
//
//class Teacher : public Person
//{
//public:
// // 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
// void identity()
// {
// // ...
// }
// // 授课
// void teaching()
// {
// //...
// }
//protected:
// string _title; // 职称
//};
//
//int main()
//{
// Student s;
// Teacher t;
//
// s.identity();
// s.study();
//
// return 0;
//}
//namespace Cx330
//{
// template<class T>
// class stack : public std::vector<T>
// {
// public:
// void push(const T& x)
// {
// // 基类是类模板时,需要指定⼀下类域,
// // 否则编译报错:error C3861: “push_back”: 找不到标识符
// // 因为stack<int>实例化时,也实例化vector<int>了
// // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
// // 调用了哪个,才实例化哪个
// vector<T>::push_back(x);
// //push_back(x);
// }
// void pop()
// {
// vector<T>::pop_back();
// }
// const T& top()
// {
// return vector<T>::back();
// }
// bool empty()
// {
// return vector<T>::empty();
// }
// };
//}
//
//int main()
//{
// Cx330::stack<int> st;
// st.push(1);
// st.push(2);
// st.push(3);
// while (!st.empty())
// {
// cout << st.top() << " ";
// st.pop();
// }
//
// // 但是模板是按需实例化,调用哪个成员函数,就实例化哪个
// // 构造/析构/push_back()
// vector<int> v;
// v.push_back(1);
//
// return 0;
//}
//class Person
//{
//protected:
// string _name; // 姓名
// string _sex; // 性别
// int _age; // 年龄
//};
//
//class Student : public Person
//{
//public:
// int _No; // 学号
//};
//
//int main()
//{
// int i = 1;
// double d = i;
// const double& rd = i;
//
// string s1 = "11111";
// const string& rs = "11111";
//
// Student s;
// Person p = s;
//
// //特殊处理
// Person& rp = s;
// Person* ptr = &s;
//
// //不支持(p = s)父不能传子
// //s = (Student)p;
//
// return 0;
//}
//派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问
//class Person
//{
//protected:
// string _name = "⼩李⼦"; // 姓名
// int _num = 111; // ⾝份证号
//};
//
//class Student : public Person
//{
//public:
// void Print()
// {
// cout << _num << endl;
// cout <<Person:: _num << endl;
// }
//protected:
// int _num = 999; // 学号
//};
//隐藏
//需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏,参数是否相同不重要
//class A
//{
//public:
// void fun()
// {
// cout << "func()" << endl;
// }
//};
//class B : public A
//{
//public:
// void fun(int i)
// {
// cout << "func(int i)" << i << endl;
// }
//};
//
//int main()
//{
// //Student s;
// //s.Print();//999
//
// B b;
// b.fun(10);
// //b.fun();//error
// b.A::fun();//解决方案
//
// return 0;
//}
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
//
class Student : public Person
{
public:
Student(const char* name = "张三", int num = 18, string address = "郑州")
:Person(name)
,_num(num)
,_address(address)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
,_num(s._num)
,_address(s._address)
{
//深拷贝,需要自己实现
}
//隐藏基类的赋值重载
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
_address = s._address;
}
return *this;
//深拷贝,需要自己实现
}
~Student()
{
//因为多态中⼀些场景析构函数需要构成重写,
//那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),
// 所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系
//不要显示调用基类析构,编译器会在派生类析构结束后自动调用基类析构
//Person::~Person();
cout << "~Student()" << endl;
}
//构造先父后子,析构先子后父,如果显式调用,无法保证先子后父的析构顺序
protected:
int _num; //学号
string _address;//地址
};
//继承的基类成员变量(整体对象)+自己的成员变量(遵循普通的规则,跟类和对象部分一样)
//本质可以把派生类当作多了一个自定义类型成员变量(基类)的普通类,跟普通类原则基本一样
//派生类一般要自己实现构造,不需要显示写析构、拷贝、赋值重载,除非派生类由深拷贝的资源需要处理
int main()
{
Student s1;
//Student s2("小明", 10);
//Student s3(s2);
//s1 = s3;
return 0;
}往期回顾:
《拿下C++ 模板进阶!》:带你从模板分类与特点到实战的每一个细节!
结语:继承作为 C++ 面向对象编程的三大特性(封装、继承、多态)之一,是连接 “通用类” 与 “专用类” 的桥梁。它通过代码复用减少重复开发,通过功能扩展满足个性化需求,同时又通过访问控制和继承方式保障代码的安全性与灵活性。掌握继承的概念、关键要素与使用原则,不仅能提升代码效率,更能帮助我们构建逻辑清晰、易于维护的面向对象系统