目录
- 前言
- 一、派生类的默认成员函数
-
- [1.1 4个常见默认成员函数](#1.1 4个常见默认成员函数)
- [1.2 实现一个不能被继承的类](#1.2 实现一个不能被继承的类)
- 二、继承与友元
- 三、继承与静态成员
- 四、多继承及菱形继承问题
-
- [4.1 继承模型](#4.1 继承模型)
- [4.2 虚继承](#4.2 虚继承)
- [4.3 多继承中指针偏移问题](#4.3 多继承中指针偏移问题)
- [4.4 IO库中的菱形虚拟继承](#4.4 IO库中的菱形虚拟继承)
- 五、继承和组合
-
- [5.1 继承和组合](#5.1 继承和组合)
- 结语


🎬 云泽Q :个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列》
⛺️遇见安然遇见你,不负代码不负卿~
前言
大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~
一、派生类的默认成员函数
1.1 4个常见默认成员函数
6个默认成员函数,默认的意思就是指我们不写,编译器会为我们自动生成一个,下面说一下在派生类中,下面结合代码说一下这几个成员函数是怎么生成的

cpp
#include<iostream>
using namespace std;
class Person
{
public:
//Person(const char* name = "peter")
Person(const char* name)
:_name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;//姓名
};
class Student : public Person
{
public:
Student(const char* name = "张三", int num = 18, const char* address = "西安")
//:_name(name) 这里写法错误,基类要当作一个整体,不能单独初始化
//这里就算不写:_name(name),默认也会调用基类的构造,只不过基类现在没有默认构造调不到
//这里需要显示调用基类的构造
:Person(name)
,_num(num)
,_address(address)
{
cout << "Student()" << endl;
}
//一般情况下派生类不需要自己写拷贝构造
//比如说现在有一个成员涉及深拷贝,需要自己写
Student(const Student& s)
//基类对象引用派生类对象引用的是派生类对象中基类的一部分
:Person(s)
,_num(s._num)
,_address(s._address)
{
cout << "Student()" << endl;
}
//一般情况也不用自己写赋值重载,但在深拷贝场景下需要自己实现
//这里不涉及深拷贝,之所以写只是为了作演示
Student& operator=(const Student& s)
{
if (this != &s)
{
//operator=(s);//运算符重载可以显示调用
//上面写法会报错到库中,并显示Stack overflow,一般无限递归会发生栈溢出
//(栈虽然不大,但完全够用,只有无限递归,调用函数,创建栈帧,才会栈溢出)
//原因是这里的赋值构成了隐藏,需指定类域
Person::operator=(s);
_num = s._num;
_address = s._address;
}
return *this;
}
~Student()
{
//这里是显示调用不到父类的析构的,原因是和父类析构构成了隐藏
//~Person(); //destructor()
//指定类域才可以,但是这里也不用显示调用基类析构,编译器会在派生类析构结束后自动调用基类析构
Person::~Person();
//...
cout << "~Student()" << endl;
}
protected:
int _num;//学号
string _address;//地址
int* _ptr;
};
//构造
//继承的基类成员变量(整体对象) + 自己的成员变量(遵循普通类 的规则,跟类和对象部分一样)
//默认生成的构造对派生类自己的成员,内置类型不确定,自定义类型调用默认构造,基类部分调用基类默认构造
//本质可以把派生类当作多了一个自定义类型成员变量(基类)的普通类,跟普通类原则基本一样
//派生类一般要自己实现构造,不需要显示写析构、拷贝构造、赋值重载、除非派生类有深拷贝的资源需要处理
int main()
{
//构造
Student s1;
//Student s2("小明", 10);
////拷贝构造
////这里对内置类型num调用值拷贝,对自定义类型string调用其拷贝构造,对基类Person调用Person的拷贝构造
//Student s3(s2);
////赋值重载
//s1 = s3;
//Person p = s1;
return 0;
}
一、派生类的构造函数(初始化逻辑)
- 派生类构造必须调用基类构造,初始化基类子对象;
- 如果基类没有默认构造(无参 / 带默认参数的构造),派生类必须在初始化列表显式调用基类的带参构造;
- 初始化顺序固定:先基类构造 → 再初始化派生类成员 → 最后执行派生类构造函数体。
cpp
// 基类Person的构造是带参的(无默认参数),因此没有默认构造
Person(const char* name) : _name(name) { ... }
// 派生类Student的构造必须显式调用基类构造
Student(const char* name = "张三", int num = 18, const char* address = "西安")
: Person(name) // ✅ 显式调用基类带参构造,初始化基类子对象
, _num(num) // 初始化自身内置类型成员
, _address(address) // 初始化自身自定义类型成员(string调用默认构造)
{
cout << "Student()" << endl;
}
- ❌ 错误写法:_name(name)
基类是一个 "整体子对象",不能直接初始化基类的成员变量,必须通过基类的构造函数来初始化。 - ✅ 运行验证:Student s1; 会先打印 Person()(基类构造),再打印 Student()(派生类构造),完全符合 "先基类后派生" 的初始化顺序。
- 🔹 编译器生成的「合成默认构造」
如果 Student 不手写构造函数,编译器会生成合成默认构造,但逻辑是:
-
- 调用基类的默认构造(如果基类无默认构造,直接编译报错);
-
- 对自身成员:自定义类型调用其默认构造,内置类型不初始化(值为随机值)。
代码中 Person 没有默认构造,因此必须手写 Student 的构造函数,否则编译报错。
- 对自身成员:自定义类型调用其默认构造,内置类型不初始化(值为随机值)。
二、派生类的拷贝构造函数(拷贝初始化逻辑)
派生类拷贝构造必须调用基类的拷贝构造,完成基类子对象的拷贝初始化。
cpp
Student(const Student& s)
: Person(s) // ✅ 调用基类拷贝构造,拷贝基类子对象(s的基类部分)
, _num(s._num) // 拷贝自身内置类型成员
, _address(s._address) // 拷贝自身自定义类型成员(string调用拷贝构造)
{
cout << "Student(const Student& s)" << endl;
}
- ✅ 隐式转换支持:Person(s) 中,s 是 Student 对象,会隐式转换为 const Person&,匹配基类拷贝构造的参数。
- 💡 何时需要手写:一般不需要,除非派生类有深拷贝成员(如代码中的 _ptr 如果是动态分配的内存,需要手动拷贝指针指向的资源)。
- 编译器生成的合成拷贝构造:会自动调用基类拷贝构造,再拷贝自身成员(内置类型值拷贝,自定义类型调用其拷贝构造)。
三、派生类的赋值运算符重载(赋值逻辑)
- 派生类 operator= 必须调用基类的 operator=,完成基类子对象的赋值;
- 派生类的 operator= 会隐藏基类的 operator=,因此显式调用时必须指定基类作用域(
Person::operator=)。
cpp
Student& operator=(const Student& s)
{
if (this != &s)
{
// ❌ 错误写法:operator=(s);
// 原因:派生类的operator=隐藏了基类的,直接调用会递归调用自己,导致栈溢出
Person::operator=(s); // ✅ 显式指定基类作用域,调用基类赋值重载
_num = s._num; // 赋值自身内置类型成员
_address = s._address;// 赋值自身自定义类型成员
}
return *this;
}
- ✅ 核心注意:必须先调用基类的 operator=,再赋值自身成员,否则基类子对象的赋值会被遗漏。
- 💡 何时需要手写:一般不需要,除非派生类有深拷贝成员(如 _ptr 指向动态内存,需要手动处理资源的赋值)。
还有一个查找栈溢出的方法就是打开调用堆栈


可以看到272行在不断地调用赋值
四、派生类的析构函数(清理逻辑)
因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个会在后续多态的文章中写)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系
- 派生类析构完成后,编译器会自动调用基类析构,保证清理顺序:先派生类成员 → 再基类成员;
cpp
~Student()
{
// ❌ 错误写法:~Person();
// 原因:析构函数名被处理为destructor(),派生类析构隐藏了基类的,直接调用会报错
// Person::~Person(); // 可以显式调用,但完全没必要,编译器会自动调用
cout << "~Student()" << endl;
}
- ✅ 清理顺序验证:s1 销毁时,先执行 ~Student()(打印 ~Student()),然后编译器自动调用 ~Person()(打印 ~Person()),符合 "先派生后基类" 的清理顺序。
- 💡 何时需要手写:一般不需要,除非派生类有需要手动释放的资源(如 _ptr 是 new 出来的,需要在析构中 delete)。

如图可以看出,这种显示调用基类析构的写法是错误的,调用了两次父类的析构。前面的构造,拷贝构造,赋值重载等等都可以显示调用基类对应的成员函数去完成,但是析构这里不可以显示调,编译器会自动调用

构造和析构的顺序可以简单的认为是一个规定,更深层次的理解就是:
初始化列表初始化的顺序是按照声明的顺序 (在内存当中放的先后顺序),继承可以当作一个整体声明在最前面,所以构造的顺序是先父后子,依旧符合按照内存中存放的顺序初始化
若析构的时候是先父后子,先把父析构了,里面就是随机堆,随机值了,此时去调用子类的析构,但是子类的析构是能访问父类的成员,但是父类的成员此时是随机值,就有可能会出现问题。先子后父就不会出现这样的问题,先析构子,子类的析构有可能会访问父类的成员,但是父类的成员此时还未析构,就不会访问到随机值,再析构父,父类的析构不会访问到子,就不会出现问题。这种先子后父的析构方式可以避免一些野指针/随机值之类的风险
1.2 实现一个不能被继承的类
二、继承与友元
三、继承与静态成员
四、多继承及菱形继承问题
4.1 继承模型
4.2 虚继承
4.3 多继承中指针偏移问题
4.4 IO库中的菱形虚拟继承
五、继承和组合
5.1 继承和组合
结语
