C++ 继承入门:从基础概念到默认成员函数,吃透类复用的核心逻辑
先想一个场景:Student 和 Teacher 都需要 “姓名、地址、身份认证”,但 Student 有学号、Teacher 有职称。如果各自写一遍,代码会很冗余 —— 继承就是把“公共部分”抽成父类(基类),子类(派生类)直接复用。本篇博客代码示例中所需头文件代码语言:javascriptAI代码解释代码仓库继承 - Gitee.com关键是 “继承方式 + 父类名”,比如 class Stu
一. 继承的概念与定义:怎么让类 “复用” 代码?
先想一个场景:Student 和 Teacher 都需要 “姓名、地址、身份认证”,但 Student 有学号、Teacher 有职称。如果各自写一遍,代码会很冗余 —— 继承就是把 “公共部分” 抽成父类(基类),子类(派生类)直接复用。
本篇博客代码示例中所需头文件:
代码语言:javascript
AI代码解释
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
#include<list>
using namespace std;
代码仓库:继承 - Gitee.com
1.1 继承的核心概念
- 父类(基类):存放公共成员的类,比如
Person类(包含姓名、地址、identity身份认证函数)。 - 子类(派生类):继承父类并扩展专属成员的类,比如
Student(加学号)、Teacher(加职称)。 - 本质:子类是父类的 “扩展”,能直接用父类的公共 / 保护成员,不用重复定义。

在这里插入图片描述
1.2 继承的定义格式
关键是 “继承方式 + 父类名”,比如 class Student : public Person。注意两点:
class默认私有继承,struct默认公有继承,推荐显式写继承方式(比如public)。- 继承方式会影响父类成员在子类中的访问权限(后面会讲)。

在这里插入图片描述
有了上面的知识储备后,我们来看一段代码示例来加深理解(注意看注释):
代码语言:javascript
AI代码解释
//基类/父类
class Person
{
// 公共成员:子类和类外都能访问
public:
//进入校园/图书馆/实验室刷二维码等身份认证
void identity()
{
cout << "void identity():" << _name << endl;
}
void func()
{
cout << _age << endl;
}
// 保护成员:子类能访问,类外不能访问(专门为继承设计)
protected:
string _name = "赵四";//姓名
string _address;//地址
string _tel;//电话
// 私有成员:子类和类外都不能直接访问(像“爸爸的私房钱”)
private:
int _age = 18;//年龄
};
// 子类Student:公有继承Person
//class的话不写默认是私有继承,struct是公有继承
// class Student:Person
class Student : public Person
{
public:
//学习
void study()
{
//……
//基类私有成员(爸爸的私房钱),派生类 中不可见,语法限制上不能直接使用
//cout << _age << endl;
//父类公有函数能间接访问私有成员
func();
}
protected://在继承中保护用的比较多
int _stuid;//学号
};
// 子类Teacher:公有继承Person
class Teacher : public Person
{
public:
//授课
void teaching()
{
//…………
}
protected:
string title;//职称
};
// 测试:子类能直接用父类的函数
int main()
{
Student s;
Teacher t;
s.identity();// 用父类的identity,输出“赵四”
s.study();// 用子类的study,调用父类的func,输出了18
return 0;
}

