👋 欢迎阅读

🔥愿旖旎 · 个人主页
📘学习专栏: 《算法专栏》《LangChain学习》《贪心算法》
🌄 钱塘江上潮信来,今日方知我是我
✨当前学习内容: 《C++学习》
📑 目录
一、前置讲解
在进入正文前,先花 10 秒了解本篇会反复用到的核心概念,带着印象去读正文,学习效率更高。
🧠 前置知识一:继承(Inheritance)
类设计层面的复用:在保持原有类特性的基础上扩展,复用成员变量 + 成员函数,形成层次结构。
✂️ 前置知识二:切片(Slicing)
派生类对象赋给基类的对象 / 指针 / 引用 时,只把属于基类的那一部分"切"出来。
🙈 前置知识三:隐藏(重定义)
基类与派生类有同名成员 时,派生类成员屏蔽 基类成员;靠 基类::成员 显式访问。
🏗️ 前置知识四:派生类默认成员函数
构造、拷贝构造、赋值重载都要显式处理基类那部分 ;析构顺序先子后父。
🔗 前置知识五:友元与静态成员的继承规则
友元关系不能继承 ;而静态成员整个继承体系只有一份。
二、继承的概念及定义
2.1 继承的概念
继承机制 是面向对象程序设计使代码可以复用的最重要的手段 :它允许程序员在保持原有类特性 的基础上进行扩展、增加新功能,由此产生的新类称为派生类(子类)。
继承体现了面向对象程序设计的层次结构 ,反映了从简单到复杂的认知过程。之前我们接触的复用主要是函数复用 ,而继承是类设计层面的复用。
cpp
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
// 继承后父类 Person 的成员(成员函数 + 成员变量)都会变成子类的一部分。
// 这里体现出了 Student 和 Teacher 复用了 Person 的成员。
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
2.2 继承定义
Person 是父类,也称基类;Student 是子类,也称派生类。

继承方式与访问限定符共同决定:基类成员在派生类中的访问权限。
| 基类成员 \ 继承方式 | public 继承 |
protected 继承 |
private 继承 |
|---|---|---|---|
public 成员 |
public |
protected |
private |
protected 成员 |
protected |
protected |
private |
private 成员 |
不可见 | 不可见 | 不可见 |
总结规则:
| 序号 | 规则 |
|---|---|
| 1 | 基类的 private 成员 在派生类中无论以什么方式继承,都是不可见的 。这里的"不可见"是指:基类的私有成员确实被继承到了派生类对象中 ,但语法上限制了派生类对象(不管类内还是类外)都不能访问它 ------并不是说它不存在,这一点需要牢记 |
| 2 | 基类的 private 成员在派生类中不能访问。如果基类成员不想在类外被直接访问、但又需要在派生类中能被访问,就应该定义为 protected ------可以看出,protected 在很大程度上是因继承才出现的 |
| 3 | 结合表格总结:基类的私有成员在派生类中始终不可见 ;基类其他成员在派生类中的访问方式,可以归纳为 访问权限 = min(基类中该成员的访问限定符, 继承方式) ,权限大小关系为 public > protected > private |
| 4 | 用 class 关键字时默认继承方式是 private ;用 struct 时默认继承方式是 public 。不过最好总是显式写出继承方式,避免混淆 |
| 5 | 实际开发中一般都用 public 继承 ,几乎很少用 protected / private 继承 ,也不提倡:因为这两种继承下来的成员只能在派生类内部使用 |

三、基类和派生类对象赋值转换(切片)
派生类对象可以赋值给基类的对象、基类的指针或基类的引用。 这里有个形象的说法叫**"切片"或"切割"** ,寓意是将派生类对象中属于基类的那一部分"切"出来,然后进行赋值或指向操作。

