跳到主要内容 C++ 继承机制详解 | 极客日志
C++
C++ 继承机制详解 基于《C++ Primer》总结 C++ 继承与多态知识。涵盖 OOP 核心思想、派生类定义、访问控制(public/protected/private)、受保护成员特性、继承方式对访问权限的影响、using 声明改变访问级别、final 关键字防止继承、派生类构造函数调用、静态成员、友元关系及类型转换。重点讲解了基类与派生类的成员可见性规则、名字隐藏处理及内存布局概念。
CloudNative 发布于 2026/3/30 更新于 2026/4/13 1 浏览OOP 概述
面向对象程序设计的核心思想是封装、继承和多态。
封装可以将类的接口与实现分离。
继承可以定义相似的类型并对其相似关系建模。
多态可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
继承
通过**继承**联系在一起的类构成一种层次关系。通常在层次关系的根部有一个**基类**,其他类型则直接或间接从基类继承而来,这些继承得到的类称为**派生类**。
【注意】基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
以人为基类,基类中拥有的特性是姓名、性别、年龄、国籍。设计两个派生类,以学生为人的派生类,派生类中拥有的特性是学号、专业、年级;以教师为人的派生类,派生类中拥有的特性是工号、教授科目。
派生类列表
派生类必须通过使用**派生类列表**明确指出派生类是从哪个基类继承而来的。派生类列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以由访问说明符。
每个基类前面可以有三种访问说明符中的一个:public 、protected 或者 private
访问控制与继承
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。正常来说,派生类与其他外部的代码(其他类)一样,能够访问基类的 public 成员,但是不能访问基类的 private 成员。但是存在一种特殊情况,基类仅仅希望派生类能够访问部分成员,而其他外部的代码(其他类)不能访问这部分成员。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 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
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online
在 C++ 中,使用受保护(protected )访问说明符说明这样的成员。
【注意】在没有使用继承关系时,外部代码(其他类)可以访问 public 成员,而不能访问 private 成员和 protected 成员。使用了继承关系(一般来说继承方式为 public)后,外部代码(其他类)只能访问基类的 public 成员,派生类可以访问基类的 public 成员和 protected 成员,对于基类的 private 成员,仅仅只能基类自己使用。
受保护的成员 每一个类控制着其成员对于派生类来说是否可以访问。基类一般会使用 protected 访问说明符来声明希望与派生类分享但是不想被其他公共访问使用的成员。protected 可以看作是 public 和 private 中和后的产物:
和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。
【protected 重要性质】派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权,也就是说,派生类不能通过基类对象访问基类的受保护成员。
class A { protected : int _num; };
class B : public A {
public :
friend void func1 (A& a) { a._num = 1 ; }
friend void func2 (B& b) { b._num = 1 ; }
};
如果派生类可以访问基类受保护的成员,那么这里就可以通过设置派生类修改原本外部无法访问基类的成员修改基类的受保护成员。
修改_num 的两种方式
(1)由 A 类内或者 A 类中的友元修改
(2)B(public 或者 protected)继承 A 的类中,使用派生类对象方式修改
【注意】这里换一种思路,如果可以通过 B 访问 A 中受保护的成员,那么需要修改 A 类的受保护成员时,只需要设置一个新类即可,那么这个类的封装特性就会生效。为了防止这种方式,C++ 做出如下规定:即派生类的成员和友元只能访问派生类对象中基类的受保护成员,对于普通的基类对象中的受保护成员不能访问。
继承关系——公有、私有和受保护继承 类成员/继承方式 public 继承 protected 继承 private 继承 基类的 public 成员 派生类的 public 成员 派生类的 protected 成员 派生类的 private 成员 基类的 protected 成员 派生类的 protected 成员 派生类的 protected 成员 派生类的 private 成员 基类的 private 成员 在派生类中不可见 在派生类中不可见 在派生类中不可见
基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
基类 private 成员在派生类中是不能被访问,如果基类成员不想再类外直接访问,但需要再派生类中能访问,就定义为 protected。可以看出保护成员限定符是因继承才出现的。
通过表格的总结可以发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private .
使用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式时 public,不过最好显示的写出继承方式。
在实际运用中一般使用都是 public 继承,几乎很少使用 protected/private 继承,也不提倡使 protected/private 继承,因为 protected/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
某个类对其继承而来的访问权限受到两个因素影响:一是基类中该成员的访问说明符二是在派生类的派生列表中的访问说明符
派生访问说明符的目的是控制派生类用户对于基类成员的访问权限:
class Base {
public :
void func1 () { cout << "Base : public" << endl; }
int num1;
protected :
void func2 () { cout << "Base : protected" << endl; }
int num2;
private :
void func3 () { cout << "Base : private" << endl; }
int num3;
};
class A : public Base {
int f1 () { return num1; }
int f2 () { return num2; }
int f3 () { return num3; }
};
class B : protected Base {
int f1 () { return num1; }
int f2 () { return num2; }
int f3 () { return num3; }
};
class C : private Base {
int f1 () { return num1; }
int f2 () { return num2; }
int f3 () { return num3; }
};
只有继承是公有继承,基类中是公有的,才可以通过派生类访问。
class Base {
public :
void func1 () { cout << "Base : public" << endl; }
protected :
void func2 () { cout << "Base : protected" << endl; }
private :
void func3 () { cout << "Base : private" << endl; }
};
class A : public Base {};
class B : protected Base {};
class C : private Base {};
int main () {
A a;
a.func1 ();
a.func2 ();
a.func3 ();
B b;
b.func1 ();
b.func2 ();
b.func3 ();
C c;
c.func1 ();
c.func2 ();
c.func3 ();
}
派生访问说明符还可以控制继承自派生类的新类的访问权限
【注意】当基类成员转换到不同访问限定符指定的派生类中,在派生类中的权限会改变,此时再使用继承自派生类的新类时,需要分析派生类的权限。
class Base {
public :
void func1 () { cout << "Base : public" << endl; }
protected :
void func2 () { cout << "Base : protected" << endl; }
private :
void func3 () { cout << "Base : private" << endl; }
};
class A : public Base {};
class B : protected Base {};
class C : private Base {};
class A_public : public A {};
class B_protected : public B {};
class C_private : public C {};
类的设计与受保护成员 不考虑继承,我们可以认为一个类有两种不同的用户:普通用户和类的实现者。这其中,普通用户编写的代码使用类的对象,这部分代码只能访问类的公有(接口)成员;实现者则复制编写类的成员和友元的代码,成员和友元既能访问类的公有部分,也能访问类的私有(实现)部分。
如果进一步考虑继承的话就会出现第三种用户,即派生类。基类把它希望派生类能够使用的部分声明成受保护的。普通用户不能访问受保护的成员,而派生类及派生类的友元不能访问基类私有成员。
和其他类一样,基类应该将其接口成员声明为公有的;同时将属于其实现部分分成两组:
一组可供派生类访问,应该声明成受保护的,这样派生类就能实现自己的功能时使用基类的这些操作和数据
另一组只能由基类和基类的友元访问,应该声明成私有的。
默认的继承保护级别
struct 和 class 关键字定义的类具有不同的默认访问说明符。
类似的,默认派生类运算符也由定义派生类所用的关键字来决定。默认情况下,使用 class 关键字定义的派生类是私有继承;而使用 struct 关键字定义的派生类是公有继承 。
class Base {};
struct A : Base {};
class B : Base {};
【注意】一般建议派生类继承基类的时候,显式地将派生访问说明符定义出来。
派生类的声明 派生类的声明与其他类的差别不大,声明中包含类名但是不包含其派生列表:
class A : class Base ;
class A ;
【注意】一条声明语句的目的是令程序执行某个名字的存在 以及该名字表示一个什么样的实体 。例如:一个类声明、一个函数或者一个变量等等。派生列表以及与定义有关的其他细节必须与类的主题一起出现。
被用作基类的类 如果**将某个类用作基类,则该类必须已经定义而非声明**:
class Base ;
class A : public Base{};
原因是:派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类需要知道这些成员是什么。
【隐含意思】一个类不能派生它本身。
直接继承与间接继承 class Base {};
class A : public Base {};
class B : public A {};
在这个继承关系中,Base 是 A 的直接基类,同时也是 B 的间接基类。直接基类出现在派生列表中,而间接基类由派生类通过其直接基类继承而来。
改变个别成员的可访问性——using
通过使用 using 声明可以改变派生类继承的某个名字的访问级别。
class Base {
public :
int get_n () { return n; }
protected :
int n;
};
class A : private Base {
public :
using Base::get_n;
protected :
using Base::n;
};
派生类使用私有继承的方式继承的基类在派生类都会变成私有成员。在派生类中,可以使用 using 声明语句改变这些成员的可访问性质,将该类中直接或者间接基类中的任何可访问成员(非私有成员)标记出来。
using 声明语句中名字的访问权限由该 using 声明语句之前的访问说明符来决定。
当 using 声明语句出现在类的 private 部分,则该名字只能被类的成员和友元访问;
如果 using 声明语句出现在类的 public 部分,则类的所有用户都能访问它;
如果 using 声明语句位于 protected 部分,则该名字对于成员、友元和派生类是可访问二点。
【注意】派生类只能为其可访问的成员提供 using 声明
防止继承的发生——final 一个类不希望其他类所继承,C++11 新标准中提供了一种防止继承发生的方法,即在类名后跟一个关键字 final :
class Base final {};
class A : public Base {};
派生类的构造函数 虽然派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。和其他创建了基类对象的代码一样,**派生类也必须使用基类的构造函数来初始化其基类部分**。
派生类对象的基类部分与派生类对象自己的数据成员都是在构造函数的初始化阶段执行初始化操作的。**派生类构造函数同样是通过构造函数初始化列表来将实参传递给基类构造函数的**。
class Base {
public :
Base (int base1, int base2) :_base1(base1), _base2(base2) {}
private :
int _base1;
int _base2;
};
class A : public Base {
public :
A (int a1, int a2, int base1, int base2) :Base (base1, base2), _a1(a1), _a2(a2) { }
private :
int _a1;
int _a2;
};
这里首先会初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
继承与静态成员 如果基类定义了一个静态成员,那么在整个继承体系中只存在该成员的唯一定义。不管是从基类派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例。
静态成员遵循通用的访问控制权限,如果基类中的静态成员是 private 的,则派生类无权访问;如果基类的静态成员是可访问的,那么这个静态成员属于基类与派生类,即基类可以使用它,派生类也可以使用它
class Base {
public :
static int get_base1 () ;
private :
static int _base1;
};
int Base::_base1 = 1 ;
int Base::get_base1 () { return _base1; }
class A : public Base {
int fun1 () { return get_base1 (); }
int fun2 () { return _base1; }
};
友元类与继承
友元关系是单向 的:A 声明 B 为友元,B 可以访问 A ,但是 A 不能访问 B 。
友元关系不能传递 :A 声明 B 为友元,B 声明 C 为友元,但是 C 不能访问 A。
友元关系不能继承 :B 是 A 的友元,C 继承 B,但是 C 不能访问 A 的受保护成员和私有成员。
class Base {
friend class FB ;
int num1;
};
class A : public Base {
int _a;
};
class FB {
int func1 (Base b) { return b.num1; }
int func2 (A a) { return a.num1; }
int func3 (A a) { return a._a; }
};
类型转换与继承
派生类对象及派生类向基类的类型转换
一个含有派生类自己定义的(非静态)成员的子对象
一个与该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。
class Base {
int num1;
};
class A : public Base {
int _a;
};
C++ 中标准中并没有明确规定派生类的对象在内存中如何分布,但是可以认为 Base 对象和 A 对象是包含这些成员的。
【注意】在一个对象中,继承自基类的成员和派生类自定义的成员不一定是连续存储的。
因为派生类对象中含有与其基类对应的组成部分,所以我们能把**派生类的对象当成基类对象来使用**,而且我们**也能将基类的指针或者引用绑定到派生类对象的基类部分**上。
Base b1;
A a1;
Base* p = &b1;
p = &a1;
Base& r = a1;
【说明】这种转换通常称为派生类到基类的类型转换 。和其他类型转换一样,编译器会隐式地指向派生类到基类地转换。所以,这种隐式特性意味着可以把派生类对象或者派生类对象的引用或者指针用在需要基类引用的地方。
在派生类对象中含有其基类对应的组成部分,这是继承的关键。
继承中的类作用域 每个类定义自己的定义域,在这个定义域内我们定义类的成员。当存在继承关系的时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内找不到合适的成员,则编译器会继续在外层的基类作用域中寻找该名字的定义。
同时也是因为存在类作用域这种继承嵌套的关系,所以派生类才能像使用自己的成员一样使用基类的成员。
继承的名字冲突——隐藏 派生类可以重新定义在其直接基类或者间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字。
class Base {
public :
Base (int num = 1 ) :_num(num) {}
protected :
int _num;
};
class A : public Base {
public :
A (int num = 2 ) :_num(num) {}
int get_num () { return _num; }
protected :
int _num;
};
通过作用域运算符来使用隐藏的成员 class Base {
public :
Base (int num = 1 ) :_num(num) {}
protected :
int _num;
};
class A : public Base {
public :
A (int num = 2 ) :_num(num) {}
int get_num () {
return Base::_num;
}
protected :
int _num;
};
作用域运算符会覆盖原有的查找规则,并指示编译器从 Base 类的作用开始查找 num。
在继承中,除了覆盖继承而来的虚函数(多态),派生类最好不要与基类定义重名成员。