继承的概念
在面向对象编程中,继承是一种使代码复用的重要机制,它允许一个类(称为派生类 / 子类)继承另一个类(称为基类 / 父类)的属性(成员变量)和方法(成员函数)。
本文详细讲解了 C++ 继承机制。内容包括继承的基本概念、三种访问方式(public/protected/private)及其权限变化规则、内存布局与对象切片原理。阐述了类模板继承中的按需实例化问题及解决方案(类域指定或 this 指针)。说明了基派生类转换规则、成员隐藏与重载的区别。介绍了 final 关键字防止继承、友元关系的不可继承性以及静态成员的全局唯一性。旨在帮助开发者深入理解 C++ 面向对象设计的核心特性。

在面向对象编程中,继承是一种使代码复用的重要机制,它允许一个类(称为派生类 / 子类)继承另一个类(称为基类 / 父类)的属性(成员变量)和方法(成员函数)。
通过继承,派生类可以直接使用基类中已有的功能,同时还能在此基础上添加新的成员或重写基类的方法,从而实现对基类的扩展或定制。这就好比现实中'子类继承父类的特征,同时又有自己独特的特点',例如'学生类'继承'人类'的姓名、年龄等属性,同时新增'学号'等专属成员。
继承主要分为不同的类型,如单继承(一个派生类只有一个直接基类)、多继承(一个派生类有多个直接基类)等,不同的继承方式在代码结构和内存布局上会有不同表现,同时也涉及到构造函数、析构函数的调用顺序、成员隐藏、访问权限控制等一系列规则。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数复用是函数层次的复用,而继承则是类设计层次的复用。
最精辟的理解:儿子具有爸爸的所有东西,除了爸爸的隐私 (即 private),这就是继承。
我们先看一下下面这段代码:
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 = "张三"; // 姓名
int _age = 18; // 年龄
string _address; // 地址
string _tel; // 电话
string _title; // 职称
};
上面我们看到,在没有使用继承时,我们设计了 Student 和 Teacher 两个类。这两个类中都有姓名、地址、电话、年龄等成员变量,以及 identity 身份认证的成员函数,这些重复的内容在两个类中分别编写,显得十分冗余且麻烦。当然,它们也有各自独有的的成员变量和函数:比如老师独有的职称这个成员变量,学生独有的是学号;学生有学习这个独有的成员函数,老师则有授课这个独有的成员函数。
因此,在 C++ 中,为解决这种冗余问题,便有了继承机制。我们可以将 Student 和 Teacher 两个类中相同的内容封装成一个类,命名为 Person,然后让 Student 和 Teacher 这两个类去继承 Person 类。这样一来,Student 和 Teacher 就能使用 Person 类中除 private 成员外的所有成员变量和成员函数,而它们自身只需要包含各自独有的成员变量和成员函数即可。如此,代码不仅简洁了很多,也更加美观。
你看 Student 和 Teacher 这两个类,就像两个'人',他们都有姓名、年龄、电话这些'共同点',也都要做'刷二维码认证'这件事。
但之前的写法是,这两个人各说各的,把这些相同的东西都自己写了一遍,多费劲啊!
继承就相当于:先找一个'大长辈'(比如叫 Person 类),把这些'姓名、年龄、电话、身份认证'这些大家都有的东西,全放到这个长辈身上。然后 Student 和 Teacher 就'认 Person 当爹',成为它的'孩子'。
这样一来:Student 和 Teacher 就不用自己再写那些重复的东西了,因为'爹有了,孩子自然就有了'(除了爹藏起来不让孩子碰的 private 内容)。孩子只需要管好自己独有的东西:Student 记上学号、写个学习的功能;Teacher 记上职称、写个授课的功能就行。
打个比方:就像你不用自己重新发明'吃饭、走路'这些人类共有的能力,因为你继承了人类的这些基础能力,你只需要学会自己的专属技能(比如编程、画画)就行。
编译器在背后会帮你做一件事:把 Person 类里能给孩子的东西,偷偷'复制'到 Student 和 Teacher 类里,但这些复制的东西算在'爹的名下',不会和孩子自己的东西冲突。所以继承的核心就是:少写重复代码,让类之间像家族一样共享基础功能,只专注于自己的特色。
上面所说的,也就是继承,即让某一个类去认另一个类为爸爸,然后那个类就能拥有它爸爸类中除了 private 之外的成员变量和成员函数了。这其实就相当于,爸爸类中除了 private 的成员变量和成员函数都存在于儿子类里面,只不过是处于爸爸类的类域中。
再换句话来说,我们在外面创建的儿子类变量,可以直接通过这个儿子类变量去调用爸爸类中除了 private 的成员变量和成员函数,而且儿子类内部也可以直接使用爸爸类中除了 private 的成员变量和成员函数。
那么我在这里再给大家一个初学者对于继承可能容易误解的一个点:
很多人可能会觉得,继承就是相当于父类的成员变量和成员函数都在子类里面加上了,那么其实不是这样的:
子类并不会'物理性地复制'父类非 private 成员的代码定义,但其对象会包含父类非 private 成员变量的存储空间,同时拥有对这些成员的访问权限。具体来说:
子类对象的内存布局中,会专门划分一块区域用于存储父类的非 private 成员变量(确保这些数据有地方存放);而子类通过类域的嵌套关系(类似'子空间包含父空间'),能够找到并访问父类的成员(包括成员变量和成员函数)。不过,父类成员的代码定义(比如成员函数的实现、成员变量的类型声明)只存在唯一一份,被所有子类共享,不会因为继承而重复创建,这避免了代码冗余。
进一步总结:每个子类对象的内存中,都会包含一块独立的空间用于存储父类的非 private 成员变量(注意:成员函数不占用对象的存储空间,它们被统一存放在单独的代码区,供所有对象共享)。这些存储空间是彼此独立的 —— 即使多个子类对象继承自同一个父类,它们各自包含的'父类成员变量空间'也互不相干。例如,学生对象 s 的_name 和老师对象 t 的_name 会存放在不同的内存地址,各自的数据修改不会影响对方。
简言之,父类的成员定义(规则和逻辑)是所有子类共享的,但每个子类对象中用于存储父类成员变量的空间是独立的,成员函数的代码也只存在一份。这种设计既保证了代码复用,又确保了不同对象的数据互不干扰。
这个是一个很重要的点,大家千万不能误会了
怎么样大家,这样子是不是就能对继承的理解更进一步了呢,大家还是要去结合生活中的例子,然后多去思考,就会好理解很多。
在正式讲解继承的格式是什么之前,我们得先来了解了解,继承的方式有哪些,毕竟继承那么重要,那么肯定花样也挺多的,所以接下来我们就来看看:
继承的方式有三种,分别是 public 继承、protected 继承和 private 继承。
| 类成员 / 继承方式 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| 父类的 public 成员 | 相当于派生类的 public 成员(外部可通过派生类对象直接访问,如derivedObj.pubFunc()) | 相当于派生类的 protected 成员(仅派生类内部及子类可访问,外部无法直接调用) | 相当于派生类的 private 成员(仅派生类内部可访问,子类无法继承此权限) |
| 父类的 protected 成员 | 相当于派生类的 protected 成员(派生类内部可自由使用,子类也能继承此权限) | 相当于派生类的 protected 成员(与 public 继承的 protected 权限效果一致) | 相当于派生类的 private 成员(派生类内部可使用,但子类无法访问) |
| 父类的 private 成员 | 在派生类中不可见(无论派生类如何操作,均无法直接访问基类 private 成员) | 在派生类中不可见(同 public 继承,基类 private 成员完全隔离) | 在派生类中不可见(所有继承方式下,基类 private 成员对派生类均不可见) |
public > protected > private,继承后的权限取'基类成员权限'和'继承方式'的较低等级(例如基类 public 成员 + protected 继承 → 派生类中为 protected)。eat() 方法让'狗''猫'都能被外部调用),选public 继承。private 成员在任何继承方式下,对派生类都完全不可见(即使派生类内部也无法直接访问),这是 C++ 封装性的严格保障。这是一点,下面则是主要的补充
所以经过上面的讲解,大家应该就会对继承方式有了个清晰的认知,那么我们接下来就来看看继承的格式是什么。
继承的格式是:在子类的名字后面加上冒号':',后面再跟上继承方式和父类名字即可。拿上面的例子来说就是:

下面我给大家一个示例代码,帮助大家理解:
#include <iostream>
#include <string>
using namespace std;
class person {
public:
void identity() { cout << "身份识别" << endl; }
protected:
string mname = "张三"; // 姓名
string maddress; // 地址
string mtel; // 电话
int mage = 18; // 年龄
};
class student : public person // 继承
{
public:
// 学习
void study() { cout << "学习" << endl; }
void definestudent() { mage = 20; maddress = "太阳系"; }
void printstudent() { cout << mage << " " << maddress << endl; }
private:
string mstuid;
};
class teacher : public person // 继承
{
public:
// 授课
void teaching() { cout << "授课" << endl; }
void defineteacher() { mage = 29; maddress = "地球"; }
void printteacher() { cout << mage << " " << maddress << endl; }
protected:
string title; // 职称
};
int main() {
student s;
teacher t;
// 创建两个子类变量
s.identity();
t.identity(); // 可以在外面直接通过子类变量调用父类的 public 的成员函数
s.definestudent();
t.defineteacher(); // 可以在子类中定义父类的成员变量
s.printstudent();
t.printteacher(); // 打印出我们可以看出,两个子类所存储的父类是独立的
return 0;
}
大家结合着例子进行理解,并不算多难。
类模板的继承,本质上就是一个类模板以另一个类模板作为基类(即'认其为父类')的机制。和普通类的继承逻辑一致,当一个子类模板继承自父类模板后,它可以使用父类模板中所有非 private 的成员变量和成员函数,无需重复定义这些共性内容,只需专注于自身特有的功能实现。
这种机制在实际开发中很常见,比如 STL 中 stack(栈)和 queue(队列)的模拟实现就是典型例子:它们通常会继承自一个底层容器类模板(如 deque 或 vector 的类模板),直接复用底层容器的存储和基础操作(如元素的插入、删除、访问等非 private 成员),再在其上封装栈的'先进后出'或队列的'先进先出'等特有逻辑,既保证了代码复用,又简化了实现过程。
#include <vector>
using namespace std;
namespace win {
template <typename T1>
class stack : public vector<T1> {
public:
void push(const T1& val = T1()) {
// push_back(val); // 注意,不能就像上面那么使用,
// 因为虽然认了 vector 为爸爸,但是编译器没那么智能
// 直到你用的 push_back 函数就是 vector 里面的
// 所以我们要指定类域
// 即指定是 vector 里面的 push_back 函数才行
vector<T1>::push_back(val);
// 因为 stack<int>实例化时,也实例化 vector<int>了
// 但是模版是按需实例化,push_back 等成员函数未实例化,所以找不到
}
void pop() { vector<T1>::pop_back(); }
T1& top() { return vector<T1>::back(); }
size_t size() const { return vector<T1>::size(); }
bool empty() const { return vector<T1>::size() == 0; }
private:
// 就不用设置容器作为成员变量了
// 因为已经认 vector 为爸爸了,所以就不需要了
// 在上面的成员函数可以直接使用父类模版 vector 的成员函数了
};
}
那么其实大家要注意一下,对于类模版继承,我们写子类模版的成员函数时,如果想要去调用父类模版的成员函数的时候,是不能就直接写父类模版里的函数名就行了,而是要去指定类域,即类域::成员函数,这样子,那么之所以要这个样子的原因是:
类模板的'按需实例化'特性
C++ 的类模板是**'蓝图'**式的存在,只有当我们用具体类型(如 int、double)去实例化它时,编译器才会真正生成对应的类代码。例如:
template<class T> class Vector { ... }; 时,编译器不会生成任何实际代码。Vector<int> v; 时,编译器才会生成 Vector<int> 这个具体类的代码(包括它的成员变量、成员函数,如 push_back(int))。
这种'用到才生成'的机制称为按需实例化,即我们有实质性调用了,编译器才会去仔细检查我们所调用的,不难编译器是只会随便检查一下就完事了,容易忽略一些错误,目的是避免生成冗余代码。类模板继承时的'成员函数可见性'问题
当一个子类模板(如 Stack<T>)继承自父类模板(如 Vector<T>)时,会出现一个关键问题:
push_back)只有在被实例化后,才能被编译器识别为'可调用的函数'。push_back(val),编译器会因为'父类模板的 push_back 还未实例化'而无法确认这个函数的存在,从而报错。类域指定(Vector<T1>::push_back(val))的作用
为了明确告诉编译器:'我要调用的是父类模板 Vector<T1> 中的 push_back 函数',我们需要显式指定类域,格式为 父类模板名<模板参数>::成员函数名 (参数)。
以代码中的 stack 和 vector 为例:
Stack<T> 继承自 Vector<T>,当我们在 Stack 的成员函数中要调用 vector 的 push_back 时,必须写成 Vector<T>::push_back(val)。push_back 属于父类模板 Vector<T>,需要等 Vector<T> 实例化后(如 Stack<int> 实例化时,Vector<int> 也会被实例化),才能调用其 push_back(int) 函数'。总结
在类模板继承场景中,由于按需实例化的机制,父类模板的成员函数在编译阶段是'不可见'的。为了让编译器明确找到要调用的父类成员函数,必须显式指定类域(如 Vector<T1>::push_back(val)),告诉编译器'该函数属于哪个父类模板',从而保证编译和实例化的正确性。
这个是一个比较关键的点,希望大家注意。
那么还有就是,除了我们可以指定类域,还可以用 this 指针:
在类模板的继承中,除了通过显式指定类域(如 父类模板名<参数>::成员函数)来使用父类模板的成员函数,还可以通过 this 指针来访问。这背后的逻辑与子类对象的内存结构和 this 指针的指向特性直接相关。
我们知道,this 指针的本质是指向当前对象自身的指针。对于子类模板(比如 Stack<T>)来说,this 指针指向的是 Stack<T> 的对象实例。而根据继承的特性,子类对象的存储空间中会包含父类模板(比如 Vector<T>)的成员变量(以及对父类成员函数的访问权)—— 就像子类对象'包含'了父类的部分一样。
正因为子类对象中'包含'父类的成员(非 private 部分),this 指针在指向子类对象时,自然也能'触及'到这些继承自父类的成员函数。也就是说,当我们在子类模板中用 this->父类成员函数 () 时,this 指针会先定位到当前子类对象,再通过对象内部包含的父类部分,找到对应的父类成员函数并调用。
这种方式无需显式写类域,是因为 this 指针已经隐含了当前对象的完整信息(包括继承自父类的部分),this->的作用是触发'延迟查找',让编译器在实例化(即外部调用了)时才执行'子类->父类'的查找流程,编译器能通过 this 指针的指向,明确要访问的成员函数来自父类模板,从而正确关联到对应的实现。
下面是示例代码:
#include <iostream>
#include <vector>
using namespace std;
namespace win {
template <typename T1>
class stack : public vector<T1> {
public:
void push(const T1& val = T1()) {
// push_back(val); // 注意,不能像上面那样直接使用,
// 因为虽然继承了 vector 作为父类,但编译器无法自动识别
// 该 push_back 函数是来自 vector 的成员
// 因此需要通过 this 指针明确访问父类成员
this->push_back(val);
// 原因:stack<int>实例化时会同时实例化 vector<int>,
// 但模板采用按需实例化机制,未显式调用的成员函数(如 push_back)不会提前实例化,
// 导致编译器无法直接找到该函数
}
void pop() { this->pop_back(); }
T1& top() { return this->back(); }
size_t size() const { return this->size(); }
bool empty() const { return this->size() == 0; }
private:
// 无需再定义容器作为成员变量,
// 因为已继承自 vector 父类,可直接使用其成员函数
};
}
int main() {
win::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty()) {
cout << st.top() << " ";
st.pop();
}
return 0;
}
OK,对于继承类模版的解析,就到这里,大家要记住一些注意点哦。
那么这就又是一个比较重要的知识点了,我们且看下文
在 C++ 的 public 继承关系中,派生类对象(子类对象)有一个非常关键的特性:它可以直接赋值给基类(父类)指针或基类引用,这个过程有个形象的名字叫'切片'(也叫'切割')。
要理解'切片',得先回到派生类对象的内存布局 —— 之前我们提到过,子类对象的内存里会完整包含父类的所有成员(数据成员存于对象内存中,继承的成员函数虽在代码区,但子类拥有访问权),相当于子类对象的内存空间里,'嵌套'了一块专属的'父类成员区域',再加上子类自己新增的成员变量(比如 Student 类继承 Person 类后,内存里既有 Person 的'姓名、年龄',又有自己的'学号')。
当把子类对象赋值给父类指针或引用时,编译器不会把整个子类对象都'搬过去',而是只'切取'其中'父类成员区域'的内容:如果是赋值给父类引用,引用会直接绑定到子类对象里的'父类成员区域',相当于'只盯着对象里属于父类的部分';如果是赋值给父类指针,指针会指向子类对象中'父类成员区域'的起始地址。正因为只'切取'了父类部分,所以后续通过这个父类指针或引用访问时,只能拿到父类定义的成员(比如用指向 Student 对象的 Person 指针,只能访问'姓名',没法访问'学号'),子类新增的专属成员是访问不到的。