| 操作 | 能否 | 说明 |
|---|---|---|
| 派生类对象 → 基类对象 | 可以 | 切片:只拷贝基类那部分,派生类新增成员被丢掉 |
| 派生类对象 → 基类指针 / 引用 | 可以 | 指针 / 引用指向的是派生类对象中的基类部分 |
| 基类对象 → 派生类对象 | 不可以 | 基类对象里没有派生类新增的成员,无法"凭空造出来" |
| 基类指针 / 引用 → 派生类指针 / 引用 | 强制转换后可以 | 但只有该基类指针实际指向的是派生类对象时,这种转换才是安全的 |
cpp
class Person // 父类
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person // 子类
{
public:
int _No; // 学号
};
void Test()
{
Student sobj;
// 1. 子类对象可以赋值给父类对象/指针/引用
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
// 2. 基类对象不能赋值给派生类对象
sobj = pobj; // 编译报错
// 3. 基类的指针可以通过强制类型转换赋值给派生类的指针
// 但必须是基类的指针【指向派生类对象】时才是安全的
pp = &sobj; // pp 指向的是子类
Student* ps1 = (Student*)pp; // 这种情况转换是可以的
ps1->_No = 10;
pp = &pobj; // pp 指向的是父类
Student* ps2 = (Student*)pp; // 这种情况转换虽然可以,但会存在越界访问的问题
ps2->_No = 10;
}
运行结果:
sobj = pobj; 编译报错:无法从 "Person" 转换为 "Student"
ps2->_No = 10; 运行时越界访问(pp 指向的 Person 对象里没有 _No 这块空间)
💡 判断安全性的口诀:"指针指向谁,就只能按谁的大小用" 。
pp实际指向Person对象时,把它当Student*用就等于按大房子去住小房子 ------_No落在了对象外面,属于未定义行为。
四、继承中的作用域(隐藏)
| 序号 | 规则 |
|---|---|
| 1 | 在继承体系中,基类和派生类都有各自独立的作用域 |
| 2 | 当子类和父类有同名成员 时,子类成员会屏蔽父类对同名成员的直接访问 ,这叫隐藏(也称重定义) 。在子类成员函数中,可以通过 基类::基类成员 的方式显式访问被隐藏的基类成员 |
| 3 | 注意:如果是成员函数的隐藏,只需要函数名相同即可构成隐藏 ,与参数列表无关 。这一点要和重载 区分:重载要求同一作用域内且函数参数不同 ,而隐藏发生在基类和派生类之间,函数名相同即触发 |
| 4 | 实际开发中,继承体系里最好不要定义同名成员,避免混淆和误用 |
cpp
// 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; // 显式访问被隐藏的基类成员
cout << " 学号:" << _num << endl; // 默认访问自己的 _num
}
protected:
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
}
运行结果:
姓名:小李子
身份证号:111
学号:999
重载和隐藏的区分:
cpp
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
// B 中的 fun 和 A 中的 fun 不是重载,因为不在同一作用域
// B 中的 fun 和 A 中的 fun 构成隐藏:成员函数只要函数名相同就隐藏
void fun(int i)
{
A::fun(); // 显式调用被隐藏的基类版本
cout << "func(int i)->" << i << endl;
}
};
int main()
{
B b;
b.fun(10);
// b.fun(); // 编译报错:基类的 fun() 被隐藏了
b.A::fun(); // 显式访问
return 0;
}
运行结果:
func()
func(int i)->10
func()
| 对比项 | 重载(Overload) | 隐藏(重定义) |
|---|---|---|
| 发生位置 | 同一作用域内 | 基类和派生类之间(不同作用域) |
| 条件 | 函数名相同 + 参数列表不同 | 函数名相同 即可(与参数无关) |
| 调用 | 编译器按参数自动匹配 | 默认访问派生类版本,基类版本要用 基类:: 显式调 |
| 本质 | 同一层级里"多个版本" | 内层作用域屏蔽外层同名名字 |
💡 隐藏最坑的地方:基类有
fun()和fun(int)两个重载,派生类只写了一个fun()------结果基类的fun(int)也被一起隐藏了 ,b.fun(10)直接编不过。这跟"参数无关"那条规则是同一个道理。
五、派生类的默认成员函数
6 个默认成员函数 ,"默认"的意思是如果我们不写,编译器会自动生成一个。那么在派生类中,这几个函数是如何生成的呢?
| 默认成员函数 | 派生类中的规则 |
|---|---|
| 构造函数 | 必须调用基类的构造函数 来初始化基类那部分成员;如果基类没有默认构造函数 ,必须在派生类构造函数的初始化列表阶段显式调用 |
| 拷贝构造函数 | 必须显式或隐式地调用基类的拷贝构造函数,完成基类部分的拷贝初始化 |
赋值运算符重载 operator= |
必须显式或隐式地调用基类的 operator=,完成基类部分的复制 |
| 析构函数 | 派生类析构执行完后,自动调用基类的析构函数清理基类成员 |
构造顺序 :先调用基类构造函数 初始化基类部分,再调用派生类自己构造函数初始化派生类新增成员(因为派生类初始化可能依赖基类成员,所以必须先完成基类构造)。
构造函数示例(基类没有默认构造函数):
cpp
#include <iostream>
#include <string>
using namespace std;
// 基类:没有默认构造函数
class Base
{
public:
Base(const string& n)
: name(n)
{}
private:
string name;
};
// 派生类
class Derived : public Base
{
public:
// 必须在初始化列表中显式调用 Base 的构造函数
Derived(const string& baseName, int v)
: Base(baseName) // 显式调用基类构造
, value(v)
{}
private:
int value;
};
int main()
{
Derived d("example", 42); // 正确:提供了基类所需的参数
return 0;
}
拷贝构造函数示例:
cpp
#include <iostream>
#include <string>
using namespace std;
class Base
{
public:
Base(const string& n)
: name(n)
{}
Base(const Base& other)
: name(other.name)
{
cout << "Base 拷贝构造" << endl;
}
private:
string name;
};
class Derived : public Base
{
public:
// 普通构造函数
Derived(const string& baseName, int v)
: Base(baseName)
, value(v)
{}
// 拷贝构造函数:必须显式调用 Base(const Base&)
Derived(const Derived& other)
: Base(other) // 显式调用基类拷贝构造
, value(other.value)
{}
private:
int value;
};
int main()
{
Derived d1("example", 42);
Derived d2 = d1; // 触发拷贝构造
return 0;
}
运行结果:
Base 拷贝构造
拷贝构造的两种情况(重要):
| 情况 | 编译器行为 | 结果 |
|---|---|---|
| 一、没有自定义派生类拷贝构造 | 自动生成默认版本,会自动调用基类的拷贝构造函数拷贝基类部分 | 不用手写代码,但"调用基类拷贝构造"这个行为依然发生了 |
| 二、自定义了派生类拷贝构造 | 如果不在初始化列表显式调用基类拷贝构造 ,编译器会默认调用基类的默认构造函数(无参构造) ,而不是拷贝构造 | 通常导致逻辑错误 :基类部分没有被"拷贝",而是被"默认构造"了,与预期拷贝行为不一致 |
析构顺序:
| 阶段 | 顺序 |
|---|---|
| 构造 | 基类构造 → 派生类构造 |
| 析构 | 派生类析构 → 基类析构 |