在这里插入图片描述
1.3 继承方式与成员访问权限
父类成员在子类中的访问权限,取决于 “父类的访问限定符” 和 “继承方式”,核心规则是:访问权限 = 两者中更严格(可以理解为Min)的那个(public > protected > private)。
我们用表格总结一下(重点记public继承,实际开发最常用):
|
父类成员类型 |
public继承(推荐) |
protected继承 |
private继承 |
|---|---|---|---|
|
父类public成员 |
子类中为public |
子类中为protected |
子类中为private |
|
父类protected成员 |
子类中为protected |
子类中为protected |
子类中为private |
|
父类private成员 |
不可见(不可访问) |
不可见(不可访问) |
不可见(不可访问) |
关键提醒:
- 父类
private成员无论怎么继承都 “不可见”(但是实际上是存在的)—— 子类想访问,只能通过父类的公有函数(比如上面的func())。 protected是为继承设计的:既不让类外访问,又能让子类用。- 在实际运用中一般都还是
public继承,几乎很少使用protected/private继承。也不提倡使用后两者,因为它们继承下来的成员实际中扩展维护性不强,受到的限制比公有继承多。
我们这里就拿我们之前的Stack来看,可以使用继承来实现(注意看注释):
代码语言:javascript
AI代码解释
namespace Lotso
{
template<class T>
class stack : public vector<T>
{
public:
void push(const T& x)
{
// 基类是类模板时,需要指定⼀下类域,
// 否则编译报错:error C3861: “push_back”: 找不到标识符
// 因为stack<int>实例化时,也实例化vector<int>了
// 但是模版是按需实例化,调用了那个成员函数就实例化那个,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()
{
Lotso::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;
}

在这里插入图片描述
二. 基类与派生类的转换:子类对象能当父类用吗?
这是继承的核心特性之一,简单说:子类对象能隐式转换成父类对象 / 指针 / 引用,反之不行。
- public继承的
派生类对象可以赋值给基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。 - 基类对象不能赋值给派生类对象
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须基类的指针是指向派生类对象时才是安全的。这里如果基类的多态类型,可以使用RTTI(Run-Time-Type Information)的dynamic_cast来进行识别后进行安全转换。(ps:这个我们后面类型转换章节再单独专门讲解,这里先提一下)

在这里插入图片描述
代码示例(注意看注释):
代码语言:javascript
AI代码解释
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 = "1111";
const string& rs = "111111";
//上面的转换在之前类和对象以及其它的一些地方都讲到过,这里就不说了,主要是对比
Student sobj;
// 1. 子类对象 → 父类指针/引用(隐式转换,安全)
Person* pp = &sobj;// 父类指针指向子类对象的“父类部分”
Person& rp = sobj;// 父类引用引用子类对象的“父类部分”
// 2. 子类对象 → 父类对象(调用父类拷贝构造,只拷贝父类部分)
// 派生类对象可以赋值给基类的对象是通过调用后面会讲解的基类的拷⻉构造完成的
Person pobj = sobj;
//3. 父类对象 → 子类对象(编译报错,不安全)
//sobj = pobj;//错误:父类没有子类的成员(比如学号)
//sobj = (Student)pobj;// 强制转换也不行
return 0;
}
为什么? 子类包含
“父类部分 + 自己的部分”,把子类当父类用,只会用到 “父类部分”,不会越界;但父类没有子类的成员,强行转子类会访问不存在的内容(比如学号),所以禁止。
三. 继承中的作用域:同名成员会冲突吗?
父类和子类有独立的作用域,如果出现同名成员(变量或函数),子类会 “隐藏” 父类的同名成员 ——— 这就是 “隐藏规则”,很容易踩坑。
- 在继承体系中基类和派生类都有独立的作用域
- 派生类和基类有同名成员,派生类成员将屏蔽基类对同名成员2的直接访问,这种情况叫做隐藏。(在派生类和成员函数中,可以使用基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏(参数什么的不重要)。
避坑提醒:继承体系中,尽量不要定义同名成员—— 如果必须同名,访问时一定要加父类作用域。
3.1 变量隐藏:同名变量只认子类的
代码示例(注意看注释):
代码语言:javascript
AI代码解释
class Person
{
protected:
string _name = "小李子"; // 姓名
int _num = 111; // 父类的_num:身份证号
};
class Student : public Person
{
public:
void Print()
{
// 同名变量:默认访问子类的_num(学号)
// 同名成员构成隐藏,只在乎是否同名,参数什么的都不用管
cout << "子类的_num:" << _num << endl;//输出999
// 想访问父类的_num:必须加“父类::”
cout << "父类的_num:"<<Person::_num << endl;//输出111
}
protected:
int _num = 999; // 学号
};
int main()
{
Student s;
s.Print();
}

在这里插入图片描述
规则:不管变量类型、参数,只要同名,子类就隐藏父类的 —— 想访问父类的,必须用 父类名::成员名。
3.2 函数隐藏:同名函数只认子类的
比变量隐藏更坑:只要函数名相同,不管参数列表,子类就隐藏父类的函数。 我们来看一看继承作用域的相关选择题吧:

在这里插入图片描述
答案:B,A 具体解析注意看下面代码的注释(附如何修改):
代码语言:javascript
AI代码解释
int main()
{
Student s;
s.Print();
}
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()
{
B b;
b.fun(10); // 调用子类的fun(int),输出“func(int i)10”
// b.fun(); // 编译报错:父类的fun()被隐藏了,不能直接调用
b.A::fun(); // 想调用父类的fun():加“父类::”,输出“func()”
return 0;
}

在这里插入图片描述
四. 派生类的默认成员函数:构造、拷贝、析构怎么写?
子类和普通类一样,有 6 个默认成员函数(构造、拷贝构造、赋值重载、析构等),但子类的默认成员函数必须先处理父类的部分。

在这里插入图片描述
前置说明:
- 继承的基成员变量(整体对象)+自己的成员变量(遵循普通的规则,跟类和对象部分一样)。默认生成的构造,派生类自己的成员,内置类型不确定,自定义类型调用默认构造,基类部分调用默认构造
- 本质可以把派生类当做一个自定义成员变量(基类)的普通类总,跟普通类原则基本一样
- 派生类一般要自己实现构造,不需要显示写析构,构造函数,赋值重载,除非派生类有深拷贝的资源需要处理
核心规则:子类的成员函数 = 父类成员的处理 + 子类成员的处理。 注意:下面的代码为了便于理解会分模板展示,大家要是需要自己实现的话全部综合一下就行。
4.1 构造函数:先调用父类构造,再初始化子类成员
子类构造时,会先自动调用父类的 “默认构造”(无参,编译器默认生成的或全缺省);如果父类没有默认构造,必须在子类构造的初始化列表中显式调用父类构造。
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
代码示例(注意看注释):
代码语言:javascript
AI代码解释
class Person
{
public:
// 父类带参构造(无默认构造)
Person(const char* name)
:_name(name) // 初始化父类的_name
{
cout << "Person()" << endl;
}
protected:
string _name;// 姓名
};
class Student :public Person
{
public:
// 子类构造:必须在初始化列表显式调用父类构造
Student(const char* name = "赵四", int num = 18, const char* address = "西安")
//这里可以显示写一下(不能直接用_name),其实就把他当成一个自定义类型成员变量就可以了。基类的成员当成一个整体。
:Person(name) // 先初始化父类(必须写在前面)
,_num(num) // 再初始化子类自己的成员
,_address(address)
{
cout << "Student()" << endl;
}
protected:
int _num; // 学号
string _address; // 地址
};
int main()
{
// 构造顺序:先调用Person(name),再调用Student()
Student s1("张三", 20, "北京");
return 0;
}

在这里插入图片描述
关键顺序:构造时 “先父后子”—— 父类先初始化,子类才能用父类的成员。
4.2 拷贝构造:先拷贝父类,再拷贝子类
子类拷贝构造时,会先调用父类的拷贝构造,拷贝父类部分的成员;再拷贝子类自己的成员。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
代码示例(注意看注释):
代码语言:javascript
AI代码解释
class Person
{
public:
// 父类拷贝构造
Person(const Person& p)//传的是派生类的话也可以转换,前面讲过
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
protected:
string _name;//姓名
};
class Student :public Person
{
public:
// 子类拷贝构造
Student(const Student& s)
:Person(s) // 调用父类拷贝构造,拷贝父类部分(s是子类,能隐式转父类)
,_num(s._num) // 拷贝子类自己的学号
,_address(s._address) // 拷贝子类自己的地址
{
// 如果有深拷贝资源(比如int*),这里要手动处理
}
protected:
int _num; // 学号
string _address; // 地址
};
4.3 赋值重载:先赋值父类,再赋值子类
子类赋值重载会 “隐藏” 父类的赋值重载,所以要显式调用父类的赋值重载(Person::operator=),避免父类部分没赋值。
- 派生类的operator=必须调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。
代码示例(注意看注释):
代码语言:javascript
AI代码解释
class Person
{
public:
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
protected:
string _name;//姓名
};
class Student :public Person
{
public:
Student& operator=(const Student& s)
{
if (this != &s) // 防止自己赋值自己
{
// 先赋值父类部分
Person::operator=(s);
// 再赋值子类自己的成员
_num = s._num;
_address = s._address;
// 如果有深拷贝资源在这里处理
}
return *this;
}
protected:
int _num; // 学号
string _address; // 地址
};
4.4 析构函数:先析构子类,再自动析构父类
子类析构时,会先执行自己的析构逻辑,结束后编译器自动调用父类的析构—— 不用显式调用,否则会析构两次。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 因为在多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。不过我们这里不显示调用,只是补充一下这个知识点。
代码示例(注意看注释):
代码语言:javascript
AI代码解释
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;//姓名
};
class Student :public Person
{
public:
~Student()
{
//Person::~Person();//前面也需要Person::,有一定历史原因,有提过
//但是这里不能写,否则无法保证析构先子后父的原则,会析构两次。
// 先执行子类析构逻辑(比如释放子类的资源)
cout << "~Student()" << endl;
// 编译器自动调用父类析构:~Person()
}
protected:
int _num;
};
int main()
{
// 构造顺序:先Person(),再Student()
// 先父后子:我们联想一下之前初始化列表按声明顺序来的原理
// 析构顺序:先~Student(),再~Person()
// 先子后父:我们可以想一下如果先析构父类,那么子类的成员如果需要访问父类就出问题了
Student s1;
return 0;
}

在这里插入图片描述
为什么先子后父? 如果先析构父类,子类的成员可能还需要访问父类资源,会出问题(比如子类析构时要打印父类的姓名,父类先析构就没了)。
4.5 实现一个不能被继承的类
方法一:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。
代码语言:javascript
AI代码解释
// 方法1:C++98
class Base
{
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
//C++98的方法:构造函数私有的类不能被继承
Base()
{}
};
class Derive : Base
{};
int main()
{
//Base b;
//Derive d;
return 0;
}
方法二:C++11新增了⼀个final关键字,final修改基类,派生类就不能继承了。
代码语言:javascript
AI代码解释
class Base final
{
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive : Base
{};
int main()
{
//Base b;
//Derive d;
return 0;
}
总结: 继承的核心是 “复用”,但不是万能的 —— 掌握这四个板块,就能应对大部分基础场景:
- 用
public继承实现代码复用,区分public/protected/private的访问权限; - 记住
“子类能转父类,父类不能转子类”,避免不安全转换; 同名成员会隐藏,访问时加父类作用域;- 子类默认成员函数要
先处理父类部分,尤其是构造和赋值重载。 - 构造顺序:
先父后子,析构顺序:先子后父。
后续还会讲 “多继承”“虚继承” 这些进阶内容,但基础阶段先把这些吃透,写继承类时就不会踩常见的坑了。
更多推荐



所有评论(0)