这里必须注意,'切片'的赋值关系是单向的 —— 绝对不能把父类对象赋值给子类对象。原因很简单:父类对象的内存里根本没有子类新增的成员,强行赋值的话,子类那些专属成员就找不到对应的初始化数据,会变成随机值;更重要的是,这违背了继承的设计逻辑 —— 父类是子类的通用模板(比如 Person 是 Student 的通用模板),子类是父类的特殊化扩展(Student 是有'学号'的特殊 Person),通用模板自然无法包含特殊扩展的内容,反过来赋值也就不成立。
说白了就是,子类对象里本来就存着父类的成员变量,只是多了些父类没有的专属成员;所以把子类对象赋值给父类指针或引用时,编译器会自动'过滤'掉子类独有的成员,只让父类指针或引用关联到子类里'属于父类的那部分成员变量',这样既符合继承的内存结构,又保证了访问的合理性。
这个还是比较重要的,我们在接下来的学习中就会用到这个知识,希望大家注意,下面给出示例代码:
#include <iostream>
#include <string>
using namespace std;
class person {
public:
void identity() { cout << "身份识别" << endl; }
protected:
string mname = "张三"; // 姓名
string maddress; // 地址
string mtel; // 电话
int mage = 18; // 年龄
};
class student : public person {
public:
// 学习
void study() { cout << "学习" << endl; }
void definestudent() { mage = 20; maddress = "太阳系"; }
void printstudent() { cout << mage << " " << maddress << endl; }
private:
string mstuid; // 学生学号
};
class teacher : public person {
public:
// 授课
void teaching() { cout << "授课" << endl; }
// 修正拼写错误:defineteavher → defineteacher
void defineteacher() { mage = 29; maddress = "地球"; }
void printteacher() { cout << mage << " " << maddress << endl; }
protected:
string title; // 教师职称
};
int main() {
// 子类对象实例化
student s;
teacher t;
// 子类对象赋值给基类指针
person* p1 = &s;
person* p3 = &t;
// 子类对象赋值给基类引用
person& p2 = s;
person& p4 = t;
// 子类对象赋值给基类对象(通过基类拷贝构造完成,后续讲解)
student sobj;
person pobj = sobj;
// 2. 基类对象不能赋值给派生类对象,以下代码编译会报错
person p; // s = p; // 错误:没有与这些操作数匹配的"="运算符
return 0;
}
大家依旧是结合着代码和文字进行理解哦。
在继承体系中,基类和派生类从始至终保持着各自独立的类域,并不会因为存在继承关系,就将两者的成员名称自动合并到同一个作用域里。这种独立的类域设计,是理解'成员隐藏'现象的关键前提。
当派生类(子类)中定义了与基类(父类)同名的成员时 —— 不管这个成员是数据成员(比如父类有 int age,子类也定义 int age),还是成员函数(比如父类有 void show(),子类也定义 void show()),就会触发'隐藏'机制。简单来说,派生类的同名成员会像一层'遮挡',屏蔽掉对基类同名成员的直接访问:在派生类内部编写代码时,直接使用这个同名成员,编译器会默认匹配派生类自己的成员;通过派生类对象访问这个成员时,同样只会找到派生类的版本,基类的同名成员则被'藏'了起来,无法直接调用。
如果确实需要在派生类中访问被隐藏的基类成员,就必须通过显式指定类域的方式实现,也就是用'基类名::基类成员名'的格式(比如父类是 Person,要访问其 age 成员,就写 Person::age)。这样能明确告诉编译器,要找的是基类作用域下的成员,从而绕开隐藏,精准定位到基类的成员。
这里需要特别注意成员函数的隐藏规则,它和我们熟悉的'函数重载'完全不同:函数重载要求多个函数在同一作用域内,且参数列表(个数、类型、顺序)不同;而成员函数的隐藏,只要派生类的函数与基类的某一函数名字相同,哪怕两者的参数列表、返回值类型完全不一样,都会构成隐藏。比如基类有 void func(int),派生类定义 void func(double),此时派生类的 func(double) 会隐藏基类的 func(int)—— 如果通过派生类对象直接调用 func(10)(传入 int 类型参数),编译器会报错,因为它只会在派生类中找 func,却找不到匹配 int 参数的版本,必须显式写 Person::func(10) 才能正确调用基类的函数。
基于这些特性,在实际开发的继承体系中,我们应尽量避免在派生类中定义与基类同名的成员。因为隐藏很容易导致代码逻辑模糊(比如开发者误以为调用的是基类成员,实际却执行了派生类成员),还可能引发访问歧义或逻辑错误,不利于代码的可读性和后续维护。当然,也存在一些无法避免的场景,比如运算符重载函数(不同类可能都需要重载 operator+),这种情况下,若要使用基类的同名运算符重载函数,就必须严格通过'基类名::运算符函数'的方式指定类域,确保代码逻辑正确。
下面给大家一个示例代码帮助大家理解:
// Student 的_num 和 Person 的_num 构成隐藏关系,
class Person {
protected:
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person {
public:
void Print() {
cout << "姓名:" << _name << endl;
cout << "身份证号:" << Person::_num << endl;//指定是父类 person 里面的_num
cout << "学号:" << _num << endl;
}
protected:
int _num = 999; // 学号
};
int main() {
Student s1;
s1.Print();
return 0;
};
//可以看出这样代码虽然能跑,但是非常容易混淆
OK 大家,接下来我们来两道选择题考考大家。
我们先看代码:
#include <iostream>
using namespace std;
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.fun();
return 0;
}
我觉得还是很简单的,下面我就给出答案啦:
答案:B(隐藏)
func 是隐藏关系。答案:A(编译报错)
fun(int i) 隐藏了 A 类的 fun(),直接调用 b.fun() 时,编译器会在 B 类中查找 fun(),但 B 类只有 fun(int i),参数不匹配,导致编译报错。(若要调用 A 类的 fun(),需显式指定类域:b.A::fun())还是很简单滴。
OK 大家,接下来按道理我们应该讲讲继承中子类和父类的默认构造函数,但是由于那一部分内容偏多,所以我就放在下一篇博客进行讲解,我们接下来来讲讲继承与友元、继承与静态成员、实现一个不能被继承的类。
OK 大家,那么大家肯定有点疑问,万一我就想让一个类不能被继承呢,那么有没有什么方法呢?诶,有的有的,肯定有的。而且有两个方法。
将基类的构造函数设为私有。由于派生类在初始化自身时,必须调用基类的构造函数才能完成初始化,而基类构造函数私有化后,派生类无法访问并调用它,因此派生类无法实例化出对象。
下面是示例代码:
class Base {
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
// C++98 的方法
Base() {}
};
class Derive : public Base // 编译报错
{
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
利用 C++11 新增的 final 关键字。用 final 修饰基类后,派生类将不能继承该基类,具体使用方式是将 final 加在父类的名字后面,即'class 父类名字 final'。
下面是示例代码:
// C++11 的方法
class Base final {
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
// C++98 的方法
/*Base() {}*/
};
class Derive : public Base // 不能将'final'类类型用作基类
{
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
这个还是很好理解的,我比较推荐大家使用 C++11 的方法,即 final 就行,比较方便,也比较直观。
在 C++ 中,友元关系的'不可继承性'是一个需要明确的核心特性,我们可以从'权限范围'和'继承传递'两个维度深入理解:
首先,友元的本质是单向的权限授予。当类 A 将函数 f(或类 B)声明为友元时,相当于 A 主动开放了自己的'私有领地'(private 成员)和'受保护区域'(protected 成员),允许 f 或 B 的成员直接访问。但这个权限有严格的范围限制 —— 仅针对 A 自身的成员,与 A 的派生类无关。
举个例子:假设基类 Parent 有一个友元函数 print,print 可以自由访问 Parent 的私有成员(如 m_data)。当 Child 类公有继承 Parent 后,Child 会继承 Parent 的 m_data(假设为 protected 成员),同时可能新增自己的私有成员 m_extra。此时,print 作为 Parent 的友元,只能直接访问 Parent 对象的 m_data,但有两个关键限制:
Child 对象访问继承自 Parent 的 m_data—— 因为 Child 是独立的类,print 没有获得 Child 的授权,哪怕 m_data 来自父类,在 Child 对象中也属于 Child 的内存布局一部分,受 Child 的访问控制约束;Child 新增的 m_extra—— 这是 Child 独有的私有成员,与 Parent 无关,自然不在 print 的权限范围内。这背后的逻辑是:友元关系是两个实体之间的直接约定(如 Parent 与 print),这种约定不会随继承关系'传递'给派生类。派生类作为独立的类,其私有 / 保护成员的访问权限,只能由它自己主动声明的友元来获得。
因此,若想让 print 既能访问 Parent 的成员,又能访问 Child 的成员,必须分别在 Parent 和 Child 中都将 print 声明为友元 —— 二者缺一不可,因为 Parent 的友元声明无法'替 Child 做主'开放权限。
总结来说,友元关系的不可继承性,本质是对类封装边界的严格保护:每个类只负责管理自己的访问权限,不会因为继承关系就将父类的友元'自动纳入'自己的信任列表,这也避免了友元权限通过继承被无限制扩散,保障了代码的封装性和安全性。
下面给出示例代码:
#include <iostream>
#include <string>
using namespace std;
class Student;
class Person {
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person {
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s) {
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main() {
Person p;
Student s;
// 编译报错:error C2248: 'Student::_stuNum': 无法访问 protected 成员
// 解决方案:Display 也变成 Student 的友元即可
Display(p, s);
return 0;
}
这个也是比较简单的,我们就直接过了就行。
在 C++ 继承体系里,基类定义的 static 静态成员(包括静态成员变量和静态成员函数)有一个核心特性 ——全局唯一实例,这个特性贯穿整个继承体系,不会因派生类的创建或对象实例化而改变。
首先要明确 static 成员的基础特性:它不属于某个具体的对象,而是属于整个类,存储在全局数据区(而非对象的内存空间)。无论创建多少个类对象,静态成员都只有一份副本,所有对象共享这一个实例。
当基类定义了静态成员后,这个'属于类、全局唯一'的属性会直接延续到继承体系中:整个继承体系(包括基类自己、所有直接派生的子类、子类再派生的孙类等)里,只会存在这一个静态成员实例,不会因为类的继承关系而产生新的副本。
假设基类 Parent 定义了静态成员变量 count 和静态成员函数 getCount(),子类 Child 公有继承 Parent,此时会有以下关键表现:
Child 不会因为继承 Parent,就额外创建一个属于自己的 count 副本,它和 Parent 共享同一个 count 实例。Parent::count、Parent::getCount()),还是通过子类访问(Child::count、Child::getCount()),本质都是操作同一个静态成员实例。Parent 对象、20 个 Child 对象,这些对象访问的 count 依然是同一个,修改其中一个对象关联的 count,所有对象访问到的 count 值都会同步变化。静态成员的'全局唯一'特性,根源在于它的归属权只属于定义它的基类,继承关系不会改变它的归属。子类虽然能访问基类的静态成员(取决于访问权限,如 public 或 protected),但这种访问是'共享式访问',而非'拥有式继承'—— 子类没有自己的静态成员副本,只是获得了访问基类静态成员的权限。
例如:
class Parent {
public:
static int count; // 基类定义静态成员变量
};
int Parent::count = 0; // 静态成员必须在类外初始化
class Child : public Parent {};
int main() {
Parent p1;
Child c1;
p1.count++; // 基类对象修改:count 变为 1
c1.count++; // 子类对象修改:count 变为 2
cout << Parent::count; // 输出 2(所有访问共享同一实例)
cout << Child::count; // 输出 2(与基类共享同一实例)
return 0;
}
上述代码中,Parent 和 Child 的所有操作,最终都作用于同一个 count 实例,充分体现了静态成员在继承中的'全局唯一性'。
还有比如下面这一段代码:
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
string _name;
static int _count;
};
int Person::_count = 0;
class Student : public Person {
protected:
int _stuNum;
};
int main() {
Person p;
Student s;
// 这里的运行结果可以看到非静态成员_name 的地址是不一样的
// 说明派生类继承下来了,父派生类对象各有一份
cout << &p._name << endl;
cout << &s._name << endl;
// 这里的运行结果可以看到静态成员_count 的地址是一样的
// 说明派生类和基类共用同一份静态成员
cout << &p._count << endl;
cout << &s._count << endl;
// 公有的情况下,父派生类指定类域都可以访问静态成员
cout << Person::_count << endl;
cout << Student::_count << endl;
return 0;
}
基类的 static 静态成员在继承体系中是'全局唯一'的:整个体系内只有一份实例,子类不生成新副本,所有类(基类、子类)和对象都共享这一份。这种特性的本质是静态成员'属于类、不依赖对象'的基础属性,继承关系只会传递其访问权限,不会改变其'唯一实例'的核心特性。
亲爱的小伙伴们,当你读到这里时,我们关于 C++ 继承的第一篇博客已经接近尾声了。或许此刻你眼前还浮现着类与类之间的'父子关系',脑海里回荡着'切片''隐藏''静态成员共享'这些陌生又熟悉的概念 —— 别急,这正是我们一步步走进 C++ 进阶世界的印记。
回顾这篇博客的旅程,我们从'代码复用'的初心出发,揭开了继承的神秘面纱。你看,当 Student 和 Teacher 类不再重复书写姓名、年龄这些共性成员,而是优雅地继承自 Person 类时,我们第一次感受到了继承的魅力:它像一把精巧的剪刀,裁掉了冗余的代码,留下了简洁与高效。就像现实中,我们不必重新学习'呼吸''行走'这些人类共有的能力,而是直接继承它们,专注于培养属于自己的独特技能 —— 编程、绘画、思考,这或许就是继承最生动的隐喻。
我们聊到了继承的三种方式,public、protected、private,它们像三道不同的门:public 继承让基类的接口坦然向世界开放,就像父母教会我们'善良'与'真诚',让我们能在社会中自然展现;protected 继承则像家族内部的秘密技艺,只传子孙,不泄外人;而 private 继承更像尘封的日记,仅当前类可翻阅。这些访问控制的规则,看似繁琐,实则是 C++ 对'封装性'的坚守 —— 既让代码复用成为可能,又严格守护着每个类的边界。
还记得'切片'现象吗?子类对象能被基类指针或引用指向,却不能反过来,这像极了生活中的'局部与整体':我们可以说'学生是一个人',却不能说'人是一个学生'。这种单向的转换规则,不仅符合逻辑,更藏着 C++ 对类型安全的深思。而当我们看到子类对象的内存中'嵌套'着父类成员时,是不是突然明白:原来继承不是简单的'复制粘贴',而是一种巧妙的'包含与共享'—— 父类的定义是公共的模板,每个子类对象却拥有独立的父类成员存储空间,就像每个孩子都继承了父母的基因,却长成了独一无二的自己。
类模板的继承或许让你有些头疼:为什么调用父类成员函数时非要加类域或 this 指针?'按需实例化'的机制告诉我们,编译器其实是个'懒家伙',不到万不得已不会生成代码 —— 这也提醒我们,写代码时要像给编译器'指路'一样清晰,才能避免不必要的错误。而成员的'隐藏'规则更像一记警钟:子类与父类的同名成员看似巧合,实则可能埋下逻辑陷阱,就像两个同名的文件放在不同文件夹,稍不注意就会拿错。
实现一个不能被继承的类、友元关系的不可继承性、静态成员的全局唯一性…… 这些知识点像一颗颗散落的珍珠,被'继承'这条线串在一起,渐渐勾勒出 C++ 面向对象设计的轮廓。你会发现,继承从来不是孤立的技术,它与封装、作用域、访问控制紧密相连,共同构建着代码的秩序与美感。
或许现在的你,对某些概念还似懂非懂,看到代码时仍会犹豫 —— 这太正常了。学习就像爬山,每向上一步,都会看到新的风景,也会遇到新的挑战。继承作为 C++ 的三大特性之一,其深度与广度需要我们在实践中慢慢品味:多写几行代码验证'隐藏'与'重载'的区别,多调试几次观察'切片'时的内存变化,多思考为什么静态成员要在类外初始化…… 这些细碎的探索,终将让你对继承的理解从'知道'变为'懂得'。
下一篇博客中,我们将深入继承体系中构造函数与析构函数的调用规则,看看当子类诞生与消亡时,父类会扮演怎样的角色。那会是另一段充满发现的旅程,既有规则的严谨,也有设计的智慧。
最后,想对每一个坚持学习的你说:编程的路上没有捷径,但每一步都算数。当你能熟练运用继承搭建出清晰的类层次结构,当你能在代码中既享受复用的便捷,又坚守封装的边界,你会发现,那些曾经让你困惑的概念,早已变成你手中灵活的工具。
休息一下,整理好心情,我们下一篇博客再见。记住,你此刻的每一分努力,都在为未来的自己铺路。加油,勇敢的代码探索者!

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online