💡 为什么先子后父 ?因为子类对象可能会用到父类的成员 ------如果先析构父类,子类析构时再访问父类成员就是访问已销毁对象。所以析构顺序必须和构造顺序相反。
六、继承与友元
友元关系不能继承 ------也就是说,基类友元不能访问子类私有和保护成员。
cpp
#include <iostream>
#include <string>
using namespace std;
class Student; // 前向声明:Person 的友元声明里用到了 Student
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name = "peter"; // 姓名
};
class Student : public Person
{
protected:
int _stuNum = 1001; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl; // 编译错误:父亲的朋友并不是我的朋友
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
运行结果:
编译报错:无法访问 protected 成员(Student::_stuNum)
💡 一句话记:"爸爸的朋友不是我的朋友" 。友元是针对某个类 单独授予的权限,它不随继承关系传递------所以基类的友元只能碰基类的私有/保护成员。
七、继承与静态成员
基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。 无论派生出多少个子类,都只有一个 static 成员实例。
cpp
class Person // 父类
{
public:
Person() { ++_count; }
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数
};
int Person::_count = 0;
class Student : public Person // 子类
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student // 子类
{
protected:
string _seminarCourse; // 研究科目
};
void TestPerson()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << " 人数 :" << Person::_count << endl; // 4
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl; // 0
}
int main()
{
TestPerson();
return 0;
}
运行结果:
人数 :4
人数 :0
| 要点 | 说明 |
|---|---|
| 只有一份 | 静态成员属于类 ,整个继承体系共享同一份实例 |
| 访问方式 | Person::_count、Student::_count、Graduate::_count 都能改到同一份 |
| 印证 | 把 Student::_count 改成 0,Person::_count 跟着变 0 |
💡 输出里的
4来自 4 个对象(3 个Student+ 1 个Graduate,Graduate也间接继承了Person的构造),而把Student::_count置 0 后Person::_count也变成 0------这正说明Student::_count并不是一个独立变量,只是同一份数据的另一个名字。
那么今天这部分内容就先讲到这里。目前的重点是一、二、四:一是类继承的重要概念,二、四则是子类和父类的联系,要理解透彻。下一章讲类继承的难点,可以期待一下!
八、复盘(附答案)
💡 思考题
-
继承的概念及定义 :
public、protected、private三种继承方式分别对基类成员在派生类中的访问权限 产生什么影响?如何选择继承方式? -
基类和派生类对象赋值转换 :什么是切片 ?基类指针指向派生类对象 和派生类指针指向基类对象分别有什么注意事项?
-
继承中的作用域 :派生类和基类同名成员 会发生什么?如何显式访问 基类中被隐藏的成员?隐藏和重载的区别是什么?
-
派生类的默认成员函数 :派生类的构造函数 为什么必须在初始化列表中显式调用基类构造函数 ?拷贝构造 和赋值重载需要注意什么?
-
继承与友元 :基类的友元函数能否访问派生类的私有成员?为什么?
-
继承与静态成员 :基类中定义的静态成员 在继承体系中有多少份实例?派生类能否访问基类的静态成员?
📝 答案
-
public继承 保持基类成员原有访问权限;protected继承 将基类public成员降为protected;private继承 将所有基类成员降为private。一般使用public继承 ,protected和private继承较少使用,主要用于实现细节隐藏 。记住口诀:访问权限 = min(成员原权限, 继承方式) ,而private成员永远不可见。 -
切片 指派生类对象赋给基类对象 / 指针 / 引用时,切掉派生类部分、只保留基类部分 。基类指针指向派生类对象 时,通过它只能访问基类成员 (安全);基类对象赋给派生类对象 是不允许的,派生类指针指向基类对象 是不安全的 ------强制转换后访问派生类成员会越界(未定义行为)。
-
派生类和基类同名成员会构成隐藏 :派生类默认访问自己的成员,要访问基类被隐藏的成员需用
基类::成员名。隐藏 发生在不同作用域 (基类和派生类),只要函数名相同 就触发;重载 必须发生在同一作用域 ,且参数列表不同。 -
派生类构造函数必须在初始化列表 中显式调用基类构造函数,因为基类成员只能通过基类构造函数初始化 。拷贝构造 也需要显式调用基类拷贝构造(
Base(other)),否则编译器会调基类的默认构造 而不是拷贝构造,导致基类部分没被拷贝;赋值重载 需显式调用基类赋值(Base::operator=(other));析构函数 在派生类析构后自动调用 基类析构,顺序是先子后父。 -
不能 。友元关系不能继承 ------基类的友元函数只能访问基类的私有/保护成员,无法访问派生类的私有和保护成员 ,因为友元是基于具体类授予的权限,不是继承关系的一部分。
-
基类中的静态成员在整个继承体系中只有一份 ,所有派生类共享同一个实例 。派生类可以通过
基类::静态成员或派生类::静态成员访问,而且改的是同一份数据。
结语
如果本文对你有帮助,欢迎:
👍 点赞 | ⭐ 收藏 | 👤 关注作者 | 💬 留言交流你的疑问或补充
你的每一次互动都是我继续更新的动力,我们下一篇见!🚀