跳到主要内容
C++ 继承入门:从基础概念到默认成员函数,掌握类复用核心 | 极客日志
C++ 算法
C++ 继承入门:从基础概念到默认成员函数,掌握类复用核心 C++ 继承机制通过基类与派生类实现代码复用。子类可访问基类公共及保护成员,私有成员不可见。继承方式决定成员在子类中的访问权限,public 继承最常用。派生类对象可隐式转换为基类指针或引用(发生切片),反之则不安全。同名成员存在隐藏规则,需显式指定作用域访问。默认成员函数执行顺序遵循构造时先基后子,析构时先子后基的原则。
www 发布于 2026/3/30 更新于 2026/4/25 2 浏览实现多个类时总遇到'重复定义'的问题?比如 Student 和 Teacher 类都要写姓名、地址、身份认证函数,改一处就要两处同步改——这就是没用到 C++ 的继承 机制。继承是面向对象复用代码的核心,能让子类直接'继承'父类的成员和方法,再扩展自己的专属功能。
一、继承的概念与定义
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要手段,它允许我们在保持原有类特性的基础上进行扩展 ,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类 。
先想一个场景:Student 和 Teacher 都需要'姓名、地址、身份认证',但 Student 有学号、Teacher 有职称。如果各自写一遍,很多代码会变得冗余——继承就是把**'公共部分'**抽成父类(基类),子类(派生类)直接复用。
1. 继承的核心概念
父类(基类) :存放公共成员的类,比如 Person 类(包含姓名、地址、identity 身份认证函数)。
子类(派生类) :继承父类并扩展专属成员的类,比如 Student(加学号)、Teacher(加职称)。
本质 :子类是父类的'扩展',能直接用父类的公共/保护成员,不用重复定义。
2. 继承的定义格式
关键是'继承方式 + 父类名',比如 class Student : public Person。
有了上面的知识储备后,我们简单来看一段代码示例来加深理解(注意看注释):
#include <iostream>
using namespace std;
class Person {
public :
void identity () { cout << "void identity()" << _name << endl; }
void age () { cout << _age << endl; }
protected :
string _name = "张三" ;
string _address;
string _tel = "123456" ;
private :
int _age = 18 ;
};
: Person {
:
{
cout << << _tel << endl;
}
:
_stuid;
};
: Person {
:
{ cout << << _tel << endl; }
:
string title;
};
{
Student s;
Teacher t;
s. ();
t. ();
s. ();
t. ();
;
}
class
Student
public
public
void study ()
"void study() "
protected
int
class
Teacher
public
public
void teaching ()
"void teaching() "
protected
int main ()
identity
identity
study
teaching
return
0
3. 继承方式与成员访问权限 父类成员在子类中的访问权限,取决于'父类的访问限定符'和'继承方式',核心规则是:访问权限 = 两者中更严格(Min)的那个 (public > protected > private)。
我们用表格总结一下(重点记public 继承,实际开发最常用):
父类成员类型 public 继承(推荐) protected 继承 private 继承 父类 public 成员 子类中为 public 子类中为 protected 子类中为 private 父类 protected 成员 子类中为 protected 子类中为 protected 子类中为 private 父类 private 成员 不可见(不可访问) 不可见(不可访问) 不可见(不可访问)
基类 private 成员在派生类中无论以什么方式继承都是不可见 的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected 。可以看出保护成员限定符是因继承才出现的。
实际上面的表格我们进行一下总结会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
使用关键字 class 时默认的继承方式是 private ,使用 struct 时默认的继承方式是 public ,不过最好显示的写出继承方式。
在实际运用中一般使用都是 public 继承 ,几乎很少使用 protetced/private 继承,也不提倡使用 protetced/private 继承,因为 protetced/private 继承下来的成员都只能在派生类的类里面使用 ,实际中扩展维护性不强 。
我们这里就拿之前的 Stack 实现来看,可以使用继承来实现(注意看注释):
#include <iostream>
#include <vector>
#include <list>
#include <deque>
using namespace std;
#define CONTAINER vector
namespace MyStack {
template <class T >
class stack : public std::CONTAINER<T> {
public :
void push (const T& x) {
CONTAINER<T>::push_back (x);
}
void pop () { CONTAINER<T>::pop_back (); }
const T& top () { return CONTAINER<T>::back (); }
bool empty () { return CONTAINER<T>::empty (); }
};
}
void Test2 () {
MyStack::stack<int > st;
st.push (1 );
st.push (2 );
st.push (3 );
while (!st.empty ()) {
cout << st.top () << " " ;
st.pop ();
}
}
int main () {
Test2 ();
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;
};
void Test3 () {
Student st;
Person* ppe = &st;
Person& rpe = st;
int i = 1 ;
double d = i;
Person pe = st;
}
int main () {
Test3 ();
return 0 ;
}
为什么?
子类包含'父类部分 + 自己的部分',把子类当父类用,只会用到'父类部分' ,不会越界 ;但父类没有子类的成员 ,强行转换成子类会访问不存在的内容 (比如学号),所以禁止。
三、继承中的作用域:同名成员会冲突吗? 父类和子类有独立的作用域,如果出现同名成员 (变量或函数),子类会 '隐藏' 父类的同名成员 ——— 这就是**'隐藏规则'**,很容易踩坑。
在继承体系中基类和派生类都有独立的作用域。
派生类和基类有同名成员,派生类成员将屏蔽基类对同名成员的直接访问 ,这种情况叫做隐藏。(在派生类和成员函数中,可以使用基类::基类成员 显示访问)
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏 (参数什么的不重要)。
避坑提醒 :继承体系中,尽量不要定义同名成员 —— 如果必须同名,访问时一定要加父类作用域。
1. 变量隐藏
class Person {
protected :
string _name = "张三" ;
int _num = 123456 ;
};
class Student : public Person {
public :
void Print () {
cout << "子类的_num:" << _num << endl;
cout << "父类的_num:" << Person::_num << endl;
}
protected :
int _num = 111 ;
};
int main () {
Student s;
s.Print ();
}
规则 :不管变量类型、参数,只要同名,子类就隐藏父类的 —— 想访问父类的,必须用 父类名::成员名。
2. 函数隐藏 比变量隐藏更坑:只要函数名相同,不管参数列表,子类就隐藏父类的函数 。
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 个默认成员函数(构造、拷贝构造、赋值重载、析构等),但子类的默认成员函数必须先处理父类的部分 。
核心规则 :子类的成员函数 = 父类成员的处理 + 子类成员的处理。
1. 构造函数:先调用父类构造,再初始化子类成员 派生类的构造函数必须调用基类的构造函数 初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用 。
class Person {
public :
Person (const char * name = "张三" ) :_name(name)
{
cout << "Person()" << endl;
}
protected :
string _name;
};
class Student : public Person {
public :
protected :
int _num;
string _address;
};
int main () {
Student s1;
return 0 ;
}
基类没有默认构造函数 的情况:(则必须在子类构造的初始化列表中显式调用父类构造 )
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()" << _name << endl;
}
protected :
int _num;
string _address;
};
int main () {
Student s1 ("李四" , 20 , "北京" ) ;
return 0 ;
}
关键顺序 :构造时**'先父后子'**——父类先初始化,子类才能用父类的成员 。
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;
};
int main () {
Student s1 ("李四" , 20 , "北京" ) ;
Student s2 (s1) ;
return 0 ;
}
3. 赋值重载:先赋值父类,再赋值子类 派生类的 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;
};
int main () {
Student s1 ("李四" , 20 , "北京" ) ;
Student s2 (s1) ;
Student s3 ("王五" , 18 , "上海" ) ;
s1 = s3;
return 0 ;
}
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;
string _address;
};
int main () {
Student s1 ("李四" , 20 , "北京" ) ;
return 0 ;
}
5. 实现一个不能被继承的类 方法一 :基类的构造函数私有 ,派生类的构成必须调用基类的构造函数 ,但是基类的构成函数私有化以后,派生类不可访问也就不能调用了,那么派生类就无法实例化出对象。
class Base {
public :
void func () { cout << "Base::func" << endl; }
protected :
int a = 1 ;
private :
Base () { }
};
class Derive : Base { };
int main () {
Derive d;
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 () {
Derive d;
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