《C++进阶之继承多态》【普通类/模板类的继承 + 父类子类的转换 + 继承的作用域 + 子类的默认成员函数】
【普通类/模板类的继承 + 父类&子类的转换 + 继承的作用域 + 子类的默认构造函数】目录
- 前言:
- ------------------------
- 一、继承的定义和使用
- 1. 什么使继承?
- 2. 为什么要引入继承?
- 3. 怎么使用继承?
- ① 父类(基类)
- ② 子类(派生类)
- ③ 继承方式
- 4. 使用继承需要注意什么?
- ------------------------
- 二、模板类的继承
- 1. 模板类的继承有哪些?
- ① 模板类继承普通类
- ② 模板类继承另一个模板类
- 2. 模板类之间的继承需要注意什么?
- ------------------------
- 三、父类与子类之间的转换
- 1. 什么是父类与子类之间的转换?
- 2. 父类与子类之间的转换有哪些类型?
- ① 向上转型(子类转父类)
- ② 向下转型(父类转子类)
- ------------------------
- 四、继承中的作用域
- 1. 什么是隐藏?
- 2. 隐藏有哪些?
- ① 变量隐藏
- ② 函数隐藏
- 3. 关于继承作用域的相关习题
- ------------------------
- 五、子类的默认成员函数
- 1. 默认构造函数
- ① 介绍
- ② 使用
- ③ 初始化
- ④ 禁用
- 2. 拷贝构造函数
- ① 介绍
- ② 使用
- ③ 禁用
- 3. 拷贝赋值运算符重载函数
- ① 介绍
- ② 使用
- ③ 禁用
- ④ 拷贝构造 vs 拷贝赋值
- 4. 析构函数
- ① 注意事项
- ② 析构顺序
- 子类中常见的四大默认成员函数的大总结:
往期《C++初阶》回顾:
《C++初阶》目录导航
前言:
hi~ 小伙伴们大家好呀(ノ´ヮ)ノ*: ・゚!今天可是末伏哦⏳(。•̀ᴗ-)✧
这意味着三伏天的前两伏已经悄悄溜走啦🏃♂️💨(°▽°)/,从今天起我们就正式进入最后一伏啦~ 末伏固定是 10 天📅(๑•̀ㅂ•́)و✧,也就是说,十天之后,夏天的暑气就会慢慢消散咯(ฅ´ωฅ)。
那今天呢,我们要开启《C++ 进阶》的第一课啦🎉(≧∇≦)/:
【普通类 / 模板类的继承 + 父类 & 子类的转换 + 继承的作用域 + 子类的默认成员函数】 📚💡(。・ω・。)ノ♡
内容是关于 “继承” 的知识点哦🧬,新的知识带来新的开始✨(ノ◕ヮ◕)ノ*:・゚✧
让我们一起加油学习吧🚀!冲鸭💪🔥(ง •̀_•́)ง
------------------------
一、继承的定义和使用
1. 什么使继承?
继承(Inheritance)
:是面向对象编程(OOP)中的核心概念之一,它允许一个类(称为子类或派生类)直接拥有另一个类(称为父类或基类)的属性和方法,并可以在此基础上扩展新的功能或修改原有实现。
- 通过继承,子类能复用父类的代码,减少重复开发,同时形成类之间的层次关系,体现
“is-a”(属于)
的逻辑关系。
2. 为什么要引入继承?
现在大家试想一下,假如说现在要求你设计两个类:学生类、教师类,具体需求如下:
- 学生类
- 成员变量:需包含学生的姓名、年龄、电话、地址、学号
- 成员函数:需实现学生的身份认证功能,以及进行学习的行为
- 教师类
- 成员变量:需包含教师的姓名、年龄、电话、地址、职称
- 成员函数:需实现教师的身份认证功能,以及进行授课的行为
小伙伴们,面对这样的需求,你会如何设计这两个类呢?
我相信有不少的小伙伴们是像下面这样进行设计的。
#include <iostream>
using namespace std;/*--------------------------定义“学生类”--------------------------*/
class Student
{
public:/*----------------成员函数(学生的行为)----------------*///1.实现:“身份的验证逻辑”的函数void identity(){// 实现身份验证逻辑(如:调用二维码扫描接口)// ...}//2.实现:“进行学习”的函数void studing(){// 实现学习逻辑(如:记录学习时间、课程等)// ...}protected:/*----------------成员变量(学生的属性)----------------*/string _name;int _age;string _tel;string _address;int _stuid; // 学号(唯一标识学生身份)
};/*--------------------------定义“教师类”--------------------------*/
class Teacher
{
public:/*----------------成员函数(教师的行为)----------------*///1.实现:“身份的验证逻辑”的函数void identity(){// 实现教师身份验证逻辑// ...}//2.实现:“进行授课”的函数void teaching(){// 实现授课逻辑(如:记录课程内容、学生出勤等)// ...}protected:/*----------------成员变量(教师的属性)----------------*/string _name;int _age;string _tel;string _address;string _title; // 职称(如"教授"、"副教授"等)
};int main()
{return 0;
}
从小伙伴们设计的
Student
(学生类)和Teacher
(教师类)可以发现,这两个类存在不少共性:
- 都有姓名、年龄、电话、地址这些成员变量
- 也都有
identity
(身份认证)这样的成员函数而这些共性内容在两个类里重复定义,造成了代码冗余。
同时,它们也存在各自的差异:
- 成员变量方面,学生独有 “学号”,老师独有 “职称”。
- 成员函数方面,学生有专属的 “学习” 函数,老师有专属的 “授课” 函数 。
所以:我们自然会想到一个问题:就是有没有一种方法,既能避免上述代码冗余问题,又能兼容它们各自的差异呢?
有,哈哈,没错这种方法就是继承
我们可以将两个类的公共成员(如:姓名、年龄、电话、地址、
identity
身份认证函数等)提取到一个基类Person
(人)中然后让
Student
(学生类)和Teacher
(教师类)继承Person
这样一来,它们既能复用基类的公共成员,又能各自添加独有的成员变量(如:学号、职称)和成员函数(如:学习、授课),完美解决代码冗余问题。
#include <iostream>
#include <string>
using namespace std;/*--------------------------定义“基类:人类”--------------------------*/
class Person
{
public:/*----------------成员函数(人的行为)----------------*///1.实现:“身份的验证逻辑”的函数void identity(){cout << "void identity()"<< endl;}protected:/*----------------成员函数(人的属性)----------------*/string _name;int _age;string _tel;string _address;
};/*--------------------------定义“派生类:学生类”--------------------------*/
class Student : public Person
{
public:/*----------------成员函数(学生的行为)----------------*///1.实现:“进行学习”的函数void studing(){// 实现学习逻辑(如:记录学习时间、课程等)// ...}protected:/*----------------成员变量(学生的属性)----------------*/int _stuid; // 学号(唯一标识学生身份)
};/*--------------------------定义“派生类:教师类”--------------------------*/
class Teacher : public Person
{
public:/*----------------成员函数(教师的行为)----------------*///1.实现:“进行授课”的函数void teaching(){// 实现授课逻辑(如:记录课程内容、学生出勤等)// ...}protected:/*----------------成员变量(教师的属性)----------------*/string _title; // 职称(如"教授"、"副教授"等)
};int main()
{//1.创建学生和教师对象Student s;Teacher t;//2.调用继承自Person类的身份认证方法s.identity();t.identity();return 0;
}
3. 怎么使用继承?
想要使用继承的话,我们首先要知道继承的格式是什么样的?
① 父类(基类)
父类(基类)
:被继承的类,包含子类共有的属性和方法。
- 例如:定义 “动物” 类作为父类,包含 “呼吸”“移动” 等通用方法。
② 子类(派生类)
子类(派生类)
:继承父类的类,除了拥有父类的成员,还可以添加新成员或重写父类方法。
- 例如:“狗” 类作为子类继承 “动物” 类,新增 “吠叫” 方法,并重写 “移动” 方法(如:“四条腿跑”)
③ 继承方式
继承的类型
- 公有继承:子类保留父类成员的访问权限(public 成员在子类中仍为 public,protected 仍为 protected)
- 保护继承:父类的 public 和 protected 成员在子类中变为 protected
- 私有继承:父类的 public 和 protected 成员在子类中变为 private(子类内部可访问,但无法继续继承)
4. 使用继承需要注意什么?
基类中的私有成员,无论派生类以何种方式继承,在派生类中都是不可见的。
- 这里的
“不可见”
指的是:基类私有成员虽然会被继承到派生类对象的内存空间中,但语法上禁止派生类在类内或类外访问它们。
另外需要注意:
使用关键字
class
定义派生类时,默认继承方式为private
使用
struct
时,默认继承方式为public
不过,为了代码清晰性,最好显式写出继承方式。
注意:在实际开发中,通常只使用
public继承
,几乎不推荐使用protected
或private
继承。
(。・ω・。)ノ♡ 看到上面的内容,估计大家都会有点懵吧~(๑•́ω•̀๑)💦
毕竟我们之前学的访问权限,一旦和继承关联起来,就会变得异常复杂(这可以说是 C++ 设计中不太好的一点啦),超级容易踩坑(╥﹏╥)!不过没关系哟~ 博主经过无数次踩坑,还综合了很多人的经验,总结出了下面的表格,希望这次能一次性解决大家的心头之痛呀~(≧∇≦)ノ✨
如果能帮到你,那博主真的会超开心的哦(。♥‿♥。)✨, (ฅ´ω`ฅ) 偷偷举手~要是能顺便关注博主一下,那就更更更开心啦!
------------------------
二、模板类的继承
前面我们看到的继承都是
普通类的继承
,其实继承也可以是模板类的继承
,那如何实现模板类的继承呢?
1. 模板类的继承有哪些?
模板类的继承的基本形式:
模板类继承普通类
模板类继承另一个模板类
① 模板类继承普通类
代码示例:实现“模板类继承普通类”
#include <iostream>
using namespace std;class Base
{
public:void commonMethod(){cout << "Base method" << endl;}
};
template <typename T>
class Derived : public Base // 类模板继承普通类
{
public:T value;void templateMethod(){cout << "Template method: " << value << endl;}
};int main()
{// 使用示例Derived<int> d;d.commonMethod(); // 继承自Based.templateMethod(); // 模板特有的方法return 0;
}
② 模板类继承另一个模板类
代码示例:实现“模板类继承另一个模板类”
#include <iostream>
#include <string>
using namespace std;template <typename T>
class BaseTemplate
{
public:T data;void print(){cout << "Base data: " << data << endl;}
};template <typename T, typename U>
class DerivedTemplate : public BaseTemplate<T> // 继承时需指定Base的模板参数
{
public:U extraData;void extendedPrint(){cout << "Derived data: " << this->data << ", " << extraData << endl;}
};int main()
{// 使用示例DerivedTemplate<int, string> dt;dt.data = 42; // 继承自BaseTemplatedt.extraData = "text";// 派生模板特有的属性dt.print(); // 继承自BaseTemplatedt.extendedPrint(); // 派生模板特有的方法return 0;
}
2. 模板类之间的继承需要注意什么?
1. 继承时需显式指定基类模板的参数
派生模板必须为基类模板提供类型参数(如:
BaseTemplate<T>
),可以是:
派生模板自身的类型参数
(如:T
)具体类型
(如:int
)其他模板参数
(如:U
)示例:
template <typename T> class Base {};template <typename T, typename U> class Derived : public Base<T>// 使用派生模板的参数T { // ... };template <typename T> class Derived2 : public Base<int> // 使用具体类型 { // ... };
2. 基类模板的成员访问
派生模板中访问基类模板的成员时,需通过this->或显式指定类域,避免编译错误。(如:
Base<T>::member
)template <typename T> class Base { public:T value; };template <typename T> class Derived : public Base<T> { public:void setValue(const T& val) {this->value = val; // 必须使用this->或Base<T>::value} };
代码示例:实现“stack模板类继承vector类模板”
#include <iostream>
#include <vector>
using namespace std;namespace mySpace
{//stack类模板:基于vector实现的适配器容器template<class T>class stack : public std::vector<T>{public://1.实现“入栈操作:将元素添加到栈顶”void push(const T& x){ vector<T>::push_back(x); //注意:我们这里继承的是“类模板”,这里一定要显示的指定类域// 等价于:this->push_back(x); 或 std::vector<T>::push_back(x);/* 注意事项:* 基类是类模板时,需显式指定类域(vector<T>::)* 原因:模板实例化是"按需进行"的* 当stack<int>实例化时,vector<int>的框架被实例化* 但vector<int>的成员函数(如:push_back)尚未实例化* 因此直接调用push_back会导致编译错误(找不到标识符)*/}//2.实现:“出栈操作:移除栈顶元素”void pop(){vector<T>::pop_back();}//3.实现:“获取栈顶元素引用”(只读)const T& top(){return vector<T>::back();}//4.实现:“判断栈是否为空”bool empty(){return vector<T>::empty();}};
}int main()
{//1.创建一个存储int类型的栈对象mySpace::stack<int> stk;//2.压入元素:1, 2, 3(栈顶为3)stk.push(1);stk.push(2);stk.push(3);//3.后进先出(LIFO)顺序弹出元素while (!stk.empty()){cout << stk.top() << " ";stk.pop();}return 0;
}
------------------------
三、父类与子类之间的转换
1. 什么是父类与子类之间的转换?
在 C++ 的继承体系中,父类(基类)与子类(派生类)之间的转换:是指不同类型对象或指针、引用之间的赋值或强制类型转换,其核心遵循 赋值兼容规则(Liskov 替换原则)
- 父类(基类)与子类(派生类)之间的转换是面向对象编程中处理类继承关系的重要机制,主要涉及
指针
、引用
和对象
三种形式的转换。
2. 父类与子类之间的转换有哪些类型?
根据转换方向可以分为两类:
向上转型(子类转父类)
和向下转型(父类转子类)
,二者的规则和使用场景差异显著。
① 向上转型(子类转父类)
向上转型(子类转父类)
:将子类
对象或指针/引用转换为父类
对象或指针/引用特点:
隐式转换
:向上转型是隐式的,不需要显式地进行类型转换,因为子类对象在逻辑上是父类对象的一种特例。
安全转换
:向上转型总是安全的,因为子类对象包含父类的所有属性和方法。
向上转型之对象转换:对象直接赋值
public继承的子类,若直接使用子类对象给父类对象赋值,子类中独有的成员会被 “截断”,仅保留从父类继承的部分,这种情况的我们称为是 对象切片
示例:
class Parent { int x; };class Child : public Parent { int y; };Parent p; Child c; p = c; // 切片:c 的 y 成员被丢弃,p 仅保留从 Parent 继承的 x
向上转型之指针/引用转换:子类指针/引用可直接赋值给父类指针/引用,指向子类对象的父类部分。
示例:
Child c;Parent* p = &c; // 合法,p 指向 c 的父类部分 Parent& ref = c; // 合法,ref 是 c 的父类部分的别名
② 向下转型(父类转子类)
向下转型(父类转子类)
:将父类
对象或指针/引用转换为子类
对象或指针/引用特点:
显式转换
:向下转型是显式的,无法自动转换,需使用static_cast
、dynamic_cast
等强制类型转换运算符,因为父类对象可能不包含子类的特有属性和方法。存在风险
:向下转型可能会导致运行时错误,因为父类对象可能不包含子类的特有成员。
向下转型的两种方式:
static_cast
(非多态场景):用于非多态类型的转换,编译期完成,不检查转换的有效性。Parent* p = new Child(); // 父类指针指向子类对象 Child* c1 = static_cast<Child*>(p); // 合法(正确转换)// 危险!需确保父类指针实际指向子类对象 Parent p_obj; Child* c2 = static_cast<Child*>(&p_obj); // 非法(p_obj 是纯父类对象,转换后访问子类成员会崩溃)
dynamic_cast
(多态场景):用于多态类型(父类含虚函数),运行时检查转换的有效性。
- 若转换成功,返回子类指针/引用。
- 若失败,指针返回
nullptr
,引用抛出std::bad_cast
异常。class Parent {virtual void func() {} // 含虚函数,支持多态 };class Child : public Parent {};Parent* p = new Child(); Child* c = dynamic_cast<Child*>(p); // 成功,c 非空Parent p_obj; Child& ref = dynamic_cast<Child&>(p_obj); // 抛出 std::bad_cast 异常(p_obj 非子类对象)
代码示例:向下转型(父类转子类)的错误使用案例
#include <iostream>
using namespace std;class Person
{
protected:string _name; string _sex; int _age;
};class Student : public Person
{
public:int _No; // 学号(Student独有成员)
};int main()
{Student sobj; // 创建子类对象//1.子类对象可以赋值给父类的指针/引用(向上转型,合法)Person* pp = &sobj; // 父类指针指向子类对象(指向子类中的父类部分)Person& rp = sobj; // 父类引用绑定到子类对象(引用子类中的父类部分)Person pobj = sobj; // 子类对象赋值给父类对象(发生对象切片,仅复制父类部分)// 注意:此处通过调用Person类的拷贝构造函数完成赋值,// 仅复制_name、_sex、_age,Student的_No成员被截断//2. 父类对象不能赋值给子类对象(向下转型,非法)sobj = pobj; // 编译错误!父类对象无法自动转换为子类对象// 原因:父类对象不包含子类的独有成员(如:_No),// 若允许赋值,会导致子类的_No成员未被初始化return 0;
}
------------------------
四、继承中的作用域
1. 什么是隐藏?
隐藏
:是指 派生类中的同名成员(函数
或变量
)覆盖了基类中的同名成员 ,导致基类成员在派生类作用域内不可直接访问的现象。
2. 隐藏有哪些?
隐藏规则主要分为以下两种情况:
- 变量隐藏:派生类变量覆盖基类变量
- 函数隐藏:派生类函数覆盖基类同名函数
① 变量隐藏
变量隐藏
:当派生类定义了与基类同名的变量时,基类变量会被隐藏,派生类对象默认访问自身的变量。
- 需要注意的是只需要变量名相同就可以构成隐藏。
代码示例1:变量隐藏
#include <iostream>
using namespace std;class Base
{
protected:int x = 10;
};class Derived : public Base
{
private:int x = 20; // 隐藏基类的x
public:void print(){cout <<"x=" << x << endl; // 输出20(派生类的x)cout << "Base::x=" << Base::x; // 显式访问基类的x(输出10)}
};int main()
{Derived d;d.print(); return 0;
}
代码示例2:变量隐藏
#include <iostream>
using namespace std;/*-----------------------定义基类:“Person类”表示通用的个人信息-----------------------*/class Person
{
protected:string _name = "张三"; // 姓名int _num = 111; // 身份证号(基类成员)
};/*-----------------------定义派生类:“Student类”继承自Person,新增学号信息-----------------------*/class Student : public Person
{
public:void Print(){//1.访问从Person继承的姓名cout << "姓名:" << _name << endl;//2.由于“身份证号”被同名的“学号”隐藏了,所以通过类域显式指定,访问基类的_num(身份证号)cout << "身份证号:" << Person::_num << endl;//3.直接访问_num,默认使用派生类隐藏的成员(学号)cout << "学号:" << _num << endl;}
protected:int _num = 999; // 学号,注意:这里派生类的成员变量和基类的成员变量“同名”了(所以:派生类成员,隐藏基类的_num)
};int main()
{Student s1;s1.Print();return 0;
}
② 函数隐藏
函数隐藏
:当派生类定义了与基类同名但参数列表不同的函数时,基类的所有同名函数会被隐藏,即使参数不同也无法直接调用。
- 需要注意的是只需要函数名相同就可以构成隐藏。
#include <iostream>
using namespace std;class Base
{
public:void func(){cout << "Base::func()" << endl;}void func(int x){cout << "Base::func(int)" << endl;}
};class Derived : public Base
{
public:void func(double x) // 隐藏基类的func()和func(int){cout << "Derived::func(double)" << endl;}
};int main()
{Derived d;d.func(3.14); // 合法,调用Derived::func(double)//d.func(); // 错误!基类的func()被隐藏d.func(10); // 错误!基类的func(int)被隐藏d.Base::func(); // 合法,显式调用基类函数return 0;
}
3. 关于继承作用域的相关习题
#include <iostream>
using namespace std;/*-----------------------定义基类:“A类”-----------------------*/
class A
{
public:void fun(){cout << "A::func()" << endl; }
};/*-----------------------定义派生类:“B类”-----------------------*/
class B : public A
{
public:void fun(int i) {cout << "B::func(int i): " << i << endl;}
};int main()
{B b;b.fun(10); b.fun(); return 0;
}
问题 1:A 类和 B 类中的两个
func
构成什么关系?( )A. 重载 B. 隐藏 C. 没关系
问题 2:上面的代码编译运行结果是什么?( )
A. 编译报错 B. 运行报错 C. 正常运行
答案【B. 隐藏】
分析:
我相信一定会有一部分的小伙伴们回选择A.重载,他们应该是这么想的:A类中的fun函数是:
void fun()
,B类中的fun函数是:void fun(int i)
,咦……这两个函数不是满足函数的重载的要求嘛,ok这道题就选A选项了。一对答案,啊,这道题为什么选B. 隐藏 啊!!!
解析:
基类
A
的fun()
与派生类B
的fun(int)
同名且参数列表不同(标准的函数重载的要求),但是这两个函数位于不同作用域(基类与派生类)重载要求函数在同一作用域内,而此处属于继承体系中的跨作用域同名函数,因此不构成重载
在 C++ 中,派生类的同名函数会隐藏基类的所有同名函数(无论参数是否相同),这种现象称为隐藏(Name Hiding)
答案【A. 编译报错】
解析:
- 在
main
函数中,b.fun();
会触发编译错误
- 派生类
B
的fun(int)
隐藏了基类A
的fun()
,编译器在B
的作用域内找不到无参的fun()
,因此报错。- 若想调用基类的
fun()
,需显式指定类域:b.A::fun();
------------------------
五、子类的默认成员函数
在 C++ 中,当定义一个子类(派生类)时,编译器会自动生成以下默认成员函数(与基类的默认成员函数行为相关)
- 这些默认函数的生成规则和行为与基类的构造函数、析构函数、赋值运算符等密切相关。
子类会继承基类的成员
变量
和函数
,但 不会继承基类的构造函数、析构函数和赋值运算符。编译器会为子类自动生成以下默认成员函数(若未手动定义):
默认构造函数
析构函数
拷贝构造函数
拷贝赋值运算符重载函数
移动构造函数
(C++11 新增)移动赋值运算符重载函数
(C++11 新增)
1. 默认构造函数
① 介绍
在 C++ 中,子类的默认构造函数的行为与普通类有所不同,因为它需要正确处理基类子对象和成员变量的初始化
子类的默认构造函数
:当子类没有显式定义任何构造函数
时,编译器会自动生成一个隐式的默认构造函数。子类的默认构造函数的主要职责是:
- 调用基类的默认构造函数
- 初始化子类的成员变量(调用它们的默认构造函数)
② 使用
情况一:
- 父类中有默认构造函数
- 如果子类没有显式定义构造函数
编译器会生成一个默认构造函数。
#include <iostream>
#include <string>
using namespace std;/*---------------------------定义:“基类:Base类”---------------------------*/
class Base
{
public:Base() // 基类有默认构造函数{cout << "Base()\n";}
};/*---------------------------定义:“派生类:Derived类”---------------------------*/
class Derived : public Base
{
public:int x; // 内置类型不初始化string s; // 成员有默认构造函数// 编译器自动生成:Derived() : Base(), s() {}
};int main()
{Derived d; // 输出 Base(),x是未定义值,s为空字符串
}
情况二:
- 父类中没有默认构造函数(如:只有带参构造函数)
- 如果子类没有显式定义构造函数
编译器会生成一个默认构造函数,但是:子类的默认构造函数会编译失败,需
手动定义子类构造函数
并在初始化列表中显式调用基类构造函数
#include <iostream>
#include <string>
using namespace std;/*---------------------------定义:“基类:Base类”---------------------------*/
class Base
{
public:Base(int val){std::cout << "Base(int)\n";}
};/*---------------------------定义:“派生类:Derived类”---------------------------*/
class Derived : public Base
{
public:Derived() : Base(42) // 必须显式调用基类构造函数{}
};int main()
{Derived d; // 输出 Base(int)
}
③ 初始化
子类自身成员的初始化
内置类型成员
:不会自动初始化(如:int、指针,其值未定义)类类型成员
:调用其默认构造函数(如:std::string
会初始化为空字符串)
#include <iostream>
#include <string>
using namespace std;/*---------------------------定义:“基类:Base类”---------------------------*/
class Base
{
public:Base(int val){cout << "Base(int)\n";}
};/*---------------------------定义:“派生类:Derived类”---------------------------*/
class Derived : public Base
{
public:int x = 10; // C++11 成员默认值Derived() : Base(42), x(5) // 构造函数初始化列表优先{}
};int main()
{Derived d; // 输出 Base(int)
}
④ 禁用
如果希望禁止子类的默认构造,可以:
- 将基类或子类的默认构造函数声明为
= delete
- 将基类构造函数设为
private
/*------------------------禁用案例1:声明为=delete------------------------*/#include <iostream>
#include <string>
using namespace std;class Base
{
public:Base() = delete; // 禁用默认构造
};class Derived : public Base
{
public:Derived() // 错误:无法调用 Base(){}
};int main()
{Derived d; // 输出 Base(int)
}/*------------------------禁用案例2:修改为private------------------------*/#include <iostream>
#include <string>
using namespace std;class Base
{
private:Base(){} // 禁用默认构造
};class Derived : public Base
{
public:};int main()
{Derived d; // 输出 Base(int)
}
2. 拷贝构造函数
① 介绍
在 C++ 中,子类的拷贝构造函数的行为与普通类不同,因为它需要正确处理基类部分和派生类新增成员的拷贝
子类的默认拷贝构造函数
:如果子类未定义拷贝构造函数
,编译器会生成一个隐式的拷贝构造函数。子类的默认拷贝构造函数的行为是:
- 调用基类的拷贝构造函数(拷贝基类部分)
- 对派生类的新增成员逐成员拷贝(调用各自的拷贝构造函数)
#include <iostream>
#include <string>
using namespace std;class Base
{
public:Base() {}Base(const Base&){cout << "Base拷贝构造\n";}
};class Derived : public Base
{
public:string s; // 类类型成员// 编译器生成://1.子类的默认构造函数//Derived() {}//2.子类的拷贝构造函数//Derived(const Derived& other)// : Base(other)// , s(other.s) //{}
};int main()
{Derived d1;Derived d2 = d1; // 输出:Base拷贝构造return 0;
}
② 使用
注意一:
子类拷贝构造函数的基本形式:子类的拷贝构造函数必须显式调用基类的拷贝构造函数,否则基类部分将被默认构造而非拷贝构造
#include <iostream>
using namespace std; class Base
{
public:Base() // 默认构造函数(无参){cout << "Base默认构造\n"; }Base(const Base& other) // 拷贝构造函数(参数为基类对象的引用){cout << "Base拷贝构造\n"; }
};class Derived : public Base
{
public:Derived() // 子类默认构造函数(无参){cout << "Derived默认构造\n"; //注意:隐式调用基类的默认构造函数Base()}Derived(const Derived& other) // 子类拷贝构造函数(参数为子类对象的引用): Base(other) {cout << "Derived拷贝构造\n"; //注意:显式调用基类的拷贝构造函数,传入子类对象other(向上转型为Base&)}
};int main()
{Derived d1; //创建Derived对象d1,触发以下构造顺序:// 1.调用基类Base的默认构造函数 → 输出"Base默认构造"// 2.调用子类Derived的默认构造函数 → 输出"Derived默认构造"Derived d2(d1); //使用d1拷贝构造d2,触发以下构造顺序:// 1.调用基类Base的拷贝构造函数(传入d1的Base部分)→ 输出"Base拷贝构造"// 2.调用子类Derived的拷贝构造函数 → 输出"Derived拷贝构造"return 0;
}
注意二:
深拷贝与浅拷贝问题:当派生类包含指针成员时,需手动实现深拷贝
#include <iostream>
using namespace std;class Base
{
public:int* data;//1.实现:“默认构造函数”Base() : data(new int(0)) //注意:初始化data指针并分配内存{}//2.实现:“拷贝构造函数”(实现深拷贝)Base(const Base& other) : data(new int(*other.data)) //注意:为新对象分配独立内存并复制原对象的值{}//3.实现:“析构函数”~Base(){delete data; //释放动态分配的内存}
};class Derived : public Base
{
public:int* more_data;//1.实现:“默认构造函数”Derived() : more_data(new int(0)) //注意:初始化基类部分并为more_data分配内存{}//2.实现:“拷贝构造函数”(实现深拷贝)Derived(const Derived& other): Base(other),more_data(new int(*other.more_data)){}//注意事项:// 1.调用基类拷贝构造函数处理基类部分// 2.为more_data分配新内存并复制原对象的值//3.实现:“析构函数”~Derived() //注意:基类析构函数会被自动调用{delete more_data; //释放派生类部分的动态内存}
};int main()
{/*--------------测试准备阶段:创建一个派生类的对象d1--------------*///1.创建第一个派生类对象Derived d1;//2.设置基类部分的值*d1.data = 10;//3.设置派生类扩展部分的值*d1.more_data = 20;/*--------------测试准备阶段:使用拷贝构造函数创建第二个派生类的对象d2--------------*/Derived d2(d1); //期望d2是d1的独立副本/*--------------验证拷贝结果:输出d2的值--------------*/cout << "d2.data: " << *d2.data << endl; cout << "d2.more_data: " << *d2.more_data << endl; /*--------------验证拷贝结果:修改原始对象的值--------------*/*d1.data = 100; //注意:深拷贝保证这不会影响d2*d1.more_data = 200; //注意:深拷贝保证这不会影响d2/*--------------验证深拷贝:d2的值应保持不变--------------*/cout << "After modification:" << endl;cout << "d2.data: " << *d2.data << endl; cout << "d2.more_data: " << *d2.more_data << endl; // 对象离开作用域时,析构函数会自动释放内存// 不会发生内存泄漏return 0;
}
③ 禁用
必须显式调用基类拷贝构造的情况:
- 如果基类的拷贝构造函数不可访问(如:
= delete
或private
)- 子类无法生成或使用隐式拷贝构造
注意:若基类没有可访问的拷贝构造函数(如:
删除
或私有
),子类的默认拷贝构造函数会被隐式删除。
/*------------------------禁用案例1:删除------------------------*/#include <iostream>
using namespace std;class Base
{
public:Base() //默认的构造函数{}Base(const Base&) = delete; // 禁止拷贝
};class Derived : public Base
{
public:Derived(){}//Derived(const Derived&) // 错误:无法调用基类拷贝构造//{}
};int main()
{Derived d1;Derived d2(d1); // 编译错误:尝试使用已删除的拷贝构造函数cout << "拷贝构造已被禁用,无法创建对象副本。" << endl;return 0;
}/*------------------------禁用案例2:私有------------------------*/#include <iostream>
using namespace std;class Base
{
public:Base() //默认的构造函数{}
private:Base(const Base&) // 禁止拷贝{}
};class Derived : public Base
{
public:Derived(){}//Derived(const Derived&) // 错误:无法调用基类拷贝构造//{}
};int main()
{Derived d1;Derived d2(d1); // 编译错误:尝试使用已删除的拷贝构造函数cout << "拷贝构造已被禁用,无法创建对象副本。" << endl;return 0;
}
3. 拷贝赋值运算符重载函数
① 介绍
在 C++ 中,子类的拷贝赋值运算符 的行为比普通类更复杂,因为它需要正确处理基类部分和派生类新增成员的赋值
子类的默认拷贝赋值运算符重载函数
:如果子类没有自定义operator=
,编译器会生成一个隐式的拷贝赋值运算符重载函数。子类的拷贝赋值运算符重载函数的行为是:
- 调用基类的operator=(赋值基类部分)
- 对派生类的新增成员逐成员赋值(调用各自的 operator=)
#include <iostream>
#include <string>
using namespace std;/*---------------------定义:“基类:Base类”---------------------*/
class Base
{
public://1.实现:“拷贝赋值运算符”Base& operator=(const Base&) {cout << "Base::operator=\n"; // 打印赋值操作信息return *this; // 返回当前对象的引用}
};/*---------------------定义:“派生类:Derived类”---------------------*/
class Derived : public Base
{string s; // 类类型成员变量//注意:编译器会自动生成如下拷贝赋值运算符:// Derived& operator=(const Derived& other) // {// Base::operator=(other); // 调用基类的拷贝赋值// // s = other.s; // 拷贝派生类成员// return *this; // 返回当前对象引用// }
};int main()
{//1.创建两个Derived对象Derived d1, d2; //2.执行拷贝赋值操作d1 = d2; //将调用编译器自动生成的拷贝赋值运算符return 0;
}
② 使用
注意一:
子类拷贝赋值运算符的基本形式:子类的operator= 必须 显式调用基类的operator=,否则基类部分不会被正确赋值(只会被默认构造,而不会拷贝)
- 原因:派生类的
operator=
会隐藏基类的operator=
- 因此:若要在派生类中显式调用基类的
operator=
,需要通过指定基类作用域来实现
#include <iostream>
#include <string>
using namespace std;/*---------------------定义:“基类:Base类”---------------------*/
class Base
{
public://1.实现:基类的“拷贝赋值运算符”Base& operator=(const Base& other) {cout << "Base::operator=\n"; // 打印调试信息return *this; // 返回当前对象的引用}
};/*---------------------定义:“派生类:Derived类”---------------------*/
/*
* 任务:
* 1.这个类展示了派生类中如何正确实现拷贝赋值运算符,
* 2.必须显式调用基类的拷贝赋值运算符,
* 3.否则基类部分不会被赋值。
*/
class Derived : public Base
{
public:/** 实现要点:* 1. 必须显式调用基类的operator=* 2. 然后处理派生类特有的成员拷贝* 3. 返回*this以支持链式赋值*///1.实现:派生类的“拷贝赋值运算符”Derived& operator=(const Derived& other) {cout << "开始Derived拷贝赋值操作...\n";//1.首先调用基类的拷贝赋值运算符Base::operator=(other); //注意:必须显式调用基类赋值//2.然后处理派生类特有的成员赋值// 如果有成员变量需要拷贝,应该在这里处理// 例如:this->member = other.member;cout << "完成Derived特有成员的赋值\n";//3.返回当前对象的引用return *this; }
};int main()
{//1.创建两个派生类的对象cout << "创建Derived对象d1和d2...\n";Derived d1, d2;//2.进行派生类的赋值操作cout << "\n执行拷贝赋值操作(d1 = d2)...\n";d1 = d2; //调用Derived::operator=cout << "\n程序正常结束\n";return 0;
}
注意二:
深拷贝与浅拷贝问题:如果派生类包含 指针或动态资源,必须手动管理深拷贝
#include <iostream>
using namespace std;/*---------------------定义:“基类:Base类”---------------------*/
class Base
{
public:int* data;//1.实现:“默认构造函数”---> 初始化并分配内存Base() : data(new int(0)) {}//2.实现:“深拷贝赋值运算符”---> 防止内存泄漏和悬挂指针Base& operator=(const Base& other) {//1.防止自赋值(如 a = a)if (this != &other) { //2.释放当前资源delete data; //3.创建新资源并复制值data = new int(*other.data); }return *this; // 返回引用以支持链式赋值(如:a = b = c)}//3.实现:“析构函数”---> 释放动态分配的内存~Base() { delete data; }
};/*---------------------定义:“派生类:Derived类”---------------------*/
class Derived : public Base
{
public:int* more_data;//1.实现:“默认构造函数”---> 初始化基类和派生类资源Derived() : more_data(new int(0)) {}//2.实现:“深拷贝赋值运算符”---> 先处理基类部分,再处理自身Derived& operator=(const Derived& other) {//1.防止自赋值if (this != &other) { //2.调用基类的赋值运算符处理基类资源Base::operator=(other); //3.释放当前派生类资源delete more_data; //4.创建新资源并复制值more_data = new int(*other.more_data); }return *this; // 返回引用以支持链式赋值}//3.实现:“析构函数”---> 释放派生类资源(基类析构函数自动调用)~Derived() { delete more_data; }
};int main()
{//1.创建两个派生类的对象并进行赋值操作Derived d1, d2;*d1.data = 10; // 设置基类部分的值*d1.more_data = 20; // 设置派生类部分的值//2.调用派生类的赋值运算符d2 = d1; //3.输出“修改前”派生类对象d2的成员变量的值cout << "修改前 d2.data: " << *d2.data << endl; cout << "修改前 d2.more_data: " << *d2.more_data << endl; //4.修改派生类的对象d1,验证深拷贝:不会影响 d2*d1.data = 100;*d1.more_data = 200;//5.输出“修改后”派生类对象d2的成员变量的值cout << "修改后 d2.data: " << *d2.data << endl; // 输出: 10cout << "修改后 d2.more_data: " << *d2.more_data << endl; // 输出: 20return 0;
}
③ 禁用
必须显式调用基类
operator=
的情况
- 如果基类的
operator=
不可访问(如:= delete
或private
)- 子类无法使用隐式拷贝赋值运算符
/*------------------------禁用案例1:删除------------------------*/#include <iostream>
using namespace std; /*---------------------定义:“基类:Base类”---------------------*/
class Base
{
public:Base& operator=(const Base&) = delete; // C++11特性:显式删除拷贝赋值,通过将拷贝赋值运算符声明为delete来禁止拷贝赋值操作/** 效果:* 1.禁止Base类的拷贝赋值(Base b1; Base b2; b1 = b2; 会编译失败)* 2.任何尝试继承Base并实现拷贝赋值的派生类也会失败*/
};/*---------------------定义:“派生类:Derived类”---------------------*/
class Derived : public Base //注意:派生类Derived,继承自不可拷贝赋值的Base,由于基类禁止拷贝赋值,这个类也无法实现有效的拷贝赋值运算符
{
public://尝试实现拷贝赋值运算符会导致编译错误:Derived& operator=(const Derived& other) {//无法调用Base::operator=,因为基类的拷贝赋值已被删除return *this;}
};int main()
{cout << "创建Base对象..." << endl;Base b1;Base b2;// 以下代码如果取消注释会导致编译错误://b1 = b2; // 错误:Base::operator=已被删除cout << "创建Derived对象..." << endl;Derived d1;Derived d2;// 以下代码如果取消注释会导致编译错误:d1 = d2; //注意:无法使用隐式生成的拷贝赋值运算符return 0;
}/*----------------------------禁用案例1:私有----------------------------*/#include <iostream>
using namespace std; /*---------------------定义:“基类:Base类”---------------------*/
class Base
{
private:Base& operator=(const Base&){return *this;}};/*---------------------定义:“派生类:Derived类”---------------------*/
class Derived : public Base
{
public:Derived& operator=(const Derived& other) {//无法调用Base::operator=,因为基类的拷贝赋值已被私有return *this;}
};int main()
{cout << "创建Base对象..." << endl;Base b1;Base b2;b1 = b2; cout << "创建Derived对象..." << endl;Derived d1;Derived d2;// 以下代码如果取消注释会导致编译错误:d1 = d2; //注意:无法使用隐式生成的拷贝赋值运算符return 0;
}
④ 拷贝构造 vs 拷贝赋值
特性 | 拷贝构造函数 | 拷贝赋值运算符 |
---|---|---|
调用时机 | 创建新对象时(如 Derived d2 = d1; ) | 已存在对象赋值时(如 d2 = d1; ) |
基类处理 | 在初始化列表调用基类拷贝构造 | 显式调用 Base::operator= |
资源管理 | 直接构造新资源 | 需先释放旧资源,再分配新资源 |
自赋值检查 | 不需要(因为是新对象) | 必须检查(if (this != &other) ) |
4. 析构函数
在 C++ 中,子类的析构函数 的行为与普通析构函数有所不同,因为它涉及继承关系和多态销毁的问题
子类的默认析构函数
:如果子类没有显式定义析构函数
,编译器会自动生成一个 隐式的析构函数
① 注意事项
当基类指针删除派生类对象时,如果基类析构函数 不是 virtual,通过基类指针删除派生类对象会导致 未定义行为
- 通常只调用基类析构函数,派生类部分内存泄漏。
注意:基类析构函数必须为 virtual(多态场景)
#include <iostream>
using namespace std; class Base
{
public:~Base(){cout << "~Base()\n";}/** 任务:演示非虚析构函数的问题* * 1.这个类展示了一个关键问题:当基类析构函数不是虚函数时* 2.通过基类指针删除派生类对象会导致派生类析构函数不被调用*/
};class Derived : public Base
{
public:~Derived(){cout << "~Derived()\n";}/** 注意事项:** 1.如果通过基类指针删除对象,且基类析构函数不是虚函数,* 2.这个析构函数将不会被调用,导致派生类特有的资源泄漏。*/
};int main()
{//1.创建派生类对象,但通过基类指针持有Base* ptr = new Derived();//2.删除对象 delete ptr; // 仅输出 ~Base(),Derived 部分泄漏!return 0;
}
解决方案:基类虚析构
思考与探究:
现在的大家可以试着想一想:
为什么通过基类指针删除派生类对象会导致派生类析构函数不被调用呢?
原因:在多态场景中,析构函数需要构成
重写
(覆盖),而重写的条件之一是函数名相同(这一点将在多态章节中详细讲解)所以:编译器会对析构函数名进行特殊处理(统一处理为
destructor()
),因此若基类析构函数未声明为virtual
的情况下,派生类析构函数与基类析构函数会构成隐藏
关系(而非重写)
② 析构顺序
析构函数的调用顺序(与构造函数相反):
- 先执行 子类的析构函数体
- 然后按 声明顺序逆序 析构子类的成员变量
- 最后自动调用 基类的析构函数(从最底层派生类向顶层基类回溯)
注:派生类的析构函数在执行完毕后,会自动调用基类的析构函数来清理基类成员。
这是为了确保派生类对象遵循
“先清理派生类成员,再清理基类成员”
的顺序。
#include <iostream>
using namespace std; class Member
{
public:~Member() //注意:当包含它的类对象被销毁时自动调用{cout << "~Member()\n";}
};class Base
{
public:virtual ~Base(){cout << "~Base()\n";}
};/** 注意事项:* 1.派生类 Derived,继承自 Base* 2.包含 Member 成员对象,演示完整析构顺序*/
class Derived : public Base
{
public://1.Member的成员对象Member m; //2.派生类的析构函数~Derived() {cout << "~Derived()\n";}
};int main()
{Derived d;// 对象离开作用域时,自动析构顺序:// 1. 调用 ~Derived() 函数体// 2. 析构成员对象 m(调用 ~Member())(成员逆序析构)// 3. 调用基类析构函数 ~Base()return 0;
}
子类中常见的四大默认成员函数的大总结:
#include <iostream>
#include <string>
using namespace std;class Person
{
public://1.实现:“构造函数”---> 初始化 _name,默认值为 "peter"Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}//2.实现:“拷贝构造函数”Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}//3.实现:“赋值运算符重载”Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p){_name = p._name;}return *this;}//4.实现:“析构函数”~Person(){cout << "~Person()" << endl;}protected:string _name; // 姓名
};class Student : public Person
{
public://1.实现:“构造函数”---> 调用基类构造函数初始化 _name,再初始化 _numStudent(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}//2.实现:“拷贝构造函数”---> 调用基类拷贝构造函数,再拷贝 _numStudent(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)" << endl;}//3.实现:“赋值运算符重载”Student& operator=(const Student& s){cout << "Student& operator=(const Student& s)" << endl;if (this != &s){Person::operator=(s); //注意:基类的赋值运算符被隐藏,需显式调用基类作用域的赋值运算符_num = s._num;}return *this;}//4.实现:“析构函数”~Student(){cout << "~Student()" << endl;}protected:int _num; // 学号
};int main()
{/*--------------测试1:子类的拷贝构造函数--------------*/cout << "-------测试1:子类的拷贝构造函数-------" << endl;cout << "创建第一个 Student 对象 s1" << endl;Student s1("jack", 18);cout << "使用拷贝构造创建 s2" << endl;Student s2(s1);/*--------------测试2:子类的拷贝赋值运算符重载函数--------------*/cout << "-------测试2:子类的拷贝赋值运算符重载函数-------" << endl;cout << "创建第三个 Student 对象 s3" << endl;//3. Student s3("rose", 17);cout << "使用拷贝赋值运算符重载函数为对象s1进行赋值" << endl;//4. s1 = s3;return 0;
}