以项目的方式学 QT 开发
以项目的方式学 QT 开发
P1 QT 介绍
1.1 QT简介
1.2 QT安装
1.2.1 Windows QT安装
1.2.2 QT Creator 使用基本介绍
P2 C++ 基础
2.1 命名空间
2.1.1 命名空间作用
2.1.2 自定义命名空间
2.2 从C语言快速入门
2.2.1 输入输出
2.2.2 基本变量类型
2.2.3 流程控制
2.2.4 函数
2.2.5 内联函数
2.2.6 Lambda 表达式
2.2.7 数组
2.2.8 练习
2.2.9 指针
2.2.9 字符串string类型
2.3 类
2.3.1 类的初探
2.3.2 结构体引入类
2.3.2.1 回忆结构体
2.3.2.2 新建C++工程来使用结构体
2.3.2.3 真正的成员函数
2.3.4 QT中经常出现的用法
2.4 权限初识
2.4.1 基本介绍
2.4.2 目前能概况的结论
2.4.3 提问和回答
2.5 引用
2.6.1 和指针的区别
2.6.2 把引用作为参数
2.6.3 把引用作为返回值
2.6 重载
2.5.1 函数重载
2.5.2 运算符重载
2.7 构造函数
2.7.1 什么是构造函数
2.7.2 带参数构造函数
2.7.3 拷贝构造函数
2.7.3.1 基本概念及发生条件
2.7.3.2 浅拷贝
2.7.3.3 深拷贝
2.7.3.4 规则三则
2.7.3.5 避免不必要的拷贝
2.7.3.6 拷贝构造函数的隐式调用
2.7.3.7 禁用拷贝构造函数2.7.3.8 拷贝构造函数总结
2.7.4 使用初始化列表
2.7.5 this关键字
2.7.6 new关键字
2.8 析构函数
2.8.1 什么是析构函数
2.9 静态成员
2.9.1 静态成员的定义
2.9.2 静态成员变量的作用
2.10 继承
2.10.1 继承基本概念
2.10.2 权限对继承的影响
2.10.3 基类构造函数
2.10.4 虚函数
virtual 关键字
override 关键字
2.10.5 多重继承
2.10.6 虚继承
菱形继承问题示例
使用虚继承解决菱形继承问题
2.11 多态
2.11.1 如何实现多态
2.11.2 抽象类
2.11.3 纯虚函数-接口
2.12 友元
2.12.1 什么是友元
2.12.2 友元函数
2.12.3 友元类
2.12.4 友元成员函数
2.13 模板
2.13.1 类模板
2.13.2 函数模板
2.13.3 模板特化
2.14 标准模板库STL
2.14.1 容器
2.14.2 vector
2.14.3 list
2.14.4 set
2.14.5 map
2.15 异常
2.15.1 异常基本
2.15.2 自定义异常
P3 记事本项目
3.1 项目概述
3.1.1 功能介绍
3.1.2 界面预览
3.2.3 工程概述
3.2 UI设计师工具
3.2.1 按键 QPushButton
3.2.2 水平布局 QHBoxLayout
3.2.3 文本编辑器 TextEdit
3.2.4 垂直布局 QVBoxLayout
3.2.5 主窗体元素设计
3.3 按键响应-初识信号与槽
3.3.1 信号与槽基本介绍
3.3.2 按键QPushButton设置信号与槽3.3.3 自定义信号与槽
3.3 文件操作类 QFile
3.3.3 QTextStream
3.4 文件选择对话框 QFileDialog
3.4.1 QFileDialog开发流程
3.4.2 QFileDialog 打开开发案例
3.4.3 QFileDialog 保存开发案例
3.6 实现文件打开功能
3.6.1 开发流程
3.6.2 代码实现
3.6.3 打开功能优化
3.6.4 QComboBox
3.6.5 记事本支持字符编码
3.6.6 添加行列显示
3.6.7 添加文件打开提示
3.6.8 设置当前行高亮
3.6.8.1 QList
3.8.2 ExtraSelection 简介
3.7 文件保存功能优化
3.7.1 开发流程
3.8 关闭优化
3.8.1 消息对话框 QMessageBox
3.7.3 代码实现
3.9 实现快捷键功能
3.9.1 快捷键开发基础
3.9.2 上官记事本添加快捷键
3.10 实现字体放大缩小功能
3.10.1 滚动调节字体大小的流程
3.10.2 本节笔记失误
3.10.3 检测Ctrl键被按下
3.10.4 记事本添加字体放大缩小
3.10.5 事件
事件处理过程
重写事件案例
事件方式实现字体放大缩小
事件过滤器
3.10.6 鼠标滚轮和字体大小
3.12 记事本项目总结
P4 串口调试助手项目
4.1 项目概述
4.2 串口通信核心代码开发
P5 网络调试助手
5.1 TCP网络调试助手
5.1.1 项目概述
5.1.2 开发流程
5.1.3 QTtcp服务器的关键流程
5.1.4 QTtcp客户端的关键流程
5.1.2 TCP协议
5.1.4 Socket
5.2 UI设计
5.3 网络通信核心代码
5.3.1 创建TCP服务端的核心代码
5.3.2 创建TCP客户端的核心代码
5.4 TCP服务端项目开发
5.5 TCP客户端项目开发
5.6 项目总结P6 自定义控件
6.1 QPaintEvent绘图事件
6.2 QPainter画家
6.2.1 概述
6.2.2 渐变色
6.2.2.1 线性渐变
6.2.2.2 径向渐变
6.2.2.3 圆锥形渐变
6.3 坐标转移
6.4 画雷达案例
6.5仪表表盘
6.5.1 初步完成
6.5.2 稍微美化
6.5.3 优化数字显示后代码整理
6.5.4 画一个指针
6.5.5 内环
6.5.6 完结
6.6 汽车表盘参考样式
P7 天气预报项目
7.1项目概述
7.2 stylesheet样式
7.3 窗体无状态栏-关闭
7.4 窗口跟随移动
7.5 天气预报数据接口
7.6 软件开发网络通信架构
7.6.1 BS架构/CS架构
7.6.2 HTTP基本概念
7.7 QT的HTTP编程
7.8 JSON数据
7.8.1 概述
7.8.2 QT生成JSON数据
7.8.3 QT解析JSON数据
P8 Ubuntu 搭建 QT 开发环境
8.1 安装Ubutnu22
8.1.1 下载和安装Vmware
8.1.2 下载和安装Ubuntu22
8.1.3 常用功能配置
8.2 安装Ubuntu环境下的QT
8.2.1下载安装UbuntuQT
8.2.2 Ubuntu中文支持
P9 加餐课
P1 QT 介绍
1.1 QT 简介
Qt 是一个跨平台的应用程序和用户界面框架,用于开发图形用户界面(GUI)应用程序以及命令行工
具。它最初由挪威的 Trolltech (奇趣科技)公司开发,现在由 Qt Company 维护,2020年12月8日发
布QT6。Qt 使用 C++ 语言编写,支持多种编程语言通过绑定进行使用。
对于许多开发者和小型企业来说,Qt 的开源版提供了一个强大且灵活的开发框架,而对于需要额外支持
和专有功能的大型企业或具有特定需求的项目,商业版则提供了所需的服务和资源。Qt 商业版
商业版提供专有许可,需要购买许可证来使用。这适用于希望在不共享源代码的情况下开发商业软
件的公司和开发人员
QT免费开源版
开源版根据 GNU Lesser General Public License (LGPL) 和 GNU General Public License (GPL) 发
布。这意味着用户可以免费使用 Qt,但必须遵守特定的开源许可条款
版本
发布年
份
关键特性
Qt
1.x
1996
初始发布,专注于 X11 平台
Qt
2.x
1999
引入了对 Microsoft Windows 的支持
Qt
3.x
2001
添加了许多新功能,包括网络和 XML 支持
Qt
4.x
2005
重大改进,增强了跨平台支持和图形视图框架
Qt
5.x
2012
专注于现代硬件的性能,引入了 QML 和 Qt Quick 用于开发流畅的动
画和触摸界面
Qt
6.x
2020
进一步增强了性能和功能,针对未来的软件开发趋势进行了优化,包括
对 3D 图形的支持
QT主要历史版本
学习者学习QT5和QT6都是可以的,无论选择哪个版本,Qt的基本概念和理念在各个版本之间是相通
的,因此你可以相对轻松地转换到其他版本。本次我们基于QT5学习
成熟和稳定性
Qt 5已经存在了一段时间,经过了多个版本的迭代和改进。它在很多项目中被广泛使用,证明了其
成熟性和稳定性。这对于在大型项目或生产环境中使用Qt的开发者来说是一个优势。
丰富的文档和社区支持
Qt 5有大量的文档和社区支持。你可以轻松找到各种教程、示例和解决方案,这对于初学者来说是
非常宝贵的
广泛的应用领域
Qt 5有大量的文档和社区支持。你可以轻松找到各种教程、示例和解决方案,这对于初学者来说是
非常宝贵的。
1.2 QT 安装
1.2.1 Windows QT 安装
下载windowsQT安装包
本教程使用的QT版本是:https://download.qt.io/archive/qt/5.12/5.12.9/ 本教程的安装包放在百度网
盘供大家获取。
QT安装
如果没有梯子,大家登录QT官网可能会失败,这里可以不需要 QT 账号 ,直接离线 安装,所以要断开网
络。
选择windows底下的编译工具,QT源代码,QT的绘图模块及QT的虚拟键盘
安装完成后打开1.2.2 QT Creator 使用基本介绍
课程录屏展示-创建并运行第一个QT项目
课程录屏展示-创建并运行第一个C++项目
课程录屏展示-QT Creator的界面介绍
功能
快捷键
中文说明
撤销
CTRL + Z
撤销最近的操作
重做
CTRL + Y
重做最近的撤销操作
复制
CTRL + C
复制选中内容
粘贴
CTRL + V
粘贴内容
复制行向下
CTRL + ALT + DOWN
将当前行复制到下一行
复制行向上
CTRL + ALT + UP
将当前行复制到上一行
运行
CTRL + R
运行当前项目
返回编辑模式
ESCAPE
返回到编辑状态
切换当前文件
CTRL + TAB
在打开的文件间切换
切换声明和定义
F2
在代码的声明与定义间切换
切换头文件和源文件
F4
在头文件和源文件间切换
开始调试
F5
启动调试
停止调试
SHIFT + F5
停止当前的调试
构建当前项目
CTRL + B
构建当前打开的项目
QT Creator常用的快捷键介绍功能
快捷键
中文说明
构建所有项目
CTRL + SHIFT + B
构建所有项目
新建文件或项目
CTRL + N
创建新文件或项目
打开文件或项目
CTRL + O
打开现有文件或项目
保存当前文件
CTRL + S
保存当前编辑的文件
保存所有文件
CTRL + SHIFT + S
保存所有打开的文件
关闭当前文件
CTRL + W
关闭当前文件
关闭所有文件
CTRL + SHIFT + W
关闭所有打开的文件
退出QT Creator
CTRL + Q
退出QT Creator
位置后退
ALT+Left
光标位置回退
P2 C++ 基础
C和C++之间的关系是紧密且复杂的。C++最初是作为C语言的一个扩展开发的,目的是在不放弃C的强大
功能和效率的同时,增加对象导向编程、泛型编程和其他一些特性。下面是C和C++之间主要的关系和区
别:
- 兼容性 :C++在很大程度上是与C兼容的。这意味着许多C程序可以在C++编译器中编译并运行,尽
管可能需要一些小的修改。
- 面向对象编程( OOP ) :C++引入了面向对象编程。它允许使用类和对象,而C是一个过程性语
言,不支持这些概念,或者说支持的不好,麻烦。
-
模板 :C++支持模板,这是一种允许程序员编写与数据类型无关的代码的功能。C没有这个功能。
-
标准库 :C++有一个更丰富的标准库,包括STL(标准模板库),这为数据结构和算法提供了广泛的
支持。而C的标准库相对较小。
- 类型检查 :C++比C提供更严格的类型检查。这意味着某些在C中可行但可能导致错误的代码在
C++中可能无法编译。
-
异常处理 :C++支持异常处理,这是一种处理程序运行时错误的机制。C没有内置的异常处理机制。
-
命名空间 :C++引入了命名空间,这有助于防止名称冲突。C没有这个概念。
2.1 命名空间
2.1.1 命名空间作用
创建自己的命名空间是 C++ 中组织代码的一种好方法,特别是在开发大型项目或库时。命名空间可以帮
助你避免名称冲突,并且清晰地组织代码。
std 是 C++ 标准库的命名空间。它是一个定义在 C++ 标准库中的所有类、函数和变量的命名空间。
我们新建一个QTCreator的C++工程,默认生成的代码在 C++ 中,如果你想使用标准库中的任何类、函数或对象,你通常有两种选择:
- 使用 std:: 前缀 :这是最常见的方式,它明确指定了你正在使用的是位于 std 命名空间中的元
素。
- 使用 using namespace std; :这允许你在不显式指定 std:: 的情况下使用 std 命名空间中的
所有元素。
std 包含的内容
std 命名空间包含了许多类、函数和对象,例如:
输入输出库(如 std::cout , std::cin , std::endl )
容器类(如 std::vector , std::map , std::set )
字符串类( std::string )
异常类( std::exception 和相关子类)
算法(如 std::sort , std::find )
实用工具(如 std::pair , std::tuple )
其他许多功能
使用建议
对于小型代码或示例代码,使用 using namespace std; 通常是安全的。
对于大型项目或库,建议显式地使用 std:: 前缀,以避免潜在的名称冲突,并提高代码的可读性
和可维护性。
std 命名空间是 C++ 编程的基础部分,理解和正确使用它对于编写健壮和高效的 C++ 代码至关重要。
2.1.2 自定义命名空间
定义命名空间
假设我们要创建一个命名空间来包含与圆形相关的功能。我们可以命名这个命名空间为 Cir :
#include <iostream>
using namespace std;
int main()
{
cout << "Hello World!" << endl;
return 0;
}
std::cout << "Hello, world!" << std::endl;
using namespace std;
cout << "Hello, world!" << endl;在这个头文件中,我们定义了一个名为 Cir 的命名空间,其中包含了计算圆的面积和周长的函数,以及
圆周率常量 PI 。
使用命名空间
在另一个文件中,我们可以使用这个命名空间中定义的函数和常量:
在 main.cpp 中,我们首先包含了定义 Cir 命名空间的头文件。然后,我们可以使用 Cir:: 前缀来访
问该命名空间中的函数和常量。
通过使用自定义命名空间,你可以有效地组织你的代码,并减少不同库之间的名称冲突。这在大型项目
和团队协作中尤其重要。
#ifndef CIR_H
#define CIR_H
namespace Cir {
const double PI = 3.141592653;
double areaOfCircle(double radius){
return PI*radius*radius;
}
double lenthOfCircle(double radius){
return 2*PI*radius;
}
}
#endif // CIR_H
#include "cir.h"
#include <stdio.h>
int main()
{
double radius = 5;
printf("半径为%f的圆,周长为%f,面积为%f\n",
radius,Cir::lenthOfCircle(radius), Cir::areaOfCircle(radius));
return 0;
}
#include "cir.h"
#include <stdio.h>
using namespace Cir;
int main()
{
double radius = 5;
printf("半径为%f的圆,周长为%f,面积为%f\n",
radius,lenthOfCircle(radius), areaOfCircle(radius));
return 0;
}2.2 从 C 语言快速入门
2.2.1 输入输出
C++ 中的输入和输出(I/O)主要是通过标准库中的输入输出流来实现的。最常用的是 iostream 库,它
提供了用于输入和输出的基本流类,包括 cin 、 cout 、 cerr 和 clog 。
标准输出流 ( cout )
cout 代表标准输出流,通常用于向屏幕输出数据。
使用操作符 << (插入操作符)向 cout 发送数据。
例如, std::cout << "Hello, world!" << std::endl; 会在屏幕上打印 "Hello, world!" 并换
行。
标准输入流 ( cin )
cin 代表标准输入流,用于从键盘接收数据。
使用操作符 >> (提取操作符)从 cin 提取数据。
例如, int x; std::cin >> x; 会从用户那里读取一个整数并存储在变量 x 中。
标准错误流 ( cerr ) 和标准日志流 ( clog )
cerr 用于输出错误消息。与 cout 不同, cerr 不是缓冲的,这意味着它会立即输出。
clog 类似于 cerr ,但它是缓冲的。它通常用于记录错误和日志信息。
示例代码
下面是一个展示如何使用这些基本流的简单示例:
2.2.2 基本变量类型
C++ 基本数据类型整理成表格。以下是一个表格,展示了不同的基本数据类型及其一般用途和大小范
围:
和C语言类似。
#include <iostream>
int main() {
// 使用 cout 输出
std::cout << "Enter a number: ";
// 使用 cin 输入
int num;
std::cin >> num;
// 输出结果
std::cout << "You entered: " << num << std::endl;
std::clog << "Logging: user entered a number." << std::endl;
return 0;
}数据类型
描述
大小(通常情况
下)
用途
int
整型
至少 16 位
存储整数
short int
短整型
至少 16 位
存储较小的整数
long int
长整型
至少 32 位
存储较大的整数
long long
int
更长的整型
至少 64 位
存储非常大的整数
unsigned
int
无符号整型
同 int
存储非负整数
float
单精度浮点类
型
32 位
存储小数,精度约为 6-7 位小数
double
双精度浮点类
型
64 位
存储小数,精度约为 15-16 位小数
long double
扩展精度浮点
类型
80 位或更多
存储小数,提供比 double 更高的精度
char
字符型
8 位
存储单个字符或小整数
unsigned
char
无符号字符型
8 位
存储较大的字符或作为字节使用
signed char
有符号字符型
8 位
明确作为带符号的字符或小整数使用
bool
布尔型
通常为 8 位
存储真值 true 或假值 false C语言
C99以上支持
wchar_t
宽字符类型
通过为16位或
32位
存储中文或者unicode
宽字符的用法
#include <iostream>
#include <locale>
#include <wchar.h>
int main() {
// 设置本地化以支持宽字符
std::setlocale(LC_ALL, "");
// 使用 wchar_t 类型定义一个宽字符串
wchar_t wstr[] = L"你好,世界!";
// 在 C++ 中打印宽字符串
std::wcout << wstr << std::endl;
return 0;
}在 C++ 中, <climits> (或在 C 中是 <limits.h> )是一个标准头文件,提供了关于整型限制的信
息。这个头文件中定义了各种整型数据类型的属性,如最大值、最小值等。使用这些信息可以帮助你了
解在特定编译器和平台上各种数据类型的大小和范围。
如何使用 <climits>
要使用 <climits> 中定义的常量,你首先需要包含这个头文件:
然后,你可以使用它提供的各种常量,例如:
INT_MAX : int 类型的最大值。
INT_MIN : int 类型的最小值。
UINT_MAX : unsigned int 类型的最大值。
LONG_MAX : long int 类型的最大值。
LONG_MIN : long int 类型的最小值。
LLONG_MAX : long long int 类型的最大值。
LLONG_MIN : long long int 类型的最小值。
示例代码
下面是一个简单的示例,展示了如何使用 <climits> 中的值:
这个程序会输出 int 、 unsigned int 和 long long int 类型的最大值和最小值。
注意事项
<climits> 提供的是编译时确定的常量,这意味着这些值在编译时就已经固定,根据编译器和平台
的不同而可能有所不同。
使用这些限制值可以帮助你编写更可移植和安全的代码,特别是在处理可能超出数据类型范围的操
作时。
#include <climits>
#include <iostream>
#include <climits>
int main() {
std::cout << "The range of int is from " << INT_MIN << " to " << INT_MAX <<
std::endl;
std::cout << "The maximum value of unsigned int is " << UINT_MAX <<
std::endl;
std::cout << "The range of long long is from " << LLONG_MIN << " to " <<
LLONG_MAX << std::endl;
return 0;
}2.2.3 流程控制
在 C++ 中,流程控制语句用于根据不同条件控制程序的执行流程。它们是编程中的基本构建块,允许程
序根据条件执行不同的代码段,重复执行某些操作,或者根据特定情况跳过某些代码段。下面是 C++ 中
最常见的流程控制语句:
条件语句
- if 语句 :基于条件的基本控制结构。如果条件为真,则执行代码块。
else 语句 :与 if 语句配合使用,当 if 的条件为假时执行。
else if 语句 :用于测试多个条件。
switch 语句 :基于变量的值选择执行不同代码块的方法。
循环语句
for 循环:当知道循环应该执行的次数时使用。
if (condition) {
// 条件为真时执行的代码
}
if (condition) {
// 条件为真时执行的代码
} else {
// 条件为假时执行的代码
}
if (condition1) {
// 第一个条件为真时执行的代码
} else if (condition2) {
// 第二个条件为真时执行的代码
} else {
// 所有条件为假时执行的代码
}
switch (expression) {
case value1:
// expression 等于 value1 时执行的代码
break;
case value2:
// expression 等于 value2 时执行的代码
break;
default:
// 没有匹配的 case 时执行的代码
}
for (initialization; condition; increment) {
// 循环体
}while 循环 :当条件为真时,重复执行代码块。
do-while 循环 :至少执行一次循环体,然后再检查条件。
跳转语句
-
break 语句 :用于立即跳出最近的 switch 或循环( for 、 while 、 do-while )。
-
continue 语句 :跳过循环的当前迭代,并继续下一次迭代。
-
goto 语句 :直接跳转到程序中的另一个点。使用 goto 通常不推荐,因为它可以使代码难以阅读
和维护。
流程控制语句是编程中非常重要的部分,允许开发者编写可以根据不同情况改变行为的灵活且强大的程
序。在使用这些语句时,应该确保逻辑清晰,以便代码易于理解和维护。
2.2.4 函数
在 C++ 中,函数是一段执行特定任务的代码块,它可以带有参数,并且可能返回一个值。函数的使用使
得代码更加模块化和可重用,有助于降低代码的复杂性,并提高可维护性。
函数的基本结构
C++ 函数的基本结构包括返回类型、函数名、参数列表和函数体:
示例
以下是一个 C++ 函数的简单示例:
while (condition) {
// 循环体
}
do {
// 循环体
} while (condition);
返回类型 函数名(参数列表) {
// 函数体
// 返回语句(如果有返回值的话)
}
#include <iostream>
using namespace std;
// 函数声明
int add(int x, int y);
int main() {
int result = add(5, 3);
cout << "Result: " << result << endl;
return 0;
}
// 函数定义
int add(int x, int y) {在这个示例中, add 函数接收两个整数参数,并返回它们的和。
函数的组成部分
-
返回类型 :指定函数返回的数据类型。如果函数不返回任何值,则使用 void 。
-
函数名 :函数的标识符,用于调用函数。
-
参数列表 :括号内的变量列表,用于从函数的调用者那里接收值。如果函数不接收任何参数,则此
列表为空。
- 函数体 :大括号 {} 内的一系列语句,定义了函数的执行操作。
2.2.5 内联函数
内联函数(Inline Function)是C++中一种特殊的函数,其定义直接在每个调用点展开。这意味着编译器
会尝试将函数调用替换为函数本身的代码,这样可以减少函数调用的开销,尤其是在小型函数中。
特点
- 减少函数调用开销 :内联函数通常用于优化小型、频繁调用的函数,因为它避免了函数调用的常规
开销(如参数传递、栈操作等)。
- 编译器决策 :即使函数被声明为内联,编译器也可能决定不进行内联,特别是对于复杂或递归函
数。
-
适用于小型函数 :通常只有简单的、执行时间短的函数适合做内联。
-
定义在每个使用点 :内联函数的定义(而非仅仅是声明)必须对每个使用它的文件都可见,通常意
味着将内联函数定义在头文件中。
使用方法
通过在函数声明前添加关键字 inline 来指示编译器该函数适合内联:
示例
在这个示例中,函数 add 被定义为内联函数。当它被调用时,编译器可能会将函数调用替换为函数体内
的代码。
return x + y;
}
inline int max(int x, int y) {
return x > y ? x : y;
}
#include <iostream>
inline int add(int a, int b) {
return a + b;
}
int main() {
int result = add(5, 3); // 编译器可能会将此替换为:int result = 5 + 3;
std::cout << "Result: " << result << std::endl;
return 0;
}注意事项
过度使用的风险 :不应滥用内联函数,因为这可能会增加最终程序的大小(代码膨胀)。对于大型
函数或递归函数,内联可能导致性能下降。
编译器的决定 :最终是否将函数内联是由编译器决定的,即使函数被标记为 inline 。
适用场景 :最适合内联的是小型函数和在性能要求高的代码中频繁调用的函数。
内联函数是一种用于优化程序性能的工具,但需要合理使用,以确保代码的可维护性和性能的平衡。
2.2.6 Lambda 表达式
Lambda 表达式是 C++11 引入的一种匿名函数 的方式,它允许你在需要函数的地方内联地定义函数,而
无需单独命名函数
Lambda 表达式的基本语法如下:
Lambda 表达式由以下部分组成:
捕获列表( Capture clause ) :用于捕获外部变量,在 Lambda 表达式中可以访问这些变量。捕
获列表可以为空,也可以包含变量列表 [var1, var2, ...] 。
参数列表( Parameters ) :与普通函数的参数列表类似,可以为空或包含参数列表 (param1,
param2, ...) 。
返回类型( Return type ) :Lambda 表达式可以自动推断返回类型auto,也可以显式指定返回类
型 -> return_type 。如果函数体只有一条返回语句,可以省略返回类型。
函数体( Body ) :Lambda 表达式的函数体,包含需要执行的代码。
Lambda 表达式最简单的案例是在需要一个小型函数或临时函数时直接使用它。以下是一个非常简单的
例子,其中使用 Lambda 表达式来定义一个加法操作,并立即使用它来计算两个数的和。
示例:使用 Lambda 表达式进行加法
capture clause\](parameters) -\> return_type {
// 函数体
// 可以使用捕获列表中的变量
return expression; // 可选的返回语句
}
#include \
}
// 要调用上面定义函数的主函数
int main ()
{
cout << "改变前的值" << endl;
for ( int i = 0; i < 5; i++ )
{
cout << "vals[" << i << "] = ";
cout << vals[i] << endl;
}
setValues(1) = 20.23; // 改变第 2 个元素
setValues(3) = 70.8; // 改变第 4 个元素
cout << "改变后的值" << endl;
for ( int i = 0; i < 5; i++ )
{
cout << "vals[" << i << "] = ";
cout << vals[i] << endl;
}
return 0;
}
改变前的值
vals[0] = 10.1
vals[1] = 12.6
vals[2] = 33.1
vals[3] = 24.1
vals[4] = 50
改变后的值
vals[0] = 10.1
vals[1] = 20.23
vals[2] = 33.1
vals[3] = 70.8
vals[4] = 50当返回一个引用时,要注意被引用的对象不能超出作用域。所以返回一个对局部变量的引用是不合法
的,但是,可以返回一个对静态变量的引用。
2.6 重载
2.5.1 函数重载
在同一个作用域内,可以声明几个功能类似的同名函数,
这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。您不能仅通过返回类型的不同来
重载函数。
下面的实例中,同名函数 print() 被用于输出不同的数据类型:
int& func() {
int q;
//! return q; // 在编译时发生错误
static int x;
return x; // 安全,x 在函数作用域外依然是有效的
}
#include <iostream>
using namespace std;
class printData
{
public:
void print(int i) {
cout << "整数为: " << i << endl;
}
void print(double f) {
cout << "浮点数为: " << f << endl;
}
void print(char c[]) {
cout << "字符串为: " << c << endl;
}
};
int main(void)
{
printData pd;
// 输出整数
pd.print(5);
// 输出浮点数
pd.print(500.263);
// 输出字符串
char c[] = "Hello C++";
pd.print(c);
return 0;
}2.5.2 运算符重载
在C++中,运算符重载是一个允许程序员自定义各种运算符(如 + , - , == , != 等)在自定义类型(类或
结构体)上的行为的特性。这意味着你可以定义类似于内置类型的运算符行为,使你的自定义类型更加
直观和易于使用。
基本原则
-
不可以创建新的运算符 :只能重载已经存在的运算符。
-
至少有一个操作数是用户定义的类型 :不能重载两个基本类型的运算符。
-
不能更改运算符的优先级 :重载的运算符保持其原有的优先级和结合性。
示例 1 :假设我们有一个Person 类,我们可以重载 == 运算符来实现两个Person是否相等的判断。
示例 2 :假设我们有一个简单的 Point 类,我们可以重载 + 运算符来实现两个点的加法。
#include <iostream>
using namespace std;
class Person
{
public:
string name;
int inNumberTail;
bool operator==(Person pTmp);
};
bool Person::operator==(Person pTmp){
return pTmp.name == name && pTmp.inNumberTail == inNumberTail;
}
int main()
{
//假设我们认定名字和身份证尾号6位一样的两个对象是同一个人!
Person p1;
p1.name = "张三";
p1.inNumberTail = 412508;
Person p2;
p2.name = "张三";
p2.inNumberTail = 412508;
bool ret = p1 == p2;
cout << ret << endl;
return 0;
}
class Point {在这个例子中, operator+ 被重载为一个成员函数,接受一个 Point 类型的常量引用作为参数,并返
回两个点相加的结果。
这里的 const 表明这个 operator+ 函数不会修改调用它的 Point 对象。它只是读取对象的 x 和 y
成员,并返回一个新的 Point 对象。这种做法在设计类的时候是很有用的,因为它可以确保某些函数不
会意外地改变对象的状态,同时也使得这个函数可以在常量对象上被调用。
注意事项
一致性 :重载的运算符应与其原始意图和常见用法保持一致。例如, + 运算符通常应该实现加法,
而不是其他意外的操作。
复杂性 :过度使用运算符重载可能导致代码难以理解和维护。确保它们的使用直观且合理。
运算符重载是C++中提高代码可读性和表达力的强大工具,但需要谨慎使用,以保证代码的清晰性和维护
性。
2.7 构造函数
2.7.1 什么是构造函数
类的构造函数 是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造,那构造的是什么呢?
构造成员变量的初始化值,内存空间等
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于
为某些成员变量设置初始值。
下面的实例有助于更好地理解构造函数的概念:
public:
int x, y;
// 重载 + 运算符
Point operator+(const Point& other) const {
return Point(x + other.x, y + other.y);
}
};
int main() {
Point p1;
p1.x = 1;
p1.y = 2;
Point p2;
p2.x = 2;
p2.y = 3;
Point p3 = p1 + p2; // 使用重载的 + 运算符
std::cout << "p3.x: " << p3.x << ", p3.y: " << p3.y << std::endl; // 输出
p3.x: 4, p3.y: 6
return 0;
}2.7.2 带参数构造函数
默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象
赋初始值,如下面的例子所示:
#include <iostream>
#include <string>
using namespace std; // 使用std命名空间
class Car {
public:
string brand; // 不需要使用std::string
int year;
// 无参构造函数
Car() {
brand = "未知";
year = 0;
cout << "无参构造函数被调用" << endl; // 不需要使用std::cout和std::endl
}
void display() {
cout << "Brand: " << brand << ", Year: " << year << endl;
}
};
int main() {
Car myCar; // 创建Car对象
myCar.display(); // 显示车辆信息
return 0;
}
#include <iostream>
#include <string>
using namespace std;
class Car {
public:
string brand;
int year;
// 带参数的构造函数,使用常规的赋值方式
Car(string b, int y) {
brand = b;
year = y;
}
void display() {
cout << "Brand: " << brand << ", Year: " << year << endl;
}2.7.3 拷贝构造函数
2.7.3.1 基本概念及发生条件
拷贝构造函数是 C++ 中的一种特殊的构造函数,用于创建一个新对象作为现有对象的副本。它在以下几
种情况下被调用:
- 当一个新对象被创建为另一个同类型的现有对象的副本时 :
例如: MyClass obj1 = obj2; 或 MyClass obj1(obj2); ,其中 obj2 是现有的对象。
- 将对象作为参数传递给函数时 (按值传递):
当对象作为参数传递给函数,并且参数不是引用时,会使用拷贝构造函数创建函数内部的对象
副本。
- 从函数返回对象时 (按值返回):
当函数返回对象,并且没有使用引用或指针时,拷贝构造函数用于从函数返回值创建副本。
- 初始化数组或容器中的元素时 :
例如,在创建一个包含对象的数组时,数组中的每个对象都是通过拷贝构造函数初始化的。
拷贝构造函数的典型声明如下:
其中, other 是对同类型对象的引用,通常是常量引用。
示例代码
};
int main() {
Car myCar("Toyota", 2020); // 使用带参数的构造函数创建Car对象
myCar.display(); // 显示车辆信息
return 0;
}
class MyClass {
public:
MyClass(const MyClass& other);
};
#include <iostream>
#include <string>
using namespace std;
class Car {
public:
string brand;
int year;
// 常规构造函数
Car(string b, int y) : brand(b), year(y) {}
// 拷贝构造函数2.7.3.2 浅拷贝
在 C++ 中,深拷贝和浅拷贝是处理对象拷贝时的两种不同方法,尤其是在对象包含指针或动态分配的内
存时。我将分别给出深拷贝和浅拷贝的例子。
浅拷贝 (Shallow Copy)
浅拷贝只复制对象的成员变量的值。如果成员变量是指针,则复制指针的值(即内存地址),而不是指
针所指向的实际数据。这会导致多个对象共享相同的内存地址。
Car(const Car& other) {
brand = other.brand;
year = other.year;
cout << "拷贝构造函数被调用" << endl;
}
void display() {
cout << "Brand: " << brand << ", Year: " << year << endl;
}
};
int main() {
Car car1("Toyota", 2020); // 使用常规构造函数
Car car2 = car1; // 使用拷贝构造函数
car1.display();
car2.display();
return 0;
}
#include <iostream>
using namespace std;
class Shallow {
public:
int* data;
Shallow(int d) {
//(d):这是初始化表达式。在这里,分配的 int 类型内存被初始化为 d 的值。如果 d 的值是
20,那么分配的内存将被初始化为 20。
data = new int(d); // 动态分配内存
cout << "观察数据:" << endl;
cout << d << endl;
cout << *data << endl;
cout << "观察内存在构造函数中:" << endl;
cout << data << endl;
}
// 默认的拷贝构造函数是浅拷贝
~Shallow() {
delete data; // 释放内存
}在这个例子中, obj2 是通过浅拷贝 obj1 创建的。这意味着 obj1.data 和 obj2.data 指向相同的内
存地址。
当 obj1 和 obj2 被销毁时,同一内存地址会被尝试释放两次,导致潜在的运行时错误。
在QT中我们不能直观看见,在Linux中我们获得如下运行结果:
2.7.3.3 深拷贝
深拷贝复制对象的成员变量的值以及指针所指向的实际数据。这意味着创建新的独立副本,避免了共享
内存地址的问题。
};
int main() {
Shallow obj1(20);
Shallow obj2 = obj1; // 浅拷贝
cout << "观察内存在main函数obj2的data地址:" << endl;
cout << obj2.data << endl;
cout << "obj1 data: " << *obj1.data << ", obj2 data: " << *obj2.data << endl;
return 0;
}
#include <iostream>
using namespace std;
class Deep {
public:
int* data;
Deep(int d) {
data = new int(d); // 动态分配内存
cout << "观察数据:" << endl;
cout << d << endl;
cout << *data << endl;
cout << "观察内存在构造函数中:" << endl;在这个例子中, obj2 是通过深拷贝 obj1 创建的。这意味着 obj1.data 和 obj2.data 指向不同的内
存地址。每个对象有自己的内存副本,因此不会相互影响,避免了潜在的运行时错误。
2.7.3.4 规则三则
在 C++ 中,规则三则(Rule of Three)是一个面向对象编程原则,它涉及到类的拷贝控制。规则三则指
出,如果你需要显式地定义或重载类的任何一个拷贝控制操作(拷贝构造函数、拷贝赋值运算符、析构
函数),那么你几乎肯定需要显式地定义或重载所有三个。这是因为这三个功能通常都是用于管理动态
分配的资源,比如在堆上分配的内存。
下面是一个遵循规则三则的简单示例:
cout << data << endl;
}
// 显式定义深拷贝的拷贝构造函数
Deep(const Deep& source) {
data = new int(*source.data); // 复制数据,而不是地址
cout << "深拷贝构造函数\n";
}
~Deep() {
delete data; // 释放内存
}
};
int main() {
Deep obj1(20);
Deep obj2 = obj1; // 深拷贝
cout << "观察内存在main函数obj2的data地址:" << endl;
cout << obj2.data << endl;
cout << "obj1 data: " << *obj1.data << ", obj2 data: " << *obj2.data << endl;
return 0;
}#include <iostream>
#include <cstring>
class MyClass {
private:
char* buffer;
public:
// 构造函数
MyClass(const char* str) {
if (str) {
buffer = new char[strlen(str) + 1];
strcpy(buffer, str);
} else {
buffer = nullptr;
}
}
// 析构函数
~MyClass() {
delete[] buffer;
}
// 拷贝构造函数
MyClass(const MyClass& other) {
if (other.buffer) {
buffer = new char[strlen(other.buffer) + 1];
strcpy(buffer, other.buffer);
} else {
buffer = nullptr;
}
}
// 拷贝赋值运算符
MyClass& operator=(const MyClass& other) {
if (this != &other) {
delete[] buffer; // 首先删除当前对象的资源
if (other.buffer) {
buffer = new char[strlen(other.buffer) + 1];
strcpy(buffer, other.buffer);
} else {
buffer = nullptr;
}
}
return *this;
}
};
int main() {
MyClass obj1("Hello");
MyClass obj2 = obj1; // 调用拷贝构造函数
MyClass obj3("World");
obj3 = obj1; // 调用拷贝赋值运算符
return 0;
}在这个例子中:
构造函数为成员变量 buffer 分配内存,并复制给定的字符串。
析构函数释放 buffer 所占用的内存,以避免内存泄露。
拷贝构造函数创建一个新对象作为另一个现有对象的副本,并为其分配新的内存,以避免多个对象
共享同一内存。
拷贝赋值运算符更新对象时,首先释放原有资源,然后根据新对象的状态分配新资源。
这个类遵循规则三则,确保了动态分配资源的正确管理,避免了内存泄露和浅拷贝问题。
2.7.3.5 避免不必要的拷贝
避免不必要的拷贝是 C++ 程序设计中的一个重要原则,尤其是在处理大型对象或资源密集型对象时。使
用引用(包括常量引用)和移动语义(C++11 引入)是实现这一目标的两种常见方法。下面是两个示
例:
1. 使用引用传递对象
通过使用引用(尤其是常量引用)来传递对象,可以避免在函数调用时创建对象的副本。
在这个例子中, processLargeObject 函数接受一个对 LargeObject 类型的常量引用,避免了在函数
调用时复制整个 LargeObject 。
2. 使用移动语义
C++11 引入了移动语义,允许资源(如动态分配的内存)的所有权从一个对象转移到另一个对象,这避
免了不必要的拷贝。
#include <iostream>
#include <vector>
using namespace std;
class LargeObject {
// 假设这是一个占用大量内存的大型对象
};
void processLargeObject(const LargeObject& obj) {
// 处理对象,但不修改它
cout << "Processing object..." << endl;
}
int main() {
LargeObject myLargeObject;
processLargeObject(myLargeObject); // 通过引用传递,避免拷贝
return 0;
}
#include <iostream>
#include <utility> // 对于 std::move
using namespace std;
class MovableObject {
public:
MovableObject() {在这个例子中, MovableObject 类有一个移动构造函数和一个移动赋值运算符,它们允许对象的资源
(如动态分配的内存)在赋值或返回时被"移动"而非复制。这减少了对资源的不必要拷贝,提高了效率。
通过这些方法,你可以在 C++ 程序中有效地减少不必要的对象拷贝,尤其是对于大型或资源密集型的对
象。
2.7.3.6 拷贝构造函数的隐式调用
在 C++ 中,拷贝构造函数可能会在几种不明显的情况下被隐式调用。这种隐式调用通常发生在对象需要
被复制时,但代码中并没有明显的赋值或构造函数调用。了解这些情况对于高效和正确地管理资源非常
重要。下面是一些典型的隐式拷贝构造函数调用的例子:
- 作为函数参数传递(按值传递)
当对象作为函数参数按值传递时,会调用拷贝构造函数来创建参数的本地副本。
// 构造函数
}
MovableObject(const MovableObject& other) {
// 拷贝构造函数(可能很昂贵)
}
MovableObject(MovableObject&& other) noexcept {
// 移动构造函数(轻量级)
// 转移资源的所有权
}
MovableObject& operator=(MovableObject&& other) noexcept {
// 移动赋值运算符
// 转移资源的所有权
return *this;
}
};
MovableObject createObject() {
MovableObject obj;
return obj; // 返回时使用移动语义,而非拷贝
}
int main() {
MovableObject obj = createObject(); // 使用移动构造函数
return 0;
}
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() {}
MyClass(const MyClass &) {
cout << "拷贝构造函数被隐式调用" << endl;
}
};2. 从函数返回对象(按值返回)
当函数返回一个对象时,拷贝构造函数会被用于创建返回值的副本。
3. 初始化另一个对象
当用一个对象初始化另一个同类型的新对象时,会使用拷贝构造函数。
在所有这些情况下,如果类包含资源管理(例如,动态内存分配),那么正确地实现拷贝构造函数是非
常重要的,以确保资源的正确复制和管理,防止潜在的内存泄漏或其他问题。此外,随着 C++11 的引
入,移动语义提供了对资源的高效管理方式,可以减少这些场景中的资源复制。
2.7.3.7 禁用拷贝构造函数
在 C++ 中,禁用拷贝构造函数是一种常用的做法,尤其是在设计那些不应该被复制的类时。这可以通过
将拷贝构造函数声明为 private 或使用 C++11 引入的 delete 关键字来实现。这样做的目的是防止类
的对象被拷贝,从而避免可能导致的问题,如资源重复释放、无意义的资源复制等。
使用 delete 关键字
在 C++11 及更高版本中,可以使用 delete 关键字明确指定不允许拷贝构造:
void function(MyClass obj) {
// 对 obj 的操作
}
int main() {
MyClass myObject;
function(myObject); // 调用 function 时,拷贝构造函数被隐式调用
return 0;
}
MyClass function() {
MyClass tempObject;
return tempObject; // 返回时,拷贝构造函数被隐式调用
}
int main() {
MyClass myObject = function(); // 接收返回值时可能还会有一次拷贝(或移动)
return 0;
}
int main() {
MyClass obj1;
MyClass obj2 = obj1; // 初始化时,拷贝构造函数被隐式调用
return 0;
}
class NonCopyable {
public:
NonCopyable() = default; // 使用默认构造函数
// 禁用拷贝构造函数
NonCopyable(const NonCopyable&) = delete;要点
描述
定义和作用
拷贝构造函数在创建对象作为另一个现有对象副本时调用,通常有一个对同
类型对象的常量引用参数。
语法
典型声明为 ClassName(const ClassName &other) 。
深拷贝与浅拷贝
浅拷贝复制值,深拷贝创建资源的独立副本。对于包含指针的类,深拷贝通
常必要。
规则三则 (Rule of
Three)
如果实现了拷贝构造函数、拷贝赋值运算符或析构函数中的任何一个,通常
应该实现所有三个。
这种方法清晰明了,它向编译器和其他程序员直接表明该类的对象不能被拷贝。
使用 private 声明( C++98/03 )
在 C++11 之前,常见的做法是将拷贝构造函数和拷贝赋值运算符声明为 private ,并且不提供实现:
在这个例子中,任何尝试拷贝 NonCopyable 类型对象的操作都会导致编译错误,因为拷贝构造函数和
拷贝赋值运算符是私有的,外部代码无法访问它们。
通过这些方法,你可以确保你的类的对象不会被意外地拷贝,从而避免可能出现的资源管理相关的错
误。
2.7.3.8 拷贝构造函数总结
C++ 中拷贝构造函数需要注意的七个要点的表格:
// 禁用拷贝赋值运算符
NonCopyable& operator=(const NonCopyable&) = delete;
};
int main() {
NonCopyable obj1;
// NonCopyable obj2 = obj1; // 编译错误,拷贝构造函数被禁用
return 0;
}
class NonCopyable {
private:
// 将拷贝构造函数和拷贝赋值运算符设为私有
NonCopyable(const NonCopyable&);
NonCopyable& operator=(const NonCopyable&);
public:
NonCopyable() {}
};
int main() {
NonCopyable obj1;
// NonCopyable obj2 = obj1; // 编译错误,因为无法访问私有的拷贝构造函数
return 0;
}要点
描述
避免不必要的拷贝
对于大型对象,使用移动语义避免不必要的拷贝,并在传递对象时使用引用
或指针。
拷贝构造函数的隐
式调用
不仅在显式复制时调用,也可能在将对象作为函数参数传递、从函数返回对
象时隐式调用。
禁用拷贝构造函数
对于某些类,可以通过将拷贝构造函数声明为私有或使用 delete 关键字
禁用拷贝。
2.7.4 使用初始化列表
在C++中,使用初始化列表来初始化类的字段是一种高效的初始化方式,尤其在构造函数中。初始化列表
直接在对象的构造过程中初始化成员变量,而不是先创建成员变量后再赋值。这对于提高性能尤其重
要,特别是在涉及到复杂对象或引用和常量成员的情况下。
初始化列表紧跟在构造函数参数列表后面,以冒号( : )开始,后跟一个或多个初始化表达式,每个表
达式通常用逗号分隔。下面是使用初始化列表初始化字段的例子:
在这个例子中, MyClass 有三个成员变量: a ( int 类型)、 b ( double 类型)和 c
( std::string 类型)。当创建 MyClass 的一个实例时,我们通过构造函数传递三个参数,这些参数
被用于通过初始化列表直接初始化成员变量。初始化列表 : a(x), b(y), c(z) 的意思是用 x 初始化
a ,用 y 初始化 b ,用 z 初始化 c 。
初始化列表的优点包括:
- 效率 :对于非基本类型的对象,使用初始化列表比在构造函数体内赋值更高效,因为它避免了先默
认构造然后再赋值的额外开销。
- 必要性 :对于引用类型和常量类型的成员变量,必须使用初始化列表,因为这些类型的成员变量在
构造函数体内不能被赋值。
- 顺序 :成员变量的初始化顺序是按照它们在类中声明的顺序,而不是初始化列表中的顺序。
使用初始化列表是C++中推荐的初始化类成员变量的方式,因为它提供了更好的性能和灵活性。
class MyClass {
private:
int a;
double b;
std::string c;
public:
// 使用初始化列表来初始化字段
MyClass(int x, double y, const std::string& z) : a(x), b(y), c(z) {
// 构造函数体
}
};2.7.5 this 关键字
在 C++ 中, this 关键字是一个指向调用对象的指针 。它在成员函数内部使用,用于引用调用该函数的
对象。使用 this 可以明确指出成员函数正在操作的是哪个对象的数据成员。下面是一个使用 Car 类来
展示 this 关键字用法的示例:
示例代码
下面的代码展示了如何使用 this 关键字:
#include <iostream>
#include <string>
using namespace std;
class Car {
private:
string brand;
int year;
public:
Car(string brand, int year) {
this->brand = brand;
this->year = year;
// cout << "构造函数中:" << endl;
// cout << this << endl;
}
void display() const {
cout << "Brand: " << this->brand << ", Year: " << this->year << endl;
// 也可以不使用 this->,直接写 brand 和 year
}
Car& setYear(int year) {
this->year = year; // 更新年份
return *this; // 返回调用对象的引用
}
};
int main()
{
Car car("宝马",2024);
car.display();
// 链式调用
car.setYear(2023).display();
// cout << "main函数中:" << endl;
// cout << &car << endl;
// Car car2("宝马",2024);
// cout << "main函数中:" << endl;
// cout << &car2 << endl;
return 0;
}在这个例子中, Car 类的构造函数使用 this 指针来区分成员变量和构造函数参数。同样, setYear
成员函数使用 this 指针来返回调用该函数的对象的引用,这允许链式调用,如
myCar.setYear(2021).display(); 。在 main 函数中创建了 Car 类型的对象,并展示了如何使用这
些成员函数。
2.7.6 new 关键字
在C++中, new 关键字用于动态分配内存。它是C++中处理动态内存分配的主要工具之一,允许在程序运
行时根据需要分配内存。
基本用法
分配单个对象 :使用 new 可以在堆上动态分配一个对象。例如, new int 会分配一个 int 类型的空
间,并返回一个指向该空间的指针。
分配对象数组 : new 也可以用来分配一个对象数组。例如, new int[10] 会分配一个包含10个整数的
数组。
初始化 :可以在 new 表达式中使用初始化。对于单个对象,可以使用构造函数的参数:
与 delete 配对使用
使用 new 分配的内存必须显式地通过 delete (对于单个对象)或 delete[] (对于数组)来释放,以
避免内存泄露:
释放单个对象:
释放数组:
注意事项
异常安全 :如果 new 分配内存失败,它会抛出 std::bad_alloc 异常(除非使用了 nothrow 版
本)。
内存泄露 :忘记释放使用 new 分配的内存会导致内存泄露。
匹配使用 delete 和 delete[] :为避免未定义行为,使用 new 分配的单个对象应该使用
delete 释放,使用 new[] 分配的数组应该使用 delete[] 释放。
示例代码
int* ptr = new int; //C语言中,int *p = (int *)malloc(sizeof(int));
int* arr = new int[10]; //C语言中,int *arr = (int *)malloc(sizeof(int)*10);
MyClass* obj = new MyClass(arg1, arg2);
delete ptr; // 释放 ptr 指向的对象
delete[] arr; // 释放 arr 指向的数组
class MyClass {
public:
MyClass() {在这个例子中, new 被用来分配一个 MyClass 类型的对象和一个整数数组,然后使用 delete 和
delete[] 来释放内存。每个 new 都对应一个 delete ,保证了动态分配的内存被适当管理。
2.8 析构函数
2.8.1 什么是析构函数
析构函数是C++中的一个特殊的成员函数,它在对象生命周期结束时被自动调用,用于执行对象销毁前的
清理工作。析构函数特别重要,尤其是在涉及动态分配的资源(如内存、文件句柄、网络连接等)的情
况下。
基本特性
-
名称 :析构函数的名称由波浪号( ~ )后跟类名构成,如 ~MyClass() 。
-
无返回值和参数 :析构函数不接受任何参数,也不返回任何值。
-
自动调用 :当对象的生命周期结束时(例如,一个局部对象的作用域结束,或者使用 delete 删除
一个动态分配的对象),析构函数会被自动调用。
-
不可重载 :每个类只能有一个析构函数。
-
继承和多态 :如果一个类是多态基类,其析构函数应该是虚的。
示例
假设我们有一个类 MyClass ,它包含了动态分配的内存或其他资源:
std::cout << "Object created" << std::endl;
}
};
int main() {
// 分配单个对象
MyClass* myObject = new MyClass();
// 分配对象数组
int* myArray = new int[5]{1, 2, 3, 4, 5};
// 使用对象和数组...
// 释放内存
delete myObject;
delete[] myArray;
return 0;
}
#include <iostream>
using namespace std;
class MyClass{
private:
int* datas;
public:
MyClass(int size){
datas = new int[size];要点
描述
定义和作
用
析构函数在对象生命周期结束时自动调用,用于清理对象可能持有的资源。
语法
析构函数名称由波浪线 (~) 后跟类名构成,例如 MyClass 的析构函数为
~MyClass() 。
资源管理
用于释放对象在生命周期中分配的资源,如动态内存、文件句柄、网络连接等。
自动调用
机制
当对象离开其作用域或通过 delete 删除时,将自动调用其析构函数。
防止资源
泄露
正确实现析构函数对防止资源泄露至关重要,特别是在涉及动态资源分配的情况。
虚析构函
数
如果类作为基类设计,应有一个虚析构函数,以确保正确调用派生类的析构函数。
析构函数
与异常
析构函数不应抛出异常,如果可能抛出,应在函数内捕获。
在这个示例中, MyClass 的构造函数分配了一块内存,而析构函数释放了这块内存。当 obj 的生命周
期结束时(即离开了它的作用域), MyClass 的析构函数被自动调用,负责清理资源,防止内存泄露。
重要性
析构函数在管理资源方面非常重要。没有正确实现析构函数,可能导致资源泄露或其他问题。在基于RAII
(资源获取即初始化)原则的C++编程实践中,确保资源在对象析构时被适当释放是非常关键的。当使用
智能指针和其他自动资源管理技术时,可以减少显式编写析构函数的需要,但了解析构函数的工作原理
仍然很重要。
以下是关于 C++ 中析构函数需要了解的十个要点的表格:
标注粗体部分,是能快速上手的内容,方便后续QT的学习,而没有粗体的部分,会在QT结束后,如果安
排C++深入讲解的课程的话,会安排到。
同理拷贝构造函数,考虑到学习策略安排,这里先陈列出来。
}
~MyClass(){
cout << "析构函数被调用" << endl;
delete[] datas;
}
};
int main()
{
MyClass m1(5);
MyClass *m2 = new MyClass(10);
delete m2;
return 0;
}要点
描述
删除的析
构函数
可以通过将析构函数声明为删除( ~MyClass() = delete; )来禁止删除某类对象。
与构造函
数的关系
每个类只能有一个析构函数,不可重载,与构造函数相比。
规则三
则/五则
如果类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符,可能也需要自定义另
外两个(规则三则)。在 C++11 后还包括移动构造函数和移动赋值运算符(规则五
则)。
这个表格概括了在学习和使用 C++ 析构函数时应考虑的主要点
2.9 静态成员
2.9.1 静态成员的定义
静态成员在C++类中是一个重要的概念,它包括静态成员变量和静态成员函数。静态成员的特点和存在的
意义如下:
静态成员变量
- 定义 :静态成员变量是类的所有对象共享的变量。与普通成员变量相比,无论创建了多少个类的实
例,静态成员变量只有一份拷贝。
-
初始化 :静态成员变量需要在类外进行初始化,通常在类的实现文件中。
-
访问 :静态成员变量可以通过类名直接访问,不需要创建类的对象。也可以通过类的对象访问。
-
用途 :常用于存储类级别的信息(例如,计数类的实例数量)或全局数据需要被类的所有实例共
享。
静态成员函数
- 定义 :静态成员函数是可以不依赖于类的实例而被调用的函数。它不能访问类的非静态成员变量和
非静态成员函数。
-
访问 :类似于静态成员变量,静态成员函数可以通过类名直接调用,也可以通过类的实例调用。
-
用途 :常用于实现与具体对象无关的功能,或访问静态成员变量。
示例代码
class MyClass {
public:
static int staticValue; // 静态成员变量
MyClass() {
// 每创建一个对象,静态变量增加1
staticValue++;
}
static int getStaticValue() {
// 静态成员函数存在的意义
共享数据 :允许对象之间共享数据,而不需要每个对象都有一份拷贝。
节省内存 :对于频繁使用的类,使用静态成员可以节省内存。
独立于对象的功能 :静态成员函数提供了一种在不创建对象的情况下执行操作的方法,这对于实现
工具函数或管理类级别状态很有用。
2.9.2 静态成员变量的作用
静态成员变量在C++中的一个典型应用是用于跟踪类的实例数量。这个案例体现了静态成员变量的特性:
它们在类的所有实例之间共享,因此适合于存储所有实例共有的信息。
下面是一个示例,展示了如何使用静态成员变量来计数一个类的实例数量:
return staticValue;
}
};
// 类外初始化静态成员变量
int MyClass::staticValue = 0;
int main() {
MyClass obj1, obj2;
std::cout << MyClass::getStaticValue(); // 输出2
}
#include <iostream>
using namespace std;
class Myclass{
private:
static int staticNumofInstance;
public:
Myclass(){
staticNumofInstance++;
}
~Myclass(){
staticNumofInstance--;
}
static int getNunofInstance(){
return staticNumofInstance;
}
};
int Myclass::staticNumofInstance = 0;
int main()
{
Myclass m1;在这个例子中:
Myclass 类有一个静态成员变量 staticNumofInstance ,用来跟踪该类的实例数量。
每当创建 Myclass 的新实例时,构造函数会增加 staticNumofInstance 。
每当一个 Myclass 实例被销毁时,析构函数会减少 staticNumofInstance 。
通过静态成员函数 getNunofInstance 可以随时获取当前的实例数量。
静态成员变量 staticNumofInstance 在类外初始化为0。
这个案例展示了静态成员变量如何在类的所有实例之间共享,并为所有实例提供了一个共同的状态(在
这个例子中是实例的数量)。这种技术在需要跟踪对象数量或实现某种形式的资源管理时特别有用。
2.10 继承
2.10.1 继承基本概念
继承是面向对象编程(OOP)中的一个核心概念,特别是在C++中。它允许一个类(称为派生类或子
类)继承另一个类(称为基类或父类)的属性和方法。继承的主要目的是实现代码重用,以及建立一种
类型之间的层次关系。
特点
-
代码重用 :子类继承了父类的属性和方法,减少了代码的重复编写。
-
扩展性 :子类可以扩展父类的功能,添加新的属性和方法,或者重写(覆盖)现有的方法。
-
多态性 :通过继承和虚函数,C++支持多态,允许在运行时决定调用哪个函数。
基本用法
在C++中,继承可以是公有(public)、保护(protected)或私有(private)的,这决定了基类成员在
派生类中的访问权限。
cout << Myclass::getNunofInstance() << endl;
Myclass m2;
cout << m2.getNunofInstance() << endl;
{
Myclass m3;
cout << Myclass::getNunofInstance() << endl;
Myclass m4;
cout << Myclass::getNunofInstance() << endl;
}
cout << Myclass::getNunofInstance() << endl;
Myclass *m5 = new Myclass;
cout << Myclass::getNunofInstance() << endl;
delete m5;
cout << Myclass::getNunofInstance() << endl;
return 0;
}
#include <iostream>
using namespace std;
//基类,父类在这个例子中, Vehicle 类公有地继承自 Vehicle 类,这意味着所有 Vehicle 类的公有成员在
Vehicle 类中也是公有的。
让我们用一个简单而有趣的案例来说明继承的概念:动物园中的动物。
想象我们正在创o在这个程序中,我们有一个基类 Animal ,它定义了所有动物共有的特性和行为。然
后,我们可以创建几个派生类,如 Lion 、 Elephant 和 Bird ,这些类继承自 Animal 类,并添加或
修改特定于它们自己的特性和行为。
基类: Animal
class Vehicle{ //交通工具,车,抽象的概念
public:
string type;
string contry;
string color;
double price;
int numOfWheel;
void run(){
cout << "车跑起来了" << endl;
}
void stop();
};
//派生类,子类
class Bickle : public Vehicle{
};
//派生类,子类
class Roadster : public Vehicle{ //跑车,也是抽象,比父类感觉上范围缩小了点
public:
int stateOfTop;
void openTopped();
void pdrifting();
};
int main()
{
Roadster ftype;
ftype.type = "捷豹Ftype";
ftype.run();
Bickle bike;
bike.type = "死飞";
bike.run();
return 0;
}
#include <iostream>
#include <string>
class Animal {
protected:派生类: Lion
派生类: Elephant
派生类: Bird
std::string name;
int age;
public:
Animal(std::string n, int a) : name(n), age(a) {}
virtual void makeSound() {
std::cout << name << " makes a sound." << std::endl;
}
virtual void display() {
std::cout << "Animal: " << name << ", Age: " << age << std::endl;
}
};
class Lion : public Animal {
public:
Lion(std::string n, int a) : Animal(n, a) {}
void makeSound() override {
std::cout << name << " roars." << std::endl;
}
void display() override {
std::cout << "Lion: " << name << ", Age: " << age << std::endl;
}
};
class Elephant : public Animal {
public:
Elephant(std::string n, int a) : Animal(n, a) {}
void makeSound() override {
std::cout << name << " trumpets." << std::endl;
}
void display() override {
std::cout << "Elephant: " << name << ", Age: " << age << std::endl;
}
};基类成员类型
public 继承
protected 继承
private 继承
public
public
protected
private
protected
protected
protected
private
使用这些类
在这个例子中:
Animal 是基类,定义了所有动物共有的属性(如 name 和 age )和方法(如 makeSound 和
display )。
Lion 、 Elephant 和 Bird 是派生类,它们继承了 Animal 的特性,并根据自身的特性重写了
makeSound 和 display 方法。
在 main 函数中,创建了各种动物的实例,并展示了它们的行为。
这个例子展示了继承如何使代码更有组织、更易于管理,并且如何通过重写基类方法来实现多态性。
2.10.2 权限对继承的影响
在C++中,访问控制符对继承的影响可以通过下表来清晰地展示。这个表格展示了不同类型的继承
( public 、 protected 、 private )如何影响基类的不同类型成员( public 、 protected 、
private )在派生类中的访问级别。
class Bird : public Animal {
public:
Bird(std::string n, int a) : Animal(n, a) {}
void makeSound() override {
std::cout << name << " sings." << std::endl;
}
void display() override {
std::cout << "Bird: " << name << ", Age: " << age << std::endl;
}
};
int main() {
Lion lion("Leo", 5);
Elephant elephant("Ella", 10);
Bird bird("Bella", 2);
lion.display();
lion.makeSound();
elephant.display();
elephant.makeSound();
bird.display();
bird.makeSound();
return 0;
}基类成员类型
public 继承
protected 继承
private 继承
private
不可访问
不可访问
不可访问
访问权限
类内部
同一个类的对象
派生类(子类)
类外部
public
✔️ 可访问
✔️ 可访问
✔️ 可访问
✔️ 可访问
private
✔️ 可访问
❌ 不可访问
❌ 不可访问
❌ 不可访问
protected
✔️ 可访问
❌ 不可访问
✔️ 可访问
❌ 不可访问
解释:
public 继承 :基类的 public 成员在派生类中仍然是 public 的, protected 成员仍然是
protected 的。基类的 private 成员在派生类中不可访问。
protected 继承 :基类的 public 和 protected 成员在派生类中都变成 protected 的。基类
的 private 成员在派生类中不可访问。
private 继承 :基类的 public 和 protected 成员在派生类中都变成 private 的。基类的
private 成员在派生类中不可访问。
这个表格提供了一个快速参考,帮助理解在不同类型的继承中基类成员的访问级别是如何变化的。记
住,无论继承类型如何,基类的 private 成员始终不可直接在派生类中访问。
访问权限回顾
课程上写的验证代码
#include <iostream>
using namespace std;
//基类,父类
class Vehicle{ //交通工具,车,抽象的概念
public:
string type;
string contry;
string color;
double price;
int numOfWheel;
protected:
int protectedData;
private:
int privateData;
public:
void run(){
cout << "车跑起来了" << endl;
}
void stop();
};
//私有继承测试
class TestClass : private Vehicle{
public:void tsetFunc(){
price = 10; //基类的公有数据被私有继承后,在派生类中权限编程私有,只限在类内部使用
}
};
//公有继承测试
class Truck : protected Vehicle{
public:
void testFunc(){
type = "数据测试"; //编程了公有权限
protectedData = 10; //保持公有权限
privateData = 10; //报错了,基类的私有成员,不管哪种方式的继承都是不可访问的。
}
};
//公有继承,基类的公有权限和保护权限不变,私有成员不能访问
class Bickle : public Vehicle{
public:
void testFunc(){
protectedData = 10;
}
};
//派生类,子类
class Roadster : public Vehicle{ //跑车,也是抽象,比父类感觉上范围缩小了点
public:
int stateOfTop;
void openTopped();
void pdrifting();
};
int main()
{
TestClass test;
test.price = 3.3; //报错了,基类的公有成员被私有继承后,降为私有权限
Truck t;
t.type = "测试"; //报错了,基类的公有成员被保护继承后,降为保护权限
t.protectedData = 10; //从报错信息看出,保护继承造成基类的保护成员还是保持保护权限
Roadster ftype;
ftype.type = "捷豹Ftype";
ftype.run();
Bickle bike;
bike.type = "死飞";
bike.run();
return 0;
}2.10.3 基类构造函数
在C++中,派生类可以通过其构造函数的初始化列表来调用基类的构造函数。这是在构造派生类对象时初
始化基类部分的标准做法 。
当创建派生类的对象时,基类的构造函数总是在派生类的构造函数之前被调用。如果没有明确指定,将
调用基类的默认构造函数。如果基类没有默认构造函数,或者你需要调用一个特定的基类构造函数,就
需要在派生类构造函数的初始化列表中明确指定 。
示例
假设我们有一个基类 Base 和一个派生自 Base 的类 Derived :
在这个例子中:
Base 类有一个接受一个整数参数的构造函数。
Derived 类继承自 Base ,它的构造函数接受一个整数和一个双精度浮点数。在其初始化列表中,
它调用 Base 类的构造函数,并传递整数参数。
当 Derived 类的对象被创建时,首先调用 Base 类的构造函数,然后调用 Derived 类的构造函
数。
通过这种方式,派生类能够确保其基类部分被正确初始化。在继承层次结构中,这是非常重要的,特别
是当基类需要一些特定的初始化操作时。
class Base {
public:
int data;
Base(int x) {
std::cout << "Base constructor with x = " << x << std::endl;
}
};
class Derived : public Base {
public:
double ydata;
Derived(int x, double y) : Base(x) { // 调用 Base 类的构造函数
std::cout << "Derived constructor with y = " << y << std::endl;
}
};
int main() {
Derived obj(10, 3.14); // 首先调用 Base(10),然后调用 Derived 的构造函数
return 0;
}
#include <iostream>
using namespace std;
//基类,父类
class Vehicle{ //交通工具,车,抽象的概念
public:
string contry;
double price;2.10.4 虚函数
在C++中, virtual 和 override 关键字用于支持多态,尤其是在涉及类继承和方法重写的情况下。正
确地理解和使用这两个关键字对于编写可维护和易于理解的面向对象代码至关重要。
virtual 关键字
-
使用场景 :在基类中声明虚函数。
-
目的 :允许派生类重写该函数,实现多态。
-
行为 :当通过基类的指针或引用调用一个虚函数时,调用的是对象实际类型的函数版本。
-
示例 :
Vehicle(string contry, double price){
cout << "基类的构造函数被调用" << endl;
this->contry = contry;
this->price = price;
};
void run(){
cout << "车跑起来了" << endl;
}
void stop();
};
//派生类,子类
class Roadster : public Vehicle{ //跑车,也是抽象,比父类感觉上范围缩小了点
public:
int stateOfTop;
Roadster(string contry, double price, int state) : Vehicle(contry, price){
cout << "派生类的构造函数被调用" << endl;
stateOfTop = state;
}
void openTopped();
void pdrifting();
};
int main()
{
Roadster FTYPE("法国",70,0);
return 0;
}override 关键字
-
使用场景 :在派生类中重写虚函数。
-
目的 :明确指示函数意图重写基类的虚函数。
-
行为 :确保派生类的函数确实重写了基类中的一个虚函数。如果没有匹配的虚函数,编译器会报
错。
- 示例 :
注意点
只在派生类中使用 override : override 应仅用于派生类中重写基类的虚函数。
虚析构函数 :如果类中有虚函数,通常应该将析构函数也声明为虚的。
默认情况下,成员函数不是虚的 :在C++中,成员函数默认不是虚函数。只有显式地使用 virtual
关键字才会成为虚函数。
继承中的虚函数 :一旦在基类中声明为虚函数,该函数在所有派生类中自动成为虚函数,无论是否
使用 virtual 关键字。
正确使用 virtual 和 override 关键字有助于清晰地表达程序员的意图,并利用编译器检查来避免常
见的错误,如签名不匹配导致的非预期的函数重写。
2.10.5 多重继承
在C++中,多重继承是一种允许一个类同时继承多个基类的特性。这意味着派生类可以继承多个基类的属
性和方法。多重继承增加了语言的灵活性,但同时也引入了额外的复杂性,特别是当多个基类具有相同
的成员时。
基本概念
在多重继承中,派生类继承了所有基类的特性。这包括成员变量和成员函数。如果不同的基类有相同名
称的成员,则必须明确指出所引用的是哪个基类的成员。
示例
假设有两个基类 ClassA 和 ClassB ,以及一个同时从这两个类继承的派生类 Derived :
class Base {
public:
virtual void func() {
std::cout << "Function in Base" << std::endl;
}
};
class Derived : public Base {
public:
void func() override {
std::cout << "Function in Derived" << std::endl;
}
};
class ClassA {
public:
void displayA() {在这个示例中, Derived 类同时继承了 ClassA 和 ClassB 。因此,它可以使用这两个类中定义的方
法。
注意事项
菱形继承问题 :如果两个基类继承自同一个更高层的基类,这可能导致派生类中存在两份基类的副
本,称为菱形继承(或钻石继承)问题。这可以通过虚继承来解决。
复杂性 :多重继承可能会使类的结构变得复杂,尤其是当继承层次较深或类中有多个基类时。
设计考虑 :虽然多重继承提供了很大的灵活性,但过度使用可能导致代码难以理解和维护。在一些
情况下,使用组合或接口(纯虚类)可能是更好的设计选择。
多重继承是C++的一个强大特性,但应谨慎使用 。合理地应用多重继承可以使代码更加灵活和强大,但不
当的使用可能导致设计上的问题和维护困难。
2.10.6 虚继承
虚继承是C++中一种特殊的继承方式,主要用来解决多重继承中的菱形继承问题。在菱形继承结构中,一
个类继承自两个具有共同基类的类时,会导致共同基类的成员在派生类中存在两份拷贝,这不仅会导致
资源浪费,还可能引起数据不一致的问题。虚继承通过确保共同基类的单一实例存在于继承层次中,来
解决这一问题。
菱形继承问题示例
考虑以下的类结构:
std::cout << "Displaying ClassA" << std::endl;
}
};
class ClassB {
public:
void displayB() {
std::cout << "Displaying ClassB" << std::endl;
}
};
class Derived : public ClassA, public ClassB {
public:
void display() {
displayA(); // 调用 ClassA 的 displayA
displayB(); // 调用 ClassB 的 displayB
}
};
int main() {
Derived obj;
obj.displayA(); // 调用 ClassA 的 displayA
obj.displayB(); // 调用 ClassB 的 displayB
obj.display(); // 调用 Derived 的 display
return 0;
}
class Base {
public:
int data;在这个例子中, FinalDerived 类通过 Derived1 和 Derived2 间接地继承自 Base 类两次。因此,
它包含了两份 Base 的成员拷贝。
使用虚继承解决菱形继承问题
要解决这个问题,应使用虚继承:
通过将 Derived1 和 Derived2 对 Base 的继承声明为虚继承( virtual public Base ),
FinalDerived 类中只会有一份 Base 类的成员。无论通过 Derived1 还是 Derived2 的路径,访问
的都是同一个 Base 类的成员。
特点和注意事项
初始化虚基类 :在使用虚继承时,虚基类(如上例中的 Base 类)只能由最派生的类(如
FinalDerived )初始化。
内存布局 :虚继承可能会改变类的内存布局,通常会增加额外的开销,比如虚基类指针。
设计考虑 :虚继承应谨慎使用,因为它增加了复杂性。在实际应用中,如果可以通过其他设计(如
组合或接口)避免菱形继承,那通常是更好的选择。
虚继承是C++语言中处理复杂继承关系的一种重要机制,但它也带来了一定的复杂性和性能考虑。正确地
使用虚继承可以帮助你建立清晰、有效的类层次结构。
C++ 继承相关的学习内容整理成表格的形式:
};
class Derived1 : public Base {
// 继承自 Base
};
class Derived2 : public Base {
// 继承自 Base
};
class FinalDerived : public Derived1, public Derived2 {
// 继承自 Derived1 和 Derived2
};
class Base {
public:
int data;
};
class Derived1 : virtual public Base {
// 虚继承 Base
};
class Derived2 : virtual public Base {
// 虚继承 Base
};
class FinalDerived : public Derived1, public Derived2 {
// 继承自 Derived1 和 Derived2
};学习内容
描述
继承的基础
理解基类和派生类的概念,以及如何通过继承扩展类功能。了解不同继
承类型(公有、私有、保护)及其影响。
构造函数和析构函数在
继承中的行为
学习派生类如何调用基类的构造函数和析构函数,以及它们的调用顺
序。
访问控制和继承
理解公有、私有和保护继承对成员访问权限的影响。掌握继承中的访问
修饰符(public, protected, private)。
函数重写和多态
学习多态和如何通过虚函数实现它,了解如何重写基类方法,以及纯虚
函数和抽象类的概念。
虚继承和解决菱形问题
理解菱形继承问题及其解决方式,学习如何使用虚继承。
C++11 新特性中的继承
相关内容
理解和应用 override 和 final 关键字,了解移动语义在继承中的应
用。
设计原则与最佳实践
学习正确使用继承的方法,区分何时使用继承,何时使用组合,以及面
向对象设计原则的应用。
实际案例分析
通过分析和编写实际代码示例加深理解,研究设计模式中继承的应用。
这个表格概述了学习 C++ 继承的关键方面和内容,有助于系统地理解和应用继承的概念。
2.11 多态
多态的基本概念 (polymorphic)
想象一下,你有一个遥控器(这就像是一个基类的指针),这个遥控器可以控制不同的电子设备(这些
设备就像是派生类)。无论是电视、音响还是灯光,遥控器上的"开/关"按钮(这个按钮就像是一个虚函
数)都能控制它们,但具体的操作(打开电视、播放音乐、开灯)则取决于你指向的设备。
2.11.1 如何实现多态
- 使用虚函数( Virtual Function ) :
我们在基类中定义一个虚函数,这个函数可以在任何派生类中被"重写"或者说"定制"。
使用关键字 virtual 来声明。
- 创建派生类并重写虚函数 :
在派生类中,我们提供该虚函数的具体实现。这就像是告诉遥控器,"当你控制我的这个设备
时,这个按钮应该这样工作"。
- 通过基类的引用或指针调用虚函数 :
当我们使用基类类型的指针或引用来调用虚函数时,实际调用的是对象的实际类型(派生类)
中的函数版本。
视频课程中的手写案例
#include <iostream>
using namespace std;class RemoteCon{
public:
virtual void openUtils(){
cout << "遥控器的开被按下" << endl;
}
};
class TvRemoteCon : public RemoteCon{
public:
void openUtils() override{
cout << "电视遥控器的开被按下" << endl;
}
void testFunc(){
}
};
class RoundspeakerCon : public RemoteCon{
public:
void openUtils() override{
cout << "音响遥控器的开被按下" << endl;
}
};
class LightCon : public RemoteCon{
public:
void openUtils() override{
cout << "灯光遥控器的开被按下" << endl;
}
};
void test(RemoteCon& r)//引用的方式
{
r.openUtils();
}
int main()
{
RemoteCon *remoteCon = new TvRemoteCon; //多态
remoteCon->openUtils();
RemoteCon *remoteCon2 = new TvRemoteCon; //多态
remoteCon2->openUtils();
RemoteCon *remoteCon3 = new LightCon; //多态
remoteCon3->openUtils();
TvRemoteCon tvRemote;
test(tvRemote);在这个例子中,不同的对象( TvRemoteCon 和 TvRemoteCon )以它们自己的方式"开",尽管调用的是
相同的函数 openUtils 。这就是多态的魅力------相同的接口,不同的行为。
为什么使用多态
灵活性 :允许我们编写可以处理不确定类型的对象的代码。
可扩展性 :我们可以添加新的派生类而不必修改使用基类引用或指针的代码。
接口与实现分离 :我们可以设计一个稳定的接口,而将具体的实现留给派生类去处理。
2.11.2 抽象类
抽象类的基本概念
想象一下,你有一个"交通工具"的概念。这个概念告诉你所有交通工具都应该能做什么,比如移动
(move),但它并不具体说明怎么移动。对于不同的交通工具,比如汽车和自行车,它们的移动方式是
不同的。在这个意义上,"交通工具"是一个抽象的概念,因为它本身并不能直接被使用。你需要一个具体
的交通工具,比如"汽车"或"自行车",它们根据"交通工具"的概念具体实现了移动的功能。
在 C++ 中,抽象类就像是这样的一个抽象概念。它定义了一组方法(比如移动),但这些方法可能没有
具体的实现。这意味着,抽象类定义了派生类应该具有的功能,但不完全实现这些功能。
抽象类的特点
- 包含至少一个纯虚函数 :
抽象类至少有一个纯虚函数。这是一种特殊的虚函数,在抽象类中没有具体实现,而是留给派
生类去实现。
纯虚函数的声明方式是在函数声明的末尾加上 = 0 。
- 不能直接实例化 :
由于抽象类不完整,所以不能直接创建它的对象。就像你不能直接使用"交通工具"的概念去任
何地方,你需要一个具体的交通工具。
- 用于提供基础结构 :
抽象类的主要目的是为派生类提供一个共同的基础结构,确保所有派生类都有一致的接口和行
为。
return 0;
}
#include <iostream>
using namespace std;
class Teacher{
public:
string name;
string shool;
string major;
virtual void goInClass() = 0;
virtual void startTeaching() = 0;2.11.3 纯虚函数 - 接口
在 C++ 中,虽然没有像其他编程语言(比如 Java 中的接口Interface)一样直接定义接口的关键字,但可
以通过抽象类和纯虚函数的方式来实现接口的概念。
接口通常用于定义类应该实现的方法,但不提供具体实现。这样的实现方式允许多个类共享相同的接
口,同时让每个类根据需要去实现这些接口。
一个类作为接口可以通过以下步骤来实现:
virtual void afterTeaching() = 0;
};
class EnglishTeacher : public Teacher{
public:
void goInClass() override{
cout << "英语老师开始进入教室" << endl;
}
void startTeaching() override{
cout << "英语老师开始教学" << endl;
}
void afterTeaching() override{
};
};
class ProTeacher : public Teacher{
public:
void goInClass() override{
cout << "编程老师开始进入教室" << endl;
}
void startTeaching() override{
cout << "编程老师开始撸代码了,拒绝读PPT" << endl;
}
void afterTeaching() override{
cout << "编程老师下课后手把手教x学员写代码" << endl;
};
};
int main()
{
// Teacher t;//抽象类,不支持被实例化
EnglishTeacher e;
e.goInClass();
ProTeacher t;
t.startTeaching();
t.afterTeaching();
//抽象类,多态
Teacher *teacher = new ProTeacher;
teacher->startTeaching();
return 0;
}1. 定义抽象类 :创建一个包含纯虚函数的抽象类,这些函数构成了接口的一部分。这些函数在抽象类
中只有声明而没有具体的实现。
- 派生类实现接口 :派生类继承抽象类,并实现其中的纯虚函数,以具体实现接口定义的方法。
以下是一个简单的示例来展示如何使用抽象类来模拟接口:
通过这种方式,您可以在 C++ 中模拟出类似接口的行为,允许多个类共享相同的接口并提供各自的实
现。
#include <iostream>
using namespace std;
class BasketBallMove{
public:
virtual void passTheBall() = 0;
};
class LiveMove{
public:
virtual void eat() = 0;
virtual void bite() = 0;
virtual void drink() = 0;
virtual void la() = 0;
};
class Human : public LiveMove,BasketBallMove{
public:
void eat() override{};
void bite() override{};
void drink() override{};
void la() override{};
void passTheBall() override{};
};
class Dog : public LiveMove{
public:
void eat() override{};
void bite() override{};
void drink() override{};
void la() override{};
};
int main()
{
Human h;
Dog g;
// LiveMove *l = new LiveMove;
return 0;
}2.12 友元
2.12.1 什么是友元
在C++中,友元(friend)关键字用于给特定的外部函数或类访问某类的私有(private)和保护
(protected)成员的权限。友元关系不是相互的,也不是可继承的。这意味着被声明为友元的函数或类
可以访问原始类的私有和保护成员,但原始类不能访问友元的私有成员,除非它们也被声明为友元。
友元主要有三种类型:
- 友元函数 :一个普通函数(不是类的成员函数)可以被声明为一个类的友元函数。它可以访问该类
的所有成员(公有、保护和私有)。
- 友元类 :一个类可以被声明为另一个类的友元。这意味着友元类的所有成员函数都可以访问原始类
的保护和私有成员。
- 友元成员函数 :一个类的成员函数可以被声明为另一个类的友元。这意味着该成员函数可以访问另
一个类的保护和私有成员。
为什么使用友元
友元提供了一种机制,允许某些外部函数或类直接访问类的私有或保护成员。这在某些情况下非常有
用,例如:
实现运算符重载,如重载输入输出运算符( << 和 >> )。
允许两个不同类的对象之间进行密切的交互。
当类设计需要一种安全控制的方式来允许非成员函数或类访问私有成员时。
2.12.2 友元函数
示例
下面是一个展示友元函数的例子:在这个例子中, showValue 函数被声明为 MyClass 的友元函数,它
可以访问 MyClass 的私有成员 value 。
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}
// 声明友元函数
friend void showValue(MyClass& obj);
};
// 友元函数的定义
void showValue(MyClass& obj) {
std::cout << "Value of MyClass: " << obj.value << std::endl;
}
int main() {
MyClass obj(42);
showValue(obj); // 可以访问 MyClass 的私有成员
return 0;
}友元函数实现运算符重载 ,如重载输入输出运算符( << 和 >> )。
在C++中,重载输入输出运算符( << 和 >> )通常需要使用友元函数。这是因为输入输出运算符通常需
要访问类的私有成员,同时又需要符合标准库中流对象(如 std::ostream 和 std::istream )的用
法。
下面是一个如何使用友元函数来重载 << 和 >> 运算符的例子:
示例
假设我们有一个 Point 类,表示二维空间中的一个点,我们想要重载 << 和 >> 运算符以输出和输入点
的坐标。
在这个例子中:
<< 运算符用于将 Point 对象的内容输出到输出流(如 std::cout )。
>> 运算符用于从输入流(如 std::cin )中读取值到 Point 对象。
这两个运算符都被声明为 Point 类的友元函数,以便它们可以访问类的私有成员变量 x 和 y 。
注意事项
虽然友元提供了强大的功能,但也应谨慎使用:
#include <iostream>
class Point {
private:
int x, y;
public:
Point() : x(0), y(0) {}
Point(int x, int y) : x(x), y(y) {}
// 重载 << 运算符(输出)
friend std::ostream& operator<<(std::ostream& os, const Point& point) {
os << "(" << point.x << ", " << point.y << ")";
return os;
}
// 重载 >> 运算符(输入)
friend std::istream& operator>>(std::istream& is, Point& point) {
is >> point.x >> point.y;
return is;
}
};
int main() {
Point p1(1, 2);
std::cout << "Point p1: " << p1 << std::endl; // 输出点 p1
Point p2;
std::cout << "Enter coordinates for p2 (x y): ";
std::cin >> p2; // 输入点 p2
std::cout << "Point p2: " << p2 << std::endl; // 输出点 p2
return 0;
}过度使用友元可能会破坏封装,使得代码难以维护和理解。
保持友元的数量最小,仅在确实需要时使用,以维持良好的封装性。
2.12.3 友元类
在C++中,一个类可以被声明为另一个类的友元类(friend class)。当一个类被声明为另一个类的友元
类时,它可以访问后者的所有成员,包括私有(private)和保护(protected)成员。
友元类是一种强大的特性,它允许在保持封装性的同时,提供对类内部的深入访问。然而,由于它允许
对另一个类的内部细节进行直接操作,因此应谨慎使用,以免破坏封装性。
示例
假设我们有两个类 ClassA 和 ClassB 。我们可以使 ClassB 成为 ClassA 的友元类,这样 ClassB
就可以访问 ClassA 的所有成员,包括私有成员。
在这个示例中, ClassB 能够访问 ClassA 的私有成员 value ,因为它被声明为 ClassA 的友元类。
注意事项
谨慎使用 :友元类应该谨慎使用,因为它们可能会破坏对象的封装性和隐藏性。
非相互性 :如果 ClassA 是 ClassB 的友元类,这并不意味着 ClassB 自动成为 ClassA 的友元
类。友元关系是单向的。
非继承性 :友元关系不会被继承。如果类 C 继承自类 B ,并且 B 是 A 的友元类,那么 C 不自动
成为 A 的友元类,除非 A 明确声明 C 为友元类。
友元类提供了一种机制,允许其他类访问私有成员,但应在不破坏封装性的前提下谨慎使用。
class ClassA {
private:
int value;
public:
ClassA(int v) : value(v) {}
// 声明 ClassB 为友元类
friend class ClassB;
};
class ClassB {
public:
void showValue(ClassA& a) {
// 可以访问 ClassA 的私有成员
std::cout << "Value of ClassA: " << a.value << std::endl;
}
};
int main() {
ClassA a(100);
ClassB b;
b.showValue(a); // 输出 "Value of ClassA: 100"
return 0;
}2.12.4 友元成员函数
在C++中,除了可以将整个类或单独的函数声明为友元外,还可以将特定类中的某个成员函数单独声明为
另一个类的友元。这样做可以让这个成员函数访问另一个类的所有成员(包括私有和受保护的成员),
而无需将整个类声明为友元,从而提供了更细粒度的访问控制。
示例
假设我们有两个类 ClassA 和 ClassB ,我们希望 ClassB 的一个特定成员函数 showValue 能够访问
ClassA 的私有成员。我们可以将 ClassB 的 showValue 函数声明为 ClassA 的友元。
在这个示例中, ClassB::showValue 成为 ClassA 的友元函数。这意味着 showValue 可以访问
ClassA 的私有成员 value 。注意,我们在 ClassA 前提供了 ClassB 的前向声明。这是必须的,因为
在声明 ClassA 时 ClassB 还未完全定义。
注意事项
精确控制 :与将整个类声明为友元相比,将特定成员函数声明为友元可以更精确地控制访问权限。
破坏封装 :即使是单个成员函数,过度使用友元也可能破坏类的封装性。应当谨慎使用。
前向声明 :在一个类中声明另一个类的成员函数为友元时,可能需要前向声明另一个类,特别是在
两个类相互引用对方的成员函数时。
友元成员函数是C++中一种强大的特性,允许开发者在保持类封装性的同时提供必要的访问权限。然而,
应当谨慎使用,以保持代码的清晰性和维护性。
#include <iostream>
class ClassB; // 前向声明
class ClassA {
private:
int value;
public:
ClassA(int v) : value(v) {}
// 声明 ClassB 的成员函数为友元
friend void ClassB::showValue(ClassA& a);
};
class ClassB {
public:
void showValue(ClassA& a) {
// 访问 ClassA 的私有成员
std::cout << "Value of ClassA: " << a.value << std::endl;
}
};
int main() {
ClassA a(100);
ClassB b;
b.showValue(a); // 输出 "Value of ClassA: 100"
return 0;
}2.13 模板
2.13.1 类模板
在 C++ 中,模板(Template)是一种通用的编程工具,允许程序员编写泛型代码 ,使得类或函数能够适
用于多种不同的数据类型 而不需要重复编写相似的代码。C++ 提供了两种主要类型的模板:类模板和函
数模板。
类模板( Class Templates ):
类模板允许定义通用的类,其中某些类型可以作为参数。这样的类可以处理不同类型的数据,而不需要
为每个数据类型编写单独的类。
2.13.2 函数模板
函数模板允许编写通用的函数,可以处理多种不同类型的数据。
// 定义一个通用的类模板
template <typename T>
class MyTemplate {
private:
T data;
public:
MyTemplate(T d) : data(d) {}
T getData() {
return data;
}
};
int main() {
// 使用类模板创建对象
MyTemplate<int> intObject(5);
MyTemplate<std::string> stringObject("Hello");
// 调用类模板中的函数
std::cout << intObject.getData() << std::endl; // 输出 5
std::cout << stringObject.getData() << std::endl; // 输出 Hello
return 0;
}
// 定义一个通用的函数模板
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
// 使用函数模板调用通用函数
int result1 = add(5, 10);
double result2 = add(3.5, 2.7);模板提供了一种在编写代码时更具通用性的方法,能够处理不同类型的数据而无需为每种类型编写特定
的函数或类。通过使用模板,可以提高代码的重用性和可维护性。
2.13.3 模板特化
模板特化(Template Specialization)是 C++ 中模板的一个概念,它允许针对特定的数据类型或特定的
模板参数提供定制化的实现。模板特化允许您为模板提供一个特殊的实现,以覆盖或扩展默认的模板行
为。
有两种类型的模板特化:完全特化(Full Specialization)和部分特化(Partial Specialization)。
完全特化( Full Specialization ):
完全特化是对模板中的所有模板参数都进行特化的情况。在完全特化中,模板参数被指定为特定的类
型,为特定的类型提供独特的实现。
以下是一个示例,演示了对模板函数的完全特化:
在这个示例中, maximum 是一个模板函数,可以比较不同类型的数据,并返回较大的值。然后,为了特
化针对 const char* 类型,我们提供了一个特化版本的 maximum 函数,该函数使用 strcmp 函数来
比较字符串,并返回较大的字符串。
部分特化( Partial Specialization ):
// 输出函数模板调用结果
std::cout << "Result 1: " << result1 << std::endl; // 输出 Result 1: 15
std::cout << "Result 2: " << result2 << std::endl; // 输出 Result 2: 6.2
return 0;
}
#include <iostream>
// 定义一个通用的模板函数
template <typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}
// 对模板函数进行完全特化,针对 char* 类型
template <>
const char* maximum<const char*>(const char* a, const char* b) {
return strcmp(a, b) > 0 ? a : b;
}
int main() {
int intMax = maximum(3, 5); // 调用模板函数
std::cout << "Maximum of integers: " << intMax << std::endl;
const char* charMax = maximum("apple", "orange"); // 调用特化的函数
std::cout << "Maximum of strings: " << charMax << std::endl;
return 0;
}容器类型
描述
特点
vector
动态数组,支持快速随
机访问和尾部插入/删除
支持动态大小调整,适用于需要随机访问
和动态增删元素的场景
list
双向链表,支持快速插
入/删除操作
插入/删除元素快速,但不支持随机访问元
素
部分特化是指对模板中的部分参数进行特化,允许更具体地特化某些模板参数。这通常在模板类中使
用。
以下是一个示例,展示了对模板类的部分特化:
在这个示例中, MyTemplate 是一个模板类,然后我们对 MyTemplate 进行部分特化,当第二个模板参
数是 int 类型时,提供了特殊的实现。因此,对于特定的类型组合,我们可以提供自定义的实现。
2.14 标准模板库 STL
2.14.1 容器
当谈到 C++ 的标准模板库(STL)容器时,通常有一些常见的容器类型。以下是一些常见的 STL 容器及
其特点的简要总结:
#include <iostream>
// 定义一个通用的模板类
template <typename T, typename U>
class MyTemplate {
public:
void display() {
std::cout << "Generic Display" << std::endl;
}
};
// 对模板类进行部分特化,针对特定的类型组合
template <typename T>
class MyTemplate<T, int> {
public:
void display() {
std::cout << "Specialized Display for T and int" << std::endl;
}
};
int main() {
MyTemplate<float, double> obj1;
obj1.display(); // 输出 "Generic Display"
MyTemplate<int, int> obj2;
obj2.display(); // 输出 "Specialized Display for T and int"
return 0;
}容器类型
描述
特点
deque
双端队列,支持在两端
快速插入/删除操作
支持在头尾快速插入/删除元素,不同于
vector 在头部操作时效率较低,但随机访
问效率比 list 高
stack
后进先出(LIFO)的堆
栈数据结构
基于 deque 或 vector 实现,只允许在栈
顶进行插入和删除操作
queue
先进先出(FIFO)的队
列数据结构
基于 deque 或 list 实现,只允许在队尾进
行插入,在队头进行删除操作
priority_queue
优先队列,按照一定顺
序维护元素
基于 vector 实现,默认情况下是最大堆,
可以通过自定义比较函数来实现不同的优
先级顺序
set
有序不重复元素集合,
基于红黑树实现
自动排序,插入/查找元素的平均时间复杂
度为 O(log n),不允许重复元素
multiset
有序可重复元素集合,
基于红黑树实现
允许存储重复元素,按序存储,插入/查找
元素的平均时间复杂度为 O(log n)
map
键值对集合,基于红黑
树实现
存储键值对,按键自动排序,不允许重复
键,插入/查找元素的平均时间复杂度为
O(log n)
multimap
键值对集合,允许重复
键,基于红黑树实现
允许存储重复键值对,按键自动排序,插
入/查找元素的平均时间复杂度为 O(log n)
unordered_set
无序不重复元素集合,
基于哈希表实现
不按顺序存储元素,插入/查找/删除元素的
平均时间复杂度为 O(1),不允许重复元素
unordered_multiset
无序可重复元素集合,
基于哈希表实现
允许存储重复元素,不按顺序存储,插入/
查找/删除元素的平均时间复杂度为 O(1)
unordered_map
无序键值对集合,基于
哈希表实现
键值对无序存储,插入/查找/删除元素的平
均时间复杂度为 O(1),不允许重复键
unordered_multimap
无序键值对集合,允许
重复键,基于哈希表实
现
允许存储重复键值对,键值对无序存储,
插入/查找/删除元素的平均时间复杂度为
O(1)
2.14.2 vector
在C++的标准模板库(STL)中, std::vector 是一种动态数组容器。它提供了动态大小的数组功能,
能够在运行时根据需要自动调整大小,允许在其尾部高效地进行元素的插入和删除操作。
- 动态大小: std::vector 允许动态增加或减少其大小。它会自动处理内存分配和释放,无需手动
管理内存。
-
随机访问: 支持使用索引进行快速的随机访问,因为它底层基于连续的内存块。
-
尾部插入 / 删除: 在数组的尾部插入或删除元素的操作非常高效,时间复杂度为常数时间
(Amortized Constant Time)。
- 连续内存存储: std::vector 中的元素在内存中是连续存储的,这有助于提高访问速度和缓存利
用率。5. 动态增长策略: 当向 std::vector 添加元素时,如果当前容量不足,它会动态地重新分配更大的
内存空间,并将现有元素复制到新的内存位置。这种动态增长策略确保了插入操作的高效性。
std::vector 的缺点是,在执行插入或删除操作时,如果不是在容器的末尾进行,可能会导致较高的时
间复杂度,因为需要移动后续元素。
以下是一个使用 C++ STL 中的 vector 容器的简单示例:
#include <iostream>
#include <vector>
int main() {
// 创建一个空的 vector 容器
std::vector<int> myVector;
// 向 vector 容器尾部添加元素
myVector.push_back(3);
myVector.push_back(7);
myVector.push_back(12);
// 使用迭代器遍历 vector 容器并输出其中的元素
// 在 C++ 中,auto 是一个关键字,用于声明变量时的类型推断。
// 它允许编译器根据变量的初始化表达式推断出变量的类型,从而简化代码书写过程。
std::cout << "Vector elements: ";
for (auto it = myVector.begin(); it != myVector.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// 获取 vector 容器的大小和访问特定位置的元素
std::cout << "Vector size: " << myVector.size() << std::endl;
std::cout << "Element at index 1: " << myVector[1] << std::endl;
// 修改特定位置的元素
myVector[2] = 20;
// 使用范围-based for 循环遍历 vector 并输出元素
std::cout << "Modified Vector elements: ";
for (int num : myVector) {
std::cout << num << " ";
}
std::cout << std::endl;
// 清空 vector 容器
myVector.clear();
// 检查 vector 是否为空
if (myVector.empty()) {
std::cout << "Vector is empty." << std::endl;
} else {
std::cout << "Vector is not empty." << std::endl;
}
return 0;
}API
描述
函数原型
参数说明
push_back()
在 vector
尾部添加一
个元素
void push_back(const T&
value);
value :要添加到尾部
的元素
pop_back()
删除
vector 尾
部的一个元
素
void pop_back();
无参数
size()
返回
vector 中
元素的数量
size_type size() const
noexcept;
无参数
capacity()
返回
vector 当
前可容纳的
元素数量
size_type capacity()
const noexcept;
无参数
resize()
改变
vector 的
大小,可以
增加或减少
元素数量
void resize(size_type
count);
void resize(size_type
count, const T& value);
count :新的 vector
大小
value :若添加元素,
初始化值为 value
reserve()
修改
vector 的
容量,预留
足够的存储
空间
void reserve(size_type
new_cap);
new_cap :新的
vector 容量
clear()
清空
vector 中
的所有元素
void clear() noexcept;
无参数
empty()
检查
vector 是
否为空
bool empty() const
noexcept;
无参数
at()
返回指定位
置的元素,
并进行边界
检查
reference at(size_type
pos);
const_reference
at(size_type pos) const;
pos :要访问的位置。
如果超出范围,会引发
std::out_of_range
异常
这个例子展示了如何创建 vector 容器、向其尾部添加元素、使用迭代器和索引访问元素、修改元素
值、清空容器以及检查容器是否为空。 vector 是一个动态数组,可以根据需要动态地增加或减少其大
小,适合需要随机访问元素且频繁进行尾部插入/删除操作的场景。
当涉及到 std::vector 的 API 方法时,参数列表、返回值以及参数的说明可以帮助更清楚地了解每个
方法的使用和含义。以下是 std::vector 常用 API 列表,包括参数列表、返回值和参数说明:API
描述
函数原型
参数说明
operator[]
重载操作
符,用于访
问指定位置
的元素
reference operator[]
(size_type pos);
const_reference
operator[](size_type
pos) const;
pos :要访问的位置。
不进行边界检查,如果
超出范围,行为未定义
front()
返回
vector 中
第一个元素
的引用
reference front();
const_reference front()
const;
无参数
back()
返回
vector 中
最后一个元
素的引用
reference back();
const_reference back()
const;
无参数
begin()
返回指向
vector 第
一个元素的
迭代器
iterator begin()
noexcept;
const_iterator begin()
const noexcept;
无参数
end()
返回指向
vector 末
尾(最后一
个元素的后
面)的迭代
器
iterator end()
noexcept;
const_iterator end()
const noexcept;
无参数
rbegin()
返回指向
vector 最
后一个元素
的逆向迭代
器(逆向开
始迭代)
reverse_iterator
rbegin() noexcept;
const_reverse_iterator
rbegin() const noexcept;
无参数
rend()
返回指向
vector 第
一个元素之
前的逆向迭
代器(逆向
结束迭代)
reverse_iterator rend()
noexcept;
const_reverse_iterator
rend() const noexcept;
无参数
insert()
在指定位置
插入一个或
多个元素
iterator
insert(const_iterator
pos, const T& value);
void
insert(const_iterator
pos, size_type count,
const T& value);
pos :插入位置
value :要插入的元素
count :要插入的元素
个数API
描述
函数原型
参数说明
erase()
删除指定位
置或指定范
围内的一个
或多个元素
iterator
erase(const_iterator
pos);
iterator
erase(const_iterator
first, const_iterator
last);
pos :要删除的元素位
置或范围的起始位置
first 、 last :要删除
的范围
swap()
交换两个
vector 容
器的内容
void swap(vector&
other);
other :要交换内容的
另一个 vector 容器
emplace()
在指定位置
就地构造一
个元素
iterator
emplace(const_iterator
pos, Args&&... args);
pos :就地构造的位置
args :构造元素所需的
参数
emplace_back()
在 vector
尾部就地构
造一个元素
void
emplace_back(Args&&...
args);
args :构造元素所需的
参数
这些详细说明包括每个函数的参数、返回值以及对参数的解释,有助于更清晰地理解 std::vector 中
常用 API 的使用方式和含义。
2.14.3 list
STL 中的 list 是双向链表(doubly linked list)的实现,它是 C++ 标准模板库中的一个容器,提供了
一种能够高效进行插入、删除操作的数据结构。与 vector 不同, list 不支持随机访问,但它允许在
任意位置快速插入和删除元素。
以下是关于 std::list 的一些特点和说明:
双向链表结构: std::list 使用双向链表来组织其元素,每个节点都包含指向前一个节点和后一
个节点的指针,因此在任意位置进行插入和删除操作的开销较小。
不支持随机访问: 与 vector 不同, list 不支持通过索引直接访问元素,因为它不具备随机访问
能力。要访问 list 中的元素,需要使用迭代器进行顺序遍历。
动态大小调整: list 具有动态大小调整的特性,可以动态增加或减少元素的数量。对于大量的插
入和删除操作, list 往往比 vector 更高效。
迭代器操作: 使用迭代器可以对 list 中的元素进行访问、插入和删除。 list 提供了
begin() 、 end() 、 rbegin() 、 rend() 等迭代器相关方法,支持正向和逆向迭代。
插入和删除操作效率高: 在 list 中,在任意位置进行插入和删除操作的时间复杂度是 O(1),因为
只需要调整相邻节点的指针,无需移动大量元素。
空间开销: 相比于 vector , list 需要额外的空间来存储指向前一个和后一个节点的指针,可能
会导致更高的存储开销。
下面是一个简单的示例,演示了如何使用 STL 中的 std::list 容器。在这个案例中,我们创建了一个
std::list 来存储整数,并展示了一些基本的操作,如插入、删除、迭代等。