跳到主要内容
C++ 继承机制详解:从概念定义到默认成员函数 | 极客日志
C++ 算法
C++ 继承机制详解:从概念定义到默认成员函数 C++ 继承允许子类复用父类成员,实现代码扩展。核心包括继承方式(public/protected/private)对访问权限的影响,派生类对象可隐式转换为基类指针或引用但反之不行。同名成员遵循隐藏规则,需显式指定作用域访问。默认成员函数执行顺序严格遵循“先父后子”构造、“先子后父”析构原则,确保资源正确初始化和释放。掌握这些基础逻辑是构建稳健面向对象体系的关键。
云朵棉花糖 发布于 2026/3/22 更新于 2026/5/11 8 浏览
在开发中常遇到重复定义的问题?比如 Student 和 Teacher 类都要写姓名、地址、身份认证函数,改一处就要两处同步改 —— 这就是没用到 C++ 的'继承'机制。继承是面向对象复用代码的核心,能让子类直接'继承'父类的成员和方法,再扩展自己的专属功能。这篇文章就从'继承的概念定义''基派生类转换''作用域隐藏''默认成员函数'四个核心板块入手,带你搞懂继承的基础逻辑,避开常见的坑。
一、继承的概念与定义:怎么让类'复用'代码?
先想一个场景:Student 和 Teacher 都需要'姓名、地址、身份认证',但 Student 有学号、Teacher 有职称。如果各自写一遍,代码会很冗余 —— 继承就是把'公共部分'抽成父类 (基类),子类 (派生类) 直接复用。
本篇博客代码示例中所需头文件 :
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <vector>
#include <list>
using namespace std;
1.1 继承的核心概念
父类(基类) :存放公共成员的类,比如 Person 类(包含姓名、地址、identity 身份认证函数)。
子类(派生类) :继承父类并扩展专属成员的类,比如 Student(加学号)、Teacher(加职称)。
本质 :子类是父类的'扩展',能直接用父类的公共 / 保护成员,不用重复定义。
1.2 继承的定义格式
关键是'继承方式 + 父类名',比如 class Student : public Person 。注意两点:
class 默认私有继承,struct 默认公有继承,推荐显式写继承方式(比如 public)。
继承方式会影响父类成员在子类中的访问权限(后面会讲)。
有了上面的知识储备后,我们来看一段代码示例来加深理解(注意看注释):
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 ;
};
class Student : public Person {
public :
void study () {
func ();
}
protected :
int _stuid;
};
class Teacher : public Person {
public :
void teaching () {
}
protected :
string title;
};
int main () {
Student s;
Teacher t;
s.identity ();
s.study ();
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 来看,可以使用继承来实现 (注意看注释):
namespace MyLib {
template <class T >
class stack : public vector<T> {
public :
void push (const T& x) {
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 () {
MyLib::stack<int > st;
st.push (1 );
st.push (2 );
st.push (3 );
while (!st.empty ()) {
cout << st.top () << " " ;
st.pop ();
}
return 0 ;
}
二、基类与派生类的转换:子类对象能当父类用吗? 这是继承的核心特性之一,简单说:子类对象能隐式转换成父类对象 / 指针 / 引用 ,反之不行。
public 继承的 派生类对象 可以赋值给 基类的指针/基类的引用 。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。
基类对象不能赋值给派生类对象。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须基类的指针是指向派生类对象时才是安全的。这里如果基类的多态类型,可以使用 RTTI(Run-Time-Type Information) 的 dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面类型转换章节再单独专门讲解,这里先提一下)
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;
Person* pp = &sobj;
Person& rp = sobj;
Person pobj = sobj;
return 0 ;
}
为什么?
子类包含 '父类部分 + 自己的部分' ,把子类当父类用,只会用到'父类部分',不会越界;但父类没有子类的成员,强行转子类会访问不存在的内容(比如学号),所以禁止。
三、继承中的作用域:同名成员会冲突吗? 父类和子类有独立的作用域,如果出现同名成员(变量或函数),子类会'隐藏'父类的同名成员 ——— 这就是 '隐藏规则' ,很容易踩坑。
在继承体系中基类和派生类都有独立的作用域。
派生类和基类有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫做隐藏。(在派生类和成员函数中,可以使用 基类::基类成员 显示访问)
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏 (参数什么的不重要)。
避坑提醒 :继承体系中,尽量不要定义同名成员 —— 如果必须同名,访问时一定要加父类作用域。
3.1 变量隐藏:同名变量只认子类的 class Person {
protected :
string _name = "小李子" ;
int _num = 111 ;
};
class Student : public Person {
public :
void Print () {
cout << "子类的_num:" << _num << endl;
cout << "父类的_num:" << Person::_num << endl;
}
protected :
int _num = 999 ;
};
int main () {
Student s;
s.Print ();
}
规则 :不管变量类型、参数,只要同名,子类就隐藏父类的 —— 想访问父类的,必须用 父类名::成员名 。
3.2 函数隐藏:同名函数只认子类的 比变量隐藏更坑:只要函数名相同,不管参数列表,子类就隐藏父类的函数。
我们来看一看继承作用域的相关选择题吧 :
答案 :B,A
具体解析注意看下面代码的注释 (附如何修改) :
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 );
b.A::fun ();
return 0 ;
}
四、派生类的默认成员函数:构造、拷贝、析构怎么写? 子类和普通类一样,有 6 个默认成员函数(构造、拷贝构造、赋值重载、析构等),但子类的默认成员函数必须先处理父类的部分 。
前置说明 :继承的基成员变量 (整体对象)+ 自己的成员变量 (遵循普通的规则,跟类和对象部分一样)。默认生成的构造,派生类自己的成员,内置类型不确定,自定义类型调用默认构造,基类部分调用默认构造本质可以把派生类当做一个自定义成员变量 (基类) 的普通类总,跟普通类原则基本一样派生类一般要自己实现构造,不需要显示写析构,构造函数,赋值重载,除非派生类有深拷贝的资源需要处理
核心规则 :子类的成员函数 = 父类成员的处理 + 子类成员的处理 。
注意 :下面的代码为了便于理解会分模板展示,大家要是需要自己实现的话全部综合一下就行。
4.1 构造函数:先调用父类构造,再初始化子类成员 子类构造时,会先自动调用父类的'默认构造'(无参,编译器默认生成的或全缺省);如果父类没有默认构造,必须在子类构造的初始化列表中显式调用父类构造 。
派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
class Person {
public :
Person (const char * name) : _name(name)
{
cout << "Person()" << endl;
}
protected :
string _name;
};
class Student : public Person {
public :
Student (const char * name = "赵四" , int num = 18 , const char * address = "西安" )
: Person (name)
, _num(num)
, _address(address)
{
cout << "Student()" << endl;
}
protected :
int _num;
string _address;
};
int main () {
Student s1 ("张三" , 20 , "北京" ) ;
return 0 ;
}
关键顺序 :构造时 '先父后子' —— 父类先初始化,子类才能用父类的成员。
4.2 拷贝构造:先拷贝父类,再拷贝子类 子类拷贝构造时,会先调用父类的拷贝构造,拷贝父类部分的成员;再拷贝子类自己的成员。
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
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)
, _num(s._num)
, _address(s._address)
{
}
protected :
int _num;
string _address;
};
4.3 赋值重载:先赋值父类,再赋值子类 子类赋值重载会'隐藏'父类的赋值重载,所以要显式调用父类的赋值重载(Person::operator=),避免父类部分没赋值。
派生类的 operator= 必须调用基类的 operator= 完成基类的复制。需要注意的是派生类的 operator= 隐藏了基类的 operator=,所以显示调用基类的 operator=,需要指定基类作用域。
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 的情况下,派生类析构函数和基类析构函数构成隐藏关系。不过我们这里不显示调用,只是补充一下这个知识点。
class Person {
public :
~Person () {
cout << "~Person()" << endl;
}
protected :
string _name;
};
class Student : public Person {
public :
~Student () {
cout << "~Student()" << endl;
}
protected :
int _num;
};
int main () {
Student s1;
return 0 ;
}
为什么先子后父?
如果先析构父类,子类的成员可能还需要访问父类资源,会出问题(比如子类析构时要打印父类的姓名,父类先析构就没了)。
4.5 实现一个不能被继承的类 方法一 :基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。
class Base {
public :
void func5 () { cout << "Base::func5" << endl; }
protected :
int a = 1 ;
private :
Base () {}
};
class Derive : Base {};
int main () {
return 0 ;
}
方法二 :C++11 新增了一个 final 关键字,final 修改基类,派生类就不能继承了。
class Base final {
public :
void func5 () { cout << "Base::func5" << endl; }
protected :
int a = 1 ;
};
class Derive : Base {};
int main () {
return 0 ;
}
总结 :
继承的核心是'复用',但不是万能的 —— 掌握这四个板块,就能应对大部分基础场景:
用 public 继承实现代码复用,区分 public/protected/private 的访问权限;
记住 '子类能转父类,父类不能转子类' ,避免不安全转换;
同名成员会隐藏 ,访问时加父类作用域;
子类默认成员函数要 先处理父类部分 ,尤其是构造和赋值重载。
构造顺序:先父后子 ,析构顺序:先子后父 。
后续还会讲 '多继承''虚继承' 这些进阶内容,但基础阶段先把这些吃透,写继承类时就不会踩常见的坑了。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online