Introduction
该文档记录部分(不太准确的)C++知识,以及一些面试题。
大部分知识均不完善,不是手册,仅仅作为笔记。
C++语言面试题汇总
-
volatile关键字有什么作用? -
const和constexpr关键字有什么作用,有什么区别? -
static关键字有什么作用?和C语言中的有什么区别? -
extern有什么作用? -
面向对象的三大特征是什么?
-
typedef和define有什么区别?
-
指针常量与常量指针各是什么,二者有什么区别?
C++基础知识
C++基础部分包括语言的核心语法与程序结构,理解从“源代码到可执行程序”的基本过程。本部分将介绍变量与常量、数据类型、运算符、控制结构、函数、数组、指针、结构体、类与对象等基础概念。
掌握这些内容后,就能编写出功能完整、逻辑清晰的小型程序,理解程序的输入输出、流程控制和内存中数据的基本表示方式。C++基础知识不仅是学习更复杂特性的起点,也是理解程序设计思想与计算机工作原理的重要桥梁。
运行环境与基础概念
初识C++应知必会的基础概念。
C++ 程序结构(Program Structure)
引言
C++ 程序的结构决定了它的组织方式、执行流程和可维护性。 从最小的“Hello, World!”程序开始,我们可以认识一个 C++ 程序通常包含的头文件、命名空间、函数定义、语句块等组成部分。
一个最小可运行的 C++ 程序
#include <iostream> // 头文件:引入输入输出库
using namespace std; // 使用标准命名空间
int main() { // 主函数:程序执行入口
cout << "Hello, World!" << endl; // 输出语句
return 0; // 返回值:表示程序是否成功结束
}
程序执行流程说明
| 行号 | 内容 | 说明 |
|---|---|---|
| 1 | #include <iostream> | 预处理指令,引入标准输入输出库 |
| 2 | using namespace std; | 告诉编译器默认使用 std 命名空间 |
| 3 | int main() { | 主函数是程序的入口点(返回类型必须是 int) |
| 4 | cout << "Hello, World!" << endl; | 使用 cout 输出字符串并换行 |
| 5 | return 0; | 返回 0 表示程序正常结束 |
程序的基本组成部分
一个完整的 C++ 程序通常由以下几个部分组成:
-
预处理指令(Preprocessor Directives) 在编译前由预处理器执行,例如:
#include <iostream> #define PI 3.14159 -
命名空间(Namespace) 命名空间用于避免名称冲突:
namespace myspace { int x = 10; } -
主函数
main()所有可执行 C++ 程序都必须有且仅有一个main()函数。 -
函数定义(Functions) 函数是代码逻辑的基本单元,示例:
int add(int a, int b) { return a + b; } -
变量与语句(Variables & Statements) 程序的逻辑通过语句执行,通过变量存储数据。
int sum = add(2, 3); cout << sum << endl;
C++ 程序的执行入口
-
程序从
main()函数开始执行。 -
main()的返回值会传递给操作系统。 -
常见的两种形式:
int main() { // 无参数版本 return 0; } int main(int argc, char* argv[]) { // 带命令行参数的版本 // argc 表示参数数量 // argv 表示参数数组 return 0; }
程序的语句块与作用域
语句块使用花括号 {} 表示,定义一个新的作用域(scope):
int x = 10;
{
int x = 20; // 内层作用域的 x 隐藏外层变量
cout << x; // 输出 20
}
cout << x; // 输出 10
作用域是 C++ 的重要概念,影响变量的可见性与生命周期。
程序结构的逻辑层次(层级模型)
| 层级 | 名称 | 说明 |
|---|---|---|
| 顶层 | 预处理部分 | 定义宏、引入头文件 |
| 全局层 | 命名空间、全局变量、函数声明 | |
| 主函数层 | 程序入口、主流程 | |
| 函数层 | 逻辑实现与局部变量 | |
| 语句层 | 执行单元(表达式、控制语句等) |
这是一种从外到内的逻辑组织结构。良好的层级划分有助于程序的模块化。
程序文件结构(大型项目)
在真实项目中,一个 C++ 程序通常被拆分为多个文件:
project/
├── main.cpp // 程序入口
├── math/
│ ├── add.cpp
│ └── add.h
└── utils/
├── log.cpp
└── log.h
通过 头文件 (.h/.hpp) 声明接口,源文件 (.cpp) 实现功能:
-
add.h
#ifndef ADD_H #define ADD_H int add(int a, int b); #endif -
add.cpp
#include "add.h" int add(int a, int b) { return a + b; } -
main.cpp
#include <iostream> #include "add.h" int main() { std::cout << add(3, 4) << std::endl; return 0; }
大型工程中,通常直接或间接地使用
Cmake或make进行编译和项目管理。
预处理器 (Preprocessor)
C++ 预处理器(Preprocessor)是 C++ 编译过程中的第一阶段。它负责处理源代码中所有以 # 开头的指令,在实际编译(生成目标代码)之前对源代码进行文本替换、文件包含、条件编译等操作。
一、预处理指令 (Directives)
预处理指令以 # 符号开头,并且占据一行。它们不是 C++ 语句,因此不需要以分号 (;) 结尾。
| 指令 | 目的 | 示例 |
|---|---|---|
#include | 文件包含。将指定文件的内容插入到当前位置。 | #include <iostream> |
#define | 宏定义。定义常量宏或函数宏。 | #define PI 3.14159 |
#undef | 取消宏定义。 | #undef PI |
#if / #elif / #else / #endif | 条件编译。根据条件决定是否编译某段代码。 | #if VERSION >= 2 |
#ifdef / #ifndef | 条件编译。检查宏是否已被定义。 | #ifndef DEBUG_MODE |
#error | 停止编译,并输出指定的错误信息。 | #error "Platform not supported" |
#warning | 输出指定的警告信息,继续编译(非标准,但广泛支持)。 | #warning "Legacy code detected" |
#pragma | 编译器特定的指令,用于向编译器发出特殊指令。 | #pragma once, #pragma warning |
二、宏定义 (#define)
宏定义是预处理器最重要的功能之一,它执行的是文本替换。
-
对象式宏 (Object-like Macros) - 定义常量
#define BUFFER_SIZE 1024 // 预处理后:所有 BUFFER_SIZE 都会被替换为 1024 -
函数式宏 (Function-like Macros) - 定义类似函数的替换
使用时注意:必须在参数和整个宏体周围加上括号,以避免运算符优先级问题。
#define SQUARE(x) ( (x) * (x) ) // 错误示例:#define SQUARE(x) x * x // SQUARE(1 + 2) -> 1 + 2 * 1 + 2 -> 5 (错误!) // 正确示例:SQUARE(1 + 2) -> ( (1 + 2) * (1 + 2) ) -> 9 (正确!)最佳实践: 除非必要,在 C++ 中应使用
const或constexpr变量代替对象式宏,使用 内联函数 (inline) 或 模板 代替函数式宏,以获得类型检查和更好的可读性。 -
宏的特殊操作符
-
#(字符串化运算符 / Stringizing Operator):将宏参数转换为字符串字面值。#define MESSAGE(x) #x // std::cout << MESSAGE(Hello World); -> 输出: Hello World -
##(连接运算符 / Token-Pasting Operator):连接两个宏标记(token),形成一个新的标记。#define GLUE(a, b) a ## b // int GLUE(my, Variable) = 10; -> int myVariable = 10;
-
三、文件包含 (#include)
用于将一个文件的内容插入到另一个文件中。
-
尖括号 (
<>):用于包含标准库头文件。编译器会在标准系统路径下查找。#include <iostream> -
双引号 (
""):用于包含用户自定义头文件。编译器首先在当前源文件目录查找,然后才在标准系统路径下查找(具体的查找路径取决于编译器设置)。#include "my_header.h"
四、条件编译 (Conditional Compilation)
条件编译允许根据预处理器定义的条件,决定是否将一段代码发送给编译器。这对于平台移植、调试开关和头文件保护至关重要。
-
头文件保护 (Header Guards) - 经典方式
用于防止头文件被重复包含,导致重定义错误。
#ifndef MY_HEADER_H // IF Not DEFined #define MY_HEADER_H // 头文件内容 #endif // MY_HEADER_H -
基于宏的条件
#define DEBUG_MODE // 开启调试模式 // ... #ifdef DEBUG_MODE // 如果 DEBUG_MODE 宏已定义,则编译以下代码 std::cout << "Debugging output." << std::endl; #endif #ifndef RELEASE_MODE // 如果 RELEASE_MODE 宏未定义,则编译以下代码 // ... #endif -
多分支条件
#define OS_LINUX 1 // 假设在 Linux 平台编译 #if OS_LINUX #include <unistd.h> #elif OS_WINDOWS #include <windows.h> #else #error "Unsupported OS!" #endif#if后面要求表达式求值为非零(真)或零(假)。表达式中未定义的宏会被当作零处理。defined运算符:#if defined(MACRO_NAME)与#ifdef MACRO_NAME效果相似,但defined可以用于更复杂的布尔表达式中,例如#if defined(A) && !defined(B)。
五、编译器特定指令 (#pragma)
#pragma 指令是一种特殊的预处理指令,用于向编译器提供特定于编译器或平台的指示。由于它们是非标准的(不属于 C++ 语言规范),因此其功能和语法可能在不同编译器之间有所不同。
-
#pragma once-
目的: 实现头文件保护,防止头文件被重复包含。
-
优点: 代码更简洁,不会污染全局宏命名空间,并且大多数现代编译器(GCC, Clang, MSVC 等)都支持且执行效率更高(编译器可以直接比较文件名或路径)。
-
用法: 放在头文件的最开始。
// my_header.h #pragma once struct MyData { /* ... */ }; -
对比
#ifndef: -
#ifndef是标准 C++ 的一部分,可移植性最高。 -
#pragma once是事实上的行业标准,使用更方便,但技术上非标准。在新的代码中通常推荐使用它。
-
-
#pragma warning(主要用于 MSVC)-
目的: 临时修改或控制编译器警告的行为。
-
关键语法:
-
#pragma warning( disable : 警告编号 ):禁用特定的警告。 -
#pragma warning( error : 警告编号 ):将特定的警告视为错误。 -
#pragma warning( push ):保存当前的警告状态到堆栈。 -
#pragma warning( pop ):恢复最近一次保存的警告状态。 -
用途示例: 临时禁用第三方库中产生的警告。
#pragma warning( push ) // 保存当前警告状态 #pragma warning( disable : 4100 ) // 禁用 MSVC 警告 C4100 (未使用的形参) void unused_param_func(int x) { // ... code that intentionally doesn't use x } #pragma warning( pop ) // 恢复之前的警告状态
GCC/Clang 对应功能: 它们通常使用
#pragma GCC diagnostic push/pop和#pragma GCC diagnostic ignored "-Wxxx"。 -
-
其它特定
#pragma#pragma pack(n):指定结构体成员的对齐字节数,常用于跨平台或与硬件交互。#pragma comment(...):向目标文件中插入注释信息或链接器指令(如链接特定的库文件)。
命名空间(Namespace)
一、概念与作用
- 定义:命名空间是声明性区域,它提供了一种将代码组织成逻辑组并避免命名冲突的机制。
- 作用:
- 将类型、函数、变量等标识符包含在一个有名称的范围(Scope)内。
- 在大型程序中,尤其当程序包含多个库时,有效解决名称冲突问题(例如,不同库中可能存在同名的函数或变量)。
二、命名空间声明
使用 namespace 关键字来声明一个命名空间。
// 文件: MyCode.h
namespace MyNamespace {
int value;
void FunctionA() {
// ...
}
class MyClass {
// ...
};
}
三、访问命名空间成员
要访问命名空间内的成员,需要使用范围解析运算符 ::。
方式一:完整限定名称(Fully Qualified Name)
在命名空间外部使用完整的名称来限定成员。
// 访问 MyNamespace 中的 value 和 FunctionA
MyNamespace::value = 10;
MyNamespace::FunctionA();
方式二:using 声明(using Declaration)
将命名空间中的特定名称引入当前作用域。
#include "MyCode.h"
// 引入 MyNamespace 中的 FunctionA
using MyNamespace::FunctionA;
void SomeOtherFunction() {
// 此时可以直接使用 FunctionA,而不需要 MyNamespace::
FunctionA();
// 访问 value 仍需限定
MyNamespace::value = 20;
}
方式三:using 指示词(using Directive)
将命名空间中的所有名称引入当前作用域。
#include "MyCode.h"
// 将 MyNamespace 中的所有名称引入
using namespace MyNamespace;
void AnotherFunction() {
// 此时可以直接使用所有成员
value = 30;
FunctionA();
}
最佳实践:
- 避免在头文件(
.h或.hpp)中使用using namespace指示词,因为这会将命名空间中的所有名称带入所有包含该头文件的文件中,可能导致难以调试的名称冲突或隐藏问题。- 在实现文件(
.cpp)的顶部或函数内部,可以使用using namespace指示词以简化代码。- 如果只使用一两个标识符,建议使用完整限定名称或
using声明。
四、命名空间的特性
1. 分散定义(Separated Definition)
命名空间可以在单个文件中的多个块中声明,也可以在多个文件中声明。编译器会将所有部分连接起来。
// 文件 A.h
namespace Utils {
void func1();
}
// 文件 B.h
namespace Utils {
void func2(); // 即使在不同文件,也属于同一个 Utils 命名空间
}
2. 全局命名空间(Global Namespace)
-
未在任何显式命名空间中声明的标识符属于隐式全局命名空间。
-
要显式访问全局命名空间中的标识符,可以使用没有名称的范围解析运算符
:::int globalVar = 100; namespace Local { int globalVar = 200; // 隐藏全局变量 void test() { int val1 = globalVar; // 使用 Local::globalVar (200) int val2 = ::globalVar; // 使用 全局命名空间 的 globalVar (100) } }
3. 嵌套命名空间(Nested Namespaces)
命名空间可以嵌套,C++17 引入了内联嵌套简化语法。
namespace Outer {
namespace Inner {
void innerFunc();
} // 结束 Inner
} // 结束 Outer
// 访问方式:
Outer::Inner::innerFunc();
// C++17 简化写法:
namespace Outer::Inner {
void anotherInnerFunc();
}
4. 匿名或未命名命名空间(Anonymous/Unnamed Namespaces)
声明一个没有名称的命名空间。
namespace {
int MyFunc() { return 0; }
int internalVar;
}
- 作用:未命名命名空间中的变量和函数只在当前编译单元(文件)内可见,它们具有内部链接(internal linkage),等同于在全局作用域中使用
static关键字。 - 用途:避免在其他文件中看到变量声明,替代老式的
static全局/函数定义。
5. 命名空间别名(Namespace Alias)
用于为过长的命名空间名称创建别名,以提高代码可读性和输入效率。
namespace VeryLongNamespaceName {
void func();
}
// 创建别名
namespace VLNN = VeryLongNamespaceName;
// 使用别名访问
VLNN::func();
五、std 命名空间
1. std 命名空间简介
- 标准库:
std是 Standard(标准)的缩写,是 C++ 标准库(Standard Library)中所有实体(Entities)的容器。 - 内容:所有 C++ 标准库提供的类、函数、变量、模板等都被声明在
std命名空间内。- 常见的例子包括:
- 输入/输出:
std::cout,std::cin,std::endl - 字符串:
std::string - 容器:
std::vector,std::map,std::list - 算法:
std::sort,std::find - 其他:
std::unique_ptr,std::exception等。
- 输入/输出:
- 常见的例子包括:
2. 使用 std 成员
由于标准库的所有内容都在 std 命名空间内,因此需要使用前面介绍的三种方式来访问它们。
示例:完整限定名称(推荐)
这是最安全、最清晰的方式,尤其是在头文件和库代码中。
#include <iostream>
void print_message() {
// 每次使用都需要前缀 std::
std::cout << "Hello, C++ World!" << std::endl;
}
示例:using 声明
将特定的标准库成员引入当前作用域。
#include <iostream>
#include <vector>
using std::cout;
using std::vector;
void process_data() {
// 可以直接使用 cout 和 vector
cout << "Processing data..." << '\n';
vector<int> numbers = {1, 2, 3};
// 但 std::endl 仍需限定
cout << "Done." << std::endl;
}
示例:using 指示词
将整个 std 命名空间引入当前作用域。
#include <iostream>
// 将所有 std:: 成员引入
using namespace std;
void dangerous_example() {
// 可以直接使用 cout 和 endl
cout << "This is quick but potentially risky." << endl;
// 风险:如果用户定义了名为 'cout' 的变量,则会发生命名冲突或名称隐藏。
// int cout = 100; // 错误:在某些编译器上可能导致二义性或隐藏 std::cout
}
3. C 库头文件的兼容性
C++ 标准库也包含了 C 语言的标准库函数(如 printf, malloc 等)。
- C++ 风格:使用
c前缀和不带.h后缀的头文件(例如,<cmath>对应 C 语言的<math.h>)。- 在 C++ 风格的头文件中,所有函数和类型都位于
std::命名空间中。
- 在 C++ 风格的头文件中,所有函数和类型都位于
- C 风格(兼容性):使用带
.h后缀的头文件(例如,<math.h>)。- 为了兼容性,在 C 风格的头文件中,函数和类型通常同时位于全局命名空间和
std::命名空间中。
- 为了兼容性,在 C 风格的头文件中,函数和类型通常同时位于全局命名空间和
| 语言 | 头文件 | 函数位置 | 示例 |
|---|---|---|---|
| C | <stdlib.h> | 全局命名空间 | malloc(...) |
| C++ | <cstdlib> | std:: 命名空间 | std::malloc(...) |
| C++ (兼容性) | <stdlib.h> | 全局 + std:: | malloc(...) 或 std::malloc(...) |
C++ 程序的编译与运行(Compilation and Execution)
引言
C++ 是一种 编译型语言(compiled language)。
这意味着在执行程序之前,必须先将源代码 (.cpp) 转换为计算机可以理解的机器指令(可执行文件)。
整个过程通常包括以下几个阶段:
源代码 (.cpp)
↓
预处理(Preprocessing)
↓
编译(Compilation)
↓
汇编(Assembly)
↓
链接(Linking)
↓
可执行文件(Executable)
↓
运行(Execution)
编译过程的四个主要阶段
| 阶段 | 作用 | 示例命令 | 输出文件 |
|---|---|---|---|
| 预处理(Preprocessing) | 展开宏、包含头文件、删除注释 | g++ -E main.cpp -o main.i | main.i |
| 编译(Compilation) | 将预处理后的代码转换为汇编代码 | g++ -S main.i -o main.s | main.s |
| 汇编(Assembly) | 将汇编代码转换为机器指令(目标文件) | g++ -c main.s -o main.o | main.o |
| 链接(Linking) | 将多个目标文件与库链接生成可执行文件 | g++ main.o -o main | main |
可以通过这些命令观察 C++ 源码在每个阶段的输出,理解“编译器到底做了什么”。
完整编译命令示例
最常用的编译命令只需一行:
g++ main.cpp -o main
g++:GNU C++ 编译器(GCC 的 C++ 前端)main.cpp:源文件-o main:指定输出文件名(不写则默认为a.out)
运行程序:
./main
输出:
Hello, World!
多文件编译示例
大型项目通常由多个源文件组成:
project/
├── main.cpp
├── add.cpp
└── add.h
编译方式一:一步完成
g++ main.cpp add.cpp -o main
编译方式二:分步编译
g++ -c main.cpp -o main.o
g++ -c add.cpp -o add.o
g++ main.o add.o -o main
分步编译的好处是:当部分文件修改时,只需重新编译变动的部分,而不是整个项目。
运行机制详解
C++ 程序编译完成后,会生成一个可执行文件(如 main 或 main.exe)。
程序的运行流程如下:
操作系统加载可执行文件
↓
执行程序的入口函数 main()
↓
程序逻辑执行
↓
return 返回值传递给操作系统
返回值 0 通常表示正常退出,非零值表示错误或异常终止。
编译器与构建工具
| 工具 | 说明 | 适用平台 |
|---|---|---|
| g++ | GNU 编译器套件中的 C++ 编译器 | Linux / macOS / Windows(MinGW) |
| clang++ | LLVM 项目的 C++ 编译器,语法检查友好 | 跨平台 |
| MSVC (cl.exe) | 微软的 C++ 编译器 | Windows |
| CMake | 构建系统生成工具,可跨平台生成编译脚本 | 所有平台 |
| Makefile | 编译自动化脚本,用于管理依赖 | Linux / macOS |
使用 IDE 进行编译运行
常见 IDE
| IDE | 编译器 | 特点 |
|---|---|---|
| Visual Studio | MSVC | Windows 下功能最全 |
| CLion | CMake + g++/clang++ | 跨平台支持强 |
| VS Code | 可结合 g++、clang++ | 轻量灵活 |
| Qt Creator | qmake / CMake | 适用于图形界面开发 |
在 IDE 中执行的隐含步骤
当你点击“运行(Run)”时,IDE 实际执行了以下操作:
- 调用编译器生成目标文件;
- 执行链接步骤生成可执行程序;
- 启动该可执行文件并显示输出。
常见编译选项
| 选项 | 含义 | 示例 |
|---|---|---|
-o <file> | 指定输出文件名 | g++ main.cpp -o app |
-Wall | 启用所有警告信息 | g++ -Wall main.cpp |
-g | 生成调试信息 | g++ -g main.cpp |
-O2 | 优化等级 2,提高执行效率 | g++ -O2 main.cpp |
-std=c++17 | 指定 C++ 标准版本 | g++ -std=c++17 main.cpp |
-I <dir> | 添加头文件搜索路径 | g++ -I include main.cpp |
-L <dir> | 添加库文件路径 | g++ -L lib -lmylib main.cpp |
-D <macro> | 定义宏 | g++ -DDEBUG main.cpp |
扩展
-
交叉编译 (Cross Compilation):在一台机器上为另一架构(如 ARM、RISC-V)生成可执行程序。 示例:
riscv64-linux-gnu-g++ main.cpp -o main_rv -
静态链接 vs 动态链接:
- 静态链接 (
.a):在编译时嵌入库文件。 - 动态链接 (
.so/.dll):运行时动态加载库。
- 静态链接 (
C++ 注释(Comments)
1. 概述
注释用于解释程序逻辑、记录设计思路、说明接口功能或在调试时暂时屏蔽代码。 C++ 编译器在编译阶段会忽略注释内容,它们不会出现在目标文件中。 良好的注释能显著提升代码的可读性与可维护性。
2. 注释的基本类型
C++ 提供两种常见的注释形式:
2.1 单行注释(Single-line Comment)
以 // 开头,直到行尾结束。
// 输出欢迎信息
std::cout << "Hello, World!" << std::endl;
// 在行末添加说明
int count = 10; // 循环次数
适用于简短说明或临时调试。
2.2 多行注释(Multi-line Comment)
以 /* 开始,以 */ 结束,可以跨越多行。
/*
此部分用于初始化程序资源。
包括:
1. 配置加载
2. 内存分配
3. 日志系统启动
*/
initialize_system();
注意:多行注释 不支持嵌套,即不能在 /* ... */ 内再次使用 /* ... */。
3. 注释编写原则
- 准确:注释必须与代码逻辑一致。
- 必要:解释“为什么这样做”,而非“代码做了什么”。
- 简洁:避免冗长或与代码重复的描述。
- 及时:修改代码时应同步更新注释。
- 一致:保持统一的注释风格与格式。
示例:
// 不推荐:重复代码逻辑
int x = x + 1; // x 加 1
// 推荐:说明设计目的
// 自增以保持计数连续性
x++;
4. 文档化注释(Documentation Comments)
在较大的工程中,可以通过特殊格式的注释自动生成接口文档。 常用工具为 Doxygen,支持从源码中提取函数、类、参数等说明信息。
4.1 格式示例
/**
* @brief 计算两个整数的和
* @param a 第一个整数
* @param b 第二个整数
* @return 两数之和
*/
int add(int a, int b) {
return a + b;
}
或者:
/// 计算矩形面积
/// @param width 宽度
/// @param height 高度
/// @return 面积
double area(double width, double height);
4.2 常用标签
| 标签 | 含义 | 示例 |
|---|---|---|
@brief | 简要说明 | @brief 初始化系统资源 |
@param | 参数说明 | @param path 配置文件路径 |
@return | 返回值说明 | @return 初始化是否成功 |
@note | 备注说明 | @note 必须在主线程调用 |
@warning | 警告信息 | @warning 该函数非线程安全 |
@todo | 待办事项 | @todo 增加异常处理 |
5. 实际使用规范
在团队开发或课程项目中,建议采用以下注释习惯:
- 文件头部:说明文件用途、作者、日期。
- 函数前:使用文档注释描述功能、参数与返回值。
- 代码内部:仅在逻辑复杂处添加必要注释。
- 调试阶段:临时屏蔽代码时使用
//,不保留旧逻辑。
示例:
/**
* @file user_manager.cpp
* @brief 用户管理模块
* @author Ding
* @date 2025-10-13
*/
/// 初始化用户系统
/// @return 初始化是否成功
bool init_user_system() {
// TODO: 支持从配置文件加载默认用户
return true;
}
6. 调试与维护中的注释
-
调试时临时屏蔽代码
// 禁用日志输出 // std::cout << "Debug info" << std::endl; -
避免保留废弃代码
// 不推荐:应使用版本控制系统管理 // old_function();
命名规则 (Naming Conventions)
命名规则是代码风格的重要组成部分,它能显著提高代码的可读性、可维护性和团队协作效率。在 C++ 社区中,存在多种主流命名规范(如 Google Style、LLVM Style)。
核心原则
-
一致性 (Consistency): 在一个项目或代码库中,一旦选定了一种命名风格,就必须始终如一地使用它,避免混用多种风格导致的混乱。
-
描述性 (Descriptive): 名称应清晰地描述其所代表的实体的用途或含义,便于其他开发者理解代码。
-
避免歧义 (Avoid Ambiguity): 避免使用容易引起误解的缩写或模糊单词,例如
tmp、data等,必要时增加上下文信息。
常用命名风格
| 风格名称 | 描述 | 示例 |
|---|---|---|
| PascalCase / 大驼峰 | 每个单词首字母大写,不使用分隔符。通常用于类型名称。 | MyClassName |
| camelCase / 小驼峰 | 第一个单词首字母小写,其余单词首字母大写。通常用于变量和函数。 | calculateSum |
| snake_case / 下划线 | 所有字母小写,单词之间用下划线 _ 分隔。 | current_balance |
| UPPER_CASE / 全大写 | 所有字母大写,单词之间用下划线 _ 分隔。通常用于常量或宏。 | MAX_SIZE |
命名实体规范
| 实体类型 | 推荐风格 | 示例 | 备注 |
|---|---|---|---|
| 类 (Class) | PascalCase | Customer, FileManager | 类名通常是名词。 |
| 结构体 (Struct) | PascalCase | Point2D, StudentInfo | 与类一致,都是用户自定义类型。 |
| 函数/方法 (Function) | camelCase | calculateArea(), getFileSize() | 通常是动词或动宾短语,表示行为。 |
| 局部变量 (Local Variable) | camelCase | age, totalCount, isValid | 保持简洁、描述清晰。 |
| 成员变量 (Member Variable) | camelCase + 后缀或前缀 | m_value 或 value_ | 用于区分成员变量和局部变量,提高可读性。 |
| 常量 (Constants) | UPPER_CASE | MAX_ITEMS, PI_VALUE | 用于 #define 或 const/constexpr 编译期常量。 |
| 枚举 (Enums/Enum Class) | PascalCase | ColorType | 枚举类型使用 PascalCase。 |
| 枚举值 (Enum Values) | PascalCase 或 UPPER_CASE | Red, Green 或 COLOR_RED, COLOR_GREEN | 建议加类型前缀避免冲突。 |
| 命名空间 (Namespace) | snake_case 或 全小写 | my_project, network | 命名空间通常是项目名或模块名。 |
| 宏 (Macros) | UPPER_CASE | #define DEBUG_MODE | 宏是全局预处理符号,必须与变量/常量区分。 |
命名限制与最佳实践
1. C++ 保留标识符
C++ 标准保留了某些名称供编译器和标准库使用,用户代码禁止使用:
1.1 全局范围(Global Scope)保留
- 包含两个连续下划线
__的名称 示例:__myVar,My__Class - 以下划线
_开头,紧跟大写字母 示例:_PrivateMember,_Max_Value
1.2 全局命名空间(Global Namespace)保留
- 以下划线
_开头的名称 示例:_name,_a_local_var在局部作用域内可能安全,但全局范围内使用会违反标准。最佳实践:避免任何下划线开头的自定义名称。
1.3 关键字 (Keywords)
绝对禁止使用 C++ 关键字作为标识符:
| C++ 关键字(部分) | |||
|---|---|---|---|
alignas | decltype | if | return |
auto | do | inline | short |
bool | double | int | sizeof |
break | else | long | struct |
case | enum | namespace | switch |
char | extern | new | template |
class | float | private | this |
const | for | public | try |
continue | goto | register | void |
default | thread_local | virtual | while |
C++11/14/17/20 新增:
constexpr,noexcept,override,concept
2. 命名技巧与长度
-
避免单字母名称:除循环迭代器(
i, j, k)和数学变量(x, y, z)外,应使用描述性名称。差:
int s;好:int studentCount; -
避免匈牙利命名法:现代 C++ 不推荐通过前缀标注类型,如
iAge、szName。类型信息已由编译器和 IDE 提供。 -
布尔值命名:布尔变量或函数应以动词开头,清晰表达状态。 示例:
bool isValid;,bool hasError();,bool isFinished; -
Getter/Setter 命名:遵循约定提高可读性。
- Getter:
getValue()或value()(变量名value_) - Setter:
setValue(int newValue)或set_value(int newValue)
- Getter:
3. 文件命名规范
| 风格 | 头文件 (.h) | 源文件 (.cpp) | 备注 |
|---|---|---|---|
| PascalCase | FileManager.h | FileManager.cpp | 常用于 Qt 等 IDE/框架,与类名一致 |
| snake_case | file_manager.h | file_manager.cc | Google 风格推荐,更便于跨平台和工具链使用 |
头文件保护 (Header Guards):确保文件内容只被编译一次。推荐使用 全大写 + 项目/目录/文件名 的宏。
// network/tcp_socket.h
#ifndef MYPROJECT_NETWORK_TCP_SOCKET_H
#define MYPROJECT_NETWORK_TCP_SOCKET_H
// ... 代码 ...
#endif // MYPROJECT_NETWORK_TCP_SOCKET_H
示例代码
// 1. 类型 (PascalCase)
class EmployeeManager {
private:
// 2. 成员变量 (推荐使用后缀,避免保留标识符)
int employee_count_;
// 错误示范(应避免)
// int __bad_var; // 双下划线
// int _AnotherBadVar; // 下划线开头 + 大写
// int _safe_in_local_scope; // 全局范围不安全
public:
// 3. 函数/方法 (camelCase)
bool isValidEmployee(const std::string& name) {
// 4. 局部变量 (camelCase)
int maxHours = 40;
if (this->employee_count_ < maxHours) { // 使用 this-> 或后缀区分成员
return true;
}
return false;
}
};
// 5. 宏常量 (UPPER_CASE)
#define MAX_BUFFER_SIZE 1024
数据类型
在 C++ 中,数据类型(Data Types) 是程序设计的基础,它定义了变量在内存中的存储方式、可表示的取值范围以及能进行的操作。 理解数据类型不仅是掌握语法的关键,也是进行性能优化、内存管理和类型安全编程的前提。
C++ 的类型系统丰富而严格,既包含直接映射到硬件的基本类型(Fundamental Types),也支持由这些类型构建的派生类型(Derived Types),此外还提供了面向对象与泛型编程所需的抽象类型系统。
本章将系统介绍 C++ 中的数据类型体系结构,内容包括:
-
基本数据类型: 描述整型、浮点型、布尔型、空类型等语言内建类型及其修饰符(
signed、unsigned、long等)。 -
派生数据类型: 介绍数组、指针、引用、函数、结构体、类、枚举等由基本类型组合或扩展而来的类型。
-
字符串类型: 涵盖 C 风格字符串 (
char[]) 与 C++ 字符串类 (std::string、std::wstring) 的使用与区别。 -
类型转换: 探讨隐式与显式类型转换、C 风格转换与 C++ 四种安全转换(
static_cast、const_cast、reinterpret_cast、dynamic_cast)。 -
类型推导: 说明
auto、decltype、模板参数推导等机制如何简化类型声明并保持类型安全。
基本数据类型
C++ 基本数据类型
| 数据类型 | 描述 | 大小(字节) | 范围/取值示例 |
|---|---|---|---|
bool | 布尔类型,表示真或假 | 1 | true 或 false |
char | 字符类型,通常用于存储 ASCII 字符 | 1 | -128 到 127 或 0 到 255(取决于有符号或无符号) |
signed char | 有符号字符类型 | 1 | -128 到 127 |
unsigned char | 无符号字符类型 | 1 | 0 到 255 |
wchar_t | 宽字符类型,用于存储 Unicode 字符 | 2 或 4 | 取决于平台,通常 2 或 4 字节 |
char16_t | 16 位 Unicode 字符类型(C++11) | 2 | 0 到 65,535 |
char32_t | 32 位 Unicode 字符类型(C++11) | 4 | 0 到 4,294,967,295 |
short | 短整型 | 2 | -32,768 到 32,767 |
unsigned short | 无符号短整型 | 2 | 0 到 65,535 |
int | 整型 | 4 | -2,147,483,648 到 2,147,483,647 |
unsigned int | 无符号整型 | 4 | 0 到 4,294,967,295 |
long | 长整型 | 4 或 8 | 取决于平台 |
unsigned long | 无符号长整型 | 4 或 8 | 取决于平台 |
long long | 长长整型(C++11) | 8 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
unsigned long long | 无符号长长整型(C++11) | 8 | 0 到 18,446,744,073,709,551,615 |
float | 单精度浮点数 | 4 | 约 ±3.4e±38(6-7 位有效数字) |
double | 双精度浮点数 | 8 | 约 ±1.7e±308(15 位有效数字) |
long double | 扩展精度浮点数 | 8、12 或 16 | 取决于平台 |
C++ 修饰符
| 修饰符 | 描述 | 示例 |
|---|---|---|
signed | 有符号类型(默认) | signed int x = -10; |
unsigned | 无符号类型 | unsigned int y = 10; |
short | 短整型 | short int z = 100; |
long | 长整型 | long int a = 100000; |
static | 静态存储期,或内部链接,或类级别共享 | static int count = 0; |
const | 常量,值不可修改 | const int b = 5; |
constexpr | 编译期常量,值在编译时计算,可用于常量表达式和元编程 | constexpr int size = 10; |
volatile | 变量可能被意外修改,禁止编译器优化 | volatile int c = 10; |
mutable | 类成员可以在 const 对象中修改 | mutable int counter; |
extern | 声明一个在其他源文件中定义的变量或函数,用于跨文件共享全局符号 | extern int global_var; |
register | 建议编译器将变量存放在 CPU 寄存器中以提高访问速度(现代编译器多已自动优化) | register int counter = 0; |
C++11 新增数据类型
| 数据类型 | 描述 | 示例 |
|---|---|---|
auto | 自动类型推断 | auto x = 10; |
decltype | 获取表达式的类型 | decltype(x) y = 20; |
nullptr | 空指针常量 | int* ptr = nullptr; |
std::initializer_list | 初始化列表类型 | std::initializer_list<int> list = {1, 2, 3}; |
std::tuple | 元组类型,可以存储多个不同类型的值 | std::tuple<int, float, char> t(1, 2.0, 'a'); |
说明
宽字符类型 (wchar_t)
wchar_t 是 C++ 中用于存储宽字符的类型,广泛应用于需要处理 Unicode 字符集的程序中。与普通的 char 类型(通常用于存储 ASCII 字符)不同,wchar_t 的设计目的是为了支持更大的字符集,特别是 Unicode。由于 wchar_t 需要存储更多的字符信息,因此其大小取决于平台,通常在 2 或 4 字节之间。在一些平台上,wchar_t 被定义为 2 字节(16 位),在其他平台上则可能是 4 字节(32 位)。使用 wchar_t 可以轻松处理如中文、日文等非拉丁字符。
#include <iostream>
int main() {
wchar_t wide_char = L'我'; // 使用 wchar_t 存储一个 Unicode 字符
std::wcout << wide_char << std::endl; // 输出:我
return 0;
}
char16_t 和 char32_t
char16_t 和 char32_t 是 C++11 引入的专门用于存储 Unicode 字符的类型,分别表示 16 位和 32 位字符类型。char16_t 是为了支持 UTF-16 编码而设计的,而 char32_t 是为了支持 UTF-32 编码。char16_t 用 2 字节来存储一个字符,而 char32_t 用 4 字节存储一个字符。这两种类型能够直接表示 Unicode 字符,而无需进行额外的编码转换。
char16_t 和 char32_t 提供了对更广泛字符集的支持,尤其适合那些需要处理全球化文本的应用程序。char16_t 和 char32_t 作为 Unicode 字符的表示方式,分别与 UTF-16 和 UTF-32 编码兼容,能够表示包括基本多语言平面(BMP)以及更高平面字符在内的所有 Unicode 字符。
#include <iostream>
int main() {
char16_t char16 = u'你'; // 使用 char16_t 存储一个 UTF-16 编码的字符
char32_t char32 = U'你'; // 使用 char32_t 存储一个 UTF-32 编码的字符
std::wcout << "char16_t: " << char16 << std::endl;
std::wcout << "char32_t: " << char32 << std::endl;
return 0;
}
volatile
在现代 CPU 中通常包含多个核心,每个核心都有独立的缓存。多个核心可能同时缓存了同一段主存的数据。一般情况下,缓存和主存的数据是一致的,但在多线程并发场景下,由于缓存写回策略的影响,数据的修改可能无法及时同步到主存,从而导致数据不一致的问题。
volatile 关键字用于告诉编译器:某个变量的值可能随时被外部因素(如其他线程、硬件设备或中断)修改,因此缓存中的值并不可靠。出于这个原因,编译器在访问该变量时不会进行过度优化,而是强制每次都从内存中读取最新的值。
-
可见性
volatile保证变量的值在不同线程或不同硬件环境下始终是“最新可见”的。即使某个核心或寄存器中有缓存数据,访问volatile变量时也必须直接从内存或硬件中获取,而不是使用缓存副本。 -
不可优化 在编译阶段,为了提高执行效率,编译器会进行多种优化。例如:
int flag = 0; while (flag == 0) { // 等待 flag 改变 }若
flag未被声明为volatile,编译器可能认为flag始终等于 0,于是直接将循环优化为while(false),导致死循环。使用
volatile可以禁止类似的优化,包括 消除优化、传播优化 和 合并优化,从而保证变量的读写行为不会被错误简化。 -
顺序性
volatile还会在一定程度上影响指令的顺序。编译器在处理volatile变量时,会保证读写操作不会因乱序优化而颠倒,从而维持必要的执行顺序。
volatile 在嵌入式编程和多线程编程中尤为重要:
- 多线程共享变量:确保不同线程读取到的值保持一致。
- 中断处理:中断可能随时修改某个变量,主程序通过
volatile保证能正确检测到变化。 - 硬件寄存器访问:在嵌入式系统中,硬件寄存器的值可能在后台自动更新,必须通过
volatile确保每次访问都直接读取硬件寄存器的当前值。
尽管 volatile 在保证可见性、防止编译器优化、维持一定顺序性方面很有用,但它并不是并发编程的“万能钥匙”,主要局限性如下:
-
不保证原子性
-
volatile仅保证读写操作不会被优化和缓存,但不能保证复合操作的原子性。 -
例如:
volatile int counter = 0; counter++; // 实际分解为:读取 -> 修改 -> 写回在多线程环境下可能发生竞态条件,导致结果错误。
-
-
不等同于内存屏障(Memory Barrier)
volatile的“顺序性”只作用于编译器层面,防止指令在编译时被重排。- 但在 CPU 的指令执行层面,仍然可能发生硬件乱序执行。若需要在多线程同步中严格保证内存访问顺序,还需要使用更强的同步原语(如 C++ 中的
std::atomic或内存屏障指令)。
-
性能开销
- 每次访问
volatile变量都要从内存中读取最新值,无法使用寄存器缓存,可能造成一定性能损失。
- 每次访问
-
局限于特定场景
-
volatile更适合用于:- 标志位(如中断标志、任务完成标志)。
- 硬件寄存器访问。
-
但在复杂的多线程共享数据同步场景下,仅依赖
volatile是不够的,往往需要互斥锁、原子操作或更高级的同步机制。
-
mutable
mutable 关键字是用来修饰类的成员变量的,意味着即使该对象是常量(const),这些成员变量也可以被修改。通常,mutable 用于那些希望在 const 方法中进行修改的成员变量,比如用于缓存的成员变量。通过 mutable,我们可以在 const 方法中修改这些成员,而不会破坏 const 对象的常量性。
以下示例展示了在 const 方法中修改 mutable 成员变量的情况:
class MyClass {
public:
mutable int cache; // 使用 mutable 修饰的成员变量
MyClass() : cache(0) {}
void updateCache() const {
// 即使 updateCache 是 const 方法,cache 依然可以被修改
cache++;
}
};
int main() {
const MyClass obj;
obj.updateCache(); // 可在 const 对象上调用
std::cout << obj.cache << std::endl; // 输出:1
return 0;
}
decltype
decltype 是 C++11 引入的一个关键字,用于获取表达式的类型,而不需要显式地声明变量的类型。它通常用于模板编程中,或者当我们不确定某个表达式的类型时。decltype 可以非常方便地获取复杂类型,尤其是当类型通过复杂的表达式推导出来时。
例如,以下代码通过 decltype 获取了变量 x 的类型,并且通过 auto 使得代码更加简洁:
int x = 5;
decltype(x) y = 10; // y 的类型是 int,因为 x 是 int
auto z = x + y; // z 的类型由编译器推断,类型为 int
std::initializer_list
std::initializer_list 是 C++11 引入的一个模板类,用于支持初始化列表。它允许在创建对象时通过花括号 {} 来传递多个值。initializer_list 主要用于支持类的构造函数接收不定数量的参数,或者将多个值传递给函数,特别适用于那些需要接收多个初始值的容器类型。
#include <initializer_list>
#include <iostream>
void printList(std::initializer_list<int> list) {
for (auto i : list) {
std::cout << i << " ";
}
std::cout << std::endl;
}
int main() {
printList({1, 2, 3, 4, 5}); // 使用 initializer_list
return 0;
}
std::tuple
std::tuple 是 C++11 引入的一个模板类,允许存储多个不同类型的元素。与数组和 std::vector 不同,tuple 允许每个元素拥有不同的类型。std::tuple 是一个非常强大的工具,可以将多个不同类型的值打包在一起,并在需要时访问这些值。它常用于函数返回多个不同类型的值,或在需要将多种类型的参数组合起来时使用。
#include <tuple>
#include <iostream>
int main() {
std::tuple<int, double, char> t(1, 3.14, 'A');
std::cout << std::get<0>(t) << ", "; // 1
std::cout << std::get<1>(t) << ", "; // 3.14
std::cout << std::get<2>(t) << std::endl; // A
// 修改 tuple 的元素
std::get<0>(t) = 42;
std::cout << std::get<0>(t) << std::endl; // 42
return 0;
}
static
static 关键字在 C++ 中有三种主要用途,取决于它所修饰的对象和作用域,而在C语言中由于不支持类从而只支持修饰局部静态变量和外部静态变量、函数。
1. 局部变量(函数内)- 改变存储期
当 static 用于函数内的局部变量时,它改变了变量的存储期(Storage Duration)。
- 存储期:
static局部变量在程序运行期间只会被初始化一次,且生命周期与整个程序相同(静态存储期),但其作用域仍限定在定义它的函数内部。 - 用途:用于记录函数被调用的次数,或者在多次调用中保持某个状态。
void func() {
static int count = 0; // 只在程序启动时初始化一次
count++;
std::cout << "Count: " << count << std::endl;
}
// 每次调用 func(),count 都会递增,而不是重置为 0
2. 全局变量和函数(文件作用域)- 改变链接性
当 static 用于全局变量或普通函数时,它改变了它们的链接性(Linkage)。
- 链接性:将默认的外部链接(External Linkage,可以在其他源文件访问)改为内部链接(Internal Linkage)。
- 用途:使变量或函数只在其定义的**当前翻译单元(源文件)**中可见和可用,避免与其他源文件中的同名标识符发生冲突。
// file1.cpp
static int global_data = 10; // 只能在 file1.cpp 中访问
static void helper_func() { // 只能在 file1.cpp 中调用
// ...
}
3. 类成员(成员变量和成员函数)- 类级别共享
当 static 用于类内部的成员时,它使成员成为类级别的共享资源,而不是每个对象独有的资源。
- 静态成员变量:
- 该变量为所有类的对象所共享,只存在一个副本。
- 它必须在类外部进行定义和初始化(除非是
const static整数类型)。 - 可以通过类名或对象访问。
- 静态成员函数:
- 它不依赖于任何特定的类对象。
- 它不能直接访问非静态的成员变量或成员函数(因为它没有
this指针)。 - 通常用于访问和操作静态成员变量,或作为工具函数。
class MyClass {
public:
static int object_count; // 静态成员变量声明
MyClass() {
object_count++;
}
static int get_count() { // 静态成员函数
return object_count;
}
};
// 在类外定义和初始化静态成员
int MyClass::object_count = 0;
int main() {
MyClass obj1;
MyClass obj2;
// 使用类名直接访问静态成员
std::cout << MyClass::get_count() << std::endl; // 输出:2
return 0;
}
auto
auto 是 C++11 引入的一个关键字,允许编译器自动推导出变量的类型。auto 使得代码更加简洁,并且减少了显式指定类型的需求,尤其在处理复杂类型时非常有用。auto 通常用于变量声明时,让编译器根据赋值的表达式自动推断类型,这在迭代器和模板编程中尤其常见。
int main() {
auto x = 10; // x 的类型是 int
auto y = 3.14; // y 的类型是 double
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
const
在 C 语言和 C++ 中,const 都用于限定变量为“只读”。
但是 C++ 对 const 的支持更为强大和灵活,它不仅影响编译器的检查机制,还会在类型系统中起作用。
-
基本特性
-
在 C 中,
const修饰的变量默认是只读存储(readonly),但本质上仍然是变量,而不是常量。const int x = 10; int *p = (int*)&x; // 通过强制转换依然能修改 *p = 20; // UB(未定义行为) -
在 C++ 中,
const更严格,编译器会将其视为类型的一部分。const int x = 10; x = 20; // 编译错误,禁止修改
-
-
修饰位置
const可以修饰不同对象,表达不同含义:-
修饰变量:值不可修改。
-
修饰指针:
const int *p; // 指向常量的指针(*p 不可改,p 可改) int *const p; // 常量指针(p 不可改,*p 可改) const int *const p; // 指向常量的常量指针 -
修饰函数参数:保证函数体内不会修改该参数。
-
修饰成员函数:表示该成员函数不会修改对象的成员变量,本质上是修饰this指针。
-
-
作用域与链接
- 在 C 中,
const全局变量默认是 外部链接,除非显式加static使得在本文件可见。 - 在 C++ 中,
const全局变量默认是 内部链接(只在本翻译单元内可见),若要在多个文件共享,需加extern。
- 在 C 中,
-
局限性
const并不保证编译期求值,它仅仅保证“运行时不能被修改”。- 如果需要编译期常量(如数组大小、模板参数),在 C++11 之前通常使用
#define或enum hack。
constexpr
constexpr 是 C++11 引入的关键字,用于声明“编译期常量表达式”。它不仅意味着值不可变,更重要的是:
编译器必须在编译期对其进行求值(只要表达式满足常量表达式要求)。
-
基本特性
-
constexpr变量一定是常量,并且能在编译期被计算:constexpr int size = 10; int arr[size]; // 合法 -
与
const不同,constexpr要求初始化表达式必须是编译期可计算的常量。
-
-
函数支持
-
constexpr还可以修饰函数:constexpr int square(int x) { return x * x; } int arr[square(5)]; // 在编译期计算为 25 -
这样的函数可以在编译期使用,也可以在运行时调用(若传入非常量参数)。
-
-
类与构造函数
- C++11 起,
constexpr可以修饰构造函数,表示该类的对象可以在编译期生成常量。 - C++14/17 对
constexpr的限制逐步放宽,例如允许有分支、循环,更接近普通函数。
- C++11 起,
-
区别于
constconst:运行期常量(只读),初始化表达式可以是运行时值,可以使用const_cast去除限定。constexpr:编译期常量,初始化表达式必须在编译期可求值。
对比示例:
const int a = std::time(nullptr); // 合法,运行期决定 constexpr int b = std::time(nullptr); // 错误,不能在编译期求值
extern
extern 关键字用于声明而非定义变量或函数。被extern标识的变量或者函数声明其定义在别的文件中,提示编译器遇到此变量和函数时在其他模块寻找其定义。
它出现在多文件项目中,用于在一个文件中访问另一个文件定义的全局变量或函数。
-
作用:
- 告诉编译器“该变量或函数在别处定义”;
- 不会为其分配存储空间(除非在定义处);
- 避免重复定义全局符号。
// file1.cpp
int count = 10; // 定义全局变量
// file2.cpp
#include <iostream>
extern int count; // 声明外部变量(非定义)
int main() {
std::cout << count << std::endl; // 输出 10
return 0;
}
-
在C++中:
C++ 默认情况下,
const全局变量具有内部链接(internal linkage),也就是仅在本文件内可见。 若希望跨文件共享一个常量变量,必须结合extern使用:// file1.cpp extern const int BUFFER_SIZE = 1024; // file2.cpp extern const int BUFFER_SIZE; // 声明外部常量 -
与函数结合使用:
对于函数来说,
extern是默认属性,即所有非static函数都具有外部链接性,因此通常可省略:extern void foo(); // 与 void foo(); 等价
register
register 是早期 C/C++ 时代用于提升变量访问速度的关键字,提示编译器将变量存储在 CPU 寄存器中,而非内存中。
-
特点:
- 变量可能被存放在 CPU 寄存器中,而非内存;
- 不能对
register变量使用取地址操作符&; - 仅能修饰局部变量或函数参数;
- 在现代编译器中通常被自动优化机制取代,因此多用于教学或历史理解。
#include <iostream>
int main() {
register int i; // 建议编译器将 i 放入寄存器中
for (i = 0; i < 5; ++i) {
std::cout << i << " ";
}
std::cout << std::endl;
return 0;
}
-
局限性:
- 编译器不保证一定会将其放入寄存器;
- 不能取地址(即
&i是非法的); - 在现代 C++ 中几乎没有实际性能提升,优化器会自动选择合适的寄存器分配策略。
派生数据类型
C++派生数据类型
在 C++ 中,派生数据类型(Derived Types) 是由基本数据类型或其他派生类型构建而来的类型。 它们扩展了语言的表达能力,使得开发者能够通过组合与抽象创建更复杂的数据结构与行为模型。
派生类型包括数组、指针、引用、函数、结构体、类、联合体以及枚举类型等。
| 数据类型 | 描述 | 示例 |
|---|---|---|
| 数组 | 相同类型元素的集合 | int arr[5] = {1, 2, 3, 4, 5}; |
| 指针 | 存储变量内存地址的类型 | int* ptr = &x; |
| 引用 | 变量的别名 | int& ref = x; |
| 函数 | 函数类型,表示函数的签名 | int func(int a, int b); |
| 结构体 | 用户定义的数据类型,可以包含多个不同类型的成员 | struct Point { int x; int y; }; |
| 类 | 用户定义的数据类型,支持封装、继承和多态 | class MyClass { ... }; |
| 联合体 | 多个成员共享同一块内存 | union Data { int i; float f; }; |
| 枚举 | 用户定义的整数常量集合 | enum Color { RED, GREEN, BLUE }; |
数组(Array)
数组用于存储相同类型元素的固定长度序列,在内存中是连续存放的。
int numbers[5] = {1, 2, 3, 4, 5};
std::cout << numbers[2]; // 输出 3
-
下标从 0 开始,越界访问会导致未定义行为(Undefined Behavior)。
-
在函数参数中,数组会退化为指针:
void printArray(int arr[], int size); // 等价于 void printArray(int* arr, int size); -
若需要安全的动态数组,请使用
std::vector。
指针(Pointer)
指针是存储变量地址的变量,是 C++ 的核心特性之一。
int a = 10;
int* p = &a; // 指针p指向a
std::cout << *p; // 输出10
关键点:
*用于定义或解引用指针;&用于取地址;- 指针类型必须与目标对象类型一致;
nullptr表示空指针(C++11 引入)。
常见错误:
-
解引用空指针或野指针会导致崩溃;
-
动态内存需成对使用:
int* p = new int(5); delete p;
推荐使用智能指针(std::unique_ptr, std::shared_ptr)来避免内存泄漏。
引用(Reference)
引用是另一个变量的别名,必须在定义时初始化。
int value = 10;
int& ref = value;
ref = 20; // value 也变为 20
-
引用不能为空;
-
绑定后不能再更改引用目标;
-
常用于函数参数传递以避免拷贝:
void modify(int& x) { x *= 2; }
C++11 还引入了 右值引用 (T&&),用于支持移动语义和完美转发。
函数类型(Function Type)
函数本身也是一种类型,其类型由返回值类型和参数类型列表组成。
int add(int a, int b) { return a + b; }
函数类型也可通过指针或引用使用:
int (*funcPtr)(int, int) = add;
std::cout << funcPtr(2, 3); // 输出5
C++11 提供了更安全的函数封装:
std::function:可存储任意可调用对象;auto和 Lambda 表达式使函数类型推导更简洁。
结构体(struct)
结构体是用户自定义的复合类型,可包含多个不同类型的成员。
struct Point {
int x;
int y;
};
Point p1 = {10, 20};
std::cout << p1.x << ", " << p1.y;
- 默认成员访问权限是 public;
- 可以包含成员函数;
- 可以与类(
class)结合使用面向对象设计。
类(class)
类是 C++ 的核心概念之一,支持 封装(Encapsulation)、继承(Inheritance) 和 多态(Polymorphism)。
class Rectangle {
private:
int width, height;
public:
Rectangle(int w, int h) : width(w), height(h) {}
int area() const { return width * height; }
};
- 成员默认为 private;
- 支持构造函数、析构函数、运算符重载等;
- 是对象与抽象数据类型的基础。
类与对象是C++语言极为重要的内容。
联合体(union)
联合体(union)允许多个成员共用同一块内存。
union Data {
int i;
float f;
char c;
};
Data d;
d.i = 10;
std::cout << d.i; // 输出10
d.f = 3.14; // i 被覆盖
- 占用的内存大小等于最大成员的大小;
- 只能同时存储一个有效成员;
- 可用于节省内存或实现类型复用。
枚举类型(enum)
枚举用于定义一组具名的整数常量,提高代码可读性与类型安全。
创建枚举类型时,使用关键字 enum。其一般形式为:
enum 枚举名 {
标识符[=整型常数],
标识符[=整型常数],
...
标识符[=整型常数]
} 枚举变量;
例如,下面的代码定义了一个颜色枚举,变量 c 的类型为 color。最后,c 被赋值为 blue:
enum color { red, green, blue } c;
c = blue;
默认情况下,第一个名称的值为 0,第二个名称的值为 1,第三个名称的值为 2,以此类推。然而,您也可以给名称赋予一个特殊的值,只需要添加一个初始值即可。例如,在下面的枚举中,green 的值为 5:
enum color { red, green=5, blue };
在此示例中,blue 的值为 6,因为默认情况下,每个名称都会比它前面一个名称大 1,而 red 的值仍然为 0。
C++11 引入 强类型枚举:
enum class Status { OK, ERROR };
Status s = Status::OK;
强类型枚举不会隐式转换为整数,作用域也更加安全。
C++类型别名
类型别名可通过 typedef 或 using 定义。
| 关键字 | 描述 | 示例 |
|---|---|---|
typedef | 为已有类型定义别名 | typedef int MyInt; |
using | C++11 引入的新语法 | using MyInt = int; |
示例:
typedef unsigned long ulong_t;
using ushort_t = unsigned short;
using 语法更直观,且支持模板别名:
template <typename T>
using Vec = std::vector<T>;
Vec<int> v = {1, 2, 3};
typedef和define有什么区别?
typedef和define有什么区别?
typedef 和 #define 虽然在表面上都可以用于“简化代码书写”,但它们的本质、用途以及生效阶段存在明显区别。
(1) 用法不同:
typedef 用于为已有的数据类型定义一个新的类型别名,从而增强程序的可读性和可维护性。例如,可以通过 typedef unsigned int uint; 为 unsigned int 定义一个更简洁的别名。
而 #define 是一种宏定义指令,主要用于定义常量或书写复杂但使用频繁的宏表达式,如 #define PI 3.14159 或 #define MAX(a, b) ((a) > (b) ? (a) : (b))。
前者作用于类型层面,后者仅进行文本替换。
(2) 执行时间不同:
typedef 属于编译阶段的一部分,编译器在处理类型定义时会进行语法与类型检查,确保类型合法。
而 #define 是在编译前的预处理阶段执行的,它仅做简单的字符串替换,并不参与类型检查,因此如果使用不当,容易造成潜在的逻辑错误。
(3) 作用域不同:
typedef 受 C/C++ 作用域规则限制,只在其定义的作用域内有效,例如在函数内定义的类型别名在函数外无法使用。
相对地,#define 不受作用域约束,只要在定义之后且未被 #undef 取消,其宏名在整个文件(甚至被包含的其他文件)中都有效,因此若管理不当,可能造成命名冲突。
(4) 对指针的操作不同:
typedef 和 #define 在定义指针类型时的行为存在显著区别。
例如:
#define PINT1 int*
typedef int* PINT2;
PINT1 a, b; // 等价于 int* a, b; => b是int类型
PINT2 c, d; // 等价于 int* c, *d; => c和d都是int*类型
这表明:#define 仅仅进行文本替换,而 typedef 定义的是一种完整的类型。使用 typedef 可以避免宏替换带来的歧义。
(5) 语法要求不同:
typedef 是一条语句,因此必须以分号结尾;
#define 是预处理指令,不属于语句,不能加分号,否则分号也会被替换到目标文本中,引发错误。
C++标准库类型
| 数据类型 | 描述 | 示例 |
|---|---|---|
std::string | 字符串类型 | std::string s = "Hello"; |
std::vector | 动态数组 | std::vector<int> v = {1, 2, 3}; |
std::array | 固定大小数组(C++11 引入) | std::array<int, 3> a = {1, 2, 3}; |
std::pair | 存储两个值的容器 | std::pair<int, float> p(1, 2.0); |
std::map | 键值对容器 | std::map<int, std::string> m; |
std::set | 唯一值集合 | std::set<int> s = {1, 2, 3}; |
C++ 字符串类型
C++ 提供了两种主要的字符串表示方式:C 风格字符串和 C++ 引入的 string 类类型。尽管 C++ 中 string 类型在许多情况下更为常用,但 C 风格字符串仍然是 C++ 中的一个重要组成部分,特别是在需要与旧有的 C 库代码兼容时。
C 风格字符串
C 风格的字符串源自 C 语言,并在 C++ 中继续得到支持。这种字符串表示方式实际上是一个以 null 字符 \0 结尾的字符数组。C 风格字符串的本质是一个一维字符数组,末尾的 null 字符标识了字符串的结束。
C 风格字符串的定义与初始化
您可以通过以下方式定义并初始化一个 C 风格字符串:
char site[6] = {'H', 'E', 'L', 'L', 'O', '\0'};
也可以使用更简便的方式进行初始化:
char site[] = "HELLO";
在这种情况下,C++ 编译器会自动在字符串的末尾添加 null 字符 \0,无需显式地写出。
#include <iostream>
using namespace std;
int main() {
char s[] = "HELLO"; // 字符串自动以 '\0' 结束
cout << s << endl;
return 0;
}
输出:
HELLO
C 风格字符串的常用操作函数
C 风格字符串有许多函数可以进行常见的操作。以下是常用的几个字符串操作函数:
| 函数 | 目的 |
|---|---|
strcpy(s1, s2) | 复制字符串 s2 到字符串 s1。 |
strcat(s1, s2) | 将字符串 s2 连接到字符串 s1 的末尾。 |
strlen(s1) | 返回字符串 s1 的长度(不包括 \0)。 |
strcmp(s1, s2) | 比较两个字符串 s1 和 s2,返回值为 0 时相同,负值表示 s1 < s2,正值表示 s1 > s2。 |
strchr(s1, ch) | 返回指针,指向字符串 s1 中字符 ch 的首次出现位置。 |
strstr(s1, s2) | 返回指针,指向字符串 s1 中子字符串 s2 的首次出现位置。 |
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char str1[13] = "hello";
char str2[13] = "world";
char str3[13];
int len;
// 复制 str1 到 str3
strcpy(str3, str1);
cout << "strcpy(str3, str1): " << str3 << endl;
// 连接 str1 和 str2
strcat(str1, str2);
cout << "strcat(str1, str2): " << str1 << endl;
// 计算连接后的长度
len = strlen(str1);
cout << "strlen(str1): " << len << endl;
return 0;
}
输出:
strcpy(str3, str1): hello
strcat(str1, str2): helloworld
strlen(str1): 10
C++ 中的 string 类
C++ 标准库提供了 string 类,它是 C 风格字符串的现代替代品。string 类提供了丰富的成员函数,支持字符串的动态操作,简化了字符串处理过程,并且避免了 C 风格字符串的许多潜在问题。
string 类基本操作
C++ 中的 string 类提供了方便的字符串处理方法,使得字符串操作变得更加高效和直观。以下是一些常见的操作。
#include <iostream>
#include <string>
using namespace std;
int main() {
string str1 = "hello";
string str2 = "world";
string str3;
int len;
// 复制 str1 到 str3
str3 = str1;
cout << "str3: " << str3 << endl;
// 连接 str1 和 str2
str3 = str1 + str2;
cout << "str1 + str2: " << str3 << endl;
// 计算连接后的总长度
len = str3.size();
cout << "str3.size(): " << len << endl;
return 0;
}
输出:
str3: hello
str1 + str2: helloworld
str3.size(): 10
string 类的常用成员函数
string 类提供了多种函数来执行常见的字符串操作,常见的成员函数包括:
append(str):将str追加到当前字符串末尾。insert(pos, str):在位置pos处插入字符串str。erase(pos, len):从位置pos开始,删除len个字符。substr(pos, len):返回从位置pos开始的长度为len的子字符串。find(str):返回子字符串str在当前字符串中首次出现的位置。replace(pos, len, str):将当前位置pos开始的len个字符替换为字符串str。
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "Hello, world!";
// 获取子字符串
string sub = str.substr(7, 5); // 从位置 7 开始,获取 5 个字符
cout << "Substring: " << sub << endl;
// 查找字符 'w' 在字符串中的位置
size_t pos = str.find('w');
cout << "'w' found at: " << pos << endl;
// 替换子字符串
str.replace(7, 5, "C++");
cout << "After replace: " << str << endl;
return 0;
}
输出:
Substring: world
'w' found at: 7
After replace: Hello, C++!
类型转换
类型转换是将一个数据类型的值转换为另一种数据类型的值。在 C++ 中,类型转换主要有四种方式:静态转换、动态转换、常量转换和重新解释转换。每种转换方式有其特定的应用场景和注意事项。
静态转换(Static Cast)
静态转换用于将一个数据类型的值强制转换为另一种类型,通常适用于类型之间存在一定的相似性。例如,可以将 int 类型转换为 float 类型。静态转换并不会进行运行时的类型检查,因此在某些情况下,它可能会导致运行时错误,尤其是在转换较为复杂的对象时。
例如:
int i = 10;
float f = static_cast<float>(i); // 将 int 转换为 float
动态转换(Dynamic Cast)
动态转换是 C++ 中用于在继承体系中进行类型转换的方式。它通常用于将基类指针或引用转换为派生类指针或引用,特别是在涉及多态的场景中。与静态转换不同,动态转换会在运行时检查转换是否合法,确保程序的类型安全。如果转换失败,对于指针类型,它会返回 nullptr,而对于引用类型,则会抛出 std::bad_cast 异常。
例如:
#include <iostream>
class Base {
public:
virtual ~Base() = default; // 基类必须有虚函数
};
class Derived : public Base {
public:
void show() {
std::cout << "Derived class method" << std::endl;
}
};
int main() {
Base* ptr_base = new Derived; // 基类指针指向派生类对象
// 将基类指针转换为派生类指针
Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base);
if (ptr_derived) {
ptr_derived->show(); // 成功转换,调用派生类方法
} else {
std::cout << "Dynamic cast failed!" << std::endl;
}
delete ptr_base;
return 0;
}
输出:
Derived class method
在转换引用时,也可以使用 dynamic_cast,不过如果转换失败,它会抛出异常:
#include <iostream>
#include <typeinfo>
class Base {
public:
virtual ~Base() = default;
};
class Derived : public Base {
public:
void show() {
std::cout << "Derived class method" << std::endl;
}
};
int main() {
Derived derived_obj;
Base& ref_base = derived_obj; // 基类引用绑定到派生类对象
try {
Derived& ref_derived = dynamic_cast<Derived&>(ref_base);
ref_derived.show(); // 成功转换,调用派生类方法
} catch (const std::bad_cast& e) {
std::cout << "Dynamic cast failed: " << e.what() << std::endl;
}
return 0;
}
输出:
Derived class method
动态转换提供了更高的安全性,确保在程序运行时可以正确地进行类型检查。尽管如此,它相对于静态转换而言,性能开销较高,因为需要进行类型验证。
常量转换(Const Cast)
常量转换(const_cast)用于去除对象的 const 限定符,从而使得一个 const 对象能够被修改。它并不会改变对象的实际类型,只是去掉了 const 属性。这种转换通常用于需要修改原本被声明为常量的对象时,尽管这种做法应谨慎使用。
例如:
const int i = 10;
int& r = const_cast<int&>(i); // 去除 const 限定符
此代码将 const int 转换为普通的 int,使得对 i 的修改变得可能。但请注意,修改一个原本是常量的对象可能导致未定义的行为。
重新解释转换(Reinterpret Cast)
重新解释转换(reinterpret_cast)允许将一个数据类型的值重新解释为另一种数据类型的值。它通常用于在指针类型之间进行转换,不会进行任何类型检查,因此可能会导致未定义的行为。这种转换应谨慎使用,特别是在不同类型之间进行转换时,必须确保数据在内存中的表示是兼容的。
例如:
int i = 10;
float f = reinterpret_cast<float&>(i); // 将 int 转换为 float
这段代码通过 reinterpret_cast 将 int 类型的数据重新解释为 float 类型。然而,重新解释转换的使用非常危险,因为它不会检查类型是否真正兼容。如果目标类型与原类型不兼容,可能会导致程序崩溃或者数据损坏。因此,reinterpret_cast 应该仅在非常确定数据类型兼容的情况下使用。
自动类型推导
在 C++ 中,自动类型推导(type inference)允许编译器根据初始化表达式推断变量的类型,从而减少显式指定类型的需求。C++11 引入了 auto 关键字,它使得变量声明更加简洁和灵活,同时也有助于避免类型错误。
auto 关键字的基本使用
auto 关键字让编译器根据初始化表达式推导出变量的类型。在声明变量时,使用 auto 代替类型名称,编译器将自动推断类型。
#include <iostream>
int main() {
auto x = 42; // 自动推导类型为 int
auto y = 3.14; // 自动推导类型为 double
std::cout << "x: " << x << ", y: " << y << std::endl;
return 0;
}
在这个例子中,auto 自动推导出 x 的类型为 int,y 的类型为 double,根据其初始化值。无需显式地指定类型。
输出:
x: 42, y: 3.14
auto 与容器
auto 在处理容器时非常有用,尤其是当容器类型复杂且冗长时,使用 auto 可以简化代码并提高可读性。C++11 引入了范围-based for 循环,配合 auto 使用可以轻松地遍历容器。
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用 auto 遍历容器
for (auto& v : vec) {
std::cout << v << " ";
}
std::cout << std::endl;
return 0;
}
在此示例中,auto 用来自动推导容器元素的类型。由于我们希望修改容器中的元素,所以使用了 auto& 来获取元素的引用。
输出:
1 2 3 4 5
auto 和指针
在使用指针时,auto 也可以自动推导出指针的类型。注意,auto 仅推导出变量本身的类型,而不推导出指针指向的类型。
#include <iostream>
int main() {
int num = 100;
int* ptr = #
// 使用 auto 推导指针类型
auto p = ptr; // p 将是 int* 类型
std::cout << "p points to: " << *p << std::endl;
return 0;
}
在这个例子中,auto 推导出变量 p 的类型为 int*,与原始指针 ptr 类型相同。
输出:
p points to: 100
auto 与函数返回类型
auto 不仅可以用于变量声明,还可以用于函数的返回类型。当返回类型较为复杂或需要根据返回的表达式推导时,auto 可以简化代码。
#include <iostream>
#include <vector>
auto get_sum(const std::vector<int>& vec) {
int sum = 0;
for (auto v : vec) {
sum += v;
}
return sum; // 返回类型由 auto 自动推导
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::cout << "Sum: " << get_sum(vec) << std::endl;
return 0;
}
在这个示例中,auto 用于函数 get_sum 的返回类型,编译器根据 sum 变量的类型推导出返回值的类型。
输出:
Sum: 15
auto 与模板
auto 也可以与模板结合使用,尤其是在模板函数中,允许推导类型从而使得函数更加灵活。使用 auto 可以避免显式指定模板参数,从而简化模板代码。
#include <iostream>
template <typename T>
auto add(T a, T b) {
return a + b; // 返回类型自动推导
}
int main() {
std::cout << add(1, 2) << std::endl; // 输出 3
std::cout << add(1.5, 2.5) << std::endl; // 输出 4.0
return 0;
}
在这个示例中,auto 自动推导 add 函数的返回类型,无论是整型还是浮点型,都能正确处理。
输出:
3
4
注意
-
auto不能用于函数参数类型auto只能用于变量声明和返回类型,不能用于函数的参数类型。例如,以下代码是错误的:auto add(auto a, auto b) { // 错误,auto 不能用于函数参数类型 return a + b; } -
auto与引用、常量结合 当使用auto时,变量的类型是根据初始化表达式的类型来推导的。如果需要引用或常量,应该明确使用auto&或const auto&来推导引用类型或常量类型。const auto& ref = some_variable; // 推导为 const 引用 -
类型推导的限制 编译器根据初始化表达式推导类型,但不能从不完整的类型推导。因此,必须确保变量在声明时能够通过表达式推导出类型。
变量与常量
一、基本概念
在 C++ 中,变量 (Variable) 是内存中的一块可命名的存储空间,用于保存数据值。 常量 (Constant) 则是在程序执行期间不能被修改的值。
变量与常量是程序存储数据和表达逻辑的基础。
二、变量 (Variable)
1. 定义变量
变量在使用前必须定义。定义时需指定类型,并可选地赋初值。
int age = 20; // 定义并初始化一个整型变量
double weight; // 定义一个浮点型变量,但未初始化
char grade = 'A'; // 定义并初始化一个字符型变量
建议始终初始化变量,避免使用未定义的值(否则可能产生未定义行为)。
2. 变量的命名规则
变量的命名要求符合一定的命名规则,具体可参考命名规则,可简要总结为:
-
只能包含字母、数字和下划线
_ -
不能以数字开头
-
区分大小写 (
score与Score不同) -
不能使用 C++ 关键字 (
int,return等) -
应体现变量含义,例如:
int studentCount; // 好 int sc; // 不推荐
3. 变量的作用域与生命周期
(1)作用域 (Scope)
变量的可见范围取决于其定义位置:
| 类型 | 定义位置 | 可见范围 |
|---|---|---|
| 局部变量 | 函数或代码块内 | 仅在当前函数或块中可见 |
| 全局变量 | 所有函数外部 | 在整个文件中可见 |
int globalVar = 10; // 全局变量
void func() {
int localVar = 5; // 局部变量
}
(2)生命周期 (Lifetime)
- 局部变量:进入作用域时创建,离开作用域时销毁。
- 全局变量/静态变量:程序启动时创建,程序结束时销毁。
void counter() {
static int count = 0; // 静态局部变量,只初始化一次
count++;
std::cout << count << std::endl;
}
4. 变量的存储类型说明符
| 关键字 | 含义 |
|---|---|
auto | 自动类型推断(C++11 引入) |
register | 建议编译器将变量存储在寄存器中(已过时) |
static | 使局部变量在函数多次调用间保持值不变 |
extern | 引用外部定义的变量 |
mutable | 允许在 const 对象中修改该成员(仅类成员有效) |
extern int count; // 声明外部变量
auto x = 3.14; // 类型自动推断为 double
还有更多的变量修饰符,详见基本数据类型。
三、常量 (Constant)
1. 字面值常量 (Literal Constant)
直接出现在代码中的固定值:
123 // 整数常量
3.14 // 浮点常量
'a' // 字符常量
"Hello" // 字符串常量
true, false // 布尔常量
有时候会以宏定义的方式存在,例如
#define pi 3.14,在预处理阶段完成替换处理,本质上还是字面值常量。使用宏替换的方式,被替换的常量在编译出来的二进制中会重复存在,因此使用
constexpr会提高空间利用率。
2. const 常量
const 修饰的变量一旦初始化,其值不能被修改,因此const常量本质上是不可变化的变量:
const double PI = 3.14159;
PI = 3.14; // 错误:不能修改常量
常量指针与指向常量的指针
| 形式 | 含义 |
|---|---|
const int* p // 常量指针 | 指针指向的值不能改 |
int* const p // 指针常量 | 指针本身不能改 |
const int* const p | 值与指针均不可改 |
指针常量和常量指针各是什么,有什么区别?
指针常量和常量指针各是什么,有什么区别?
指针常量是指定义了一个指针,这个指针的值只能在定义时进行初始化,其他时候不能改变。
常量指针是指定义了一个指针,这个指针指向了一个只读的对象,不能通过常量指针来修改这个对象的值。
指针常量强调的是指针本身的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。
3. constexpr 常量表达式(C++11 起)
constexpr 表示在编译期即可确定值的常量。它比 const 更严格:
constexpr int max_size = 100;
constexpr int square(int x) { return x * x; }
int arr[square(4)]; // 编译期确定数组大小
const表示值不可改,而constexpr表示在编译时确定值。
4. 枚举常量 (Enumeration)
枚举用于定义一组命名的整型常量:
enum Color { RED, GREEN, BLUE };
Color c = GREEN;
自定义起始值:
enum ErrorCode { OK = 0, NotFound = 404, ServerError = 500 };
四、变量与常量的对比
| 特性 | 变量 | 常量 |
|---|---|---|
| 值是否可修改 | 可修改 | 不可修改 |
| 是否必须初始化 | 否(但推荐) | 是 |
| 存储位置 | 栈 / 堆 / 静态区 | 通常在只读区 |
| 示例 | int x = 5; | const int x = 5; |
五、示例代码
#include <iostream>
using namespace std;
constexpr double PI = 3.14159;
int main() {
int radius = 5;
const double area = PI * radius * radius;
cout << "半径:" << radius << endl;
cout << "圆面积:" << area << endl;
// area = 100; // 常量不可修改
return 0;
}
输出:
半径:5
圆面积:78.5397
控制结构(Control Structures)
在 C++ 中,控制结构用于控制程序语句的执行顺序。C++ 的控制流主要分为三种类型:
- 顺序结构:从上到下依次执行语句,是程序的默认执行方式。
- 选择结构:根据条件判断,执行不同的分支语句。
- 循环结构:重复执行一段代码,直到某个条件满足为止。
在理解控制结构时,最关键的是掌握表达式求值时机、作用域规则与循环/分支退出机制。
一、循环结构(Loop Structure)
循环结构的本质是在某个条件满足时重复执行代码块。
C++ 提供了三种主要循环语法:for、while 和 do...while。
虽然三者都能实现循环逻辑,但它们在执行顺序与条件判断时机上存在显著差异。
1. for 循环
for (初始化表达式; 条件表达式; 迭代表达式) {
// 循环体
}
在执行时,for 循环遵循如下过程:
- 初始化表达式在循环开始时执行一次;
- 计算条件表达式,若为
true,则进入循环体; - 执行循环体中的语句;
- 执行迭代表达式(通常为自增或自减操作);
- 再次判断条件表达式;
- 若条件仍为真,继续循环;否则退出循环。
#include <iostream>
using namespace std;
int main() {
for (int i = 0; i < 5; ++i) {
cout << "当前 i = " << i << endl;
}
}
程序执行顺序如下:
初始化:i = 0
判断条件:i < 5 → true
执行循环体:输出 i
迭代表达式:i++
再判断...
-
局部作用域:在
for循环中声明的变量(如int i)仅在循环内部可见,循环结束后变量被销毁。 -
可省略项:
for (;;) { // 无限循环 }如果条件表达式被省略,默认为
true。 -
循环控制建议:
- 在循环内部修改循环变量容易出错;
- 条件表达式应当保证能在有限次迭代后终止,否则会造成死循环。
C++11 引入了基于范围的 for 循环(range-based for loop):
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3, 4, 5};
for (int x : nums) {
cout << x << " ";
}
}
等价于传统形式:
for (auto it = nums.begin(); it != nums.end(); ++it)
cout << *it << " ";
这种写法更简洁、安全,也更符合 STL 容器的使用习惯。
2. while 循环
while (条件表达式) {
// 循环体
}
while 循环先判断条件,再执行循环体。执行流程如下:
- 判断条件;
- 若条件为
true,执行循环体; - 循环体执行完毕后,再次判断条件;
- 若条件为
false,循环终止。
int i = 0;
while (i < 5) {
cout << "i = " << i << endl;
++i;
}
与 for 相比,while 更适用于循环次数不确定但终止条件明确的情况,例如文件读取或网络接收:
string line;
while (getline(cin, line)) {
cout << line << endl;
}
这种结构常见于 IO 流循环,因为输入次数事先未知。
一个经典的初学者错误:
int i = 0;
while (i < 10);
{
cout << i << endl;
++i;
}
这里的分号 ; 结束了 while 语句,导致循环体为空。
实际运行时这段代码会陷入死循环,因为条件判断永远为真而循环体无法修改变量。
3. do...while 循环
do {
// 循环体
} while (条件表达式);
与 while 不同,do...while 先执行循环体一次,再判断条件。
因此,无论条件是否为真,循环体至少会被执行一次。
int n;
do {
cout << "请输入一个正数:";
cin >> n;
} while (n <= 0);
程序会反复要求输入,直到输入值大于零。 这种结构常用于需要先执行一次再决定是否继续的交互逻辑。
4. 循环控制语句
-
break—— 提前结束循环break用于立即终止当前循环或switch语句,不会再执行后续的循环体和条件判断。for (int i = 0; i < 10; ++i) { if (i == 5) break; cout << i << " "; } -
continue—— 跳过当前迭代continue会跳过本次循环剩余部分,直接进入下一次条件判断。for (int i = 0; i < 10; ++i) { if (i % 2 == 0) continue; // 跳过偶数 cout << i << " "; } -
goto—— 无条件跳转(极少使用)int i = 0; loop_start: if (i < 5) { cout << i << endl; ++i; goto loop_start; }虽然语法合法,但这种写法不符合结构化编程思想,现代 C++ 中几乎不使用。
5. 嵌套循环与性能考量
循环可以嵌套,例如输出九九乘法表:
for (int i = 1; i <= 9; ++i) {
for (int j = 1; j <= i; ++j) {
cout << j << "x" << i << "=" << i * j << "\t";
}
cout << endl;
}
嵌套循环会带来O(n²) 级别的时间复杂度,因此在数据量大时应当考虑优化:
- 减少内层循环的迭代次数;
- 将不必要的计算提前至循环外;
- 使用
break或条件提前退出。
二、判断结构(Selection Structure)
判断结构的目的是让程序在不同条件下执行不同语句。
C++ 提供了两种主要形式:if 与 switch,另有三目运算符用于简单条件判断。
1. if 条件语句
if (条件表达式) {
// 条件成立时执行
}
else {
// 条件不成立时执行
}
在执行时,编译器首先计算条件表达式的布尔值:
- 若为
true,执行if块; - 否则执行
else块(如果存在)。
除此以外,if还支持多分支判断:
if (score >= 90)
cout << "优秀";
else if (score >= 80)
cout << "良好";
else if (score >= 60)
cout << "及格";
else
cout << "不及格";
编译器从上至下依次判断条件表达式。 当某个条件为真时,后续分支将被跳过。
- 短路求值(short-circuit evaluation)
逻辑运算符 && 和 || 在判断中具有短路行为:
if (x != 0 && 10 / x > 1) // 安全
若 x == 0,第一个条件为 false,第二个表达式不会被求值,从而避免除零错误。
短路机制是 C++ 逻辑判断中非常重要的性能与安全特性。
- 悬挂 else(dangling else)问题
if (a > 0)
if (b > 0)
cout << "A";
else
cout << "B";
此时 else 默认匹配最近的未匹配 if,即第二个 if。
建议始终使用大括号 {} 明确逻辑边界。
2. switch 语句
switch (表达式) {
case 常量1:
// 语句
break;
case 常量2:
// 语句
break;
default:
// 默认语句
break;
}
switch 表达式的值会与每个 case 标签进行匹配,若相等,则从该处开始执行,直到遇到 break 或 switch 结束。
int day = 3;
switch (day) {
case 1: cout << "Monday"; break;
case 2: cout << "Tuesday"; break;
case 3: cout << "Wednesday"; break;
default: cout << "Invalid";
}
-
语义分析与注意事项
-
switch的判断值必须是整数类型或枚举类型; -
case标签必须是常量表达式; -
若
break省略,将导致贯穿 (fall through),继续执行后续分支; -
default分支可选,但建议始终保留。 -
现代 C++ 的枚举与
switch
enum class Color { Red, Green, Blue };
Color c = Color::Green;
switch (c) {
case Color::Red: cout << "Red"; break;
case Color::Green: cout << "Green"; break;
case Color::Blue: cout << "Blue"; break;
}
使用 enum class 可避免命名冲突和隐式转换,提高类型安全性。
3. 条件运算符(三目运算符)
C++ 提供了简洁的条件表达式:
表达式1 ? 表达式2 : 表达式3;
求值逻辑:
- 若表达式1为真,则整个表达式结果为表达式2;
- 否则结果为表达式3。
示例:
int a = 10, b = 20;
int max = (a > b) ? a : b;
该语句的效果等价于:
if (a > b) max = a;
else max = b;
三目运算符常用于简单赋值,但不适合复杂逻辑,因为可读性较差。
操作符(Operators)
操作符(Operator)是 C++ 表达式的核心组成部分,用于对操作数(Operand)进行各种运算、比较或逻辑处理。 在编译阶段,操作符会被转化为相应的汇编指令或函数调用(例如运算符重载时)。
C++ 拥有丰富的操作符体系,并且允许程序员通过运算符重载(operator overloading)定义自定义类型的操作方式,从而实现与内置类型一致的自然语法。
一、操作符的分类
| 类别 | 示例 | 用途 |
|---|---|---|
| 算术操作符 | +, -, *, /, % | 数值计算 |
| 自增/自减操作符 | ++, -- | 累加或递减 |
| 关系操作符 | ==, !=, <, >, <=, >= | 比较 |
| 逻辑操作符 | &&, ||, ! | 逻辑判断 |
| 位操作符 | &, |, ^, ~, <<, >> | 位级运算 |
| 赋值操作符 | =, +=, -=, *=, /=, %= 等 | 赋值与复合赋值 |
| 条件运算符 | ?: | 三目条件判断 |
| 逗号运算符 | , | 顺序求值 |
| 成员操作符 | ., ->, .*, ->* | 成员访问 |
| 指针与地址操作符 | *, & | 指针解引用与取地址 |
| 类型转换操作符 | (type), static_cast, dynamic_cast 等 | 类型转换 |
| 其他特殊操作符 | sizeof, typeid, new, delete | 特殊用途 |
二、算术操作符(Arithmetic Operators)
1. 基本算术运算
| 操作符 | 含义 | 示例 |
|---|---|---|
+ | 加法 | a + b |
- | 减法 | a - b |
* | 乘法 | a * b |
/ | 除法 | a / b |
% | 取余(模运算) | a % b |
示例:
int a = 10, b = 3;
cout << a + b << endl; // 13
cout << a - b << endl; // 7
cout << a * b << endl; // 30
cout << a / b << endl; // 3
cout << a % b << endl; // 1
2. 注意事项
-
整数除法:两个整数相除结果仍为整数,小数部分会被截断。
-
浮点除法:若任一操作数为浮点型,则结果为浮点数。
-
取余运算
%:仅适用于整数类型。 -
负数取余:结果的符号与被除数相同,例如:
cout << (-7) % 4; // 输出 -3
三、自增与自减操作符(++ 与 --)
1. 区分前置与后置
| 形式 | 名称 | 求值顺序 | 示例 |
|---|---|---|---|
++i | 前置自增 | 先自增再返回值 | int a = 1; cout << ++a; // 输出 2 |
i++ | 后置自增 | 先返回值再自增 | int a = 1; cout << a++; // 输出 1 |
同理适用于 --。
2. 编译层面的区别
前置自增:
int& operator++(int& i) { i += 1; return i; }
后置自增:
int operator++(int& i, int) { int old = i; ++i; return old; }
因此后置版本通常会创建临时对象,性能略低。
四、关系操作符(Relational Operators)
用于比较两个值的大小或相等性,结果为 bool 类型。
int a = 5, b = 10;
cout << (a < b) << endl; // 1
cout << (a == b) << endl; // 0
| 操作符 | 含义 | 示例 |
|---|---|---|
== | 等于 | a == b |
!= | 不等于 | a != b |
< | 小于 | a < b |
> | 大于 | a > b |
<= | 小于等于 | a <= b |
>= | 大于等于 | a >= b |
五、逻辑操作符(Logical Operators)
逻辑操作符用于布尔表达式组合,返回 true 或 false。
| 操作符 | 含义 | 示例 | 特性 |
|---|---|---|---|
&& | 逻辑与 | (x > 0 && y > 0) | 短路:若左侧为假,右侧不求值 |
|| | 逻辑或 | (x < 0 || y < 0) | 短路:若左侧为真,右侧不求值 |
! | 逻辑非 | !flag | 取反 |
短路求值(short-circuit evaluation)是逻辑运算的重要特性,它不仅提升性能,也避免非法运算(例如除零)。
if (ptr != nullptr && *ptr > 10)
cout << "安全访问";
六、位操作符(Bitwise Operators)
位操作符直接作用于整数的二进制表示。
| 操作符 | 含义 | 示例 | ||
|---|---|---|---|---|
& | 按位与 | a & b | ||
| ` | ` | 按位或 | `a | b` |
^ | 按位异或 | a ^ b | ||
~ | 按位取反 | ~a | ||
<< | 左移 | a << n (相当于乘以 2ⁿ) | ||
>> | 右移 | a >> n (相当于除以 2ⁿ) |
示例:
unsigned int a = 5; // 00000101
unsigned int b = 3; // 00000011
cout << (a & b); // 00000001 -> 1
cout << (a | b); // 00000111 -> 7
cout << (a ^ b); // 00000110 -> 6
cout << (~a); // 11111010 -> 取反
注意:
- 位移运算的右操作数必须为非负;
- 有符号右移的高位填充由实现定义;
- 通常使用无符号类型进行位操作以避免歧义。
七、赋值与复合赋值操作符(Assignment Operators)
1. 基本赋值
int x = 10;
2. 复合赋值
| 操作符 | 等价形式 | ||
|---|---|---|---|
+= | a = a + b | ||
-= | a = a - b | ||
*= | a = a * b | ||
/= | a = a / b | ||
%= | a = a % b | ||
&= | a = a & b | ||
| ` | =` | `a = a | b` |
^= | a = a ^ b | ||
<<= | a = a << b | ||
>>= | a = a >> b |
复合赋值的优点是只计算一次左值,因此在复杂左值表达式中效率更高:
arr[getIndex()] += 10; // getIndex() 只调用一次
八、条件运算符(Ternary Operator)
三目运算符语法:
条件 ? 表达式1 : 表达式2;
示例:
int max = (a > b) ? a : b;
等价于:
if (a > b)
max = a;
else
max = b;
注意:
- 运算符返回一个值,因此可嵌入表达式;
- 但应避免多层嵌套,影响可读性。
九、逗号运算符(Comma Operator)
逗号运算符从左到右依次求值,整个表达式的结果是最后一个表达式的值:
int a = (b = 3, b + 2); // b=3执行后,再计算b+2,a=5
在 for 循环中常见:
for (int i = 0, j = 10; i < j; ++i, --j)
cout << i << " " << j << endl;
十、成员与指针操作符(Member & Pointer Operators)
| 操作符 | 含义 | 示例 |
|---|---|---|
. | 访问对象成员 | obj.member |
-> | 访问指针所指对象的成员 | ptr->member |
.* | 访问对象的成员指针 | (obj.*ptrMember) |
->* | 通过指针访问成员指针 | (ptr->*ptrMember) |
示例:
struct Test { int x; void show() { cout << x; } };
Test t{42};
Test* p = &t;
cout << t.x; // 使用 .
cout << p->x; // 使用 ->
十一、指针相关操作符
| 操作符 | 作用 | 示例 |
|---|---|---|
& | 取地址 | int* p = &x; |
* | 解引用 | cout << *p; |
这两个符号在声明与使用中含义不同:
int* p; // 声明指针类型
*p = 10; // 解引用赋值
十二、类型转换操作符(Cast Operators)
1. C 风格强制类型转换
int a = 10;
double b = (double)a;
2. C++ 风格强制类型转换
| 操作符 | 用途 |
|---|---|
static_cast<T>(expr) | 编译期已知的安全类型转换 |
dynamic_cast<T>(expr) | 用于多态类型的安全向下转换(运行期检查) |
const_cast<T>(expr) | 去除或添加 const 属性 |
reinterpret_cast<T>(expr) | 强制重解释类型(高风险) |
示例:
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // 安全类型检查
十三、其他特殊操作符
1. sizeof
返回对象或类型的字节大小,结果类型为 size_t:
cout << sizeof(int); // 通常为4
对数组时返回整个数组的大小,而不是指针大小:
int arr[10];
cout << sizeof(arr); // 40 (假设int为4字节)
2. typeid
用于在运行时获取类型信息,常与 RTTI(运行时类型识别)配合使用。
#include <typeinfo>
int a = 5;
cout << typeid(a).name(); // 输出类型名
3. new 与 delete
动态内存分配与释放:
int* p = new int(10);
delete p;
int* arr = new int[5];
delete[] arr;
new 会自动调用构造函数,而 delete 调用析构函数。
十四、运算符优先级与结合性
| 优先级(高→低) | 操作符 | 结合方向 |
|---|---|---|
| 1 | :: | 左到右 |
| 2 | () [] -> . ++ -- | 左到右 |
| 3 | ! ~ ++ -- + - (type) * & sizeof new delete | 右到左 |
| 4 | * / % | 左到右 |
| 5 | + - | 左到右 |
| 6 | << >> | 左到右 |
| 7 | < <= > >= | 左到右 |
| 8 | == != | 左到右 |
| 9 | & | 左到右 |
| 10 | ^ | 左到右 |
| 11 | | | 左到右 |
| 12 | && | 左到右 |
| 13 | || | 左到右 |
| 14 | ?: | 右到左 |
| 15 | =, +=, -=, *=, /=, %= 等 | 右到左 |
| 16 | , | 左到右 |
建议使用括号明确运算顺序,尤其在混合表达式中。
十五、运算符重载(Operator Overloading)
C++ 允许为自定义类型定义运算符行为。例如:
class Vector {
public:
int x, y;
Vector(int x, int y): x(x), y(y) {}
Vector operator+(const Vector& v) const {
return Vector(x + v.x, y + v.y);
}
};
使用:
Vector a(1, 2), b(3, 4);
Vector c = a + b; // 等价于 a.operator+(b)
注意:
- 不能重载
.、::、?:、sizeof; - 运算符重载不会改变操作符优先级;
- 应保证语义一致性(例如重载
==时应与!=保持逻辑对称)。
函数 (Functions)
函数是 C++ 程序的基本组成部分,它们用于将程序分解为可管理、可重复使用的代码块。
一、函数的基本概念
1. 定义
函数是一段执行特定任务的命名代码块。
2. 优点
- 模块化 (Modularity): 使程序结构清晰,易于理解和维护。
- 代码重用 (Code Reusability): 一次编写,多次调用,避免重复劳动。
- 简化调试 (Easier Debugging): 易于隔离和测试代码。
二、函数的构成要素
一个函数通常由以下几个部分组成:
1. 函数原型 (Function Prototype) / 函数声明 (Declaration)
告诉编译器函数的名称、返回类型和参数列表,但没有函数体。
格式:
返回类型 函数名(参数类型 参数名, ...);
示例:
int add(int a, int b); // 函数原型
- 注意: 函数原型通常放在
main函数之前或头文件 (.h文件) 中。
2. 函数定义 (Function Definition)
包含函数头和函数体(实际执行的代码)。
格式:
返回类型 函数名(参数类型 参数名, ...)
{
// 函数体:执行任务的代码
// ...
return 表达式; // 如果返回类型不是 void
}
示例:
int add(int a, int b) // 函数头
{
// 函数体
return a + b;
}
3. 函数调用 (Function Call)
在程序中执行函数。
格式:
函数名(实参1, 实参2, ...);
示例:
int sum = add(5, 3); // 调用 add 函数,5 和 3 是实参
三、函数的关键要素
1. 返回类型 (Return Type)
函数执行完毕后返回给调用者的值的类型。
- 可以是任何有效的数据类型(如
int,double,bool, 自定义类型等)。 void: 如果函数不返回任何值,则使用void。
2. 函数名 (Function Name)
用于唯一标识函数。遵循 C++ 命名规则。
3. 参数 (Parameters) / 形参 (Formal Parameters)
定义在函数头中,用于接收调用者传递的数据。
- 格式:
类型 名称(例如int x)
4. 实参 (Arguments)
调用函数时传递给形参的实际值。
5. return 语句
用于:
- 返回值: 将一个值返回给调用者。
- 结束执行: 立即终止函数的执行。
- 注意: 对于返回类型非
void的函数,必须有return语句返回一个与返回类型兼容的值。
四、参数传递 (Argument Passing)
函数调用时,将实参的值复制或引用到形参中,主要有三种方式:
1. 值传递 (Pass by Value) (最常用)
-
机制: 将实参的值复制给形参。
-
效果: 函数内部对形参的任何修改,不会 影响到外部的实参。
-
声明:
void func(int x); // x 是一个副本
2. 地址传递 (Pass by Pointer) (C 风格,C++ 中不推荐)
-
机制: 传递实参的内存地址(指针)。
-
效果: 函数可以通过指针修改实参的值。
-
声明:
void func(int *ptr); // ptr 是一个指向 int 的指针
3. 引用传递 (Pass by Reference) (C++ 推荐)
-
机制: 形参成为实参的别名(引用)。
-
效果: 函数内部对形参的任何修改,会 直接影响到外部的实参。
-
声明:
void func(int &ref); // ref 是实参的引用- 优点: 既能修改外部变量,又避免了指针的复杂性和值传递的开销。
五、函数的进阶特性
1. 默认参数 (Default Arguments)
允许在函数声明时给参数设定一个默认值。调用时若不提供该实参,则使用默认值。
- 规则: 默认参数必须从右向左依次设置。一旦一个参数有了默认值,它右边的所有参数都必须有默认值。
示例:
void print_info(int a, int b = 10, int c = 20); // b 和 c 是默认参数
// 调用方式
print_info(1); // a=1, b=10, c=20
print_info(1, 5); // a=1, b=5, c=20
print_info(1, 5, 8); // a=1, b=5, c=8
2. 函数重载 (Function Overloading)
在同一作用域内,允许存在多个同名函数,但它们的 参数列表 必须不同(数量、类型或顺序)。
- 作用: 使程序能够使用一个统一的名称来执行相似但操作不同数据类型的任务。
示例:
int operate(int a, int b)
{ return a + b; }
double operate(double a, double b)
{ return a * b; }
int operate(int a) // 与前两个函数参数数量不同
{ return a * a; }
3. 内联函数 (Inline Functions)
使用 inline 关键字修饰的函数。
- 目的: 建议编译器在函数被调用的地方直接将函数体的代码展开,而不是执行常规的函数调用机制。
- 优点: 消除函数调用开销,提高小函数的执行效率。
- 缺点: 可能导致代码膨胀(占用更多内存)。
- 适用场景: 函数体非常简单(只有一两行代码)的情况。
示例:
inline int square(int x) { return x * x; }
- 注意:
inline只是对编译器的建议,编译器可以忽略它。
示例代码
#include <iostream>
// 函数原型 (声明)
int add(int a, int b);
void swap_by_ref(int &x, int &y);
// 带有默认参数的函数原型
void greet(std::string name, std::string greeting = "Hello");
// main 函数 (程序入口)
int main() {
int num1 = 10, num2 = 5;
// 1. 函数调用
int sum = add(num1, num2); // num1, num2 是实参
std::cout << "Sum: " << sum << std::endl; // 输出: 15
// 2. 引用传递示例
int x = 100, y = 200;
std::cout << "Before swap: x=" << x << ", y=" << y << std::endl;
swap_by_ref(x, y); // 交换 x 和 y 的值
std::cout << "After swap: x=" << x << ", y=" << y << std::endl; // 输出: x=200, y=100
// 3. 默认参数示例
greet("Alice"); // 使用默认问候语: Hello Alice
greet("Bob", "Good Morning"); // 使用自定义问候语: Good Morning Bob
return 0;
}
// 函数定义
int add(int a, int b) // a, b 是形参 (值传递)
{
return a + b;
}
// 引用传递实现值交换
void swap_by_ref(int &x, int &y) // x, y 是引用
{
int temp = x;
x = y;
y = temp;
// x 和 y 的改变会影响到 main 函数中的实参
}
// 默认参数实现
void greet(std::string name, std::string greeting)
{
std::cout << greeting << " " << name << std::endl;
}
类与对象
C++ 是一种支持面向对象编程(Object-Oriented Programming, OOP)的语言。 在面向对象的设计思想中,类(Class)是 C++ 的核心特性之一,常被称为用户自定义的数据类型(user-defined type)。
类用于定义对象的结构与行为,是一种将数据与操作这些数据的函数封装在一起的抽象描述。 在类中,用于存储数据的部分称为成员变量(Member Variables),而用于操作这些数据的函数称为成员函数(Member Functions)。
类本质上是一种模板(Template)或蓝图(Blueprint),通过它可以创建出多个具有相同属性和行为的具体实例,这些实例被称为对象(Objects)。每个对象都拥有属于自己的成员变量副本,并可以通过成员函数来执行特定的操作。
一、类的结构
在 C++ 中,类(Class)是一种由用户定义的数据类型(User-defined Data Type), 它将数据(成员变量)与操作数据的函数(成员函数)有机地结合在一起, 从而实现封装(Encapsulation)与抽象(Abstraction)。
一个类的基本结构如下:
class ClassName {
private:
// 私有成员(数据与函数)
protected:
// 受保护成员
public:
// 公有成员
};
1. 类的基本组成
类由以下几个主要部分构成:
| 组成部分 | 说明 |
|---|---|
| 类名(Class Name) | 类的标识符,用于定义和引用该类。 |
| 成员变量(Member Variables) | 用于存储对象状态的数据。 |
| 成员函数(Member Functions) | 用于操作数据或定义对象行为的函数。 |
| 访问控制符(Access Specifiers) | 控制外部对类成员的访问权限:private、protected、public。 |
| 构造函数与析构函数(Constructor & Destructor) | 对象创建与销毁时自动调用的特殊成员函数。 |
| 静态成员(Static Members) | 属于类本身而非某个对象的成员。 |
| 友元(Friend) | 特殊访问权限,允许外部函数或类访问私有成员。 |
2. 访问控制符(Access Specifiers)
访问控制符用于限定类成员的可见性和访问范围:
| 控制符 | 访问范围 | 典型用途 |
|---|---|---|
private | 仅类内可访问 | 封装内部实现细节 |
protected | 类内和子类可访问 | 继承时保留部分访问权限 |
public | 任何地方都可访问 | 提供对外接口 |
示例:
class Example {
private:
int secret; // 私有成员
protected:
int semi_secret; // 受保护成员
public:
int visible; // 公有成员
};
默认情况下,
class的成员默认是 private, 而struct的成员默认是 public。
3. 成员变量(Member Variables)
成员变量用于存储对象的状态。 每个对象都会拥有独立的一份成员变量副本。
class Student {
private:
std::string name;
int age;
};
成员变量可以是基本类型、对象、指针、引用、数组、容器等。
也可以为成员变量提供默认初始值(C++11 起支持):
class Point {
int x = 0;
int y = 0;
};
4. 成员函数(Member Functions)
成员函数用于定义对象的行为,通常用于访问和修改成员变量。
class Student {
private:
std::string name;
int age;
public:
void setInfo(const std::string& n, int a) {
name = n;
age = a;
}
void display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
也可以在类外定义成员函数:
void Student::display() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
5. 构造函数(Constructor)
构造函数在对象创建时自动执行,用于初始化成员变量。 它与类同名,无返回值。
class Student {
private:
std::string name;
int age;
public:
// 构造函数
Student(const std::string& n, int a) : name(n), age(a) {
std::cout << "Constructor called." << std::endl;
}
};
构造函数的种类包括:
| 类型 | 说明 | 示例 |
|---|---|---|
| 默认构造函数 | 无参版本 | Student() |
| 有参构造函数 | 初始化时传参 | Student("张三", 20) |
| 拷贝构造函数 | 用已有对象创建新对象 | Student(const Student& s) |
构造函数支持多态,可以使用多个参数不同的构造函数,在使用时会自动匹配。
6. 析构函数(Destructor)
析构函数在对象销毁时自动调用,用于资源释放或清理。
class Student {
public:
~Student() {
std::cout << "Destructor called." << std::endl;
}
};
析构函数名以
~开头,无参、无返回值,每个类最多有一个析构函数。
7. this 指针
this 是一个隐含指针,指向调用成员函数的当前对象。
它可用于区分成员变量与同名参数:
class Student {
private:
std::string name;
public:
Student(const std::string& name) {
this->name = name; // 区分成员变量与参数
}
};
8. 静态成员(Static Members)
静态成员属于类本身,而非具体对象。 所有对象共享同一份静态数据。
静态成员分为两类:
- 静态成员变量(Static Member Variables)
- 静态成员函数(Static Member Functions)
(1)静态成员变量
静态成员变量在所有对象之间共享同一份存储空间。 它不依赖于任何对象存在,无论创建多少个对象,这个变量都只有一份。
因此,静态成员变量常用于表示类级别的公共信息,例如计数器、配置、全局状态等。
class Counter {
public:
static int count;
Counter() { count++; }
};
int Counter::count = 0;
(2)静态函数变量
静态成员函数同样属于类本身,而不是对象。 它的主要特征是:
- 不依赖任何对象实例
- 无法访问非静态成员变量或函数(因为没有具体对象可供操作)
- 通常用于类级别的操作,如访问静态数据或执行与对象无关的逻辑。
- 没有
this指针,因为它不属于任何对象
class Counter {
private:
static int count;
public:
Counter() { count++; }
// 静态成员函数
static void showCount() {
std::cout << "Current count: " << count << std::endl;
}
};
// 类外定义静态变量
int Counter::count = 0;
int main() {
Counter a, b;
Counter::showCount(); // 通过类名访问
a.showCount(); // 也可通过对象访问
}
9. 常成员(const Members)
- 常成员函数:在函数后加
const,表示不修改成员变量。 - 常对象:对象一旦定义,其状态不可更改。
class Point {
private:
int x, y;
public:
void display() const {
std::cout << "(" << x << ", " << y << ")" << std::endl;
}
};
10. 友元(Friend)
友元函数或友元类可以访问类的私有成员。 这在操作符重载、类间紧密协作时非常有用。
#include <iostream>
class Box {
private:
int width;
public:
void set_width(int val){
width = val;
}
friend void printWidth(Box b);
};
void printWidth(Box b) {
std::cout << "Width: " << b.width << std::endl;
}
int main(){
Box a;
a.set_width(20);
printWidth(a);
}
友元会破坏封装性,应谨慎使用。
友元函数不是成员函数,不能用 对象.函数() 方式调用。它只是可以访问类私有成员的普通函数,调用方式与普通函数相同。
11. 类的组合与嵌套
一个类可以将另一个类作为成员,这种关系称为组合(Composition)。
class Address {
public:
std::string city;
};
class Person {
public:
std::string name;
Address addr; // 组合
};
当对象销毁时,其组合成员也会自动销毁。
二、对象
在 C++ 中,对象(Object)是类的实例(Instance)。 当我们定义了一个类后,这个类本身只是一个抽象的模板或蓝图, 而对象才是真正占用内存、可操作的具体实体。
类定义了“事物的共性”,对象体现了“事物的个性”。
1. 对象的创建与定义
定义一个类对象与定义普通变量非常相似:
class Student {
public:
std::string name;
int age;
};
int main() {
Student s1; // 创建对象 s1
s1.name = "张三";
s1.age = 20;
Student s2; // 再创建一个对象 s2
s2.name = "李四";
s2.age = 21;
}
每个对象都有独立的成员变量副本,互不影响。
例如:
s1.age = 20;
s2.age = 21;
// 修改 s1 的 age 不会影响 s2
2. 对象的初始化
当类中定义了构造函数时,对象可以在定义时直接初始化:
class Point {
private:
int x, y;
public:
Point(int a, int b) { // 构造函数
x = a;
y = b;
}
void show() {
std::cout << "(" << x << ", " << y << ")" << std::endl;
}
};
int main() {
Point p1(1, 2); // 调用构造函数
Point p2 = Point(3, 4); // 另一种写法
p1.show();
p2.show();
}
注意:对象创建时,构造函数会被自动调用;对象销毁时,析构函数会被自动调用。
3. 对象的作用域与生命周期
对象的生命周期与其定义的位置有关。
| 定义位置 | 生命周期说明 |
|---|---|
| 局部对象(栈上) | 作用域结束时自动销毁 |
| 全局对象 | 程序结束时销毁 |
| 静态对象 | 程序结束时销毁 |
| 动态对象(堆上) | 需要手动释放(使用 new / delete) |
示例:
class Example {
public:
Example() { std::cout << "Constructed\n"; }
~Example() { std::cout << "Destructed\n"; }
};
int main() {
Example local; // 局部对象
static Example global; // 静态对象
Example* ptr = new Example(); // 动态对象
delete ptr; // 手动释放
}
输出顺序体现了不同对象的生命周期管理。
4. 对象数组
可以定义一个包含多个对象的数组:
class Point {
public:
int x, y;
Point(int a = 0, int b = 0) : x(a), y(b) {}
};
int main() {
Point arr[3] = { {1,2}, {3,4}, {5,6} };
for (auto& p : arr)
std::cout << "(" << p.x << ", " << p.y << ")\n";
}
若类中没有默认构造函数,则必须在定义对象数组时显式提供初始化参数。
5.对象的指针与引用
(1)对象指针
和基本类型类似,可以使用指针指向对象。
Student s1;
Student* ptr = &s1;
ptr->name = "王五";
ptr->age = 22;
使用
->运算符访问对象的成员。
也可以使用 new 创建动态对象:
Student* stu = new Student;
stu->name = "赵六";
stu->age = 18;
delete stu; // 释放内存
(2)对象引用
引用可以直接操作已有对象:
Student s1;
Student& ref = s1;
ref.name = "张三";
引用不会创建新对象,只是为已有对象取别名。
6. 对象的拷贝与赋值
当我们用一个对象初始化另一个对象时,会自动调用拷贝构造函数。
class Box {
public:
int width;
Box(int w) : width(w) {}
Box(const Box& b) { // 拷贝构造函数
width = b.width;
std::cout << "Copy constructor called\n";
}
};
int main() {
Box b1(10);
Box b2 = b1; // 调用拷贝构造函数
}
赋值操作调用的是赋值运算符(operator=),而不是拷贝构造。
7. const 对象
可以将对象声明为常量,使其内容不可被修改:
class Point {
public:
int x, y;
void show() const {
std::cout << x << ", " << y << std::endl;
}
};
int main() {
const Point p = {1, 2};
p.show();
// p.x = 5; // 错误:常对象不可修改成员
}
常对象只能调用常成员函数(即声明为
void func() const的函数)。
8. 对象之间的比较与赋值
对象可以相互赋值:
Student s1, s2;
s1.name = "A";
s2 = s1; // 成员变量逐个拷贝
但若希望对象间的比较(==、< 等)有意义,需要重载运算符(进阶内容,后续讲述)。
9. 对象与内存模型
每个对象在内存中都有独立的成员变量副本:
Student s1 ──► name="张三", age=20
Student s2 ──► name="李四", age=21
而静态成员在所有对象间共享。
三、完整示例
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
string name;
int age;
Student(string n = "未知", int a = 0) : name(n), age(a) {
cout << "Constructed: " << name << endl;
}
~Student() {
cout << "Destructed: " << name << endl;
}
void show() const {
cout << "Name: " << name << ", Age: " << age << endl;
}
};
int main() {
Student s1("张三", 20);
Student s2("李四", 21);
s1.show();
s2.show();
Student* p = new Student("王五", 22);
p->show();
delete p; // 手动释放动态对象
}
输出:
Constructed: 张三
Constructed: 李四
Constructed: 王五
Name: 张三, Age: 20
Name: 李四, Age: 21
Name: 王五, Age: 22
Destructed: 王五
Destructed: 李四
Destructed: 张三
四、面向对象的三大特征
面向对象编程(OOP)的核心思想可以概括为三大特征:封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。这三大特征是 C++ 类与对象设计的基础,也是理解和应用面向对象程序设计的关键。
继承与多态的内容属于C++面向对象进阶内容。
1. 封装(Encapsulation)
封装是面向对象最基本的特征之一,也是类和对象概念的核心。
封装的核心思想是将对象的状态(数据)和行为(函数)组合到一个整体——类中,并通过访问控制机制对外部可见性进行限制和保护,对受保护的私有数据仅可以使用成员函数进行操作。
作用:
- 对内部数据进行保护,只允许可信的方法或对象访问;
- 隐藏实现细节,使类的使用者无需了解内部工作原理;
- 提供统一接口,提高模块化和可维护性。
示例:
class Student {
private:
std::string name; // 私有成员,外部无法直接访问
int age;
public:
void setInfo(const std::string& n, int a) { // 提供接口修改数据
name = n;
age = a;
}
void display() const { // 提供接口访问数据
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};
在上例中,name 和 age 仅能通过 setInfo 和 display 访问和修改,这就是封装的典型应用。
封装就是把客观事物封装成抽象类,并控制外部访问权限,保护数据安全并提高代码可维护性。
2. 继承(Inheritance)
继承是面向对象中的第二大特征,它允许新建的类复用已有类的属性和行为,并在此基础上进行扩展或修改。
被继承的类称为基类(Base Class)或父类,从基类继承的类称为派生类(Derived Class)或子类。
作用:
- 代码复用:无需重复编写已有功能;
- 构建类层次:形成“父类-子类”的组织结构;
- 支持多态:继承是实现运行时多态的前提。
实现方式:
- 公有继承(public inheritance):基类公有成员在派生类中仍为公有
- 保护继承(protected inheritance):基类公有/保护成员在派生类中变为保护
- 私有继承(private inheritance):基类公有/保护成员在派生类中变为私有
- 组合(Composition):在类中包含另一个类对象作为成员,用于实现“has-a”关系
示例:
class Person {
protected:
std::string name;
public:
void setName(const std::string& n) { name = n; }
};
class Student : public Person { // Student 继承 Person
private:
int grade;
public:
void setGrade(int g) { grade = g; }
void showInfo() const {
std::cout << "Name: " << name << ", Grade: " << grade << std::endl;
}
};
Student继承了Person的name,同时扩展了grade,这就是继承的典型应用。 如果需要类间关系更紧密,还可以通过组合在类中嵌套其他类。
继承是在无需重写已有类功能的前提下扩展功能,实现类复用与层次化管理。
3. 多态(Polymorphism)
多态是面向对象的第三大特征,允许同一个操作作用于不同对象表现出不同的行为。
多态允许父类指针或引用指向子类对象,并根据对象实际类型执行不同操作。
英文 polymorphism 来自“多形性”,意为“一个接口,多种形态”。
类型:
-
编译时多态(静态多态):
- 通过函数重载、运算符重载实现
- 在编译阶段就确定调用哪个函数
-
运行时多态(动态多态):
- 通过虚函数(virtual)实现
- 在程序运行时根据实际对象类型决定调用哪个函数
实现方式:
- 覆盖(Override):派生类重新定义基类的虚函数,运行时根据实际对象调用
- 重载(Overload):同名函数参数不同,编译时决定调用哪个函数
示例(运行时多态):
class Animal {
public:
virtual void speak() const { std::cout << "Animal sound" << std::endl; }
};
class Dog : public Animal {
public:
void speak() const override { std::cout << "Woof!" << std::endl; }
};
int main() {
Animal* a = new Dog();
a->speak(); // 输出 "Woof!",根据实际对象类型调用
delete a;
}
speak被声明为虚函数,父类指针调用时根据子类实际类型执行函数,这就是运行时多态。
多态使得统一接口处理不同对象成为可能,提高了程序的灵活性和可扩展性。
模板 (Template)
模板是C++中实现泛型编程的基础工具,泛型编程即以一种独立于任何特定类型的方式编写代码。模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。它允许我们编写与类型无关的代码(如类或函数),从而提高代码的重用性。编译器在编译时会根据用户提供的类型参数,从模板生成具体的类型或函数,这个过程称为模板实例化(Template Instantiation)。
一、模板的基本类型
C++主要有两种模板:
1. 函数模板 (Function Template)
用于创建可处理不同数据类型的函数。
语法:
template <typename T>
T minimum(const T& lhs, const T& rhs) {
return lhs < rhs ? lhs : rhs;
}
// 或使用 class 关键字,效果等同于 typename
template <class T>
T maximum(const T& lhs, const T& rhs) {
return lhs > rhs ? lhs : rhs;
}
使用:
int a = 10, b = 20;
int min_val = minimum(a, b); // 编译器推导 T 为 int
double x = 3.14, y = 1.618;
double max_val = maximum<double>(x, y); // 显式指定 T 为 double
2. 类模板 (Class Template)
用于创建可处理不同数据类型的类,常用于实现容器(如 std::vector、std::pair)。
语法:
template <typename T1, typename T2>
class Pair {
public:
T1 first;
T2 second;
Pair(T1 f, T2 s) : first(f), second(s) {}
// 成员函数也可以使用模板参数
void print() const;
};
template <typename T1, typename T2>
void Pair<T1, T2>::print() const {
// ... 实现细节
}
使用:
Pair<int, double> p(10, 3.14); // 实例化 Pair<int, double> 类
二、模板参数 (Template Parameters)
模板参数可以是以下几种类型:
| 参数类型 | 描述 | 示例 |
|---|---|---|
| 类型参数 | 作为类型的占位符。使用 typename 或 class 关键字声明。 | template <typename T> |
| 非类型参数 | 模板参数可以是整型、枚举、指针、引用、std::nullptr_t 等编译期常量。 | template <typename T, size_t N> (如 std::array) |
| 模板的模板参数 | 参数本身是一个模板(较高级)。 | template <typename T, template<typename> class Container> |
默认模板参数:
模板参数可以拥有默认值(从C++11开始,函数模板也可以拥有默认模板参数)。
template <typename T, size_t N = 10> // N 的默认值为 10
struct Array { T arr[N]; };
Array<int> a; // N 默认为 10
Array<double, 5> b; // N 显式指定为 5
三、模板特化 (Template Specialization)
模板特化允许为特定的类型参数提供一个不同的实现,以优化性能或处理通用模板无法处理的情况。
1. 完全特化 (Full Specialization)
为模板的所有参数提供具体的类型或值。
语法:
// 原始函数模板
template <typename T>
T process(T value) { /* 通用实现 */ }
// int 类型的完全特化
template <> // 尖括号内为空
int process<int>(int value) {
// 针对 int 类型的优化或特殊实现
return value * 2;
}
2. 部分特化 (Partial Specialization)
只对模板的部分参数进行特化,或对参数的形式进行限制(例如,特化为指针类型)。注意:只有类模板可以进行部分特化,函数模板不能。
语法 (类模板):
// 原始类模板
template <typename T1, typename T2>
class MyPair { /* 通用实现 */ };
// 部分特化:当第二个参数为 int 时
template <typename T1>
class MyPair<T1, int> { /* T2 固定为 int 的实现 */ };
// 部分特化:当两个参数都是指针时
template <typename T1, typename T2>
class MyPair<T1*, T2*> { /* 两个参数都是指针的实现 */ };
四、关键字 typename
typename 关键字有两个主要用途:
-
在模板参数列表中,指示一个模板参数是一个类型参数(等同于
class)。template <typename T> // 或 template <class T> -
在模板定义内部,用于显式告诉编译器一个依赖于模板参数的嵌套名称是一个类型。 这是因为在编译时,编译器无法确定依赖名称是否是一个类型或静态成员。
template <typename T> void func(T value) { // 假设 T::InnerType 是 T 内部定义的类型 typename T::InnerType *ptr; // 必须加 typename }
五、模板参数推导 (Argument Deduction)
1. 函数模板参数推导
调用函数模板时,编译器通常可以根据传递的实参类型自动推导出模板参数的类型。
template <typename T> void print(T val) { /* ... */ }
print(42); // 编译器推导 T 为 int
print("abc"); // 编译器推导 T 为 const char*
2. 类模板参数推导 (CTAD, C++17)
从 C++17 开始,类模板的实例化也可以进行类型推导,无需显式指定模板参数。
// 假设 Pair 是一个类模板
// C++17 之前:
Pair<int, double> p1(1, 2.3);
// C++17 之后:
Pair p2(1, 2.3); // 编译器推导为 Pair<int, double>
C++进阶知识
C++进阶部分将深入语言内部机制与底层实现,理解编译器如何处理对象模型、内存布局、虚函数表、模板实例化以及异常机制等。
本部分不再局限于语法层面的使用,而是关注性能、抽象与控制力。内容包括高级内存管理、面向对象、多线程与并发、智能指针等内容。
只有理解语言设计背后的思想,真正做到“以C++思考”,才能具备面向底层系统优化与大型工程开发的能力。
C++ 内存管理
内存管理是 C++ 编程中的核心概念之一,它直接影响程序的性能、可靠性和稳定性。C++ 提供了灵活且高效的内存管理机制,这也使得开发者能够更精细地控制程序的内存使用。C++ 中的内存管理分为两大类:静态内存管理和动态内存管理。
在 C++ 中,静态内存管理主要涉及由编译器管理的栈内存,通常用于存储局部变量和函数调用的相关信息。与此不同,动态内存管理则涉及到程序运行时动态分配的内存区域,这通常需要程序员显式地分配和释放内存。
C++ 提供了一些工具来帮助开发者更高效地管理内存,避免内存泄漏、悬空指针和野指针等常见问题。在传统的内存管理方式中,开发者需要通过 new 和 delete 来手动管理动态内存的分配和释放。而为了提高开发效率和安全性,C++11 引入了智能指针,如 std::unique_ptr、std::shared_ptr 和 std::weak_ptr,它们自动管理内存的生命周期,减少了人为错误的可能性。
此外,C++ 还提供了内存池和内存对齐等更底层的内存管理技术,使得开发者可以根据需要进一步优化程序的内存使用。
在本章中,将深入探讨 C++ 中的内存管理机制,重点介绍手动内存管理和智能指针的使用。
内存布局与模型
C++ 程序的内存管理建立在计算机的存储模型(Memory Model)之上。理解程序在内存中的布局,是掌握变量生命周期、作用域、动态分配与释放机制的基础。 一个典型的 C++ 程序在运行时会被划分为多个区域,每个区域负责存储不同类型的数据,并具有各自的生命周期与访问特性。
Smart Pointer
内存管理是 C++ 编程中的一个关键问题,尤其是当涉及到动态分配的内存时,程序员需要确保及时释放资源以避免内存泄漏或其他潜在问题。为了解决这个问题,C++ 引入了智能指针,它们通过自动管理内存的生命周期来帮助开发者更安全地进行内存操作。智能指针不仅简化了内存管理的复杂性,还提高了程序的安全性,避免了许多常见的内存错误。
智能指针是 C++ 标准库中的模板类,它们的作用是通过自动释放所持有的资源来管理动态分配的内存。智能指针的核心特点是,它们会在不再需要资源时自动释放内存,从而减少内存泄漏的风险。C++11 引入了三种主要类型的智能指针:std::unique_ptr、std::shared_ptr 和 std::weak_ptr,它们适用于不同的使用场景。除此之外,std::auto_ptr 是 C++98 中的一个智能指针,但由于设计缺陷,它在 C++11 中被弃用了。
在本节中,我们将详细介绍这四种智能指针的特点、适用场景以及如何使用它们。
| 智能指针类型 | 所有权类型 | 主要特点 | 适用场景 | 创建方式 |
|---|---|---|---|---|
std::auto_ptr | 独占所有权(已弃用) | 自动管理内存,但存在隐式所有权转移的问题,已在 C++11 中弃用 | 不推荐使用(已被 std::unique_ptr 取代) | std::auto_ptr<T> ptr(new T); |
std::unique_ptr | 独占所有权 | 不能拷贝,只能移动,自动销毁所管理的资源 | 单一所有者的资源管理,适用于工厂模式等 | std::unique_ptr<T> ptr = std::make_unique<T>(); |
std::shared_ptr | 共享所有权 | 引用计数,多个指针共享同一资源,最后一个销毁时释放资源 | 多个对象共享资源,如缓存、资源池 | std::shared_ptr<T> ptr = std::make_shared<T>(); |
std::weak_ptr | 不管理资源生命周期 | 防止 shared_ptr 引用计数循环引用,不能直接访问资源 | 避免 shared_ptr 的循环引用问题 | std::weak_ptr<T> weak_ptr = shared_ptr; |
std::auto_ptr
std::auto_ptr 是 C++98 中引入的智能指针类型,它提供了一种自动管理动态内存的方式。其主要功能是通过自动销毁对象来避免内存泄漏。然而,由于其所有权转移的语义问题(例如,拷贝操作时所有权隐式转移),std::auto_ptr 在 C++11 中被 弃用,并且在 C++17 中被 完全移除。因此,建议开发者使用 std::unique_ptr 来替代 std::auto_ptr。
1. 定义
std::auto_ptr 的模板定义如下:
template<class T>
class auto_ptr;
它是一个模板类,接受一个类型 T,并提供对该类型的自动内存管理。
std::auto_ptr 还有一个专用的模板版本:
template<>
class auto_ptr<void>;
这个版本用于不处理任何具体类型的 void 指针,但它通常不用于常规编程中,因为它并没有实际的意义。
2. 基本功能
std::auto_ptr 自动管理其指向的动态内存资源。其基本特点是:
- 在
auto_ptr被销毁时,自动释放其指向的对象。 - 所有权转移:当
auto_ptr被拷贝时,源auto_ptr的所有权被转移到目标auto_ptr,这意味着源指针不再持有资源。这一行为可能会导致意外的内存管理问题。
3. 重要特点
-
所有权转移:
std::auto_ptr的最具争议的特性就是它的 所有权转移。在拷贝构造函数或拷贝赋值运算符中,auto_ptr会转移所有权(即源指针将失去对对象的所有权,而目标指针会成为新的所有者)。这种行为与 C++ 中其他类型的智能指针(如std::unique_ptr)不同,容易导致不易察觉的 bug,尤其是在多次使用相同的对象时。
-
析构时自动释放资源:
- 在
auto_ptr被销毁时,它会自动释放它持有的内存。其析构函数会调用所管理对象的析构函数来释放资源,因此,开发者不需要显式地调用delete。
- 在
-
不支持复制语义:
- 由于所有权转移,
std::auto_ptr不支持复制操作(即不支持拷贝构造和拷贝赋值),它只支持移动语义。
- 由于所有权转移,
4. 使用示例
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor\n"; }
~MyClass() { std::cout << "MyClass destructor\n"; }
};
int main() {
// 创建一个 auto_ptr 指向 MyClass
std::auto_ptr<MyClass> ptr1(new MyClass());
// 通过拷贝构造将 ptr1 的所有权转移到 ptr2
std::auto_ptr<MyClass> ptr2 = ptr1; // ptr1 的所有权转移给 ptr2
// ptr1 现在为空,不能再使用它
if (!ptr1) {
std::cout << "ptr1 is now null.\n";
}
// ptr2 仍然拥有 MyClass 的资源
// 在程序结束时,ptr2 将自动释放 MyClass 对象
return 0;
}
5. 为什么 std::auto_ptr 被弃用
-
所有权转移的问题:
std::auto_ptr的一个关键问题是它的拷贝构造函数和拷贝赋值运算符会隐式地转移所有权,这会导致意外的行为和 bug。开发者可能会错误地认为一个对象仍然拥有资源,而实际上的资源已经被转移给另一个对象,导致悬空指针或多次释放同一资源。 -
现代 C++ 的替代方案: 在 C++11 中,
std::unique_ptr被引入作为std::auto_ptr的替代品。与std::auto_ptr不同,std::unique_ptr不允许拷贝操作,只允许移动操作,避免了隐式所有权转移的风险,从而更符合现代 C++ 的设计理念。 -
移动语义:
std::unique_ptr支持移动语义,它允许资源的所有权在不同对象间转移,而不会引起混淆或潜在错误。
6. 替代品
-
std::unique_ptr:std::unique_ptr是std::auto_ptr的现代替代品,它通过明确的所有权控制来避免潜在的错误。std::unique_ptr不允许拷贝,只支持通过std::move转移所有权,因此避免了所有权转移时产生的问题。 -
std::shared_ptr: 对于共享所有权的场景,std::shared_ptr可以提供引用计数功能,使得多个指针可以共同拥有资源,而在最后一个指针被销毁时释放资源。
std::unique_ptr
std::unique_ptr 是 C++11 引入的智能指针类型,它为动态内存提供了更加安全和高效的管理方式。与 std::auto_ptr 相比,std::unique_ptr 引入了更严格的所有权管理机制,避免了隐式所有权转移的问题,并且仅支持通过移动语义来转移资源的所有权。这使得 std::unique_ptr 在现代 C++ 中成为管理动态内存的推荐方式。
定义与基本功能
std::unique_ptr 是一个模板类,用于管理指向动态分配内存的指针。它确保在其生命周期结束时自动释放资源,避免了开发者手动管理内存的复杂性。该类型的主要目标是明确地控制资源的所有权,确保资源只会被一个指针持有,避免内存泄漏和资源重复释放的风险。
template<
class T,
class Deleter = std::default_delete<T>
> class unique_ptr;
template <
class T,
class Deleter
> class unique_ptr<T[], Deleter>;
与 std::auto_ptr 不同,std::unique_ptr 不允许拷贝操作,它只支持移动操作。这样,std::unique_ptr 可以避免由于不明确的所有权转移所引发的错误和 bug。资源的所有权转移只能通过 std::move 完成,这使得程序员可以清晰地看到何时发生了所有权的转移,避免了拷贝构造和赋值操作带来的隐式问题。
资源管理的所有权控制
std::unique_ptr 的核心特性之一就是它持有资源的 唯一所有权。每个 std::unique_ptr 都确保它是唯一拥有所指向对象的所有者。当 std::unique_ptr 被销毁时,它会自动释放所管理的内存,无需开发者手动调用 delete,从而降低了内存泄漏的风险。由于不允许拷贝构造或拷贝赋值操作,std::unique_ptr 使得资源的所有权在多个指针之间的共享变得不可能,从而避免了多个指针指向同一资源时可能导致的多重释放或悬空指针问题。
例如,考虑以下代码片段:
std::unique_ptr<MyClass> ptr1(new MyClass());
std::unique_ptr<MyClass> ptr2 = std::move(ptr1); // 转移所有权
在这段代码中,ptr1 的所有权通过 std::move 转移给 ptr2,ptr1 变为一个空指针,无法再访问原始资源。资源的所有权不再在多个指针之间共享,因此 ptr1 不再需要释放资源。
移动语义
std::unique_ptr 支持 移动语义,意味着资源的所有权可以从一个 unique_ptr 移动到另一个 unique_ptr。通过移动构造函数或移动赋值操作,资源的所有权会被安全地转移,而不会引发不必要的复制操作。与拷贝构造不同,移动操作并不会增加资源的引用计数,也不会创建多个指针指向同一个对象。移动语义使得内存管理更加高效,并避免了性能上的冗余开销。
std::unique_ptr<MyClass> ptr1(new MyClass());
std::unique_ptr<MyClass> ptr2 = std::move(ptr1); // 使用移动语义转移所有权
这里,ptr1 的所有权被移动到 ptr2,ptr1 被置为空。这样,std::unique_ptr 能够在不产生冗余复制的情况下实现资源所有权的转移。
自动销毁与内存管理
std::unique_ptr 在其生命周期结束时会自动释放它所持有的资源。它的析构函数会在 unique_ptr 被销毁时自动调用所管理对象的析构函数,这意味着开发者无需手动管理内存的释放操作。通过这种方式,std::unique_ptr 减少了内存泄漏的可能性,也避免了因为忘记释放内存而导致的错误。
例如,在函数的作用域结束时,std::unique_ptr 会自动释放它管理的资源:
void createObject() {
std::unique_ptr<MyClass> ptr(new MyClass());
// 自动释放 ptr 管理的内存
}
当 createObject 函数结束时,ptr 会被销毁,它所管理的 MyClass 对象也会被自动释放。
不支持复制,支持移动
与 std::auto_ptr 不同,std::unique_ptr 设计上不支持拷贝构造和拷贝赋值操作,因此不会发生隐式所有权转移的问题。若需要转移所有权,开发者必须显式地使用 std::move 进行资源的转移,这使得代码中资源所有权的转移变得更加明确和安全。这种设计使得 std::unique_ptr 在资源管理上更加清晰,避免了由于误用或隐式操作而引发的错误。
std::unique_ptr<MyClass> ptr1(new MyClass());
std::unique_ptr<MyClass> ptr2 = ptr1; // 编译错误,不能拷贝
这段代码会编译错误,因为 std::unique_ptr 不允许拷贝构造。唯一的方式是使用 std::move 来进行所有权的转移:
std::unique_ptr<MyClass> ptr1(new MyClass());
std::unique_ptr<MyClass> ptr2 = std::move(ptr1); // 通过移动操作转移所有权
std::shared_ptr
std::shared_ptr 是 C++11 中引入的智能指针类型,用于管理动态分配的内存。它与 std::unique_ptr 和 std::auto_ptr 的主要区别在于,std::shared_ptr 支持 共享所有权,即多个 shared_ptr 可以共同拥有同一个资源。当最后一个持有该资源的 shared_ptr 被销毁时,资源会被自动释放。因此,std::shared_ptr 适用于需要多个指针共同管理同一资源的场景。
定义与基本功能
std::shared_ptr 是一个模板类,能够在多个 shared_ptr 之间共享资源的所有权, 其定义如下:
template< class T > class shared_ptr;
它通过 引用计数 机制来管理资源,确保在多个指针指向同一个对象时,资源只有在最后一个指针被销毁时才会被释放。每当一个新的 shared_ptr 被创建或拷贝时,引用计数会增加;当一个 shared_ptr 被销毁或被重置时,引用计数会减少。当引用计数降到 0 时,std::shared_ptr 会自动释放资源。
这种共享所有权的特性使得 std::shared_ptr 特别适用于需要多个对象共同管理资源的场景,而不必担心资源在多个指针之间的管理冲突。
引用计数机制
std::shared_ptr 的核心特点是它的 引用计数 机制。每个 shared_ptr 都维护着一个与资源关联的引用计数,记录当前有多少个 shared_ptr 正在共享同一资源。每当一个新的 shared_ptr 被拷贝或赋值时,引用计数增加;当一个 shared_ptr 被销毁时,引用计数减少。当引用计数降到 0 时,std::shared_ptr 会自动删除所指向的对象,从而避免了内存泄漏。
例如,考虑以下代码:
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> ptr2 = ptr1; // 引用计数增加
此时,ptr1 和 ptr2 都指向同一个 MyClass 对象,引用计数为 2。当 ptr1 或 ptr2 被销毁时,引用计数会减少。只有当最后一个 shared_ptr 被销毁时,所管理的对象才会被释放。
共享所有权
std::shared_ptr 允许 多个指针共享同一资源的所有权。这种共享所有权的特性使得它在一些需要多个对象共同访问资源的场景中非常有用。例如,在多线程环境中,多个线程可能需要访问共享资源,而 std::shared_ptr 可以确保该资源在没有明确的所有权控制的情况下得到正确管理。
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> ptr2 = ptr1; // 多个 shared_ptr 共享同一资源
在上面的例子中,ptr1 和 ptr2 都指向同一个对象,它们共享对资源的所有权,引用计数增加为 2。即使某个 shared_ptr 被销毁,另一个 shared_ptr 仍然可以访问资源。
自动销毁与内存管理
与 std::unique_ptr 相似,std::shared_ptr 会在其生命周期结束时自动释放它所管理的资源。资源的释放是通过引用计数机制来实现的,当最后一个指向资源的 shared_ptr 被销毁时,资源会自动释放,从而避免内存泄漏的风险。
这一点非常适合需要共享资源的场景,因为 shared_ptr 会自动管理资源的生命周期,不需要开发者显式地调用 delete。
void createObject() {
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> ptr2 = ptr1; // 引用计数增加
// ptr1 和 ptr2 都会在函数结束时自动销毁
// 当最后一个 shared_ptr 被销毁时,MyClass 对象将被自动删除
}
在上述示例中,ptr1 和 ptr2 都指向同一个 MyClass 对象。当这两个 shared_ptr 都超出作用域并被销毁时,引用计数变为 0,MyClass 对象会被自动销毁。
线程安全
std::shared_ptr 的引用计数机制是线程安全的,即多个线程可以安全地同时操作同一个 shared_ptr,而不必担心引用计数的竞争条件。这使得 std::shared_ptr 非常适合多线程程序中的共享资源管理。
然而,值得注意的是,虽然 std::shared_ptr 在引用计数方面是线程安全的,但并不是对资源本身的访问是线程安全的。如果多个线程需要同时访问资源的成员,则需要额外的同步机制(如互斥锁)来保护资源。
不支持循环引用
一个需要注意的事项是,std::shared_ptr 可能会导致 循环引用 的问题。如果两个或多个 shared_ptr 互相持有对方的引用,且它们的引用计数永远不会降到 0,那么这些资源将无法释放,从而导致内存泄漏。
例如,考虑以下代码:
struct Node {
std::shared_ptr<Node> next;
};
void createCircularReference() {
std::shared_ptr<Node> node1 = std::make_shared<Node>();
std::shared_ptr<Node> node2 = std::make_shared<Node>();
node1->next = node2;
node2->next = node1; // 形成循环引用
}
在这个例子中,node1 和 node2 互相持有对方的 shared_ptr,这将导致它们的引用计数永远不为 0,因此资源无法自动释放,造成内存泄漏。为了避免这种情况,可以使用 std::weak_ptr 来解决循环引用问题。
std::weak_ptr
std::weak_ptr 是 C++11 中引入的智能指针类型,它与 std::shared_ptr 紧密相关,用于解决共享指针管理过程中可能出现的 循环引用 问题。std::weak_ptr 本身不拥有所指向的对象,它只作为 std::shared_ptr 的辅助工具,允许观察某个对象而不改变其引用计数。因此,std::weak_ptr 可以防止由于多个 std::shared_ptr 相互引用而导致的内存泄漏。
定义与基本功能
std::weak_ptr 是一个模板类,它与 std::shared_ptr 配合使用,提供对对象的弱引用。弱引用不增加所管理对象的引用计数,因此不会阻止对象的销毁。std::weak_ptr 的主要功能是:它允许你“观察”一个 std::shared_ptr 管理的对象,而不会影响该对象的生命周期管理。通过 std::weak_ptr,你可以检查对象是否仍然存在,但不需要担心导致资源不会被销毁。
std::weak_ptr 的最大应用场景就是避免 循环引用,这是当两个或多个 std::shared_ptr 互相持有对方时所导致的问题,通常会使得对象无法被释放。
解决循环引用问题
循环引用是 std::shared_ptr 的一个典型问题。例如,当两个 std::shared_ptr 互相引用对方时,它们的引用计数永远不会降到零,导致对象无法被销毁,造成内存泄漏。为了避免这种情况,std::weak_ptr 可以作为一种“观察者”角色,用于打破循环引用。
考虑以下示例,假设 Node 类有一个指向另一个 Node 对象的指针,如果两个节点互相持有 std::shared_ptr,就会形成一个循环引用:
struct Node {
std::shared_ptr<Node> next;
};
void createCircularReference() {
std::shared_ptr<Node> node1 = std::make_shared<Node>();
std::shared_ptr<Node> node2 = std::make_shared<Node>();
node1->next = node2;
node2->next = node1; // 形成循环引用
}
在上述代码中,node1 和 node2 互相持有对方的 shared_ptr,导致它们的引用计数永远不会为 0,造成内存泄漏。为了解决这个问题,我们可以使用 std::weak_ptr 来消除循环引用。例如,改用 std::weak_ptr 来表示 node1 对 node2 的引用:
struct Node {
std::weak_ptr<Node> next; // 使用 weak_ptr 代替 shared_ptr
};
void createCircularReference() {
std::shared_ptr<Node> node1 = std::make_shared<Node>();
std::shared_ptr<Node> node2 = std::make_shared<Node>();
node1->next = node2;
node2->next = node1; // 现在不会形成循环引用
}
通过将 next 指针改为 std::weak_ptr,我们打破了循环引用。现在,当 node1 和 node2 被销毁时,它们的引用计数会正常降到 0,资源将被正确释放。
访问管理对象
尽管 std::weak_ptr 本身并不管理资源的生命周期,它可以通过 std::shared_ptr 来访问所管理的对象。当你想要访问一个由 std::weak_ptr 观察的对象时,必须先将其转换为 std::shared_ptr。这个转换过程称为 提升(lock),通过 std::weak_ptr::lock() 函数完成。该函数返回一个 std::shared_ptr,如果原始对象仍然存在,则该 shared_ptr 会指向它;否则,返回一个空的 shared_ptr。
std::shared_ptr<MyClass> ptr = weakPtr.lock();
if (ptr) {
// 使用 ptr 来访问 MyClass 对象
} else {
// 对象已被销毁
}
这里,lock() 方法会尝试从 std::weak_ptr 获取一个有效的 std::shared_ptr,如果对象已经被销毁(即引用计数为 0),则返回一个空的 shared_ptr。
不增加引用计数
std::weak_ptr 通过不增加引用计数来提供弱引用,因此它不会干扰对象生命周期的管理。它只用作对对象的观察者,让你能够安全地检查对象是否仍然存在。换句话说,std::weak_ptr 不控制对象的生命周期,只是“看”着它,并且通过 lock() 函数来查询对象是否有效。
std::shared_ptr<MyClass> sp1 = std::make_shared<MyClass>();
std::weak_ptr<MyClass> wp1 = sp1; // weak_ptr 不增加引用计数
// 当 sp1 超出作用域时,wp1 不再有效,且 MyClass 对象会被销毁
在这个例子中,wp1 观察了 sp1 管理的对象,但并没有增加引用计数。这样,当 sp1 被销毁时,wp1 变得无效,所管理的对象也会被正确释放。
转换为 std::shared_ptr
通常,我们使用 std::weak_ptr 来避免循环引用,但如果需要,我们可以将其转换回 std::shared_ptr 来访问资源。转换过程是通过 std::weak_ptr::lock() 完成的,这将返回一个新的 std::shared_ptr,并增加该资源的引用计数。如果资源已经被销毁(即引用计数为 0),则返回一个空的 std::shared_ptr。
std::shared_ptr<MyClass> ptr = wp.lock(); // 转换为 shared_ptr
if (ptr) {
// 使用 ptr 来访问 MyClass 对象
}
通过这种方式,我们可以在必要时访问由 std::weak_ptr 管理的对象,但前提是该对象仍然存在。
面向对象进阶
在这一阶段,需要解决的问题不再是“能不能用类”,而是“如何让类体系设计得更合理、更高效、更安全”。
这一部分的核心是深入理解对象间的关系与行为机制,理解编译器在背后为我们做了什么,从而能在面对复杂系统时,自信地控制对象生命周期、类型转换和继承结构。
-
运算符重载: 对象能否像内置类型一样使用
+、==、[]等运算符?怎样定义它们才能符合直觉又不出错?特别是赋值运算符、比较运算符的语义要与类设计保持一致。 -
继承与派生结构: 为什么要继承?什么时候该用继承、什么时候该用组合?多继承会引发哪些问题(比如二义性和菱形继承)?虚继承又是如何解决的?
-
虚函数与多态: 运行时多态是如何实现的?虚表(vtable)是怎么工作的?析构函数为什么要定义为虚函数?什么场景下多态反而会带来性能或逻辑问题?
-
友元与类关系: 当类之间需要共享私有数据时,友元机制如何使用?为什么要谨慎使用友元?在大型项目中,友元往往是“设计不合理”的信号还是必要的桥梁?
-
模板高级应用: 模板不只是泛型编程,它还能实现多态、类型推导、SFINAE 等复杂行为。要理解模板与继承、多态的结合——如何用模板替代虚函数以获得更高性能。
-
类型转换与转换运算符: 从基本类型到类类型的转换(构造函数隐式转换),从类类型到基本类型(转换运算符)之间隐藏着哪些风险?如何避免意外转换或歧义?
-
拷贝控制: 拷贝构造、赋值运算符、移动构造、移动赋值和析构函数的完整语义——RAII 的核心。理解“拷贝与移动”意味着理解对象的完整生命周期。
-
C++新特性在面向对象中的影响: 现代C++(C++11及之后)的特性,如智能指针、右值引用、
override、final、=default、=delete等,改变了我们对类设计的传统思维。要掌握如何用这些新特性强化封装性和安全性。
这一章的目标,不是单纯记住语法,而是要能回答:
“当我写下一个类、继承一个父类、重载一个函数时,编译器究竟在背后做了什么?”
运算符重载(Operator Overloading)
一、概述
C++ 是一种支持面向对象编程的语言,在其类型系统中,除了内置类型(如 int、double)外,用户还可以自定义类(Class)来表示复杂数据结构。
然而,类的对象默认并不能像内置类型那样使用 +、==、<< 等运算符。为了让自定义类型具备直观、自然的操作方式,C++ 允许程序员重定义(overload)运算符的行为,使得运算符能作用于类的对象。
这种机制称为运算符重载(Operator Overloading)。它是 C++ 多态性的重要体现之一,使自定义类型可以拥有与内置类型一致的使用体验。
二、运算符重载的定义形式
运算符重载的本质是一个函数定义,它的函数名以 operator 开头,后接要重载的运算符符号。
基本形式如下:
返回类型 operator运算符(参数列表);
运算符重载函数既可以定义为类的成员函数,也可以定义为友元函数(friend function)。
对于成员函数形式,函数的第一个操作数是当前对象本身(通过隐式的 this 指针传递);
对于友元函数形式,则需要显式地接收两个参数。
三、可重载与不可重载的运算符
并非所有运算符都可以被重载。C++ 出于语言安全性和可维护性的考虑,对部分运算符做出了限制。
| 可重载的运算符 | 不可重载的运算符 |
|---|---|
+ - * / % | . (成员访问) |
== != < > <= >= | :: (作用域解析) |
+= -= *= /= | sizeof |
[] () -> | ?: (条件运算符) |
<< >> (常用于输入输出) | 类型转换运算符(如 typeid、dynamic_cast) |
此外,重载不会改变运算符的优先级和结合性,也不能引入新的运算符符号。
四、成员函数与友元函数重载的区别
-
成员函数形式 适用于当左操作数是类对象时。例如:
Complex a, b, c; c = a + b; // 调用 a.operator+(b)在这种情况下,函数原型为:
Complex operator+(const Complex& rhs) const; -
友元函数形式 当运算符左边不是该类的对象,或者需要访问私有成员时,使用友元函数更合适。
friend Complex operator+(const Complex& lhs, const Complex& rhs); -
主要区别
| 项目 | 成员函数 | 友元函数 |
|---|---|---|
| 左操作数 | 必须是类对象 | 可以不是类对象 |
| 调用形式 | a.operator+(b) | operator+(a, b) |
| 访问权限 | 可直接访问成员 | 若声明为 friend,可访问私有成员 |
| 适用场景 | 一般用于类内操作 | 用于输入输出、双操作数等场合 |
这两种形式在使用时完全一致,可以直接使用a+b调用重载的符号。
五、算术运算符的重载
算术运算符(如 +、-、*、/)是最常见的重载形式之一。
下面以复数类 Complex 为例,说明如何重载加法运算符 +。
#include <iostream>
class Complex {
private:
double real, imag;
public:
Complex(double r=0, double i=0) : real(r), imag(i) {}
Complex operator+(const Complex& rhs) const {
return Complex(real + rhs.real, imag + rhs.imag);
}
void display() const {
std::cout << real << " + " << imag << "i" << std::endl;
}
};
int main() {
Complex a(2.0, 3.0), b(1.5, 2.0);
Complex c = a + b; // 调用 a.operator+(b)
c.display(); // 输出 3.5 + 5i
}
该示例中,运算符函数返回一个新的对象,不改变原操作数的内容。
六、比较运算符的重载
对象之间的比较并不会自动发生。若要实现“对象是否相等”的判断,需要重载比较运算符。
示例:
class Point {
private:
int x, y;
public:
Point(int a, int b) : x(a), y(b) {}
bool operator==(const Point& rhs) const {
return x == rhs.x && y == rhs.y;
}
bool operator!=(const Point& rhs) const {
return !(*this == rhs);
}
};
int main() {
Point p1(1, 2), p2(1, 2), p3(2, 3);
if (p1 == p2) std::cout << "p1 and p2 are equal" << std::endl;
if (p1 != p3) std::cout << "p1 and p3 are not equal" << std::endl;
}
比较运算符通常返回布尔值,并应保证逻辑自洽性(如 == 和 != 应互为补充)。
七、赋值运算符的重载
C++ 默认会生成一个“浅拷贝”的赋值运算符,这在涉及动态内存管理的类中往往会引发问题。
因此,通常需要手动重载 operator=,以实现深拷贝(deep copy)。
#include <cstring>
class String {
private:
char* data;
public:
String(const char* s = "") {
data = new char[strlen(s) + 1];
strcpy(data, s);
}
String& operator=(const String& rhs) {
if (this != &rhs) { // 防止自赋值
delete[] data;
data = new char[strlen(rhs.data) + 1];
strcpy(data, rhs.data);
}
return *this; // 返回自身引用
}
~String() { delete[] data; }
};
重载赋值运算符的注意事项:
- 检查自赋值(防止对象赋给自己)。
- 正确释放旧内存并重新分配。
- 返回
*this的引用以支持链式赋值。
八、输入输出运算符的重载
流插入运算符 << 和流提取运算符 >> 无法作为成员函数重载,因为左操作数通常是标准流对象(如 std::cout 或 std::cin)。
因此,通常将它们重载为友元函数。
#include <iostream>
using namespace std;
class Complex {
private:
double real, imag;
public:
Complex(double r=0, double i=0) : real(r), imag(i) {}
friend ostream& operator<<(ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}
friend istream& operator>>(istream& is, Complex& c) {
is >> c.real >> c.imag;
return is;
}
};
int main() {
Complex c1;
cin >> c1;
cout << c1 << endl;
}
这种方式使得自定义类对象可以与标准输入输出流无缝交互。
九、自增与自减运算符的重载
C++ 区分前置与后置两种形式:
- 前置
++a:函数无形参; - 后置
a++:函数带一个虚拟的int形参,用于区分。
class Counter {
private:
int value;
public:
Counter(int v = 0) : value(v) {}
Counter& operator++() { // 前置 ++
++value;
return *this;
}
Counter operator++(int) { // 后置 ++
Counter temp = *this;
++value;
return temp;
}
int get() const { return value; }
};
继承
继承(Inheritance)是面向对象编程的三大特征之一(封装、继承、多态)之一,它使得我们可以在已有类的基础上创建新的类,从而实现代码复用与层次结构的抽象建模。
C++ 的继承机制极其灵活,既可以实现简单的单继承(Single Inheritance),也可以进行复杂的多继承(Multiple Inheritance),甚至支持通过**虚继承(Virtual Inheritance)**来解决多重继承带来的“菱形问题”。
一、继承的基本概念
继承是从一个已有的类(称为基类 / 父类 Base Class)派生出一个新的类(称为派生类 / 子类 Derived Class)。 派生类自动拥有基类的成员(数据与函数),并可以在此基础上新增成员或重写行为。
语法格式如下:
class 派生类名 : 继承方式 基类名 {
// 派生类成员
};
二、基类构造与派生类构造顺序
派生类对象中包含了基类子对象,因此在创建派生类实例时,必须先调用基类构造函数来完成基类部分的初始化。
调用顺序如下:
- 按声明顺序调用所有基类构造函数(从上到下)。
- 再调用派生类自身构造函数。
销毁顺序则相反: 先调用派生类析构函数,再调用基类析构函数。
示例:
class Base {
public:
Base() { std::cout << "Base constructed\n"; }
~Base() { std::cout << "Base destroyed\n"; }
};
class Derived : public Base {
public:
Derived() { std::cout << "Derived constructed\n"; }
~Derived() { std::cout << "Derived destroyed\n"; }
};
int main() {
Derived d;
}
输出结果为:
Base constructed
Derived constructed
Derived destroyed
Base destroyed
若基类构造函数需要参数,必须在派生类的构造函数初始化列表中显式调用:
class Base {
public:
Base(int x) { std::cout << "Base(" << x << ")\n"; }
};
class Derived : public Base {
public:
Derived(int x) : Base(x) {
std::cout << "Derived(" << x << ")\n";
}
};
构造函数调用顺序是从“上至下”,析构顺序是从“下至上”。 这种严格的顺序保证了对象的完整性和资源的正确释放。
三、访问控制与继承权限
访问控制是继承体系中非常重要的机制,它决定了哪些成员可以在派生类中被访问。
继承方式主要有三种:
| 继承方式 | 基类 public 成员在派生类中的访问属性 | 基类 protected 成员在派生类中的访问属性 | 特点说明 |
|---|---|---|---|
public 继承 | public → public | protected → protected | 常用方式,保持原有访问级别 |
protected 继承 | public → protected | protected → protected | 用于希望限制外部访问但允许派生使用的情况 |
private 继承 | public → private | protected → private | 派生类对外隐藏基类接口 |
-
public 成员
- 在
public继承中保持public,外部依然可以访问。 - 在
protected/private继承中则变为不可外部访问。
- 在
-
protected 成员
- 在
public/protected继承中可被派生类访问。 - 在
private继承中仅在本类可访问。
- 在
-
private 成员
- 永远不能被派生类直接访问。
class Base {
public:
int a;
protected:
int b;
private:
int c;
};
class Derived : public Base {
public:
void show() {
a = 1; // 可访问(public继承下保持public)
b = 2; // 可访问(protected继承下保持protected)
// c = 3; // 不可访问(private成员永远不能被继承访问)
}
};
在继承中,private 成员虽然被继承,但不可直接访问,只能通过基类的 public 或 protected 接口间接访问。
四、单继承与多继承
1. 单继承
最常见的继承方式,一个派生类只有一个直接基类:
class Animal {
public:
void eat() { std::cout << "Eating\n"; }
};
class Dog : public Animal {
public:
void bark() { std::cout << "Barking\n"; }
};
Dog 继承了 Animal 的所有非私有成员,因此:
Dog d;
d.eat(); // 继承自 Animal
d.bark(); // 自身成员
2. 多继承
C++ 支持一个类继承自多个基类,从而组合多种功能:
class A {
public:
void funcA() { std::cout << "A\n"; }
};
class B {
public:
void funcB() { std::cout << "B\n"; }
};
class C : public A, public B {
public:
void funcC() { std::cout << "C\n"; }
};
C 同时拥有 A 和 B 的成员:
C obj;
obj.funcA();
obj.funcB();
obj.funcC();
但多继承容易引入命名冲突:
class A { public: void show() { std::cout << "A\n"; } };
class B { public: void show() { std::cout << "B\n"; } };
class C : public A, public B {};
C c;
c.show(); // 二义性错误:不知该调用 A::show 还是 B::show
需使用作用域限定符解决:
c.A::show();
五、多重继承的菱形问题
多继承中最著名的问题是“菱形继承问题(Diamond Problem)”。
示例:
class A {
public:
int value = 1;
};
class B : public A {};
class C : public A {};
class D : public B, public C {};
继承关系如下:
A
/ \
B C
\ /
D
此时,D 同时继承了两份 A,因此 value 出现二义性:
D d;
d.value = 10; // 二义性:B::A::value 与 C::A::value 冲突
六、虚继承(Virtual Inheritance)
为解决菱形问题,C++ 引入了 虚继承(Virtual Inheritance)。
通过在继承声明前加上关键字 virtual,可以让共同的基类只保留一份共享副本。
修改上例:
class A {
public:
int value = 1;
};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
此时:
D d;
d.value = 10; // 不再二义性,A仅保留一份
在对象布局上,编译器会通过额外的“虚基表指针(vbptr)”来实现共享基类的唯一性。 因此,虚继承的对象模型更复杂,但解决了最棘手的多继承冲突问题。
虚基表指针(vbptr)如何实现共享基类的唯一性?
虚基表指针(vbptr)如何实现共享基类的唯一性?
在普通多继承中,如果一个基类被多个派生类重复继承(如“菱形继承”结构),最底层派生类对象中会包含多份相同的基类成员,造成 数据冗余 和 二义性。
为了解决这一问题,C++ 提供了 虚继承(virtual inheritance)。编译器通过在对象中引入一套特殊的指针机制——虚基表指针(vbptr) 和 虚基表(vbtable),来确保所有派生路径最终共享同一个虚基类实例。
- 当一个类以
virtual方式继承基类时,编译器会在该类的对象布局中添加一个隐藏成员:vbptr。 vbptr指向一张由编译器生成的 虚基表(vbtable)。vbtable中记录了从当前对象地址到虚基类子对象地址的偏移量。- 当程序访问虚基类的成员时,编译器通过
vbptr查表计算出正确的虚基类位置,确保所有继承路径共享同一个虚基类实例。
class A { int x; };
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
在 D 的对象布局中:
D
├── B 子对象(含 vbptr → 指向 B 的 vbtable)
├── C 子对象(含 vbptr → 指向 C 的 vbtable)
└── A 虚基类子对象(唯一一份)
访问 A::x 时:
- 若通过
B或C访问,编译器都会通过vbptr找到唯一的A子对象; - 从而避免了重复继承带来的“二义性”与“多份拷贝”问题。
虚继承常用于框架或接口设计中,尤其当多个中间层类共享同一顶层基类时。
虚函数与多态
多态(Polymorphism)是面向对象的核心特征之一,它让我们可以通过统一的接口来操作不同类型的对象。C++ 的多态是通过 虚函数(virtual function)机制 实现的,它使得函数调用在运行时(而非编译时)才确定具体执行哪个版本——这被称为 动态绑定(Dynamic Binding)。
一、虚函数表机制(vtable)
理解虚函数,必须先理解它背后的实现机制:虚函数表(Virtual Table)。
当一个类中包含虚函数时,编译器会为该类自动生成一张“虚函数表”,记录所有虚函数的地址。 每个含虚函数的对象中,会隐式包含一个指向这张表的指针(称为 vptr)。
- 生成虚函数表(
vtable): 为该类自动生成一张只读表,其中记录了所有虚函数的地址。 - 植入虚指针(
vptr): 每个含虚函数的对象中,会隐式包含一个指向这张表的指针(vptr)。在主流编译器中,vptr通常是对象内存布局中的第一个成员。
换句话说,每个对象在内存中都携带一张“函数地址目录”,在调用虚函数时,程序会通过这张表找到该对象对应的函数实现。
一个典型的过程如下:
- 为该类生成一张虚函数表(vtable)。
- 每个对象内部会有一个指针(vptr),在构造函数中初始化,指向所属类的 vtable。
- 当通过基类指针或引用调用虚函数时,程序会查找 vtable 来决定实际执行哪个函数。
例如:
class Base {
public:
virtual void show() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void show() override { std::cout << "Derived\n"; } // C++11 推荐使用 override
};
Base* p = new Derived();
p->show(); // 输出:Derived
编译器生成的伪逻辑相当于:
(*(p->vptr[0]))(p); // 从 vtable[0] 取出函数指针并调用
这就是 动态绑定 的本质。
二、纯虚函数与抽象类
有时,基类只定义接口而不提供具体实现,这时就需要 纯虚函数(pure virtual function)。
纯虚函数的定义方式是在声明末尾加上 = 0:
class Shape {
public:
// 纯虚函数,只提供接口,没有实现体
virtual void draw() = 0;
};
当类中含有至少一个纯虚函数时,该类就成为 抽象类(Abstract Class)。
抽象类不能被实例化,只能被继承。
它的作用是定义一个统一的接口,让派生类去实现具体行为。
例如:
class Shape {
public:
virtual void draw() = 0;
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing Circle\n"; } // 必须实现 draw()
};
// Shape s; // 错误:不能实例化抽象类
Circle c; // 正确:Circle 实现了所有纯虚函数
Shape 不能直接创建对象,但 Circle 可以,因为它实现了 draw()。
这种方式让我们能通过“基类接口”统一管理不同对象,实现真正意义上的多态。
三、动态绑定 vs 静态绑定
静态绑定(Static Binding) 是在编译阶段就决定函数调用的目标,依据是变量的声明类型。 动态绑定(Dynamic Binding) 则在运行时根据对象的实际类型决定调用哪个函数。
| 绑定类型 | 发生时机 | 调用依据 | 示例 |
|---|---|---|---|
| 静态绑定 | 编译期 | 变量声明类型 | 非虚函数调用、重载函数 |
| 动态绑定 | 运行期 | 对象实际类型 | 虚函数调用 |
示例:
class Base {
public:
void func() { std::cout << "Base::func\n"; }
virtual void vfunc() { std::cout << "Base::vfunc\n"; }
};
class Derived : public Base {
public:
void func() { std::cout << "Derived::func\n"; }
void vfunc() override { std::cout << "Derived::vfunc\n"; }
};
Base* p = new Derived();
p->func(); // 静态绑定 -> 调用 Base::func (基类指针只能看到基类声明的非虚函数)
p->vfunc(); // 动态绑定 -> 调用 Derived::vfunc (通过 vtable 确定实际类型)
强烈建议在派生类中重写虚函数时使用 override 关键字。
override 显式告知编译器该函数意图是重写基类的虚函数。如果签名(函数名、参数、const 性)与基类不匹配,编译器将报错,从而避免了重写失败导致的行为错误。
四、析构函数的虚函数问题
一个常见的陷阱:基类的析构函数必须声明为虚函数。
否则当通过基类指针 delete 派生类对象时,如果析构函数不是虚函数,将发生静态绑定,只会调用基类的析构函数。
会导致派生类中特有的资源(如动态分配的内存、文件句柄等)将不会被释放,导致内存泄漏或资源泄漏。
class Base {
public:
// 必须是虚函数,确保通过基类指针删除时能触发动态绑定
virtual ~Base() { std::cout << "Base destroyed\n"; }
};
class Derived : public Base {
private:
int* data;
public:
Derived() : data(new int[10]) {}
~Derived() override {
delete[] data; // 派生类特有的资源释放
std::cout << "Derived destroyed\n";
}
};
Base* p = new Derived();
delete p; // 1. 动态绑定调用 Derived::~Derived() 2. 调用 Base::~Base()
// 保证了所有资源的正确释放
五、虚函数的开销
虚函数机制提供了强大的灵活性(多态),但它不是零代价的。使用虚函数会带来一定的性能和内存开销:
- 空间开销 (对象层面): 每个含有虚函数的对象,都会增加一个
vptr指针的存储空间(在 64 位系统上通常是 8 字节)。 - 空间开销 (类层面): 每个含有虚函数的类都需要生成一张
vtable来存储函数指针。 - 时间开销: 虚函数的调用比普通函数(静态绑定)多了一步间接寻址(查找
vptr\(\to\) 访问vtable\(\to\) 调用函数)。虽然开销微小,但在高度性能敏感的循环中,可能会产生可见的影响。
因此,只有当确实需要多态特性时,才应该将函数声明为虚函数。
友元与类关系
友元函数与友元类 类的组合与聚合
类模板与函数模板高级应用
模板特化与偏特化 模板与继承结合
类型转换与转换运算符
显式转换
转换构造函数与 operator type()
拷贝控制
拷贝构造函数 赋值运算符 析构函数 Rule of Three / Five / Zero
C++新特性
移动语义与移动构造函数
右值引用与 std::move
constexpr、noexcept 与类设计
STL
C++ 标准模板库(Standard Template Library,STL)是一套功能强大的 C++ 模板类和函数的集合,基于泛型编程,因此适合各种数据结构,它提供了一系列通用的、可复用的算法和数据结构。将STL结合到程序中可以更方便地编写出更简洁且优质的代码。
| 组件 | 描述 |
|---|---|
| 容器 (Containers) | 容器是 STL 中最基本的组件之一,提供了各种数据结构,包括向量(vector)、链表(list)、队列(queue)、栈(stack)、集合(set)、映射(map)等。这些容器具有不同的特性和用途,可以根据实际需求选择合适的容器。 |
| 算法 (Algorithms) | STL 提供了大量的算法,用于对容器中的元素进行各种操作,包括排序、搜索、复制、移动、变换等。这些算法在使用时不需要关心容器的具体类型,只需要指定要操作的范围即可。 |
| 迭代器 (Iterators) | 迭代器用于遍历容器中的元素,允许以统一的方式访问容器中的元素,而不用关心容器的内部实现细节。STL 提供了多种类型的迭代器,包括随机访问迭代器、双向迭代器、前向迭代器和输入输出迭代器等。 |
| 函数对象 (Function Objects) | 函数对象是可以像函数一样调用的对象,可以用于算法中的各种操作。STL 提供了多种函数对象,包括一元函数对象、二元函数对象、谓词等,可以满足不同的需求。 |
| 适配器 (Adapters) | 适配器用于将一种容器或迭代器适配成另一种容器或迭代器,以满足特定的需求。STL 提供了多种适配器,包括栈适配器(stack adapter)、队列适配器(queue adapter)和优先队列适配器(priority queue adapter)等。 |
Utility
在 C++ 中,有一些实用的工具类和函数,这些工具类和函数在编写高效、可读性强的代码时非常有用且大多是C++新特性
| 工具名称 | 说明 | 所属头文件 | C++版本引入 |
|---|---|---|---|
std::pair | 存储两个相关值的通用模板类。 | <utility> | C++98 |
std::make_pair | 辅助函数,用于创建 std::pair。 | <utility> | C++98 |
std::move | 将对象显式转换为右值引用,用于触发移动语义。 | <utility> | C++11 |
std::forward | 完美转发函数,用于保持值类别(左值/右值)。 | <utility> | C++11 |
std::swap | 通用交换函数,支持用户自定义类型的交换。 | <utility> | C++98 |
std::tuple | 可变长异质容器,存储多个不同类型的值。 | <tuple> | C++11 |
std::make_tuple | 辅助函数,用于创建 std::tuple。 | <tuple> | C++11 |
std::tie | 将多个左值绑定为 tuple,用于结构化绑定或比较。 | <tuple> | C++11 |
std::ignore | 与 std::tie 搭配,用于忽略某个绑定的值。 | <tuple> | C++11 |
std::optional | 表示一个可选值,可能含值也可能为空。 | <optional> | C++17 |
std::variant | 类型安全的联合体,可以在多个候选类型中存储其中之一。 | <variant> | C++17 |
std::any | 类型擦除的容器,可以存放任意类型的值,并在运行时安全提取。 | <any> | C++17 |
std::function | 通用函数包装器,可存储可调用对象(函数指针、lambda、仿函数等)。 | <functional> | C++11 |
std::bind | 绑定函数参数,生成新的可调用对象。 | <functional> | C++11 |
std::ref | 将对象包装为引用,以便在需要值语义时保持引用语义。 | <functional> | C++11 |
std::cref | 常量引用版本的 std::ref。 | <functional> | C++11 |
std::pair
std::pair 是一个结构体模板,其可于一个单元内存储两个相异对象。是 std::tuple 的拥有两个元素的特殊情况。一般来说,pair 可以封装任意类型的对象,可以生成各种不同的 std::pair<T1, T2> 对象,可以是数组对象或者包含 std::pair<T1,T2> 的 vector 容器。pair 还可以封装两个序列容器或两个序列容器的指针。
1. 引入
被包括在<utility>头文件中。
#include <utility>
2. 存储方式
std::pair 存储两个对象,分别通过 first 和 second 成员访问。std::pair 自动将初始化参数的值赋给 first 和 second。
3. 方法
(1)构造方法
-
默认构造函数:创建一个未初始化的
std::pair对象。std::pair<int, std::string> myPair; -
值构造函数:通过传入两个对象初始化
std::pair。std::pair<int, std::string> myPair(1, "example"); -
工厂函数
make_pair:生成std::pair对象。auto anotherPair = std::make_pair(2, "test");
(2)成员访问
- first:访问或修改
std::pair的第一个元素。 - second:访问或修改
std::pair的第二个元素。
(3)比较操作符
- 相等 (
==) 和不相等 (!=):比较两个std::pair对象是否相等。 - 小于 (
<) 和大于 (>)和小于等于 (<=) 和大于等于 (>=):根据first和second的字典顺序比较两个std::pair对象。
(4)交换
-
swap:交换两个
std::pair对象的值。myPair.swap(anotherPair);
std::move
将一个对象的所有权从一个地方(特指引用或是指针)转移到另一个地方,相当于rust语言的所有权转让,对于std::string或是更复杂的数据结构使用std::move转移所有权可以比直接复制有着更高的效率。
其模板定义如下:
template< class T >
typename std::remove_reference<T>::type&& move( T&& t ) noexcept; // (since C++11 and until C++14)
template< class T >
constexpr std::remove_reference_t<T>&& move( T&& t ) noexcept; // (since C++14)
1. 引入
被包括在<utility>头文件中。
#include <utility>
2. std::move的工作原理
std::move并不真正“移动”对象的数据,它的作用是将一个左值引用转化为右值引用,从而启用移动语义。在C++中,右值引用和左值引用的主要区别在于右值引用允许资源的所有权转移,而左值引用则保持资源的所有权。
通过使用std::move,编译器能够区分何时使用拷贝构造(复制数据)和移动构造(转移所有权),从而优化程序性能,特别是在涉及大量数据拷贝的场景中。
(1)右值引用与左值引用
- 左值:表示可以被引用且具有持久地址的对象(例如,变量)。
- 右值:表示临时对象或可以被销毁的对象(例如,常量、字面值、函数返回值)。
std::move的作用是将左值转换为右值引用,允许资源的移动。
(2)为什么需要std::move?
C++11引入了移动语义(Move Semantics),目标是提高性能。复制一个大对象时,可能涉及到大量的内存分配和数据拷贝,而如果能将资源的所有权直接转移给另一个对象,就可以避免这些开销。std::move通过将左值转换为右值引用,促使移动构造函数或移动赋值运算符被调用。
例如:
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // 这里v2会接管v1的所有权,v1会处于一个"空"状态
在这个例子中,std::move将v1转换为右值引用,从而触发了移动构造函数。结果是,v2接管了v1的数据,而v1进入了一个有效但不确定的状态。
3. 使用std::move的场景
(1)移动构造与移动赋值
在使用std::move时,通常会看到它与移动构造函数和移动赋值运算符一起使用。
- 移动构造函数:当一个对象通过右值引用初始化另一个对象时,调用移动构造函数,通常用于资源的转移。
- 移动赋值运算符:当一个对象被右值引用赋值给另一个已存在的对象时,调用移动赋值运算符。
例如:
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // 使用移动构造函数
对于赋值操作:
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2;
v2 = std::move(v1); // 使用移动赋值运算符
不管是哪一种操作,v1都会被剥夺所有权。
(2)自定义类型与std::move
如果你有自定义类型,并希望通过移动语义来优化性能,那么你需要为这个类型定义移动构造函数和移动赋值运算符。这些函数通常需要显式地将资源的所有权从一个对象转移到另一个对象。
例如:
class MyClass {
public:
MyClass(MyClass&& other) noexcept {
// 移动构造函数
this->data = std::move(other.data); // 移动数据
}
MyClass& operator=(MyClass&& other) noexcept {
// 移动赋值运算符
if (this != &other) {
this->data = std::move(other.data); // 移动数据
}
return *this;
}
private:
std::vector<int> data;
};
4. std::move的常见误用
(1)移动之后使用原对象
在将对象通过std::move转移所有权之后,原对象的状态是未定义的。因此,不应再使用原对象,例如:
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);
// v1现在处于未定义状态,不能再安全使用
std::cout << v1.size(); // 错误
正确的做法是,只在转移所有权后“放弃”原对象的使用。
(2)不必要的使用std::move
std::move的作用是告诉编译器“这不是一个左值,你可以安全地移动它”,但是如果你已经明确知道对象不会被复制(例如返回一个对象的右值引用),就不必使用std::move。
例如:
std::vector<int> createVector() {
std::vector<int> v = {1, 2, 3};
return v; // 编译器会自动进行返回值优化(RVO),不需要手动使用std::move
}
这里的返回值优化(RVO)允许编译器在返回v时进行移动,而不需要显式调用std::move。
std::optional
std::optional 是 C++17 引入的一个工具类,用来表示一个 可选值。它可以包含某个类型的值,也可以为空(即没有值)。这在函数返回值和状态表达上尤其有用,可以避免使用特殊标志值(如 -1 或 nullptr)来表示“无效”或“不存在”的情况。它本质上是对 “可空值” 概念的抽象:
- 有值 (engaged):存储一个
T类型对象,保证其生命周期绑定到optional对象上。 - 无值 (disengaged):表示空状态,此时不存储任何对象。
相比于传统的返回 nullptr 或 std::pair<T,bool> 表示失败,std::optional 更加语义化、类型安全,尤其适合构造成本较高的对象。
其模板定义如下:
template< class T >
class optional;
1. 引入
被包括在 <optional> 头文件中。
#include <optional>
T 必须满足 Destructible(可析构)。
不允许是引用类型、数组类型、函数类型或 void。
从 C++26 起,optional<T> 还支持作为 range view。
2. std::optional 的工作原理
optional<T>内部通过一个 布尔标志 + 可能存在的值存储区 来实现:- 当 engaged 时,
optional内部构造并管理一个T对象。 - 当 disengaged 时,标志为 false,存储区未构造对象。
- 当 engaged 时,
这使得 optional 更接近于 “可能为空的对象”,而不是指针。
即使提供了 operator* 和 operator->,它依旧是值语义而非指针语义。
(1) 为什么需要 std::optional?
传统做法:
- C 风格函数通常用返回值来表示错误,用输出参数来返回数据。
- 某些场景需要用
nullptr或-1表示“无效”,但这并不类型安全。
std::optional 提供了一个清晰、安全的方式:返回“要么有值,要么没有值”。
(2) 基本使用
std::optional<int> maybeInt; // 默认构造:无值
std::optional<int> hasInt = 42; // 构造:有值
std::optional<int> emptyInt = std::nullopt; // 显式无值
if (hasInt) {
std::cout << "Value: " << *hasInt << "\n"; // 解引用访问
}
3. 使用 std::optional 的场景
(1) 函数返回值
最常见的场景是函数可能“有值”或“无值”。
std::optional<int> findValue(const std::vector<int>& vec, int target) {
for (auto v : vec) {
if (v == target) return v;
}
return std::nullopt;
}
auto result = findValue({1, 2, 3}, 2);
if (result) {
std::cout << "Found: " << *result << "\n";
}
(2) 延迟初始化
std::optional 可以用来延迟对象构造,仅在需要时才赋值。
std::optional<std::string> cache;
if (!cache) {
cache = "Hello"; // 仅在需要时才构造
}
(3) 配合结构化绑定
std::optional<std::pair<int, int>> divide(int a, int b) {
if (b == 0) return std::nullopt;
return std::make_pair(a / b, a % b);
}
if (auto res = divide(10, 3)) {
auto [q, r] = *res;
std::cout << "Quotient: " << q << " Remainder: " << r << "\n";
}
4. 常用操作
(0) 构造与析构
// 默认构造:无值
std::optional<int> a;
// 从值构造
std::optional<int> b = 42;
std::optional<std::string> c{"hello"};
// 拷贝构造 / 赋值
std::optional<int> d = b;
// 移动构造 / 赋值
std::optional<std::string> e = std::move(c);
// 使用 nullopt 构造空对象
std::optional<int> f = std::nullopt;
// 析构:生命周期结束时自动析构其中的值(如果有值)
{
std::optional<std::string> g{"RAII"};
} // g 的析构会自动调用 std::string 的析构函数
(1) value() 与 value_or()
std::optional<int> x = 10;
std::cout << x.value() << "\n"; // 获取值,若为空抛出 std::bad_optional_access
std::cout << x.value_or(0) << "\n"; // 若为空返回默认值
(2) emplace() 与 reset()
std::optional<std::string> name;
name.emplace("Alice"); // 原地构造值
std::cout << *name << "\n";
name.reset(); // 清空,变为无值
(3) 布尔上下文与 has_value()
std::optional<int> maybe;
if (maybe) { // 等价于 maybe.has_value()
std::cout << "有值\n";
} else {
std::cout << "无值\n";
}
(4) 访问操作符 operator* 与 operator->
std::optional<std::string> msg = "Hello";
std::cout << (*msg).size() << "\n"; // 解引用访问
std::cout << msg->size() << "\n"; // 箭头访问
(5) swap()
std::optional<int> a = 1, b = 2;
a.swap(b); // 交换内容
std::cout << *a << " " << *b << "\n"; // 输出 2 1
(6) 工厂函数 std::make_optional 与 std::nullopt
auto a = std::make_optional<int>(42); // 创建有值 optional
std::optional<int> b = std::nullopt; // 创建空 optional
(7) 常见 value_or 模式
std::optional<std::string> maybe_name;
std::string name = maybe_name.value_or("Default"); // 若无值,使用默认值
(8) C++23 单子操作(Monadic operations)
std::optional<int> num = 5;
// and_then: 仅在有值时继续计算
auto squared = num.and_then([](int x) { return std::optional<int>(x * x); });
// transform: 值存在时映射为新值
auto str = num.transform([](int x) { return std::to_string(x); });
// or_else: 若无值则提供替代
auto val = num.or_else([] { return std::optional<int>(100); });
5. std::optional 的常见误用
(1) 滥用在“必然有值”的场景
如果某个值在逻辑上 必然存在,使用 optional 反而多此一举,降低可读性。
(2) 忘记检查是否有值
错误示例:
std::optional<int> x;
std::cout << *x; // 未检查直接解引用,未定义行为
正确示例:
if (x) std::cout << *x;
(3) 不当替代指针或容器
std::optional 适合表示“零或一”的情况,但如果可能有多个值,应该用 std::vector 或其他容器,而不是 optional<std::vector<T>> 滥用。
6. 与 Rust 的类比
- Rust 的
Option<T>与 C++ 的std::optional<T>类似,都是“要么有值(Some),要么无值(None)”。 - 不同点:Rust 强制模式匹配,保证空值情况不会被遗漏,而 C++ 中
optional需要程序员手动检查。
std::any
std::any 是 C++17 引入的一个工具类,用来表示 类型安全的任意类型容器。它可以存储任何 可拷贝构造 的类型,并且在运行时动态管理其类型和生命周期。std::any 常用于需要存储不同类型对象,但在编译时无法确定类型的场景。
- 有值 (engaged):存储一个特定类型的对象。
- 无值 (disengaged):表示空状态,没有存储任何对象。
相比于 void* 或不安全的类型转换,std::any 提供了类型安全的访问和异常检测机制。
其模板定义如下:
class any;
1. 引入
被包括在 <any> 头文件中。
#include <any>
存储的类型必须满足 CopyConstructible(可拷贝构造)。
2. std::any 的工作原理
- 内部通过 指针 + 类型信息(typeid) 或小对象优化实现:
- 当对象有值时,存储实际类型对象。
- 当对象为空时,存储状态标记为空。
- 通过
any_cast提供类型安全访问,如果类型不匹配会抛出std::bad_any_cast异常。
(1) 为什么需要 std::any?
- 当需要存储 不同类型的对象,但类型在编译时未知时。
- 用于实现 动态类型容器、事件系统或通用缓存。
- 避免使用不安全的
void*或union。
(2) 基本使用
std::any a = 1; // 存储 int
a = 3.14; // 改为存储 double
a = std::string("hello"); // 改为存储 std::string
if (a.has_value()) {
std::cout << std::any_cast<std::string>(a) << "\n";
}
3. 使用 std::any 的场景
(1) 动态类型存储
std::vector<std::any> values;
values.push_back(10);
values.push_back(3.14);
values.push_back(std::string("text"));
for (auto& v : values) {
if (v.type() == typeid(int))
std::cout << std::any_cast<int>(v) << "\n";
}
(2) 类型安全访问
通过 any_cast 访问内部对象,类型不匹配会抛异常:
std::any a = 42;
try {
double d = std::any_cast<double>(a); // 类型错误,会抛出 std::bad_any_cast
} catch (const std::bad_any_cast& e) {
std::cout << e.what() << "\n";
}
(3) 指针访问
any_cast 也可返回指针,类型错误时返回 nullptr:
std::any a = 3;
if (int* p = std::any_cast<int>(&a)) {
std::cout << *p << "\n"; // 输出 3
}
4. 常用操作
(0) 构造与析构
std::any a; // 默认构造:无值
std::any b = 42; // 从值构造
std::any c = std::string("hi"); // 从其他类型构造
// 拷贝构造 / 拷贝赋值
std::any d = b;
d = c;
// 移动构造 / 移动赋值
std::any e = std::move(c);
e = std::move(b);
// 析构:生命周期结束时自动析构存储的对象
{
std::any f = std::string("RAII");
} // f 的析构自动调用 std::string 析构函数
(1) has_value() 与 type()
std::any a = 42;
if (a.has_value()) {
std::cout << "类型: " << a.type().name() << "\n";
}
(2) emplace() 与 reset()
std::any a;
a.emplace<std::string>("Hello"); // 原地构造新值
std::cout << std::any_cast<std::string>(a) << "\n";
a.reset(); // 清空
(3) 赋值操作 operator=
std::any a, b;
a = 10; // 赋值 int
b = std::string("hi"); // 赋值 string
a = b; // 拷贝 b 的值到 a
(4) swap()
std::any a = 1, b = 2;
a.swap(b);
std::cout << std::any_cast<int>(a) << " " << std::any_cast<int>(b) << "\n"; // 输出 2 1
(5) 工厂函数 std::make_any
auto a = std::make_any<int>(42); // 创建 any 并存储 int
auto b = std::make_any<std::string>("hi"); // 创建 any 并存储 string
(6) 非成员函数 any_cast
std::any a = 123;
int* p = std::any_cast<int>(&a); // 指针访问
int v = std::any_cast<int>(a); // 值访问,类型错误抛异常
5. std::any 的常见误用
(1) 滥用
- 不应将
std::any用作所有类型的通用容器,它不能替代容器或指针管理。 - 仅在 存储单个动态类型对象 且类型不确定时使用。
(2) 忘记类型检查
std::any a = 42;
std::cout << std::any_cast<double>(a); // 未检查类型,抛异常
std::variant
std::variant 是 C++17 引入的 类型安全联合(type-safe union)。在任一时刻,variant<...> 要么保存其候选类型列表中的某一类型的对象(active alternative),要么在异常情况等导致的特殊情形下处于无值状态(valueless_by_exception())。
头文件:
#include <variant>
1. 模板定义
template< class... Types >
class variant;
- 模板参数为
Types...:每个T必须满足 Destructible(能被析构)。 - 不允许持有引用类型、数组类型或
void。 - 可以重复出现相同类型(例如
variant<int,int>合法),也可以出现不同 cv 限定的同一基础类型(如int与const int)。 - 注意:如果你用同一个具体类型多次,基于类型的访问(
std::get<T>/get_if<T>)会变得歧义 / 编译失败(只能用索引或明确in_place_type/in_place_index)。 - 默认构造:默认构造会构造第一个候选类型的默认值,如果第一个候选类型不可默认构造,则
variant本身也不可默认构造。可以把std::monostate放在首位以保证可默认构造。
2. 存储与对象布局
variant内部存储了 discriminator(索引)与一个能容纳最大候选类型的缓冲区;当variant持有某个类型T时,一个T对象会嵌套(placement-new)在该缓冲区内。- 因此
variant的大小≈(max sizeof(alternatives))+ 对齐 + discriminator 大小。 - 在异常情况下(构造/赋值期间)有可能变为 valueless_by_exception(见下文)。
3. 主要成员函数 / 重载
下面列出常用操作、签名(伪签名风格)与行为说明与例子。
构造与析构
-
variant()- 默认构造:构造第一个候选类型的默认值(若可行)。
- 否则
variant不可默认构造。
-
variant(const variant&)/variant(variant&&)- 拷贝 / 移动构造。条件:候选类型支持相应操作;若某些候选类型不可拷贝/移动,相应操作会被删除。
- 如果在移动过程中抛出异常,可能导致
valueless_by_exception(取决于具体实现与异常传播)。
-
converting constructors(从某个值构造)
- 如果传入
U可明确/唯一地构造某个候选类型,variant会构造该候选。若存在二义性(能构造多个候选),编译失败。
- 如果传入
-
in-place 构造(直接在 variant 内就地构造)
variant(in_place_type<T>, Args&&...); variant(in_place_index<I>, Args&&...);in_place_type_t/in_place_index_t用于在 variant 内直接构造目标 alternative,避免先创建临时再赋值。
-
~variant()- 默认析构:会调用当前活动 alternative 的析构函数(如果有值)。
赋值(operator=)
-
variant& operator=(const variant&); -
variant& operator=(variant&&);- 这两个做拷贝/移动赋值。赋值行为在不同情况下(同类型 index / 不同 index)会调用相应 alternative 的赋值/析构+构造。
- 赋值过程中若抛异常,可能使
variant进入valueless_by_exception。
-
template<class T> variant& operator=(T&&);- converting assignment:当
T可以唯一构造某个候选类型时,执行相应赋值/替换。
- converting assignment:当
-
variant& operator=(std::monostate)等(视候选类型而定)。
观察器(Observers)
-
std::size_t index() const noexcept;- 返回当前活动的候选类型的零基索引(0..N-1)。
- 如果处于
valueless_by_exception,返回variant_npos(常为std::size_t(-1))。
-
bool valueless_by_exception() const noexcept;- 如果
variant处于无值状态(例如在变更 active alternative 时异常导致)返回true。
- 如果
修改(Modifiers)
-
template<class T, class... Args> T& emplace(Args&&...);emplace<T>(args...):在variant中就地构造类型T(T 必须是某个 alternative);会销毁旧的 active 值(若有),然后就地构造新值。- 异常安全:如果构造抛出,
variant可能进入valueless_by_exception(取决于实现与被替换对象的销毁时机)。
-
template<size_t I, class... Args> variant& emplace(in_place_index_t<I>, Args&&...);- 使用索引 I 就地构造。
-
void swap(variant& other) noexcept( /* depends */ );- 交换两个 variant 的状态与内容。noexcept 与具体候选类型的 swap/移动操作相关。
访问(get / get_if)
-
std::get<T>(variant&)/std::get<I>(variant&)get<T>(按类型访问)要求T在候选类型中唯一,否则编译错误。get<I>(按索引访问)直接访问索引为I的候选类型。- 若
variant当前不保存请求的 alternative,std::get会 抛出std::bad_variant_access(运行时异常)。
-
std::get_if<T>(&variant)/std::get_if<I>(&variant)get_if返回指向当前值的指针(非nullptr表示匹配),失败时返回nullptr。不会抛异常,通常是更安全的访问方式。- 有
const/ 非const重载:const T* get_if<const T>(&const variant)等。
示例:
std::variant<int,std::string> v = "hi";
if (auto p = std::get_if<std::string>(&v)) {
std::cout << *p << "\n";
}
try {
std::cout << std::get<int>(v); // 抛出 std::bad_variant_access
} catch (const std::bad_variant_access& e) { ... }
访问辅助:std::holds_alternative<T>(v)
- 返回
true当且仅当v当前持有类型T(同get_if<T>非空)。T必须唯一出现在 alternatives 中。
访问索引常量
-
constexpr std::size_t variant_npos = /* often size_t(-1) */;- 表示无效索引(用于
index()返回值在valueless_by_exception()时)。
- 表示无效索引(用于
4. 访问与遍历:std::visit(Visitor 模式)
非成员 std::visit(自 C++17 起)
签名(概念):
template <class Visitor, class... Variants>
decltype(auto) visit(Visitor&& vis, Variants&&... vars);
std::visit会将 visitor(可调用对象)以当前 variant(或多个 variants)的活动值作为参数调用。- 当传入多个
variant时,visitor 会被调用,参数顺序与variant顺序对应。 - 如果任一
variant为valueless_by_exception(),std::visit通常会抛出std::bad_variant_access。 - 返回值类型由 visitor 决定(可以返回
void或其他类型)。 - 常用技巧:用
overloaded(多个 lambda 继承合并)来实现多分支处理:
// helper
template<class... Fs> struct overloaded : Fs... { using Fs::operator()...; };
template<class... Fs> overloaded(Fs...) -> overloaded<Fs...>;
// 使用
std::variant<int,std::string> v = 42;
std::visit(overloaded {
[](int i){ std::cout<<"int "<<i<<"\n"; },
[](const std::string& s){ std::cout<<"str "<<s<<"\n"; }
}, v);
成员 visit(C++26 提案:member visit)
- C++26 引入(或将引入)
v.visit(visitor)的成员形式作为便捷写法(请注意你使用的编译器/标准支持情况)。非成员std::visit在 C++17 就有。
5. 比较运算与哈希
-
operator==等(C++17 起)与operator<=>(C++20)有定义:通常两个variant先比较是否都valueless_by_exception(),再比较index(),在 index 相同时比较包含的值(按对应类型的比较运算)。==:若两者index()相同且 contained values 相等 => true;若两个都 valueless => true;否则 false。</>:若index()不同,通常以index()的大小决定排序;若相同,则调用 contained type 的<。- 详细边界(valueless 等)以标准详细定义为准,但通常结果符合“按 index 首先排序,然后按值比较”的直觉。
-
std::hash<std::variant<...>>在标准库有特化(要求所有候选类型可哈希)。
6. 辅助类型与特性(type traits / helper classes)
-
std::monostate(C++17)- 一个空占位类型,常用于将
variant设置为默认可构造:std::variant<std::monostate, T1, T2>。
- 一个空占位类型,常用于将
-
std::bad_variant_access(C++17)- 当用
std::get<T>/std::get<I>访问但variant未持有该 alternative 时抛出。
- 当用
-
std::variant_size<Variant>/std::variant_size_v<Variant>(C++17)- 编译期获取候选类型数量(常量表达式)。
- 例:
std::variant_size_v<std::variant<int,double>> == 2。
-
std::variant_alternative<I, Variant>::type/std::variant_alternative_t<I, Variant>(C++17)- 编译期获取索引
I对应的类型(类型别名)。 - 例:
std::variant_alternative_t<0,std::variant<int,double>>等于int。
- 编译期获取索引
-
variant_npos:表示无值索引(如index()在 valueless 时返回此值)。
7. 异常安全与 valueless_by_exception
-
在某些变更 active alternative 的操作中(例如赋值、就地构造时),如果构造/移动/复制新的 alternative 的构造函数抛出异常,而旧对象已被销毁,
variant可能无法恢复到原先状态,从而进入valueless_by_exception()。 -
一旦处于
valueless_by_exception():index()返回variant_npos;std::get抛出std::bad_variant_access;std::get_if返回nullptr;- 一些操作(比如
std::visit)会抛出bad_variant_access(取决于实现)。
-
预防策略:当替换可能抛异常的类型时,优先使用
emplace并在必要时进行异常处理;确保候选类型的构造/移动操作尽可能noexcept,可以降低进入无值状态的风险。
8. 常用例子
基本使用与 get/get_if/holds_alternative
std::variant<int,std::string> v = "hello";
if (std::holds_alternative<std::string>(v)) {
std::cout << std::get<std::string>(v) << "\n";
}
if (auto p = std::get_if<int>(&v)) {
std::cout << "int: " << *p << "\n";
} else {
std::cout << "not int\n";
}
emplace / in_place
std::variant<std::monostate, std::string, std::vector<int>> v;
v.emplace<std::string>("abc"); // 就地构造 std::string
v.emplace<std::vector<int>>(3, 42); // 就地构造 vector(3,42)
v.emplace<in_place_index_t<1>>("xyz"); // 使用索引就地构造(index=1 => std::string)
visit 与 overloaded 工具
auto handle = overloaded {
[](int i){ std::cout<<"int "<<i<<"\n"; },
[](const std::string& s){ std::cout<<"str "<<s<<"\n"; }
};
std::variant<int,std::string> v = 10;
std::visit(handle, v);
使用 monostate 使可默认构造
std::variant<std::monostate, std::string> v; // 默认构造后 v 持有 monostate
9. 实用建议 / 常见误用
- 不要把
variant作为替代所有情况:类型过多会导致代码复杂和 visitor 分支膨胀。若候选类型集合非常大或松散,考虑设计别的抽象(多态/策略等)。 - 当候选类型有重复的具体类型时,避免
get<T>:因为会编译错误;使用get<index>或in_place_type显式选择。 - 注意异常安全:替换 active alternative(赋值、emplace)如果构造抛异常,可能进入
valueless_by_exception;为关键路径确保候选类型的移动/复制构造尽可能noexcept。 - 避免把对
variant的访问当作频繁反射:大量类型判断/切换会影响可读性和性能(虽然variant本质上是常数时间的判定与访问,但分支与 visitor 的实现复杂度需考虑)。 - std::variant 类似于 Rust 的 enum,都能表示“一种类型中的多种可能”。 不同点在于Rust 的 enum 语法更简洁,且模式匹配是强制的;C++ 的 std::variant 需要 std::visit 或 get 来显式处理。
10. 标准/特性备注
std::variant自 C++17 引入(特性宏:__cpp_lib_variant等)。- 标准后续对
variant做过修订(例如std::visit扩展、constexpr 能力增强等)。例如有成员形式visit(C++26 提议/扩展),以及使variant更多操作支持constexpr(不同标准版本的支持程度由编译器/标准库实现决定)。 - 使用时注意你的编译器和标准库版本对
variant的各项特性的支持情况(尤其是constexpr、成员visit等较新特性)。
11. 快速 API 参考
- 头文件:
<variant> - 构造:
variant(),variant(in_place_type_t<T>, ...),variant(in_place_index_t<I>, ...), converting constructors - 赋值:
operator=(variant),operator=(T&&)(converting) - 访问:
std::get<T>(v),std::get<I>(v),std::get_if<T>(&v),std::get_if<I>(&v) - 情况检测:
v.index(),v.valueless_by_exception(),std::holds_alternative<T>(v) - 就地构造:
v.emplace<T>(args...),v.emplace<in_place_index_t<I>>(args...) - 访问模式:
std::visit(visitor, v1, v2, ...),C++26 可能支持成员v.visit(visitor) - 辅助类型:
std::monostate,std::bad_variant_access,std::variant_size,std::variant_alternative_t,std::hash<std::variant<...>>
Containers
摘要
C++ 标准模板库(STL)容器是用于存储数据的对象集合,它们提供了不同的存储方式、内存管理机制和访问模式。理解不同容器的底层结构、时间复杂度和内存特性,是高效进行C++编程的关键。
STL 容器主要分为三大类:序列容器、关联容器和无序容器。此外,容器适配器提供受限接口以模拟特定的数据结构(如栈和队列)。C++23 新增的扁平容器则代表了对内存局部性和性能优化的新探索。
容器总览表
| 容器类别 | 容器名称 | 描述 | 存储特性 | 核心操作复杂度 |
|---|---|---|---|---|
| 序列容器 (Sequence) | std::array (C++11) | 固定大小的静态数组。 | 栈上,连续内存 | 随机访问 \(O(1)\) |
| std::vector | 动态数组。 | 堆上,连续内存 | 随机访问 \(O(1)\),末尾增删平均 \(O(1)\) | |
| std::deque | 双端队列。 | 分段连续内存 | 随机访问 \(O(1)\),头尾增删 \(O(1)\) | |
| std::list | 双向链表。 | 堆上,非连续内存 | 任意位置增删 \(O(1)\) | |
| std::forward_list (C++11) | 单向链表。 | 堆上,非连续内存 | 头部增删 \(O(1)\) | |
| 关联容器 (Associative) | std::set/multiset | 存储键,基于红黑树。 | 红黑树结构,有序 | 查找/增删 \(O(\log n)\) |
| std::map/multimap | 存储键值对,基于红黑树。 | 红黑树结构,有序 | 查找/增删 \(O(\log n)\) | |
| 无序容器 (Unordered) | std::unordered_set/multiset | 存储键,基于哈希表。 | 哈希表结构,无序 | 查找/增删 平均 \(O(1)\) |
| std::unordered_map/multimap | 存储键值对,基于哈希表。 | 哈希表结构,无序 | 查找/增删 平均 \(O(1)\) | |
| 容器适配器 (Adaptors) | std::stack | LIFO(后进先出)。 | 默认底层 \(\text{std::deque}\) | \(O(1)\) |
| std::queue | FIFO(先进先出)。 | 默认底层 \(\text{std::deque}\) | \(O(1)\) | |
| std::priority_queue | 优先级队列(最大堆)。 | 默认底层 \(\text{std::vector}\) | 插入/删除 \(O(\log n)\) |
1. 序列容器 (Sequence Containers)
序列容器以线性方式排列元素,元素的位置由程序员控制,通常用于构建列表、数组等基础数据结构。
| 特性 | std::array | std::vector | std::deque | std::forward_list | std::list |
|---|---|---|---|---|---|
| 内存结构 | 连续内存 (栈/全局) | 连续内存 (堆) | 分段连续内存 | 非连续 (单向链表) | 非连续 (双向链表) |
| 随机访问 | \(O(1)\) (最快) | \(O(1)\) (快) | \(O(1)\) (快) | 不支持 | 不支持 |
| 头部增删 | 不支持 | \(O(n)\) | \(O(1)\) (快) | \(O(1)\) (最快) | \(O(1)\) (快) |
| 尾部增删 | 不支持 | 平均 \(O(1)\) (最快) | \(O(1)\) (快) | \(O(n)\) | \(O(1)\) (快) |
| 迭代器稳定性 | 稳定 | 插入可能失效,删除指向被删元素的失效。 | 插入/删除头尾稳定,中间失效。 | 增删不会使其他迭代器失效。 | 增删不会使其他迭代器失效。 |
| 优势场景 | 编译期确定大小,极高性能。 | 默认首选,需随机访问,主要在末尾操作。 | 需头尾快速操作和随机访问的场景。 | 极度频繁的插入/删除,内存占用要求低。 | 频繁在任意位置插入/删除,需双向遍历。 |
2. 有序关联容器 (Ordered Associative Containers)
关联容器基于键 (Key) 进行有序存储,通常使用红黑树实现。它们自动保持元素/键的排序,适用于需要排序和快速查找的场景。
| 特性 | std::set | std::multiset | std::map | std::multimap |
|---|---|---|---|---|
| 底层结构 | 红黑树 | 红黑树 | 红黑树 | 红黑树 |
| 操作复杂度 | 查找、插入、删除均为 \(O(\log n)\) | 查找、插入、删除均为 \(O(\log n)\) | 查找、插入、删除均为 \(O(\log n)\) | 查找、插入、删除均为 \(O(\log n)\) |
| 存储内容 | 仅存储唯一键 | 存储可重复键 | 存储唯一键值对 | 存储可重复键值对 |
| 元素顺序 | 始终保持排序 (按键) | 始终保持排序 (按键) | 始终保持排序 (按键) | 始终保持排序 (按键) |
| 迭代器稳定性 | 插入或删除不会使指向其他元素的迭代器失效。 | 插入或删除不会使指向其他元素的迭代器失效。 | 插入或删除不会使指向其他元素的迭代器失效。 | 插入或删除不会使指向其他元素的迭代器失效。 |
3. 无序容器 (Unordered Containers)
无序容器 (C++11) 基于 哈希表 实现。它们不保证元素顺序,但能提供极快的平均性能,适用于不关心元素顺序、追求极致查找速度的场景。
| 特性 | std::unordered_set | std::unordered_multiset | std::unordered_map | std::unordered_multimap |
|---|---|---|---|---|
| 底层结构 | 哈希表 (桶和链表/树) | 哈希表 | 哈希表 | 哈希表 |
| 操作复杂度 | 平均 \(O(1)\),最坏 \(O(n)\) | 平均 \(O(1)\),最坏 \(O(n)\) | 平均 \(O(1)\),最坏 \(O(n)\) | 平均 \(O(1)\),最坏 \(O(n)\) |
| 存储内容 | 仅存储唯一键 | 存储可重复键 | 存储唯一键值对 | 存储可重复键值对 |
| 元素顺序 | 无序 (取决于哈希值) | 无序 | 无序 | 无序 |
| 迭代器稳定性 | 不稳定。 \(\text{rehash}\) (重新散列) 时所有迭代器和引用都会失效。 | 不稳定。 \(\text{rehash}\) 时所有迭代器和引用都会失效。 | 不稳定。 \(\text{rehash}\) 时所有迭代器和引用都会失效。 | 不稳定。 \(\text{rehash}\) 时所有迭代器和引用都会失效。 |
4. C++23 扁平容器 (Flat Containers)
C++23 引入的扁平容器旨在优化内存局部性。它们使用有序的 \(\text{std::vector}\) 作为底层存储,将键或键值对连续存储,从而利用现代CPU的缓存机制。
| 容器名称 | 对应关联容器 | 底层结构 | 查找性能 | 插入/删除性能 | 优势/劣势 |
|---|---|---|---|---|---|
| std::flat_set / multiset | \(\text{std::set/multiset}\) | 有序 \(\text{std::vector}\) | \(O(\log n)\) (二分查找,比红黑树更快) | \(O(n)\) (需要移动元素) | 优势:极低的内存占用和极佳的遍历性能。劣势:高昂的插入/删除成本。 |
| std::flat_map / multimap | \(\text{std::map/multimap}\) | 一个或两个有序 \(\text{std::vector}\) | \(O(\log n)\) (二分查找,比红黑树更快) | \(O(n)\) (需要移动元素) | 适用于元素数量相对稳定、需要高查找速度和高效遍历的场景。 |
5. 容器适配器 (Container Adaptors)
容器适配器不是独立的容器,而是提供受限接口的类模板。它们使用底层容器来模拟特定的数据结构行为。
| 容器名称 | 接口模型 | 核心操作 | 默认底层容器 | 可选底层容器 |
|---|---|---|---|---|
| std::stack | LIFO (Last-In, First-Out) | \(\text{push()}\), \(\text{pop()}\), \(\text{top()}\) | \(\text{std::deque}\) | \(\text{std::vector}\), \(\text{std::list}\) |
| std::queue | FIFO (First-In, First-Out) | \(\text{push()}\), \(\text{pop()}\), \(\text{front()}\)/\(\text{back()}\) | \(\text{std::deque}\) | \(\text{std::list}\) |
| std::priority_queue | 优先级排序 (最大堆) | \(\text{push()}\), \(\text{pop()}\), \(\text{top()}\) | \(\text{std::vector}\) | \(\text{std::deque}\) |
6. 容器关键概念
迭代器 (Iterators)
迭代器是 STL 的核心,它提供了一种统一访问容器元素的方式,类似于指针。
- 随机访问迭代器:支持 \(O(1)\) 时间内的任意跳转(如 \(\text{std::vector}\), \(\text{std::deque}\), \(\text{std::array}\))。
- 双向迭代器:支持向前和向后移动(如 \(\text{std::list}\), \(\text{std::set}\), \(\text{std::map}\))。
- 前向迭代器:仅支持向前移动(如 \(\text{std::forward_list}\))。
内存分配
- 连续内存:如 \(\text{std::vector}\) 和 \(\text{std::array}\)。优点是内存局部性好,CPU缓存利用率高;缺点是插入/删除中间元素成本高(需要移动后续元素)。
- 非连续内存:如 \(\text{std::list}\) 和红黑树/哈希表容器。优点是插入/删除效率高;缺点是内存碎片化,CPU缓存效率低。
异常安全 (Exception Safety)
- 强保证:如果操作失败(抛出异常),容器保持不变。
- 基本保证:如果操作失败,容器处于可用状态,但可能不是原来的状态。
- \(\text{std::vector}\) 的 \(\text{push_back}\) 在需要重新分配内存时,如果复制构造函数抛出异常,可能导致强保证失效。
std::vector
向量(Vector)是一个封装了动态大小数组的顺序容器(Sequence Container)。跟任意其它类型容器一样,它能够存放各种类型的对象。可以简单的认为,向量是一个能够存放任意类型的动态数组。Vector支持快速随机访问。
1. 引入
#include <vector>
2. 存储方式
为了支持随机访问,vector将元素连续存储–每个元素紧挨着前一个元素存储。容器中元素是连续存储的,且容器的大小是可变的。
在容器中增加元素时。vector根据存储元素的大小,在内存上申请一个空间,用于存储数据,空间的大小通常会大于所存储元素的实际大小,并且预留出一部分预留的空间,以便再次增加数据时,可以不用重新开辟空间。
当容器再次增加新的元素后,首先判断预留的空间是否够用,如果够用直接在预留空间中存储。如果预留的空间不够,需要在内存中开辟一整块新的更大的空间,并将vector原来的存储的数据拷贝过来,存储到新的内存中,然后在新的内存中增加需要增加的元素,这样保证存储的空间是连续的。所开的空间会预留出一部分空间,以便后续增加数据。
当 vector 增长时,容量通常会按一定比例(通常是1.5或2)增长。这有助于减少频繁的重新分配,提升性能。
3. 方法
(1)构造方法
-
vector(): 创建一个空vector,创建时也可以使用迭代器进行初始化std::vector<int> vec; std::vector<int> vec = {1, 2, 3, 4, 5}; -
vector(size_t nSize): 创建一个vector,元素个数为nSize,默认构造函数会赋值与默认值std::vector<int> vec(10); -
vector(size_t nSize,const t& t): 创建一个vector,元素个数为nSize,且值均为tstd::vector<int> vec(10, 5); -
vector(const vector&): 复制构造函数std::vector<int> vec1 = {1, 2, 3, 4, 5}; std::vector<int> vec2(vec1); // vec2 是 vec1 的一个复制版本 -
vector(begin,end): 复制[begin,end)区间内另一个数组的元素到vector中std::array<int, 5> arr = {1, 2, 3, 4, 5}; // 创建一个 std::array 或 std::vector std::vector<int> vec(arr.begin(), arr.begin() + 3); // 复制前3个元素
(2)大小函数
-
size_t size() const: 返回vector中存放元素的实际数量(实际存储元素的个数) -
size_t capacity() const: 返回vector在内存中,开辟空间的容量(最多能放所少个元素不需要重新扩容) -
size_t max_size() const: 返回最大可允许的vector元素数量值 -
bool empty() const: 返回数组是否为空 -
void shrink_to_fit(): 调整数组大小(capacity)刚好适应当前的大小,节省内存 -
void resize(size_t size): 修改 vector 的大小。如果新大小比当前大小大,则扩大数组容量,会导致控制帧。如果新大小比当前大小小,则不做处理 -
void reserve(size_t n): 修改 vector 的capacity,为数组预留空间,不改变size。
(3) 增加函数
-
push_back(const T& value): 将元素value添加到 vector 的末尾。如果 vector 已满,push_back会自动扩展容量。std::vector<int> vec = {1, 2, 3}; vec.push_back(4); // 向 vec 中添加元素 4,vec 变为 {1, 2, 3, 4} -
emplace_back(Args&&... args): 在 vector 的末尾就地构造一个元素,使用提供的参数直接构造该元素,而不是首先创建元素再添加。这样可以减少不必要的拷贝或移动操作。std::vector<std::pair<int, int>> vec; vec.emplace_back(1, 2); // 在末尾构造一个 pair<int, int>,值为 {1, 2} -
insert(iterator pos, const T& value): 在指定位置pos插入一个元素。元素会被插入到pos之前,pos之后的元素会被向后移动。std::vector<int> vec = {1, 2, 4, 5}; vec.insert(vec.begin() + 2, 3); // 在位置2插入3,vec 变为 {1, 2, 3, 4, 5} -
insert(iterator pos, size_t count, const T& value): 在指定位置pos插入count个元素,所有的元素值为value。std::vector<int> vec = {1, 2, 4, 5}; vec.insert(vec.begin() + 2, 2, 3); // 在位置2插入两个 3,vec 变为 {1, 2, 3, 3, 4, 5} -
insert(iterator pos, InputIterator first, InputIterator last): 将[first, last)区间的元素插入到pos位置。std::vector<int> vec1 = {1, 2, 3}; std::vector<int> vec2 = {4, 5}; vec1.insert(vec1.begin() + 2, vec2.begin(), vec2.end()); // 在位置 2 插入 vec2 中的元素,vec1 变为 {1, 2, 4, 5, 3} -
emplace(iterator pos, Args&&... args): 在指定位置pos就地构造一个元素,使用提供的参数直接构造该元素。std::vector<std::pair<int, int>> vec; vec.emplace(vec.begin(), 1, 2); // 在位置0处构造一个 pair<int, int>,值为 {1, 2}
(4) 删除函数
-
pop_back(): 删除 vector 中的最后一个元素。该函数不会改变容器的容量,只是移除最后一个元素并缩小容器的大小。std::vector<int> vec = {1, 2, 3, 4}; vec.pop_back(); // 删除最后一个元素,vec 变为 {1, 2, 3} -
erase(iterator pos): 删除指定位置pos处的元素。删除后,后面的元素会向前移动。std::vector<int> vec = {1, 2, 3, 4}; vec.erase(vec.begin() + 2); // 删除索引为2的元素,vec 变为 {1, 2, 4} -
erase(iterator first, iterator last): 删除[first, last)区间内的所有元素。此操作删除从first到last之间的元素(不包括last)。std::vector<int> vec = {1, 2, 3, 4, 5}; vec.erase(vec.begin() + 1, vec.begin() + 4); // 删除索引从 1 到 3 的元素,vec 变为 {1, 5} -
clear(): 删除 vector 中的所有元素,容器变为空,但容器的容量不会立即改变,直到发生重新分配。std::vector<int> vec = {1, 2, 3, 4}; vec.clear(); // 删除所有元素,vec 变为空 { }
(5)遍历函数
-
reference at(int pos): 返回pos位置元素的引用。与operator[]类似,但会检查边界,如果访问无效的位置会抛出std::out_of_range异常。std::vector<int> vec = {1, 2, 3, 4, 5}; int& element = vec.at(2); // 返回位置 2 处元素的引用,即值为 3 element = 10; // 修改元素为 10 std::cout << vec[2] << std::endl; // 输出: 10 -
reference front(): 返回 vector 的第一个元素的引用。如果 vector 为空,调用该方法会导致未定义行为。std::vector<int> vec = {1, 2, 3, 4, 5}; int& firstElement = vec.front(); // 返回第一个元素的引用,即值为 1 firstElement = 20; // 修改第一个元素为 20 std::cout << vec.front() << std::endl; // 输出: 20 -
reference back(): 返回 vector 的最后一个元素的引用。如果 vector 为空,调用该方法会导致未定义行为。std::vector<int> vec = {1, 2, 3, 4, 5}; int& lastElement = vec.back(); // 返回最后一个元素的引用,即值为 5 lastElement = 50; // 修改最后一个元素为 50 std::cout << vec.back() << std::endl; // 输出: 50 -
iterator begin(): 返回指向 vector 第一个元素的迭代器。这个迭代器指向 vector 的首元素。std::vector<int> vec = {1, 2, 3, 4, 5}; std::vector<int>::iterator it = vec.begin(); // 返回指向第一个元素的迭代器 std::cout << *it << std::endl; // 输出: 1 -
iterator end(): 返回指向 vector 最后一个元素之后位置的迭代器。这个迭代器指向 vector 的尾元素的下一个位置。std::vector<int> vec = {1, 2, 3, 4, 5}; std::vector<int>::iterator it = vec.end(); // 返回指向最后一个元素之后位置的迭代器 --it; // 移动到最后一个元素 std::cout << *it << std::endl; // 输出: 5 -
reverse_iterator rbegin(): 返回指向 vector 最后一个元素的反向迭代器。该迭代器可以用来从后往前遍历元素。std::vector<int> vec = {1, 2, 3, 4, 5}; std::vector<int>::reverse_iterator rit = vec.rbegin(); // 返回指向最后一个元素的反向迭代器 std::cout << *rit << std::endl; // 输出: 5 -
reverse_iterator rend(): 返回指向 vector 第一个元素之前位置的反向迭代器。该迭代器指向 vector 的第一个元素之前的位置。std::vector<int> vec = {1, 2, 3, 4, 5}; std::vector<int>::reverse_iterator rit = vec.rend(); // 返回指向第一个元素之前位置的反向迭代器 ++rit; // 移动到第一个元素 std::cout << *rit << std::endl; // 输出: 1 -
使用基于范围的
for循环(C++11 及以上): 可以直接遍历std::vector中的每个元素,语法简洁。std::vector<int> vec = {1, 2, 3, 4, 5}; for (const int& num : vec) { std::cout << num << " "; // 输出: 1 2 3 4 5 } -
使用传统的
for循环(基于索引): 使用索引来遍历std::vector,适合在需要访问元素索引的情况下使用。std::vector<int> vec = {1, 2, 3, 4, 5}; for (size_t i = 0; i < vec.size(); ++i) { std::cout << vec[i] << " "; // 输出: 1 2 3 4 5 }
(6)其他函数
-
swap(vector& other): 交换当前 vector 和另一个 vector 的内容。如果需要“删除”当前 vector 中的元素,可以通过交换将其与一个空的 vector 交换,从而达到清空的效果。std::vector<int> vec = {1, 2, 3, 4}; std::vector<int> emptyVec; vec.swap(emptyVec); // vec 变为空,emptyVec 变为 {1, 2, 3, 4} -
assign(size_t count, const T& value): 将count个value元素赋值给当前 vector,替换原有内容。std::vector<int> vec; vec.assign(5, 10); // 将 vec 赋值为 {10, 10, 10, 10, 10} -
assign(InputIterator first, InputIterator last): 使用区间[first, last)的元素来填充当前 vector,替换原有内容。std::vector<int> vec1 = {1, 2, 3}; std::vector<int> vec2; vec2.assign(vec1.begin(), vec1.end()); // vec2 变为 {1, 2, 3}
std::array
std::array 是一个封装了固定大小数组的容器。它将 C 风格数组的性能和内存布局与标准容器的优点结合起来,例如能够知道自己的大小、支持赋值和随机访问迭代器等。std::array 的大小是模板参数的一部分,在编译时固定,因此它不支持动态大小的调整(如插入或删除元素)。
1. 引入
#include <array>
2. 存储方式
std::array 是一个固定大小的顺序容器,其元素是存储在连续内存中的,在栈中存储。它的内存布局与 C 风格数组 T[N] 完全相同,并且不包含任何额外的开销(例如,不需要存储大小)。
- 固定大小: 数组的大小
N是一个模板参数,在编译时确定,不能在运行时改变。 - 连续存储: 元素在内存中是连续存放的,这使得
std::array支持高效的随机访问(通过索引 \(O(1)\) 时间复杂度)和迭代器操作。 - 无数据成员开销: 数组本身不存储除了元素之外的任何数据(例如,不需要存储大小),因此它的空间效率与 C 风格数组相同。
由于大小固定,std::array 不支持诸如 push_back、pop_back 或 insert、erase 等会改变容器大小的操作。
3. 方法
std::array 的许多操作都是隐式定义的,因为它是一个聚合类型(Aggregate Type),遵循 C 风格数组的初始化和复制规则。
(1) 构造方法
std::array 是一个聚合类型,因此它使用聚合初始化(Aggregate Initialization)规则:
-
默认构造:
std::array<int, 5> arr; // 包含 5 个 int 元素,默认初始化(对于基本类型,值可能是不确定的) -
列表初始化/聚合初始化:
std::array<int, 3> arr1 = {1, 2, 3}; // 包含 3 个元素,值为 1, 2, 3 std::array<int, 5> arr2 = {1, 2}; // 包含 5 个元素,前两个为 1, 2,其余为 0 (零初始化) -
复制/移动构造:
std::array<int, 3> arr3 = {1, 2, 3}; std::array<int, 3> arr4(arr3); // 复制构造
(2) 大小函数
-
size_t size() const: 返回std::array中元素的个数(即模板参数 \(N\))。std::array<int, 4> arr; std::cout << arr.size(); // 输出 4 -
bool empty() const: 检查std::array是否为空。由于 \(N\) 在编译时固定,对于 \(N > 0\) 的数组总是返回false。std::array<int, 0> arr0; std::cout << arr0.empty(); // 输出 true std::array<int, 5> arr5; std::cout << arr5.empty(); // 输出 false -
size_t max_size() const: 返回容器可能包含的最大元素数,与size()相同。
(3) 元素访问
-
reference at(size_type pos): 访问指定位置pos的元素,带边界检查。如果pos超出范围,则抛出std::out_of_range异常。std::array<int, 3> arr = {1, 2, 3}; std::cout << arr.at(1); // 输出 2 // arr.at(5); // 运行时抛出异常 -
reference operator[](size_type pos): 访问指定位置pos的元素,不带边界检查。std::array<int, 3> arr = {1, 2, 3}; std::cout << arr[1]; // 输出 2 // arr[5]; // 未定义行为 -
reference front(): 访问第一个元素。std::array<int, 3> arr = {1, 2, 3}; std::cout << arr.front(); // 输出 1 -
reference back(): 访问最后一个元素。std::array<int, 3> arr = {1, 2, 3}; std::cout << arr.back(); // 输出 3 -
T* data(): 返回指向存储元素的首元素的底层 C 风格数组的指针。std::array<int, 3> arr = {1, 2, 3}; int* p = arr.data(); std::cout << p[0]; // 输出 1 -
std::get<I>(array): 使用元组式接口访问第 \(I\) 个元素,编译时进行边界检查。std::array<int, 3> arr = {1, 2, 3}; std::cout << std::get<1>(arr); // 输出 2
(4) 迭代器函数
std::array 支持随机访问迭代器。
iterator begin()/const_iterator cbegin(): 返回指向第一个元素的迭代器。iterator end()/const_iterator cend(): 返回指向最后一个元素之后位置的迭代器。reverse_iterator rbegin()/const_reverse_iterator crbegin(): 返回指向最后一个元素的反向迭代器。reverse_iterator rend()/const_reverse_iterator crend(): 返回指向第一个元素之前位置的反向迭代器。
(5) 修改函数
std::array 不支持改变大小的操作(如 push_back, pop_back, insert, erase)。
-
void fill(const T& value): 将所有元素的值设置为value。std::array<int, 5> arr; arr.fill(10); // arr 变为 {10, 10, 10, 10, 10} -
void swap(array& other): 交换当前std::array和另一个大小、类型相同的std::array的内容。std::array<int, 3> arr1 = {1, 2, 3}; std::array<int, 3> arr2 = {4, 5, 6}; arr1.swap(arr2); // arr1 变为 {4, 5, 6},arr2 变为 {1, 2, 3}
(6) 遍历函数
由于 std::array 提供了随机访问迭代器,因此可以使用多种方式遍历。
-
使用基于范围的
for循环(C++11 及以上):std::array<int, 3> arr = {1, 2, 3}; for (const int& num : arr) { std::cout << num << " "; // 输出: 1 2 3 } -
使用传统的
for循环(基于迭代器):std::array<int, 3> arr = {1, 2, 3}; for (auto it = arr.begin(); it != arr.end(); ++it) { std::cout << *it << " "; // 输出: 1 2 3 } -
使用传统的
for循环(基于索引):std::array<int, 3> arr = {1, 2, 3}; for (size_t i = 0; i < arr.size(); ++i) { std::cout << arr[i] << " "; // 输出: 1 2 3 }
std::list
List 是一个支持在容器的任意位置快速插入和删除元素的一个顺序容器,支持存储各种类型的对象,链表在存储时不需要重新分配内存或是初始化时预留大小,对象存储在内存中的不连续位置,因此随机访问能力较差。
1. 引入
#include <list>
2. 存储方式
std::list 是基于双向链表实现的容器。与 std::vector 的连续内存不同,std::list 的元素存储在不连续的内存块中。每个元素由一个节点(节点中包含元素和指向前后元素的指针)组成,这些节点通过指针连接成一个链表。由于每个节点都是独立分配的,std::list 不需要连续的内存空间来存储元素。
在 std::list 中插入或删除元素时,由于元素是通过指针连接的,因此插入和删除操作通常是非常高效的。插入、删除操作不会影响链表中其他元素的存储位置。
然而,由于每个元素都需要额外的内存来存储指向前后元素的指针,所以相较于 std::vector,std::list 在存储上较为低效,且不支持快速随机访问。访问元素时,必须从链表的起始或终止节点开始,逐步沿着指针遍历到目标元素。
3. 方法
(1) 构造方法
-
list(): 创建一个空的std::list,默认构造函数。std::list<int> lst; -
list(size_t nSize): 创建一个包含nSize个默认构造元素的std::list。std::list<int> lst(10); // 创建一个包含10个默认构造的元素的列表 -
list(size_t nSize, const T& value): 创建一个包含nSize个元素,并且所有元素的值为value的std::list。std::list<int> lst(10, 5); // 创建一个包含10个元素,值都为5的列表 -
list(const list&): 复制构造函数,创建一个与另一个std::list完全相同的副本。std::list<int> lst1 = {1, 2, 3, 4, 5}; std::list<int> lst2(lst1); // lst2 是 lst1 的一个复制版本 -
list(begin, end): 使用另一个容器或迭代器区间中的元素初始化std::list。std::vector<int> vec = {1, 2, 3, 4, 5}; std::list<int> lst(vec.begin(), vec.end()); // 复制 vector 的元素到 list
(2) 大小函数
-
size_t size() const: 返回std::list中元素的个数。std::list<int> lst = {1, 2, 3, 4}; std::cout << lst.size(); // 输出 4 -
bool empty() const: 检查std::list是否为空,若为空返回true,否则返回false。std::list<int> lst; if (lst.empty()) { std::cout << "List is empty." << std::endl; } -
void resize(size_t size): 调整std::list的大小。若新大小大于当前大小,std::list会插入默认值的元素;若小于当前大小,则会删除多余的元素。std::list<int> lst = {1, 2, 3}; lst.resize(5, 10); // 增加两个元素,元素值为 10
(3) 增加函数
-
push_back(const T& value): 将元素value添加到std::list的末尾。std::list<int> lst = {1, 2, 3}; lst.push_back(4); // 向 lst 中添加元素 4,lst 变为 {1, 2, 3, 4} -
push_front(const T& value): 将元素value添加到std::list的开头。std::list<int> lst = {2, 3, 4}; lst.push_front(1); // 向 lst 中添加元素 1,lst 变为 {1, 2, 3, 4} -
emplace_back(Args&&... args): 在std::list的末尾就地构造一个元素,使用提供的参数直接构造该元素。std::list<std::pair<int, int>> lst; lst.emplace_back(1, 2); // 在末尾构造一个 pair<int, int>,值为 {1, 2} -
emplace_front(Args&&... args): 在std::list的前端就地构造一个元素。std::list<std::pair<int, int>> lst; lst.emplace_front(1, 2); // 在前端构造一个 pair<int, int>,值为 {1, 2} -
insert(iterator pos, const T& value): 在指定位置pos插入元素value,插入操作会将pos后面的元素向后移动。std::list<int> lst = {1, 2, 4, 5}; lst.insert(std::next(lst.begin(), 2), 3); // 在位置2插入3,lst 变为 {1, 2, 3, 4, 5} -
insert(iterator pos, size_t count, const T& value): 在指定位置pos插入count个元素,所有的元素值为value。std::list<int> lst = {1, 2, 4, 5}; lst.insert(std::next(lst.begin(), 2), 2, 3); // 在位置2插入两个3,lst 变为 {1, 2, 3, 3, 4, 5} -
insert(iterator pos, InputIterator first, InputIterator last): 将[first, last)区间的元素插入到指定位置pos。std::list<int> lst1 = {1, 2, 3}; std::list<int> lst2 = {4, 5}; lst1.insert(std::next(lst1.begin(), 2), lst2.begin(), lst2.end()); // 在位置2插入 lst2 中的元素,lst1 变为 {1, 2, 4, 5, 3}
(4) 删除函数
-
pop_back(): 删除std::list中的最后一个元素。std::list<int> lst = {1, 2, 3, 4}; lst.pop_back(); // 删除最后一个元素,lst 变为 {1, 2, 3} -
pop_front(): 删除std::list中的第一个元素。std::list<int> lst = {1, 2, 3, 4}; lst.pop_front(); // 删除第一个元素,lst 变为 {2, 3, 4} -
erase(iterator pos): 删除指定位置pos处的元素,删除操作后,pos后面的元素会向前移动。std::list<int> lst = {1, 2, 3, 4}; lst.erase(std::next(lst.begin(), 2)); // 删除索引为2的元素,lst 变为 {1, 2, 4} -
erase(iterator first, iterator last): 删除区间[first, last)内的所有元素。std::list<int> lst = {1, 2, 3, 4, 5}; lst.erase(std::next(lst.begin(), 1), std::next(lst.begin(), 4)); // 删除从索引1到3的元素,lst 变为 {1, 5} -
clear(): 删除std::list中的所有元素,使容器变为空。std::list<int> lst = {1, 2, 3, 4}; lst.clear(); // 删除所有元素,lst 变为空 {} -
void remove(const T& val):remove函数用于删除链表中所有等于指定值val的元素。它会遍历整个链表,删除所有匹配的元素。std::list<int> lst = {1, 2, 3, 4, 3, 5}; lst.remove(3); // 删除所有值为3的元素,lst 变为 {1, 2, 4, 5} -
void remove_if (bool (*pred)(const T&));: pred: 一个谓词(通常是一个函数或函数对象),用于判断元素是否满足删除条件。pred 返回 true 表示删除该元素,返回 false 表示保留该元素。用于删除满足特定条件的所有元素。与 remove 不同的是,remove_if 允许你使用自定义的条件来判断哪些元素需要被删除。remove_if 会遍历整个容器,将所有满足谓词 pred 条件的元素移到容器的末尾,并返回一个指向容器中第一个不满足条件的元素的迭代器。然后可以使用 erase 来删除这些元素。
需要注意的是,remove_if 并不会实际删除元素,它只是将符合条件的元素移动到容器的末尾,删除操作需要通过 erase 来完成。
假设我们有一个 std::list,并希望删除所有大于 3 的元素:
#include <iostream> #include <list> #include <algorithm> // 包含 std::remove_if bool greater_than_three(int n) { return n > 3; // 条件:删除大于3的元素 } int main() { std::list<int> lst = {1, 2, 3, 4, 5, 6}; // 使用 remove_if 删除大于 3 的元素 lst.remove_if(greater_than_three); // 输出结果 for (int n : lst) { std::cout << n << " "; // 输出: 1 2 3 } return 0; }也可以使用 lambda 表达式作为谓词:
#include <iostream> #include <list> #include <algorithm> int main() { std::list<int> lst = {1, 2, 3, 4, 5, 6}; // 使用 lambda 表达式删除大于 3 的元素 lst.remove_if([](int n) { return n > 3; }); // 输出结果 for (int n : lst) { std::cout << n << " "; // 输出: 1 2 3 } return 0; }
(5) 遍历函数
-
iterator begin(): 返回指向std::list第一个元素的迭代器。std::list<int> lst = {1, 2, 3}; std::list<int>::iterator it = lst.begin(); // 返回指向第一个元素的迭代器 std::cout << *it << std::endl; // 输出: 1 -
iterator end(): 返回指向std::list最后一个元素之后位置的迭代器。std::list<int> lst = {1, 2, 3}; std::list<int>::iterator it = lst.end(); // 返回指向最后一个元素之后位置的迭代器 --it; // 移动到最后一个元素 std::cout << *it << std::endl; // 输出: 3 -
reverse_iterator rbegin(): 返回指向std::list最后一个元素的反向迭代器。std::list<int> lst = {1, 2, 3}; std::list<int>::reverse_iterator rit = lst.rbegin(); // 返回指向最后一个元素的反向迭代器 std::cout << *rit << std::endl; // 输出: 3 -
reverse_iterator rend(): 返回指向std::list第一个元素之前位置的反向迭代器。std::list<int> lst = {1, 2, 3}; std::list<int>::reverse_iterator rit = lst.rend(); // 返回指向第一个元素之前位置的反向迭代器 ++rit; // 移动到第一个元素 std::cout << *rit << std::endl; // 输出: 1 -
使用基于范围的
for循环(C++11 及以上): 直接遍历std::list中的每个元素。std::list<int> lst = {1, 2, 3}; for (const int& num : lst) { std::cout << num << " "; // 输出: 1 2 3 } -
使用传统的
for循环(基于迭代器): 使用迭代器来遍历std::list。std::list<int> lst = {1, 2, 3}; for (auto it = lst.begin(); it != lst.end(); ++it) { std::cout << *it << " "; // 输出: 1 2 3 }
(6) 其他函数
-
swap(list& other): 交换当前std::list和另一个std::list的内容。std::list<int> lst1 = {1, 2, 3}; std::list<int> lst2 = {4, 5, 6}; lst1.swap(lst2); // lst1 变为 {4, 5, 6},lst2 变为 {1, 2, 3} -
assign(size_t count, const T& value): 将count个value元素赋值给当前std::list,替换原有内容。std::list<int> lst; lst.assign(5, 10); // 将 lst 赋值为 {10, 10, 10, 10, 10} -
assign(InputIterator first, InputIterator last): 使用区间[first, last)的元素来填充当前std::list,替换原有内容。std::list<int> lst1 = {1, 2, 3}; std::list<int> lst2; lst2.assign(lst1.begin(), lst1.end()); // lst2 变为 {1, 2, 3} -
void sort():sort函数用于对链表中的元素进行排序。默认情况下,它会按照元素的升序排列。如果需要按降序排序,可以提供自定义比较函数。std::list<int> lst = {5, 3, 4, 1, 2}; lst.sort(); // 按升序排序,lst 变为 {1, 2, 3, 4, 5}如果要进行降序排序,可以使用自定义的比较函数:
lst.sort(std::greater<int>()); // 按降序排序,lst 变为 {5, 4, 3, 2, 1} -
void merge(list& other):merge函数用于将另一个已排序的链表other合并到当前链表中。合并后的链表仍然是有序的。注意,merge函数要求两个链表必须是排序过的,否则结果无法保证是有序的。std::list<int> lst1 = {1, 3, 5}; std::list<int> lst2 = {2, 4, 6}; lst1.merge(lst2); // lst1 合并 lst2 后,变为 {1, 2, 3, 4, 5, 6} -
void reverse():reverse函数用于将链表中的元素反转。它会改变链表中元素的顺序。std::list<int> lst = {1, 2, 3, 4, 5}; lst.reverse(); // 反转链表,lst 变为 {5, 4, 3, 2, 1}
std::forward_list
std::forward_list 是一个支持在容器的任意位置快速插入和删除元素的顺序容器。它是一个单向链表(singly-linked list)实现,因此它只支持向前遍历。相比于 std::list,它不存储指向前一个节点的指针,从而提供更节省空间的存储方式,但代价是失去了双向迭代的能力以及快速获取 size() 的能力。
1. 引入
#include <forward_list>
2. 存储方式
std::forward_list 是基于单向链表实现的容器。它的元素存储在内存中的不连续位置,每个元素由一个节点(包含元素和指向下一个元素的指针)组成。
- 单向连接: 每个节点只知道其后继节点,因此只能从头到尾单向遍历。
- 高效的插入和删除: 与
std::list类似,在任意位置插入和删除元素的时间复杂度为 \(O(1)\),但操作通常需要一个指向目标位置之前元素的迭代器。 - 空间优化: 由于每个元素只存储一个指针(指向下一个元素),它比双向链表
std::list更节省内存。 - 不支持随机访问和
size(): 由于是单向链表,它不支持 \(O(1)\) 复杂度的随机访问(例如operator[]),并且为了保持 \(O(1)\) 插入/删除的效率,它通常不提供size()成员函数(除非是 C++20 引入的std::ssize全局函数)。
3. 方法
由于 std::forward_list 的单向特性,其插入和删除操作与 std::list 有显著区别:它没有 pop_back()、push_back() 或 back(),并且 insert 和 erase 操作是在指定位置的后方进行。
(1) 构造方法
-
forward_list(): 创建一个空的std::forward_list,默认构造函数。std::forward_list<int> lst; -
forward_list(size_t nSize): 创建一个包含nSize个默认构造元素的std::forward_list。std::forward_list<int> lst(10); // 创建一个包含10个默认构造的元素的列表 -
forward_list(size_t nSize, const T& value): 创建一个包含nSize个元素,并且所有元素的值为value的std::forward_list。std::forward_list<int> lst(10, 5); // 创建一个包含10个元素,值都为5的列表 -
forward_list(const forward_list&): 复制构造函数,创建一个与另一个std::forward_list完全相同的副本。std::forward_list<int> lst1 = {1, 2, 3}; std::forward_list<int> lst2(lst1); // lst2 是 lst1 的一个复制版本 -
forward_list(begin, end): 使用另一个容器或迭代器区间中的元素初始化std::forward_list。std::vector<int> vec = {1, 2, 3}; std::forward_list<int> lst(vec.begin(), vec.end()); // 复制 vector 的元素到 forward_list
(2) 大小函数
-
bool empty() const: 检查std::forward_list是否为空。若为空返回true,否则返回false。std::forward_list<int> lst; if (lst.empty()) { std::cout << "List is empty." << std::endl; } -
size_t max_size() const: 返回容器可能包含的最大元素数。
(3) 增加函数
std::forward_list 的所有插入操作都围绕前部或指定位置之后进行。
-
push_front(const T& value): 将元素value添加到std::forward_list的开头。std::forward_list<int> lst = {2, 3, 4}; lst.push_front(1); // lst 变为 {1, 2, 3, 4} -
emplace_front(Args&&... args): 在std::forward_list的前端就地构造一个元素。std::forward_list<std::pair<int, int>> lst; lst.emplace_front(1, 2); // 在前端构造一个 pair<int, int>,值为 {1, 2} -
insert_after(const_iterator pos, const T& value): 在指定位置pos的后面插入元素value。std::forward_list<int> lst = {1, 2, 4, 5}; auto it = std::next(lst.begin()); // 指向元素 2 lst.insert_after(it, 3); // 在 2 后面插入 3,lst 变为 {1, 2, 3, 4, 5} -
insert_after(const_iterator pos, size_t count, const T& value): 在指定位置pos的后面插入count个元素,所有元素值为value。std::forward_list<int> lst = {1, 2, 4, 5}; auto it = std::next(lst.begin()); lst.insert_after(it, 2, 3); // 在 2 后面插入两个 3,lst 变为 {1, 2, 3, 3, 4, 5} -
insert_after(const_iterator pos, InputIterator first, InputIterator last): 将[first, last)区间的元素插入到指定位置pos的后面。
(4) 删除函数
std::forward_list 的所有删除操作都围绕前部或指定位置之后进行。
-
pop_front(): 删除std::forward_list中的第一个元素。std::forward_list<int> lst = {1, 2, 3, 4}; lst.pop_front(); // 删除第一个元素,lst 变为 {2, 3, 4} -
erase_after(const_iterator pos): 删除指定位置pos后面的元素。std::forward_list<int> lst = {1, 2, 3, 4}; auto it = lst.begin(); // 指向元素 1 lst.erase_after(it); // 删除 1 后面的元素 2,lst 变为 {1, 3, 4} -
erase_after(const_iterator first, const_iterator last): 删除区间(first, last)内的所有元素。std::forward_list<int> lst = {1, 2, 3, 4, 5}; auto it1 = lst.begin(); // 指向 1 auto it2 = std::next(it1, 3); // 指向 4 lst.erase_after(it1, it2); // 删除 1 后面直到 4 之前的所有元素 {2, 3},lst 变为 {1, 4, 5} -
clear(): 删除std::forward_list中的所有元素,使容器变为空。 -
void remove(const T& val): 删除链表中所有等于指定值val的元素。 -
void remove_if (bool (*pred)(const T&)): 删除满足谓词pred条件的所有元素。std::forward_list<int> lst = {1, 2, 3, 4, 5}; lst.remove_if([](int n) { return n % 2 == 0; }); // 删除偶数,lst 变为 {1, 3, 5}
(5) 遍历函数
std::forward_list 只支持前向迭代器。它提供了一个特殊的迭代器 before_begin() 用于方便地在链表头部之前进行插入/删除操作。
-
const_iterator before_begin(): 返回指向链表第一个元素之前位置的特殊迭代器。用于配合insert_after或erase_after在链表头部操作。std::forward_list<int> lst = {1, 2, 3}; // 在链表头部插入元素 0 lst.insert_after(lst.before_begin(), 0); // lst 变为 {0, 1, 2, 3} -
iterator begin(): 返回指向std::forward_list第一个元素的迭代器。 -
iterator end(): 返回指向std::forward_list最后一个元素之后位置的迭代器。 -
使用基于范围的
for循环(C++11 及以上):std::forward_list<int> lst = {1, 2, 3}; for (const int& num : lst) { std::cout << num << " "; // 输出: 1 2 3 }
(6) 其他函数
-
swap(forward_list& other): 交换当前std::forward_list和另一个std::forward_list的内容。 -
assign(size_t count, const T& value): 将count个value元素赋值给当前std::forward_list,替换原有内容。 -
assign(InputIterator first, InputIterator last): 使用区间[first, last)的元素来填充当前std::forward_list,替换原有内容。 -
void sort(): 对链表中的元素进行排序。默认升序。std::forward_list<int> lst = {5, 3, 4, 1, 2}; lst.sort(); // 按升序排序,lst 变为 {1, 2, 3, 4, 5} -
void merge(forward_list& other): 将另一个已排序的链表other合并到当前链表中。要求两个链表都已排序。 -
void reverse(): 将链表中的元素反转。std::forward_list<int> lst = {1, 2, 3}; lst.reverse(); // 反转链表,lst 变为 {3, 2, 1} -
void unique(): 删除链表中所有连续重复的元素。std::forward_list<int> lst = {1, 2, 2, 3, 3, 3, 4}; lst.unique(); // lst 变为 {1, 2, 3, 4} -
void splice_after(const_iterator pos, forward_list& other): 将other中的所有元素移动到当前列表*this中,放在pos之后。other变为空。std::forward_list<int> lst1 = {1, 4}; std::forward_list<int> lst2 = {2, 3}; lst1.splice_after(lst1.begin(), lst2); // lst1 变为 {1, 2, 3, 4},lst2 变为空 {}
std::deque
std::deque(双端队列)是一个支持在两端快速插入和删除元素的顺序容器。与 std::vector 不同,std::deque 在内存中采用分段连续的存储方式,即它的元素分布在多个不连续的内存块中,这些内存块通过指针数组进行管理。由于这一内存布局,std::deque 可以在头尾两端高效地进行插入和删除操作,并且支持随机访问(可以使用operator[])。尽管如此,它的随机访问性能较 std::vector 差一些,因为每个内存块并不连续。
std::deque 不需要像 std::vector 一样每次扩展时重新分配整个内存空间,而是通过扩展指向内存块的指针数组来增加容量。这使得它在动态变化的场景中,尤其是在需要频繁在两端插入和删除元素时,表现得尤为高效。
1. 引入
#include <deque>
2. 存储方式
std::deque 是双端队列,支持在两端进行高效的插入和删除。为了实现这一点,std::deque 使用的是一种分段连续的内存结构。它的内存布局由多个固定大小的内存块(即“段”)组成,每个块内的元素是连续存储的,但这些块在内存中并不连续。通过维护一个指向这些块的指针数组,std::deque 可以实现高效的两端插入和删除操作。
当 std::deque 容量不足时,它会动态扩展新的内存块,并在原有的内存块之间插入新的块。这样,由于每个块都是独立分配的,std::deque 既能保证高效的插入和删除操作,又能提供支持随机访问的能力(虽然随机访问性能不如 std::vector)。
与 std::vector 不同的是,std::deque 不需要一次性重新分配整个内存空间,它只是扩展指向内存块的指针数组。这样,std::deque 可以高效地在头尾进行扩展,并且仍然保持较好的访问性能。
3. 方法
(1) 构造方法
-
deque(): 创建一个空的std::deque。std::deque<int> deq; -
deque(size_t nSize): 创建一个包含nSize个默认构造元素的std::deque。std::deque<int> deq(10); // 创建一个包含10个默认构造元素的 deque -
deque(size_t nSize, const T& value): 创建一个包含nSize个元素,且每个元素的值为value的std::deque。std::deque<int> deq(10, 5); // 创建一个包含10个元素,值都为5的 deque -
deque(const deque&): 复制构造函数,创建一个与另一个std::deque完全相同的副本。std::deque<int> deq1 = {1, 2, 3, 4, 5}; std::deque<int> deq2(deq1); // deq2 是 deq1 的一个复制版本 -
deque(begin, end): 使用另一个容器或迭代器区间中的元素初始化std::deque。std::vector<int> vec = {1, 2, 3, 4, 5}; std::deque<int> deq(vec.begin(), vec.end()); // 复制 vector 的元素到 deque
(2) 大小函数
-
size_t size() const: 返回std::deque中元素的个数。std::deque<int> deq = {1, 2, 3, 4}; std::cout << deq.size(); // 输出 4 -
bool empty() const: 检查std::deque是否为空,若为空返回true,否则返回false。std::deque<int> deq; if (deq.empty()) { std::cout << "Deque is empty." << std::endl; } -
void resize(size_t size): 调整std::deque的大小。若新大小大于当前大小,std::deque会插入默认值的元素;若小于当前大小,则会删除多余的元素。std::deque<int> deq = {1, 2, 3}; deq.resize(5, 10); // 增加两个元素,元素值为 10 -
void shrink_to_fit(): 调整队列预留内存刚好适应当前的大小,节省内存 -
size_t max_size() const: 返回最大可允许的vector元素数量值
(3) 增加函数
-
push_back(const T& value): 将元素value添加到std::deque的末尾。std::deque<int> deq = {1, 2, 3}; deq.push_back(4); // 向 deq 中添加元素 4,deq 变为 {1, 2, 3, 4} -
push_front(const T& value): 将元素value添加到std::deque的开头。std::deque<int> deq = {2, 3, 4}; deq.push_front(1); // 向 deq 中添加元素 1,deq 变为 {1, 2, 3, 4} -
emplace_back(Args&&... args): 在std::deque的末尾就地构造一个元素,使用提供的参数直接构造该元素。std::deque<std::pair<int, int>> deq; deq.emplace_back(1, 2); // 在末尾构造一个 pair<int, int>,值为 {1, 2} -
emplace_front(Args&&... args): 在std::deque的前端就地构造一个元素。std::deque<std::pair<int, int>> deq; deq.emplace_front(1, 2); // 在前端构造一个 pair<int, int>,值为 {1, 2} -
insert(iterator pos, const T& value): 在指定位置pos插入元素value,插入操作会将pos后面的元素向后移动。std::deque<int> deq = {1, 2, 4, 5}; deq.insert(deq.begin() + 2, 3); // 在位置2插入3,deq 变为 {1, 2, 3, 4, 5} -
insert(iterator pos, size_t count, const T& value): 在指定位置pos插入count个元素,所有的元素值为value。std::deque<int> deq = {1, 2, 4, 5}; deq.insert(deq.begin() + 2, 2, 3); // 在位置2插入两个3,deq 变为 {1, 2, 3, 3, 4, 5} -
insert(iterator pos, InputIterator first, InputIterator last): 将[first, last)区间的元素插入到指定位置pos。std::deque<int> deq1 = {1, 2, 3}; std::deque<int> deq2 = {4, 5}; deq1.insert(deq1.begin() + 2, deq2.begin(), deq2.end()); // 在位置2插入 deq2 中的元素,deq1 变为 {1, 2, 4, 5, 3}
(4) 删除函数
-
pop_back(): 删除std::deque中的最后一个元素。std::deque<int> deq = {1, 2, 3, 4}; deq.pop_back(); // 删除最后一个元素,deq 变为 {1, 2, 3} -
pop_front(): 删除std::deque中的第一个元素。std::deque<int> deq = {1, 2, 3, 4}; deq.pop_front(); // 删除第一个元素,deq 变为 {2, 3, 4} -
erase(iterator pos): 删除指定位置pos处的元素,删除操作后,pos后面的元素会向前移动。std::deque<int> deq = {1, 2, 3, 4}; deq.erase(deq.begin() + 2); // 删除索引为2的元素,deq 变为 {1, 2, 4} -
erase(iterator first, iterator last): 删除区间[first, last)内的所有元素。std::deque<int> deq = {1, 2, 3, 4, 5}; deq.erase(deq.begin() + 1, deq.begin() + 4); // 删除从索引1到3的元素,deq 变为 {1, 5} -
clear(): 删除std::deque中的所有元素,使容器变为空。std::deque<int> deq = {1, 2, 3, 4}; deq.clear(); // 删除所有元素,deq 变为空 {}
(5) 遍历函数
-
reference at(int pos): 同Vector, 返回pos位置元素的引用。与operator[]类似,但会检查边界,如果访问无效的位置会抛出std::out_of_range异常。std::vector<int> vec = {1, 2, 3, 4, 5}; int& element = vec.at(2); // 返回位置 2 处元素的引用,即值为 3 element = 10; // 修改元素为 10 std::cout << vec[2] << std::endl; // 输出: 10 -
iterator begin(): 返回指向std::deque第一个元素的迭代器。std::deque<int> deq = {1, 2, 3}; std::deque<int>::iterator it = deq.begin(); // 返回指向第一个元素的迭代器 std::cout << *it << std::endl; // 输出: 1 -
iterator end(): 返回指向std::deque最后一个元素之后位置的迭代器。std::deque<int> deq = {1, 2, 3}; std::deque<int>::iterator it = deq.end(); // 返回指向最后一个元素之后位置的迭代器 --it; // 移动到最后一个元素 std::cout << *it << std::endl; // 输出: 3 -
reverse_iterator rbegin(): 返回指向std::deque最后一个元素的反向迭代器。std::deque<int> deq = {1, 2, 3}; std::deque<int>::reverse_iterator rit = deq.rbegin(); // 返回指向最后一个元素的反向迭代器 std::cout << *rit << std::endl; // 输出: 3 -
reverse_iterator rend(): 返回指向std::deque第一个元素之前位置的反向迭代器。std::deque<int> deq = {1, 2, 3}; std::deque<int>::reverse_iterator rit = deq.rend(); // 返回指向第一个元素之前位置的反向迭代器 ++rit; // 移动到第一个元素 std::cout << *rit << std::endl; // 输出: 1 -
使用基于范围的
for循环(C++11 及以上): 直接遍历std::deque中的每个元素。std::deque<int> deq = {1, 2, 3}; for (const int& num : deq) { std::cout << num << " "; // 输出: 1 2 3 } -
使用传统的
for循环(基于迭代器): 使用迭代器来遍历std::deque。std::deque<int> deq = {1, 2, 3}; for (auto it = deq.begin(); it != deq.end(); ++it) { std::cout << *it << " "; // 输出: 1 2 3 }
(6) 其他函数
-
swap(deque& other): 交换当前std::deque和另一个std::deque的内容。std::deque<int> deq1 = {1, 2, 3}; std::deque<int> deq2 = {4, 5, 6}; deq1.swap(deq2); // deq1 变为 {4, 5, 6},deq2 变为 {1, 2, 3} -
assign(size_t count, const T& value): 将count个value元素赋值给当前std::deque,替换原有内容。std::deque<int> deq; deq.assign(5, 10); // 将 deq 赋值为 {10, 10, 10, 10, 10} -
assign(InputIterator first, InputIterator last): 使用区间[first, last)的元素来填充当前std::deque,替换原有内容。std::deque<int> deq1 = {1, 2, 3}; std::deque<int> deq2; deq2.assign(deq1.begin(), deq1.end()); // deq2 变为 {1, 2, 3} -
std::allocator<T> get_allocator() const: 返回容器所使用的分配器(allocator)。分配器负责容器内存的管理,包括内存的分配、释放等操作。在默认情况下,C++ 标准库容器使用 std::allocator 作为默认的分配器。通过调用 get_allocator(),你可以获取容器使用的分配器,并利用该分配器进行自定义内存操作,或者检查容器如何管理内存。#include <iostream> #include <vector> int main() { std::vector<int> vec; // 获取分配器 std::allocator<int> alloc = vec.get_allocator(); // 使用分配器进行内存分配 int* p = alloc.allocate(5); // 分配5个int元素的内存 // 显示分配的内存地址 std::cout << "Allocated memory at: " << p << std::endl; // 记得释放分配的内存 alloc.deallocate(p, 5); // 释放之前分配的5个int内存 return 0; }
std::set
std::set 是一个排序的关联容器,它存储唯一的元素,并根据元素的顺序进行自动排序。与 std::list 不同,std::set 提供了对元素的高效查找、插入和删除操作,但不支持直接的索引访问。其底层通常是基于平衡二叉树实现(红黑树),因此在查找、插入和删除操作上的时间复杂度通常为 O(log n)。
1. 引入
#include <set>
2. 存储方式
std::set 是基于平衡二叉树(通常是红黑树)实现的容器。与 std::list 和 std::vector 不同,std::set 具有自动排序的特性,元素会按升序(默认情况下)进行排列。每个元素都是唯一的,无法存储重复的元素。
在 std::set 中,元素会以节点的形式存储,每个节点包含一个元素以及指向父节点、左子节点和右子节点的指针。通过这种方式,std::set 保证了所有元素的排序特性,并支持高效的查找、插入和删除操作。
- 插入:由于元素在插入时会自动排序并且不允许重复,插入一个新元素的时间复杂度为 O(log n),这也保证了元素始终保持有序。
- 查找:在
std::set中查找元素的时间复杂度为 O(log n),这是因为它依赖于平衡二叉树的结构。 - 删除:删除操作也有 O(log n) 的时间复杂度,因为删除节点时需要保持树的平衡。
与 std::vector 和 std::list 不同,std::set 的元素是按照某种顺序排列的,因此它支持基于顺序的迭代。由于不允许重复元素,std::set 会自动忽略重复的插入请求。
尽管 std::set 提供了很高效的插入、删除和查找操作,但它并不支持快速的随机访问。要访问集合中的某个元素,必须按顺序遍历元素,无法通过索引直接访问。
3. 方法
(1)构造方法
-
set(): 创建一个空的std::set,默认构造函数。std::set<int> s; std::set<int> s = {1, 2, 3, 4, 5}; std::set<int> s{1, 2, 3, 4, 5}; -
set(const set&): 复制构造函数,创建一个与另一个std::set完全相同的副本。std::set<int> s1 = {1, 2, 3, 4, 5}; std::set<int> s2(s1); // s2 是 s1 的一个复制版本 std::set<int> s3(std::move(s3)); // 移动构造(s3 变为有效但未指定状态,移除所有权) -
set(begin, end): 使用另一个容器或迭代器区间中的元素初始化std::set。std::vector<int> vec = {1, 2, 3, 4, 5}; std::set<int> s(vec.begin(), vec.end()); // 复制 vector 的元素到 set -
std::set<T&, std::greater<T&>>: 按照降序创建set
(2)大小函数
-
size_t size() const: 返回std::set中元素的个数。std::set<int> s = {1, 2, 3, 4}; std::cout << s.size(); // 输出 4 -
bool empty() const: 检查std::set是否为空,若为空返回true,否则返回false。std::set<int> s; if (s.empty()) { std::cout << "Set is empty." << std::endl; } -
size_t max_size() const: 返回最大可允许的set元素数量值
(3)增加函数
-
std::pair<iterator, bool> insert(const T& value): 插入元素到set中,返回一个pair,其first是指向插入元素所在位置的迭代器,其second是插入是否成功(存在重复元素几插入失败)s.insert({1,2,3}); // initializer_list s.insert(s.begin(), 42); // 带 hint 的插入 s.insert(vec.begin(), vec.end()); // 区间插入 -
std::pair<iterator, bool> emplace(_Args&&... __args): 就地构建并插入元素 -
iterator emplace_hint(const_iterator __pos, _Args&&... __args): 就地构建并插入元素,且指定插入位置,但是如果指定位置错误会发生重排序,最坏情况下可能会降低插入的效率
(4)删除元素
-
void clear(): 删除std::set中的所有元素,使容器变为空。std::set<int> s = {1, 2, 3, 4}; s.clear(); // 删除所有元素,s 变为空 {} -
void erase(iterator pos): 删除指定位置pos处的元素std::set<int> s = {1, 2, 3, 4}; s.erase(std::next(s.begin(), 2)); // 删除索引为2的元素,lst 变为 {1, 2, 4} -
size_t erase(const key_type& key): 删除指定键,返回删除个数。 -
erase(first, last): 删除迭代器区间。
(5)查找函数
-
iterator find(const T& value): 查找某元素在set中的位置,返回一个指向该元素的迭代器,如果找不到会返回end() -
std::pair<const_iterator, const_iterator> equal_range(const key_type& __x) const: 返回一对迭代器,表示所有具有等于k的键的元素的范围。由于set包含唯一元素,因此下界将是元素本身,上限将指向键k之后的下一个元素。如果没有与键K匹配的元素,则根据容器的内部比较对象(key_comp),返回的范围的长度为0,两个迭代器均指向大于k的第一个元素。如果键超过了set容器中的最大元素,它将返回一个指向set容器中最后一个元素的迭代器。std::set<int> s = {1, 2, 3, 4}; std::pair p = s.equal_range(2); std::cout<<*p.first<<std::endl; // 输出2 std::cout<<*p.second<<std::endl;// 输出3 -
size_t count(const T& value): 返回是否存在(0 或 1)。 -
iterator lower_bound(const T& value): 返回第一个不小于 value 的迭代器。 -
iterator upper_bound(const T& value): 返回第一个大于 value 的迭代器。 -
bool contains(const T& value) const(C++20): 检查是否包含元素。
(6)遍历函数
-
iterator begin(): 返回指向std::set第一个元素的迭代器。std::set<int> s = {1, 2, 3}; std::set<int>::iterator it = s.begin(); // 返回指向第一个元素的迭代器 std::cout << *it << std::endl; // 输出: 1 -
iterator end(): 返回指向std::set最后一个元素之后位置的迭代器。std::set<int> s = {1, 2, 3}; std::set<int>::iterator it = s.end(); // 返回指向最后一个元素之后位置的迭代器 --it; // 移动到最后一个元素 std::cout << *it << std::endl; // 输出: 3 -
reverse_iterator rbegin(): 返回指向std::set最后一个元素的反向迭代器。std::set<int> s = {1, 2, 3}; std::set<int>::reverse_iterator rit = s.rbegin(); // 返回指向最后一个元素的反向迭代器 std::cout << *rit << std::endl; // 输出: 3 -
reverse_iterator rend(): 返回指向std::set第一个元素之前位置的反向迭代器。std::set<int> s = {1, 2, 3}; std::set<int>::reverse_iterator rit = s.rend(); // 返回指向第一个元素之前位置的反向迭代器 ++rit; // 移动到第一个元素 std::cout << *rit << std::endl; // 输出: 1 -
使用基于范围的
for循环(C++11 及以上): 直接遍历std::set中的每个元素。std::set<int> s = {1, 2, 3}; for (const int& num : s) { std::cout << num << " "; // 输出: 1 2 3 } -
使用传统的
for循环(基于迭代器): 使用迭代器来遍历std::set。std::set<int> s = {1, 2, 3}; for (auto it = s.begin(); it != s.end(); ++it) { std::cout << *it << " "; // 输出: 1 2 3 }
(7)其他函数
-
swap(set& other): 交换当前std::set和另一个std::set的内容。std::set<int> s1 = {1, 2, 3}; std::set<int> s2 = {4, 5, 6}; s1.swap(s2); // s1 变为 {4, 5, 6},s2 变为 {1, 2, 3} -
void merge(set& other):merge会将另一个 set 中不与当前容器重复的元素“移动”过来(O(log n) 插入),保持有序性。即使other是乱序的(但它本身仍然是一个 set,天然有序),结果依旧有序。std::set<int> s1 = {1, 3, 5}; std::set<int> s2 = {2, 4, 6}; s1.merge(s2); // s1 合并 s2 后,变为 {1, 2, 3, 4, 5, 6} -
std::allocator<T> get_allocator() const: 返回容器所使用的分配器(allocator)。分配器负责容器内存的管理,包括内存的分配、释放等操作。在默认情况下,C++ 标准库容器使用 std::allocator 作为默认的分配器。通过调用 get_allocator(),你可以获取容器使用的分配器,并利用该分配器进行自定义内存操作,或者检查容器如何管理内存。#include <iostream> #include <set> int main() { std::set<int> s; // 获取分配器 std::allocator<int> alloc = s.get_allocator(); // 使用分配器进行内存分配 int* p = alloc.allocate(5); // 分配5个int元素的内存 // 显示分配的内存地址 std::cout << "Allocated memory at: " << p << std::endl; // 记得释放分配的内存 alloc.deallocate(p, 5); // 释放之前分配的5个int内存 return 0; } -
比较运算符:支持 ==, !=, <, <=, >, >=,C++20 起支持 <=>,但移除了!=, <, <=, >, >=。
例如
<(小于比较)比较两个 std::set 的顺序,即按照元素的字典顺序(升序排列)来进行比较。如果第一个集合的元素在第一个不相等的地方小于第二个集合,则返回 true。
std::multiset
std::multiset 是与 std::set 类似的一个容器,但与 std::set 不同,std::multiset 允许存储重复的元素。在 std::multiset 中,元素会按顺序排列,并且可以包含多个相同的元素。std::multiset 也提供了高效的查找、插入和删除操作,底层通常使用红黑树结构实现,时间复杂度通常为 O(log n)。
1. 引入
#include <set>
2. 存储方式
std::multiset 是基于平衡二叉树(通常是红黑树)实现的容器。与 std::set 相似,std::multiset 也具有自动排序的特性,元素会按升序(默认情况下)进行排列。但是,不同于 std::set,std::multiset 允许存储重复的元素。
在 std::multiset 中,元素存储在节点中,每个节点包含一个元素及其数量(即该元素出现的次数)。这种方式保证了所有元素的排序特性,同时允许插入多个相同的元素。
- 插入:插入一个新元素的时间复杂度为 O(log n),并且插入重复元素时,
std::multiset会将元素的数量增加,而不是替换现有的元素。 - 查找:在
std::multiset中查找元素的时间复杂度为 O(log n),因为它依赖于平衡二叉树的结构。 - 删除:删除操作也有 O(log n) 的时间复杂度,因为删除节点时需要保持树的平衡。
与 std::set 不同,std::multiset 允许插入重复元素,因此 std::multiset 可以包含多个相同的元素。std::multiset也不支持快速的随机访问。
3. 方法
(1) 构造方法
-
multiset(): 创建一个空的std::multiset,默认构造函数。std::multiset<int> ms; std::multiset<int> ms = {1, 2, 3, 4, 5}; -
multiset(const multiset&): 复制构造函数,创建一个与另一个std::multiset完全相同的副本。std::multiset<int> ms1 = {1, 2, 3, 4, 5}; std::multiset<int> ms2(ms1); // ms2 是 ms1 的一个复制版本 -
multiset(begin, end): 使用另一个容器或迭代器区间中的元素初始化std::multiset。std::vector<int> vec = {1, 2, 3, 4, 5}; std::multiset<int> ms(vec.begin(), vec.end()); // 复制 vector 的元素到 multiset -
std::multiset<T&, std::greater<T&>>: 按照降序创建multiset。
(2) 大小函数
-
size_t size() const: 返回std::multiset中元素的个数。std::multiset<int> ms = {1, 2, 3, 4}; std::cout << ms.size(); // 输出 4 -
bool empty() const: 检查std::multiset是否为空,若为空返回true,否则返回false。std::multiset<int> ms; if (ms.empty()) { std::cout << "Multiset is empty." << std::endl; } -
size_t max_size() const: 返回最大可允许的multiset元素数量。
(3) 增加函数
-
std::pair<iterator, bool> insert(const T& value): 插入元素到multiset中,返回一个pair,其first是指向插入元素所在位置的迭代器,second是插入是否成功(是否为重复元素)。ms.insert({1, 2, 3}); // initializer_list ms.insert(ms.begin(), 42); // 带 hint 的插入 ms.insert(vec.begin(), vec.end()); // 区间插入 -
std::pair<iterator, bool> emplace(_Args&&... __args): 就地构建并插入元素。 -
iterator emplace_hint(const_iterator __pos, _Args&&... __args): 就地构建并插入元素,且指定插入位置。
(4) 删除元素
-
void clear(): 删除std::multiset中的所有元素,使容器变为空。std::multiset<int> ms = {1, 2, 3, 4}; ms.clear(); // 删除所有元素,ms 变为空 {} -
void erase(iterator pos): 删除指定位置pos处的元素。std::multiset<int> ms = {1, 2, 3, 4}; ms.erase(std::next(ms.begin(), 2)); // 删除索引为2的元素,ms 变为 {1, 2, 4} -
size_t erase(const key_type& key): 删除指定键,返回删除个数。 -
erase(first, last): 删除迭代器区间。
(5) 查找函数
-
iterator find(const T& value): 查找某元素在multiset中的位置,返回一个指向该元素的迭代器,如果找不到会返回end()。 -
std::pair<const_iterator, const_iterator> equal_range(const key_type& __x) const: 返回一对迭代器,表示所有具有等于k的键的元素的范围。由于multiset允许重复元素,因此该范围可能包含多个相同的元素。如果没有与键K匹配的元素,则根据容器的内部比较对象(key_comp),返回的范围的长度为0,两个迭代器均指向大于k的第一个元素。如果键超过了multiset容器中的最大元素,它将返回一个指向multiset容器中最后一个元素的迭代器。std::multiset<int> ms = {1, 2, 3, 3, 4}; std::pair p = ms.equal_range(3); std::cout << *p.first << std::endl; // 输出 3 std::cout << *p.second << std::endl;// 输出 4 -
size_t count(const T& value): 返回元素出现的次数。 -
iterator lower_bound(const T& value): 返回第一个不小于value的迭代器。 -
iterator upper_bound(const T& value): 返回第一个大于value的迭代器。 -
bool contains(const T& value) const(C++20):检查是否包含元素。
(6) 遍历函数
-
iterator begin(): 返回指向std::multiset第一个元素的迭代器。std::multiset<int> ms = {1, 2, 3}; std::multiset<int>::iterator it = ms.begin(); // 返回指向第一个元素的迭代器 std::cout << *it << std::endl; // 输出: 1 -
iterator end(): 返回指向std::multiset最后一个元素之后位置的迭代器。 -
reverse_iterator rbegin(): 返回指向std::multiset最后一个元素的反向迭代器。 -
reverse_iterator rend(): 返回指向std::multiset第一个元素之前位置的反向迭代器。 -
使用基于范围的
for循环(C++11 及以上): 直接遍历std::multiset中的每个元素。std::multiset<int> ms = {1, 2, 3}; for (const int& num : ms) { std::cout << num << " "; // 输出: 1 2 3 } -
使用传统的
for循环(基于迭代器): 使用迭代器来遍历std::multiset。std::multiset<int> ms = {1, 2, 3}; for (auto it = ms.begin(); it != ms.end(); ++it) { std::cout << *it << " "; // 输出: 1 2 3 }
(7) 其他函数
-
swap(multiset& other): 交换当前std::multiset和另一个std::multiset的内容。 -
void merge(multiset& other): 将另一个multiset中的元素合并到当前multiset,如果元素重复,则保留所有元素。std::multiset<int> ms1 = {1, 3, 5}; std::multiset<int> ms2 = {2, 4, 6}; ms1.merge(ms2); // ms1 合并 ms2 后,变为 {1, 2, 3, 4, 5, 6} -
std::allocator<T> get_allocator() const: 返回容器所使用的分配器(allocator)。
std::map
std::map 是 C++ 标准库中的一个关联容器,它用于存储一组键值对(key-value),并根据键进行自动排序。每个键在 std::map 中是唯一的,因此不允许存储重复的键。std::map 底层通常使用红黑树结构实现,时间复杂度通常为 O(log n) 用于插入、查找和删除操作。
1. 引入
#include <map>
2. 存储方式
std::map 是基于平衡二叉树(通常是红黑树)实现的容器。与 std::set 类似,std::map 也具有自动排序的特性,元素会根据键(key)进行升序排序(默认情况下)。与 std::set 的区别是,std::map 存储的是键值对,每个键对应一个值,且键是唯一的。
在 std::map 中,元素存储为节点,每个节点包含一个键值对和指向父节点、左子节点和右子节点的指针。通过这种方式,std::map 保证了所有键的排序特性,并支持高效的查找、插入和删除操作。
- 插入:插入一个新元素的时间复杂度为 O(log n),并且如果插入的键已经存在,新的值会覆盖原来的值。
- 查找:在
std::map中查找元素的时间复杂度为 O(log n),这是因为它依赖于平衡二叉树的结构。 - 删除:删除操作也有 O(log n) 的时间复杂度,因为删除节点时需要保持树的平衡。
std::map 不允许重复的键,因此每个键只能对应一个值。如果尝试插入一个已存在的键,插入操作会被忽略。
3. 方法
(1) 构造方法
-
map(): 创建一个空的std::map,默认构造函数。// std::map<key_type, value_type> myMap; std::map<int, std::string> m; std::map<int, std::string> m = {{1, "one"}, {2, "two"}, {3, "three"}}; -
map(const map&): 复制构造函数,创建一个与另一个std::map完全相同的副本。std::map<int, std::string> m1 = {{1, "one"}, {2, "two"}}; std::map<int, std::string> m2(m1); // m2 是 m1 的一个复制版本 -
map(begin, end): 使用另一个容器或迭代器区间中的元素初始化std::map。std::vector<std::pair<int, std::string>> vec = {{1, "one"}, {2, "two"}}; std::map<int, std::string> m(vec.begin(), vec.end()); // 复制 vector 的元素到 map -
std::map<Key, T, Compare>: 使用自定义的比较器来进行键的排序。
(2) 大小函数
-
size_t size() const: 返回std::map中元素的个数。std::map<int, std::string> m = {{1, "one"}, {2, "two"}}; std::cout << m.size(); // 输出 2 -
bool empty() const: 检查std::map是否为空,若为空返回true,否则返回false。std::map<int, std::string> m; if (m.empty()) { std::cout << "Map is empty." << std::endl; } -
size_t max_size() const: 返回最大可允许的map元素数量。
(3) 增加函数
-
std::pair<iterator, bool> insert(const value_type& value): 插入一个元素到std::map中,返回一个pair,其中first是指向插入元素所在位置的迭代器,second表示插入是否成功(如果键已存在,则插入失败)。std::map<int, std::string> m; m.insert({1, "one"}); m.insert({2, "two"}); -
std::pair<iterator, bool> emplace(_Args&&... args): 就地构建并插入元素。 -
iterator emplace_hint(const_iterator hint, _Args&&... args): 就地构建并插入元素,且指定插入位置。 -
std::pair<iterator, bool> try_emplace(const key_type& key, Args&&... args): 是 C++17 引入的一种插入方式。它尝试插入一个元素,如果给定的键已经存在,则不会插入新元素,并且不会更改已有元素的值。它还允许通过参数传递构造函数的参数,避免不必要的拷贝或移动。std::map<int, std::string> m; auto result = m.try_emplace(1, "one"); if (result.second) { std::cout << "Element inserted: " << result.first->second << std::endl; } else { std::cout << "Element with key " << result.first->first << " already exists." << std::endl; } auto result2 = m.try_emplace(1, "uno"); // 不会插入新元素,因为键1已存在 std::cout << "Element: " << result2.first->second << std::endl; // 输出: "one" -
std::pair<iterator, bool> insert_or_assign(const key_type& key, const mapped_type& obj)和std::pair<iterator, bool> insert_or_assign(const key_type& key, mapped_type&& obj): 是 C++17 引入的另一种插入方式,它的作用是:如果键不存在,就插入该键值对;如果键已经存在,则更新其对应的值。 -
operator[]插入方式(mapped_type& operator[](const key_type& key)或mapped_type& operator[](key_type&& key);): 最常见的插入方式。它会检查 std::map 中是否存在给定的键。如果键存在,它返回该键对应的值;如果键不存在,它会插入该键并且初始化对应的值为 mapped_type 的默认值(对于非内置类型,是通过默认构造函数进行初始化)。同样的,这样的方式还可以用来获取键对应的值。std::map<int, std::string> m; m[1] = "one"; // 插入新元素 std::cout << m[1] << std::endl; // 输出: one m[1] = "uno"; // 更新已有的元素 std::cout << m[1] << std::endl; // 输出: uno m[2]; // 键2不存在,会插入键2,并将对应的值初始化为默认值 "" (空字符串) std::cout << m[2] << std::endl; // 输出: (空字符串)
(4) 删除元素
-
void clear(): 删除std::map中的所有元素,使容器变为空。std::map<int, std::string> m = {{1, "one"}, {2, "two"}}; m.clear(); // 删除所有元素,m 变为空 {} -
void erase(iterator pos): 删除指定位置pos处的元素。std::map<int, std::string> m = {{1, "one"}, {2, "two"}}; m.erase(m.begin()); // 删除第一个元素 -
size_t erase(const key_type& key): 删除指定键,返回删除个数。std::map<int, std::string> m = {{1, "one"}, {2, "two"}}; m.erase(1); // 删除键为1的元素 -
erase(first, last): 删除迭代器区间。
(5) 查找函数
-
iterator find(const key_type& key): 查找某个键在map中的位置,返回一个指向该元素的迭代器,如果找不到会返回end()。std::map<int, std::string> m = {{1, "one"}, {2, "two"}}; auto it = m.find(1); // 返回指向键1的迭代器 std::cout << it->second; // 输出 "one" -
std::pair<const_iterator, const_iterator> equal_range(const key_type& key) const: 返回一对迭代器,表示所有具有等于键key的元素的范围。在std::map中,由于键是唯一的,因此这个范围包含一个元素。std::map<int, std::string> m = {{1, "one"}, {2, "two"}}; auto range = m.equal_range(1); std::cout << range.first->second << std::endl; // 输出 "one" -
size_t count(const key_type& key): 返回容器中是否包含指定键,map中每个键最多出现一次,因此返回值要么是0,要么是1。 -
iterator lower_bound(const key_type& key): 返回第一个不小于key的迭代器。 -
iterator upper_bound(const key_type& key): 返回第一个大于key的迭代器。 -
bool contains(const key_type& key) const(C++20):检查是否包含该键。
(6) 遍历函数
-
iterator begin(): 返回指向std::map第一个元素的迭代器。std::map<int, std::string> m = {{1, "one"}, {2, "two"}}; auto it = m.begin(); // 返回指向第一个元素的迭代器 std::cout << it->first << ": " << it->second << std::endl; // 输出 1: one -
iterator end(): 返回指向std::map最后一个元素之后位置的迭代器。 -
reverse_iterator rbegin(): 返回指向std::map最后一个元素的反向迭代器。 -
reverse_iterator rend(): 返回指向std::map第一个元素之前位置的反向迭代器。 -
使用基于范围的
for循环(C++11 及以上): 直接遍历std::map中的每个元素。std::map<int, std::string> m = {{1, "one"}, {2, "two"}}; for (const auto& pair : m) { std::cout << pair.first << ": " << pair.second << " "; // 输出: 1: one 2: two } -
使用传统的
for循环(基于迭代器): 使用迭代器来遍历std::map。std::map<int, std::string> m = {{1, "one"}, {2, "two"}}; for (auto it = m.begin(); it != m.end(); ++it) { std::cout << it->first << ": " << it->second << " "; // 输出: 1: one 2: two }
(7) 其他函数
-
swap(map& other): 交换当前std::map和另一个std::map的内容。 -
std::allocator<T> get_allocator() const: 返回容器所使用的分配器(allocator)。 -
std::map<Key, T, Compare> merge(map& other): 将另一个map容器合并过来,保持有序性。
std::multimap
std::multimap 是一个与 std::map 类似的关联容器,但与 std::map 不同,std::multimap 允许存储重复的键。在 std::multimap 中,键是可以重复的,而每个键都可以关联多个不同的值。与 std::map 相同,std::multimap 也会自动按照键的顺序进行排序。std::multimap 的底层实现通常使用红黑树,时间复杂度通常为 O(log n) 用于插入、查找和删除操作。
1. 引入
#include <map>
2. 存储方式
std::multimap 与 std::map 类似,都是基于平衡二叉树(通常是红黑树)实现的容器。与 std::map 不同,std::multimap 允许同一个键对应多个值,因此在插入重复的键时,std::multimap 会将新的键值对插入到现有键的下方,而不是替换原有的键值对。
std::multimap 中的每个元素都是一个键值对(key-value),并且元素根据键自动排序。每个键在 std::multimap 中可以重复多次,不同的键值对可以共享同一个键。
- 插入:插入一个新元素的时间复杂度为 O(log n),并且允许插入多个具有相同键的元素。
- 查找:在
std::multimap中查找元素的时间复杂度为 O(log n),因为它依赖于平衡二叉树的结构。 - 删除:删除操作也有 O(log n) 的时间复杂度,因为删除节点时需要保持树的平衡。
std::multimap 适用于需要存储多个值并能够按键排序的场景。
3. 方法
(1) 构造方法
-
multimap(): 创建一个空的std::multimap,默认构造函数。std::multimap<int, std::string> mm; std::multimap<int, std::string> mm = {{1, "one"}, {2, "two"}, {3, "three"}}; -
multimap(const multimap&): 复制构造函数,创建一个与另一个std::multimap完全相同的副本。std::multimap<int, std::string> mm1 = {{1, "one"}, {2, "two"}}; std::multimap<int, std::string> mm2(mm1); // mm2 是 mm1 的一个复制版本 -
multimap(begin, end): 使用另一个容器或迭代器区间中的元素初始化std::multimap。std::vector<std::pair<int, std::string>> vec = {{1, "one"}, {2, "two"}}; std::multimap<int, std::string> mm(vec.begin(), vec.end()); // 复制 vector 的元素到 multimap -
std::multimap<Key, T, Compare>: 使用自定义的比较器来进行键的排序。
(2) 大小函数
-
size_t size() const: 返回std::multimap中元素的个数。std::multimap<int, std::string> mm = {{1, "one"}, {2, "two"}}; std::cout << mm.size(); // 输出 2 -
bool empty() const: 检查std::multimap是否为空,若为空返回true,否则返回false。std::multimap<int, std::string> mm; if (mm.empty()) { std::cout << "Multimap is empty." << std::endl; } -
size_t max_size() const: 返回最大可允许的multimap元素数量。
(3) 增加函数
-
std::pair<iterator, bool> insert(const value_type& value): 插入一个元素到std::multimap中,返回一个pair,其中first是指向插入元素所在位置的迭代器,second表示插入是否成功(即该键是否已经存在)。std::multimap<int, std::string> mm; mm.insert({1, "one"}); mm.insert({2, "two"}); mm.insert({1, "uno"}); // 允许插入重复键 -
std::pair<iterator, bool> emplace(_Args&&... args): 就地构建并插入元素。 -
iterator emplace_hint(const_iterator hint, _Args&&... args): 就地构建并插入元素,且指定插入位置。 -
std::pair<iterator, bool> try_emplace(const key_type& key, Args&&... args): 是 C++17 引入的一种插入方式。它尝试插入一个元素,如果给定的键已经存在,则不会插入新元素,并且不会更改已有元素的值。它还允许通过参数传递构造函数的参数,避免不必要的拷贝或移动。std::multimap<int, std::string> m; auto result = m.try_emplace(1, "one"); if (result.second) { std::cout << "Element inserted: " << result.first->second << std::endl; } else { std::cout << "Element with key " << result.first->first << " already exists." << std::endl; } auto result2 = m.try_emplace(1, "uno"); // 不会插入新元素,因为键1已存在 std::cout << "Element: " << result2.first->second << std::endl; // 输出: "one" -
std::pair<iterator, bool> insert_or_assign(const key_type& key, const mapped_type& obj)和std::pair<iterator, bool> insert_or_assign(const key_type& key, mapped_type&& obj): 是 C++17 引入的另一种插入方式,它的作用是:如果键不存在,就插入该键值对;如果键已经存在,则更新其对应的值。 -
operator[]插入方式(mapped_type& operator[](const key_type& key)或mapped_type& operator[](key_type&& key);): 最常见的插入方式。它会检查 std::multimap 中是否存在给定的键。如果键存在,它返回该键对应的值;如果键不存在,它会插入该键并且初始化对应的值为 mapped_type 的默认值(对于非内置类型,是通过默认构造函数进行初始化)。同样的,这样的方式还可以用来获取键对应的值。std::multimap<int, std::string> m; m[1] = "one"; // 插入新元素 std::cout << m[1] << std::endl; // 输出: one m[1] = "uno"; // 更新已有的元素 std::cout << m[1] << std::endl; // 输出: uno m[2]; // 键2不存在,会插入键2,并将对应的值初始化为默认值 "" (空字符串) std::cout << m[2] << std::endl; // 输出: (空字符串)
(4) 删除元素
-
void clear(): 删除std::multimap中的所有元素,使容器变为空。std::multimap<int, std::string> mm = {{1, "one"}, {2, "two"}}; mm.clear(); // 删除所有元素,mm 变为空 {} -
void erase(iterator pos): 删除指定位置pos处的元素。std::multimap<int, std::string> mm = {{1, "one"}, {2, "two"}}; mm.erase(mm.begin()); // 删除第一个元素 -
size_t erase(const key_type& key): 删除指定键,返回删除的元素个数。如果键有重复元素,则会删除所有具有该键的元素。std::multimap<int, std::string> mm = {{1, "one"}, {1, "uno"}, {2, "two"}}; mm.erase(1); // 删除键为 1 的所有元素 -
erase(first, last): 删除迭代器区间。
(5) 查找函数
-
iterator find(const key_type& key): 查找某个键在multimap中的位置,返回一个指向该元素的迭代器,如果找不到会返回end()。std::multimap<int, std::string> mm = {{1, "one"}, {2, "two"}}; auto it = mm.find(1); // 返回指向键1的迭代器 std::cout << it->second; // 输出 "one" -
std::pair<const_iterator, const_iterator> equal_range(const key_type& key) const: 返回一对迭代器,表示所有具有等于键key的元素的范围。在std::multimap中,由于键是可以重复的,因此这个范围可能包含多个相同的元素。std::multimap<int, std::string> mm = {{1, "one"}, {1, "uno"}, {2, "two"}}; auto range = mm.equal_range(1); std::cout << range.first->second << " " << range.second->second << std::endl; -
size_t count(const key_type& key): 返回容器中某个键的元素个数(在std::multimap中可以有多个相同键)。std::multimap<int, std::string> mm = {{1, "one"}, {1, "uno"}, {2, "two"}}; std::cout << mm.count(1); // 输出 2,因为键1出现了两次 -
iterator lower_bound(const key_type& key): 返回第一个不小于key的迭代器。 -
iterator upper_bound(const key_type& key): 返回第一个大于key的迭代器。 -
bool contains(const key_type& key) const(C++20):检查是否包含该键。
(6) 遍历函数
-
iterator begin(): 返回指向std::multimap第一个元素的迭代器。std::multimap<int, std::string> mm = {{1, "one"}, {2, "two"}}; auto it = mm.begin(); // 返回指向第一个元素的迭代器 std::cout << it->first << ": " << it->second << std::endl; // 输出 1: one -
iterator end(): 返回指向std::multimap最后一个元素之后位置的迭代器。 -
reverse_iterator rbegin(): 返回指向std::multimap最后一个元素的反向迭代器。 -
reverse_iterator rend(): 返回指向std::multimap第一个元素之前位置的反向迭代器。 -
使用基于范围的
for循环(C++11 及以上): 直接遍历std::multimap中的每个元素。std::multimap<int, std::string> mm = {{1, "one"}, {2, "two"}}; for (const auto& pair : mm) { std::cout << pair.first << ": " << pair.second << " "; // 输出: 1: one 2: two } -
使用传统的
for循环(基于迭代器): 使用迭代器来遍历std::multimap。std::multimap<int, std::string> mm = {{1, "one"}, {2, "two"}}; for (auto it = mm.begin(); it != mm.end(); ++it) { std::cout << it->first << ": " << it->second << " "; // 输出: 1: one 2: two }
(7) 其他函数
-
swap(multimap& other): 交换当前std::multimap和另一个std::multimap的内容。 -
std::allocator<T> get_allocator() const: 返回容器所使用的分配器(allocator)。 -
std::multimap<Key, T, Compare> merge(multimap& other): 将另一个multimap容器合并过来,保持有序性。
std::unordered_set
std::unordered_set 是一个无序的关联容器,它与 std::set 的主要区别在于元素的存储顺序。std::set 按照元素的升序排列,而 std::unordered_set 不保持任何顺序,它依赖于哈希表实现,因此元素的存储位置是由哈希函数决定的。std::unordered_set 提供了高效的查找、插入和删除操作,底层通常使用哈希表,查找、插入和删除操作的平均时间复杂度为 O(1),但最坏情况下为 O(n)。
unordered_map是一个模板类,其定义如下:
std::unordered_set<Key, Hash = std::hash<Key>, Pred = std::equal_to<Key>, Alloc = std::allocator<Key>>
- Key 是存储在
unordered_set中的元素类型。 - Hash 是一个函数或函数对象,用于生成元素的哈希值,默认为
std::hash<Key>。 - Pred 是一个二元谓词,用于比较两个元素是否相等,默认为
std::equal_to<Key>。 - Alloc 是分配器类型,用于管理内存分配,默认为
std::allocator<Key>。
1. 引入
#include <unordered_set>
2. 存储方式
std::unordered_set 使用哈希表(hash table)来存储元素。每个元素的存储位置由哈希函数计算出来,因此它不保证元素的顺序。哈希表允许非常高效的查找、插入和删除操作,通常时间复杂度为 O(1),但在哈希冲突的情况下,最坏情况的时间复杂度可能退化为 O(n)。
- 插入:插入一个元素时,哈希函数会计算该元素的哈希值,并将元素存储在哈希表中。
- 查找:查找操作的时间复杂度为 O(1),但是哈希冲突可能导致时间复杂度退化为 O(n)。
- 删除:删除操作的时间复杂度通常为 O(1),但最坏情况下可能退化为 O(n)。
std::unordered_set 与 std::set 最大的区别是,它使用哈希表来存储元素,因此没有顺序性,无法进行按顺序的遍历。
3. 方法
(1)构造方法
-
unordered_set(): 创建一个空的std::unordered_set,默认构造函数。std::unordered_set<int> us; std::unordered_set<int> us = {1, 2, 3, 4, 5}; -
unordered_set(const unordered_set&): 复制构造函数,创建一个与另一个std::unordered_set完全相同的副本。std::unordered_set<int> us1 = {1, 2, 3, 4, 5}; std::unordered_set<int> us2(us1); // us2 是 us1 的一个复制版本 -
unordered_set(begin, end): 使用另一个容器或迭代器区间中的元素初始化std::unordered_set。std::vector<int> vec = {1, 2, 3, 4, 5}; std::unordered_set<int> us(vec.begin(), vec.end()); // 复制 vector 的元素到 unordered_set
(2) 大小函数
-
size_t size() const: 返回std::unordered_set中元素的个数。std::unordered_set<int> us = {1, 2, 3, 4}; std::cout << us.size(); // 输出 4 -
bool empty() const: 检查std::unordered_set是否为空,若为空返回true,否则返回false。std::unordered_set<int> us; if (us.empty()) { std::cout << "Unordered Set is empty." << std::endl; } -
size_t max_size() const: 返回最大可允许的unordered_set元素数量。 -
void reserve(size_t n): 用于预先分配哈希表的桶数量。这可以有效减少动态扩容的开销,特别是在你知道将要插入大量元素时。
(3) 增加函数
-
std::pair<iterator, bool> insert(const T& value): 插入元素到unordered_set中,返回一个pair,其中first是指向插入元素所在位置的迭代器,second是插入是否成功(如果存在重复元素,则插入失败)。us.insert({1, 2, 3}); // initializer_list us.insert(us.begin(), 42); // 带 hint 的插入 us.insert(vec.begin(), vec.end()); // 区间插入 -
std::pair<iterator, bool> emplace(_Args&&... args): 就地构建并插入元素。 -
iterator emplace_hint(const_iterator hint, _Args&&... args): 就地构建并插入元素,且指定插入位置,但是如果指定位置错误会发生重排序,最坏情况下可能会降低插入的效率。在哈希索引的前提下是无序的,因此该插入操作等效于insert。
(4) 删除元素
-
void clear(): 删除std::unordered_set中的所有元素,使容器变为空。std::unordered_set<int> us = {1, 2, 3, 4}; us.clear(); // 删除所有元素,us 变为空 {} -
void erase(iterator pos): 删除指定位置pos处的元素。std::unordered_set<int> us = {1, 2, 3, 4}; us.erase(std::next(us.begin(), 2)); // 删除索引为2的元素,us 变为 {1, 2, 4} -
size_t erase(const key_type& key): 删除指定键,返回删除个数。 -
erase(first, last): 删除迭代器区间。
(5) 查找函数
-
iterator find(const T& value): 查找某元素在unordered_set中的位置,返回一个指向该元素的迭代器,如果找不到会返回end()。std::unordered_set<int> us = {1, 2, 3, 4}; auto it = us.find(1); // 返回指向键1的迭代器 if (it != us.end()) { std::cout << *it << std::endl; // 输出: 1 } -
std::pair<const_iterator, const_iterator> equal_range(const key_type& __x) const: 返回一对迭代器,表示所有具有等于k的键的元素的范围。由于set包含唯一元素,因此下界将是元素本身,上限将指向键k之后的下一个元素。如果没有与键K匹配的元素,则根据容器的内部比较对象(key_comp),返回的范围的长度为0,两个迭代器均指向大于k的第一个元素。如果键超过了set容器中的最大元素,它将返回一个指向set容器中最后一个元素的迭代器。 -
size_t count(const T& value): 返回元素是否存在(0 或 1)。 -
bool contains(const T& value) const(C++20):检查是否包含该元素。
(6) 遍历函数
-
iterator begin(): 返回指向std::unordered_set第一个元素的迭代器。 -
iterator end(): 返回指向std::unordered_set最后一个元素之后位置的迭代器。 -
使用基于范围的
for循环(C++11 及以上): 直接遍历std::unordered_set中的每个元素。std::unordered_set<int> us = {1, 2, 3}; for (const int& num : us) { std::cout << num << " "; // 输出: 1 2 3 } -
使用传统的
for循环(基于迭代器): 使用迭代器来遍历std::unordered_set。std::unordered_set<int> us = {1, 2, 3}; for (auto it = us.begin(); it != us.end(); ++it) { std::cout << *it << " "; // 输出: 1 2 3 }
(7)哈希相关函数
-
size_type max_bucket_count() const noexcept: max_bucket_count() 返回 std::unordered_set 中可用的最大哈希桶数量。哈希表是由一组桶(bucket)构成的,每个桶存储一定数量的元素。当元素的数量增加时,std::unordered_set 可能会自动扩展桶的数量,以减少哈希冲突。max_bucket_count() 允许你查看哈希表最多能支持多少个桶。 -
float max_load_factor() const noexcept: max_load_factor() 返回或设置哈希表的最大负载因子。负载因子是哈希表中元素的数量与桶数量之比。它决定了哈希表何时扩展——当负载因子超过最大值时,std::unordered_set 会增加桶的数量,减少哈希冲突,从而提高性能。- 负载因子是指:负载因子 = 元素个数 / 桶的数量
- 最大负载因子是哈希表在扩展之前允许的最大负载因子。负载因子越高,意味着桶的数量相对较少,这可能导致更多的哈希冲突,降低查找效率。
-
void max_load_factor(float __z): 设置最大负载因子。 -
size_t bucket_count() const: 返回当前 unordered_set 的桶数量。哈希表的桶数量与容器中元素的数量以及负载因子相关,通常哈希表会在元素数量增加时自动扩展桶的数量。 -
size_t bucket_size(size_t n) const: 返回指定桶的元素数量。哈希表中的每个桶可能包含多个元素,特别是当哈希冲突发生时,多个元素可能会存储在同一个桶中。 -
size_t bucket(const key_type& key) const: 返回给定键所在的桶的索引。这个函数对于理解元素是如何分布在哈希表中的非常有用。 -
float load_factor() const: 返回当前哈希表的负载因子,即元素的数量与桶数量的比值。负载因子越高,哈希冲突的可能性越大,因此负载因子对于性能非常重要。 -
void rehash(size_t n): 用来调整哈希表中的桶数量。它的作用是根据新的元素数量来调整桶的数量,避免哈希冲突过多。rehash() 会导致重新分配桶并重新哈希元素。n 是希望容器能够容纳的元素数量。
(8) 其他函数
-
swap(unordered_set& other): 交换当前std::unordered_set和另一个std::unordered_set的内容。 -
std::allocator<T> get_allocator() const: 返回容器所使用的分配器(allocator)。 -
void merge(unordered_set& other):merge会将另一个 unordered_set 中不与当前容器重复的元素“移动”过来。std::unordered_set<int> s1 = {1, 3, 5}; std::unordered_set<int> s2 = {2, 4, 6}; s1.merge(s2); // s1 合并 s2 后,变为 {1, 2, 3, 4, 5, 6} -
key_equal_type key_eq() const: 返回一个比较器,用于判断两个键是否相等std::unordered_set<Person, PersonHash> people; Person p1 = {"Alice", 30}; Person p2 = {"Bob", 25}; people.insert(p1); people.insert(p2); // 使用 key_eq 获取比较函数 auto eq = people.key_eq(); // 使用 key_eq 函数比较两个键 std::cout << "Are p1 and p2 equal? " << std::boolalpha << eq(p1, p2) << std::endl;
std::unordered_multiset
std::unordered_multiset 是一个类似于 std::unordered_set 的容器,唯一的区别在于它允许元素重复。它同样使用哈希表实现,不保证元素的顺序,能够提供高效的查找、插入和删除操作。
为了支持重复键,unordered_multiset通过将每个元素存储为一个“桶”中的链表或其他结构。每个“桶”会存储多个相同的元素,而 unordered_set 只允许每个桶中最多存储一个元素。
unordered_multiset 的模板定义如下:
template<
class Key,
class Hash = std::hash<Key>,
class KeyEqual = std::equal_to<Key>,
class Allocator = std::allocator<Key>
> class unordered_multiset; // since C++11
namespace pmr {
template<
class Key,
class Hash = std::hash<Key>,
class Pred = std::equal_to<Key>
> using unordered_multiset = std::unordered_multiset<Key, Hash, Pred,
std::pmr::polymorphic_allocator<Key>>;
} // since C++17
- Key:存储在
unordered_multiset中的元素类型。 - Hash:用于计算元素哈希值的函数或函数对象,默认为
std::hash<Key>。 - Pred:用于比较两个元素是否相等的二元谓词,默认为
std::equal_to<Key>。 - Alloc:分配器类型,用于内存分配,默认为
std::allocator<Key>。
1. 引入
#include <unordered_set>
2. 存储方式
std::unordered_multiset 使用哈希表(hash table)来存储元素。每个元素的存储位置由哈希函数计算出来,多个相同的元素会被存储在同一个桶中。
- 插入:插入一个元素时,哈希函数计算该元素的哈希值,将元素存储在相应的桶中,允许同一元素的多个副本被插入。
- 查找:查找操作的时间复杂度通常为 O(1),但最坏情况下会退化为 O(n),具体取决于哈希表的负载因子和哈希冲突。
- 删除:删除操作通常是 O(1),但最坏情况下会退化为 O(n)。
与 unordered_set 不同,unordered_multiset 允许在同一个哈希表的桶中存储多个相同的元素。
3. 方法
(1) 构造方法
-
unordered_multiset():创建一个空的std::unordered_multiset,使用默认构造函数。std::unordered_multiset<int> us; std::unordered_multiset<int> us = {1, 2, 3, 4, 5}; -
unordered_multiset(const unordered_multiset&):复制构造函数,创建一个与另一个std::unordered_multiset完全相同的副本。std::unordered_multiset<int> us1 = {1, 2, 3, 4, 5}; std::unordered_multiset<int> us2(us1); // us2 是 us1 的一个复制版本 -
unordered_multiset(begin, end):使用另一个容器或迭代器区间中的元素初始化std::unordered_multiset。std::vector<int> vec = {1, 2, 3, 4, 5}; std::unordered_multiset<int> us(vec.begin(), vec.end()); // 复制 vector 的元素到 unordered_multiset
(2) 大小函数
-
size_t size() const:返回std::unordered_multiset中元素的个数。std::unordered_multiset<int> us = {1, 2, 3, 4}; std::cout << us.size(); // 输出 4 -
bool empty() const:检查std::unordered_multiset是否为空,若为空返回true,否则返回false。std::unordered_multiset<int> us; if (us.empty()) { std::cout << "Unordered Multiset is empty." << std::endl; } -
size_t max_size() const:返回最大可允许的unordered_multiset元素数量。 -
void reserve(size_t n):用于预先分配哈希表的桶数量。这可以有效减少动态扩容的开销,特别是在你知道将要插入大量元素时。
(3) 增加函数
-
std::pair<iterator, bool> insert(const T& value):插入元素到unordered_multiset中,返回一个pair,其中first是指向插入元素所在位置的迭代器,second是插入是否成功(如果存在重复元素,则插入成功并增加该元素的数量)。us.insert({1, 2, 3}); // 插入多个元素 us.insert(us.begin(), 42); // 带 hint 的插入 us.insert(vec.begin(), vec.end()); // 区间插入 -
std::pair<iterator, bool> emplace(_Args&&... args):就地构建并插入元素。 -
iterator emplace_hint(const_iterator hint, _Args&&... args):就地构建并插入元素,并指定插入位置。与insert一样,最坏情况下会发生重排序。在哈希索引的前提下是无序的,因此该插入操作等效于insert。
(4) 删除元素
-
void clear():删除std::unordered_multiset中的所有元素,使容器变为空。std::unordered_multiset<int> us = {1, 2, 3, 4}; us.clear(); // 删除所有元素,us 变为空 {} -
void erase(iterator pos):删除指定位置pos处的元素。std::unordered_multiset<int> us = {1, 2, 3, 4}; us.erase(std::next(us.begin(), 2)); // 删除索引为2的元素,us 变为 {1, 2, 4} -
size_t erase(const key_type& key):删除指定键的元素并返回删除的数量,注意一个键可能有多个副本,因此返回的数量可能大于 1。std::unordered_multiset<int> us = {1, 2, 2, 3, 4}; us.erase(2); // 删除所有值为 2 的元素,us 变为 {1, 3, 4} -
erase(first, last):删除指定范围的元素。
(5) 查找函数
-
iterator find(const T& value):查找某元素在unordered_multiset中的位置,返回一个指向该元素的迭代器。如果找不到会返回end()。std::unordered_multiset<int> us = {1, 2, 3, 4}; auto it = us.find(1); // 返回指向键1的迭代器 if (it != us.end()) { std::cout << *it << std::endl; // 输出: 1 } -
std::pair<const_iterator, const_iterator> equal_range(const key_type& __x) const:返回一个迭代器对,表示所有具有等于x的键的元素的范围。由于unordered_multiset允许重复元素,这个范围包含所有相同的键。 -
size_t count(const T& value):返回指定元素在unordered_multiset中出现的次数。对于unordered_multiset,返回的次数可能大于 1,因为允许重复元素。 -
bool contains(const T& value) const(C++20):检查是否包含该元素。
(6) 遍历函数
-
iterator begin():返回指向std::unordered_multiset第一个元素的迭代器。 -
iterator end():返回指向std::unordered_multiset最后一个元素之后位置的迭代器。 -
使用基于范围的
for循环(C++11 及以上): 直接遍历std::unordered_multiset中的每个元素。std::unordered_multiset<int> us = {1, 2, 2, 3}; for (const int& num : us) { std::cout << num << " "; // 输出: 1 2 2 3 } -
使用传统的
for循环(基于迭代器): 使用迭代器来遍历std::unordered_multiset。std::unordered_multiset<int> us = {1, 2, 2, 3}; for (auto it = us.begin(); it != us.end(); ++it) { std::cout << *it << " "; // 输出: 1 2 2 3 }
(7) 哈希相关函数
-
size_type max_bucket_count() const noexcept:返回unordered_multiset中可用的最大哈希桶数量。 -
float max_load_factor() const noexcept:返回或设置哈希表的最大负载因子。 -
void max_load_factor(float __z):设置最大负载因子。 -
size_t bucket_count() const:返回当前unordered_multiset的桶数量。 -
size_t bucket_size(size_t n) const:返回指定桶的元素数量。 -
size_t bucket(const key_type& key) const:返回给定键所在的桶的索引。 -
float load_factor() const:返回当前哈希表的负载因子。 -
void rehash(size_t n):用来调整哈希表中的桶数量。
(8) 其他函数
-
swap(unordered_multiset& other):交换当前unordered_multiset和另一个unordered_multiset的内容。 -
std::allocator<T> get_allocator() const:返回容器所使用的分配器(allocator)。 -
void merge(unordered_multiset& other):合并另一个unordered_multiset中不重复的元素。 -
key_equal_type key_eq() const:返回一个比较器,用于判断两个键是否相等。
std::unordered_map
std::unordered_map 是 C++ 标准库中的一个无序关联容器,用于存储键值对(key-value)。与 std::map 不同的是,unordered_map 内部使用 哈希表 来组织元素,因此元素的顺序不固定,完全由哈希函数决定。
它能在平均 O(1) 时间内完成查找、插入和删除操作(最坏情况下退化为 O(n))。
unordered_map 的定义如下:
template<
class Key,
class T,
class Hash = std::hash<Key>,
class KeyEqual = std::equal_to<Key>,
class Allocator = std::allocator<std::pair<const Key, T>>
> class unordered_map; // since C++11
namespace pmr {
template<
class Key,
class T,
class Hash = std::hash<Key>,
class KeyEqual = std::equal_to<Key>
> using unordered_map =
std::unordered_map<Key, T, Hash, KeyEqual,
std::pmr::polymorphic_allocator<std::pair<const Key, T>>>;
} // since C++17
- Key:键的类型(必须支持哈希和比较相等操作)。
- T:映射值的类型。
- Hash:哈希函数对象,默认为
std::hash<Key>。 - Pred:相等比较函数对象,默认为
std::equal_to<Key>。 - Alloc:分配器类型,默认为
std::allocator<std::pair<const Key, T>>。
1. 引入
#include <unordered_map>
2. 存储方式
std::map使用 红黑树,有序,查找/插入/删除是 O(log n)。std::unordered_map使用 哈希表,无序,查找/插入/删除平均是 O(1),但在哈希冲突严重时,最坏情况可能退化为 O(n)。
每个键值对通过哈希函数映射到某个 桶(bucket) 中,冲突时使用链表或开链方式解决。 键必须唯一,如果插入相同的键,新值会覆盖旧值。
3. 方法
(1) 构造方法
-
unordered_map(): 默认构造函数std::unordered_map<int, std::string> m; std::unordered_map<int, std::string> m = {{1, "one"}, {2, "two"}}; -
unordered_map(const unordered_map&): 复制构造函数std::unordered_map<int, std::string> m1 = {{1, "one"}, {2, "two"}}; std::unordered_map<int, std::string> m2(m1); -
unordered_map(begin, end): 使用另一个容器或迭代器的区间中的元素初始化std::vector<std::pair<int, std::string>> vec = {{1, "one"}, {2, "two"}}; std::unordered_map<int, std::string> m(vec.begin(), vec.end()); -
std::unordered_map<key, T, Compare>: 使用自定义的比较器来进行键的排序struct MyHash { size_t operator()(int x) const { return x % 10; } }; std::unordered_map<int, std::string, MyHash> m;
(2) 大小函数
size_t size() const: 返回元素个数bool empty() const: 是否为空size_t max_size() const: 返回最大可允许元素数量void reserve(size_type __n): 预分配桶的数量,减少扩容开销
(3) 增加函数
-
std::pair<iterator, bool> insert(const value_type& __x): 插入一个键值对,返回保存指向添加键值对位置的迭代器和是否添加成功的标志的pairm.insert({1, "one"}); m.insert(std::make_pair(2, "two")); -
std::pair<iterator, bool> emplace(Key&& key, T&& obj): 如果键 key 不存在,就直接插入一个新元素。m.emplace(3, "three"); -
std::pair<iterator, bool> try_emplace(const Key& key, Args&&... args): 只有在 key 不存在时,才插入新元素(避免额外的拷贝)。m.try_emplace(1, "one"); -
std::pair<iterator, bool> insert_or_assign(const Key& key, T&& obj)m.insert_or_assign(1, "uno"); -
operator[](const Key& key): 如果 key 存在,返回对应的值;如果 key 不存在,插入一个新的键值对,值为默认构造的类型 T。m[4] = "four"; // 插入 m[1] = "uno"; // 更新 -
at(const Key& key):如果 key 已存在,更新对应的值;如果 key 不存在,插入一个新的键值对。std::cout << m.at(1);
(4) 删除元素
void clear():清空所有元素iterator erase(iterator pos):删除迭代器位置的元素iterator erase(const key_type& key):删除指定键,返回删除个数iterator erase(first, last):删除范围
(5) 查找函数
-
iterator find(const key_type& key): 查找某个键在unordered_map中的位置,返回一个指向该元素的迭代器,如果找不到会返回end()。std::unordered_map<int, std::string> m = {{1, "one"}, {2, "two"}}; auto it = m.find(1); // 返回指向键1的迭代器 std::cout << it->second; // 输出 "one" -
std::pair<const_iterator, const_iterator> equal_range(const key_type& key) const: 返回一对迭代器,表示所有具有等于键key的元素的范围。在std::unordered_map中,由于键是唯一的,因此这个范围包含一个元素。std::unordered_map<int, std::string> m = {{1, "one"}, {2, "two"}}; auto range = m.equal_range(1); std::cout << range.first->second << std::endl; // 输出 "one" -
size_t count(const key_type& key): 返回容器中是否包含指定键,unordered_map中每个键最多出现一次,因此返回值要么是0,要么是1。 -
iterator lower_bound(const key_type& key): 返回第一个不小于key的迭代器。 -
iterator upper_bound(const key_type& key): 返回第一个大于key的迭代器。 -
bool contains(const key_type& key) const(C++20):检查是否包含该键。
(6) 遍历函数
-
iterator begin(): 返回指向std::unordered_map第一个元素的迭代器。std::unordered_map<int, std::string> m = {{1, "one"}, {2, "two"}}; auto it = m.begin(); // 返回指向第一个元素的迭代器 std::cout << it->first << ": " << it->second << std::endl; // 输出 1: one -
iterator end(): 返回指向std::unordered_map最后一个元素之后位置的迭代器。 -
reverse_iterator rbegin(): 返回指向std::unordered_map最后一个元素的反向迭代器。 -
reverse_iterator rend(): 返回指向std::unordered_map第一个元素之前位置的反向迭代器。 -
使用基于范围的
for循环(C++11 及以上): 直接遍历std::unordered_map中的每个元素。std::unordered_map<int, std::string> m = {{1, "one"}, {2, "two"}}; for (const auto& pair : m) { std::cout << pair.first << ": " << pair.second << " "; // 输出: 1: one 2: two } -
使用传统的
for循环(基于迭代器): 使用迭代器来遍历std::unordered_map。std::unordered_map<int, std::string> m = {{1, "one"}, {2, "two"}}; for (auto it = m.begin(); it != m.end(); ++it) { std::cout << it->first << ": " << it->second << " "; // 输出: 1: one 2: two }
(7) 哈希相关函数
-
size_type max_bucket_count() const noexcept: max_bucket_count() 返回 std::unordered_set 中可用的最大哈希桶数量。哈希表是由一组桶(bucket)构成的,每个桶存储一定数量的元素。当元素的数量增加时,std::unordered_set 可能会自动扩展桶的数量,以减少哈希冲突。max_bucket_count() 允许你查看哈希表最多能支持多少个桶。 -
float max_load_factor() const noexcept: max_load_factor() 返回或设置哈希表的最大负载因子。负载因子是哈希表中元素的数量与桶数量之比。它决定了哈希表何时扩展——当负载因子超过最大值时,std::unordered_set 会增加桶的数量,减少哈希冲突,从而提高性能。- 负载因子是指:负载因子 = 元素个数 / 桶的数量
- 最大负载因子是哈希表在扩展之前允许的最大负载因子。负载因子越高,意味着桶的数量相对较少,这可能导致更多的哈希冲突,降低查找效率。
-
void max_load_factor(float __z): 设置最大负载因子。 -
size_t bucket_count() const: 返回当前 unordered_set 的桶数量。哈希表的桶数量与容器中元素的数量以及负载因子相关,通常哈希表会在元素数量增加时自动扩展桶的数量。 -
size_t bucket_size(size_t n) const: 返回指定桶的元素数量。哈希表中的每个桶可能包含多个元素,特别是当哈希冲突发生时,多个元素可能会存储在同一个桶中。 -
size_t bucket(const key_type& key) const: 返回给定键所在的桶的索引。这个函数对于理解元素是如何分布在哈希表中的非常有用。 -
float load_factor() const: 返回当前哈希表的负载因子,即元素的数量与桶数量的比值。负载因子越高,哈希冲突的可能性越大,因此负载因子对于性能非常重要。 -
void rehash(size_t n): 用来调整哈希表中的桶数量。它的作用是根据新的元素数量来调整桶的数量,避免哈希冲突过多。rehash() 会导致重新分配桶并重新哈希元素。n 是希望容器能够容纳的元素数量。
(8) 其他函数
-
swap(unordered_map& other): 交换当前std::unordered_map和另一个std::unordered_map的内容。 -
std::allocator<T> get_allocator() const: 返回容器所使用的分配器(allocator)。 -
void merge(unordered_map& other):merge会将另一个 unordered_map 中不与当前容器重复的元素“移动”过来。std::unordered_map<int> s1 = {1, 3, 5}; std::unordered_map<int> s2 = {2, 4, 6}; s1.merge(s2); // s1 合并 s2 后,变为 {1, 2, 3, 4, 5, 6} -
key_equal_type key_eq() const: 返回一个比较器,用于判断两个键是否相等std::unordered_map<Person, PersonHash> people; Person p1 = {"Alice", 30}; Person p2 = {"Bob", 25}; people.insert(p1); people.insert(p2); // 使用 key_eq 获取比较函数 auto eq = people.key_eq(); // 使用 key_eq 函数比较两个键 std::cout << "Are p1 and p2 equal? " << std::boolalpha << eq(p1, p2) << std::endl; -
hasher hash_function() const:返回当前使用的哈希函数对象
std::unordered_multimap
std::unordered_multimap 是 C++ 标准库中的一个无序关联容器,存储一组键值对(key-value)。与 std::unordered_map 类似,std::unordered_multimap 也使用 哈希表 来组织元素,但不同的是,它允许 键重复。
由于其底层实现为哈希表,元素顺序并不固定,完全由哈希函数决定。平均查找、插入、删除的时间复杂度为 O(1),最坏情况下会退化为 O(n)。
std::unordered_multimap 的定义如下:
template<
class Key,
class T,
class Hash = std::hash<Key>,
class KeyEqual = std::equal_to<Key>,
class Allocator = std::allocator<std::pair<const Key, T>>
> class unordered_multimap; // since C++11
namespace pmr {
template<
class Key,
class T,
class Hash = std::hash<Key>,
class Pred = std::equal_to<Key>
> using unordered_multimap =
std::unordered_multimap<Key, T, Hash, Pred,
std::pmr::polymorphic_allocator<std::pair<const Key, T>>>;
} // since C++17
- Key:键的类型(必须支持哈希和比较相等操作)。
- T:映射值的类型。
- Hash:哈希函数对象,默认为
std::hash<Key>。 - Pred:相等比较函数对象,默认为
std::equal_to<Key>。 - Alloc:分配器类型,默认为
std::allocator<std::pair<const Key, T>>。
1. 引入
#include <unordered_map>
2. 存储方式
std::unordered_multimap底层使用 哈希表 来存储元素,每个元素(键值对)会根据其 键 被哈希到一个桶中。- 键的唯一性:与
std::unordered_map不同,std::unordered_multimap允许 多个相同的键 存在,每个键对应多个值。相同键的元素会存储在同一个桶中。 - 插入与查找:所有操作(查找、插入、删除)平均时间复杂度为 O(1),但在哈希冲突严重时,最坏情况可能退化为 O(n)。
3. 方法
(1) 构造方法
-
unordered_multimap(): 默认构造函数std::unordered_multimap<int, std::string> m; std::unordered_multimap<int, std::string> m = {{1, "one"}, {2, "two"}}; -
unordered_multimap(const unordered_multimap&): 复制构造函数std::unordered_multimap<int, std::string> m1 = {{1, "one"}, {2, "two"}}; std::unordered_multimap<int, std::string> m2(m1); -
unordered_multimap(begin, end): 使用另一个容器或迭代器的区间中的元素初始化std::vector<std::pair<int, std::string>> vec = {{1, "one"}, {2, "two"}}; std::unordered_multimap<int, std::string> m(vec.begin(), vec.end()); -
std::unordered_multimap<key, T, Hash>: 使用自定义的哈希函数struct MyHash { size_t operator()(int x) const { return x % 10; } }; std::unordered_multimap<int, std::string, MyHash> m;
(2) 大小函数
size_t size() const: 返回元素个数bool empty() const: 是否为空size_t max_size() const: 返回最大可允许元素数量void reserve(size_type __n): 预分配桶的数量,减少扩容开销
(3) 插入函数
-
std::pair<iterator, bool> insert(const value_type& __x): 插入一个键值对,返回保存指向添加键值对位置的迭代器和是否添加成功的标志的pairm.insert({1, "one"}); m.insert(std::make_pair(2, "two")); -
std::pair<iterator, bool> emplace(Key&& key, T&& obj): 如果键key不存在,就直接插入一个新元素。m.emplace(3, "three"); -
std::pair<iterator, bool> try_emplace(const Key& key, Args&&... args): 只有在key不存在时,才插入新元素(避免额外的拷贝)。m.try_emplace(1, "one"); -
std::pair<iterator, bool> insert_or_assign(const Key& key, T&& obj): 如果key存在,更新对应的值;如果key不存在,插入一个新的键值对。m.insert_or_assign(1, "uno"); -
operator[](const Key& key): 如果key存在,返回对应的值;如果key不存在,插入一个新的键值对,值为默认构造的类型 T。m[4] = "four"; // 插入 m[1] = "uno"; // 更新 -
at(const Key& key): 如果key已存在,返回对应的值;如果key不存在,抛出std::out_of_range。std::cout << m.at(1); // 输出值
(4) 删除元素
-
void clear(): 清空所有元素 -
iterator erase(iterator pos): 删除迭代器位置的元素 -
size_t erase(const key_type& key): 删除指定键的所有元素,返回删除的数量m.erase(1); // 删除键为1的所有元素 -
iterator erase(first, last): 删除指定迭代器区间的元素
(5) 查找函数
-
iterator find(const key_type& key): 查找指定键的第一个匹配元素,返回一个指向该元素的迭代器,如果找不到返回end()。auto it = m.find(2); if (it != m.end()) { std::cout << it->second << std::endl; } -
std::pair<const_iterator, const_iterator> equal_range(const key_type& key) const: 返回一对迭代器,表示所有具有等于键key的元素的范围。auto range = m.equal_range(2); for (auto it = range.first; it != range.second; ++it) { std::cout << it->second << std::endl; } -
size_t count(const key_type& key): 返回容器中指定键的元素数量(对于unordered_multimap,返回值可能大于 1)。size_t n = m.count(2); // 返回键2的数量 -
bool contains(const key_type& key) const(C++20):检查是否包含指定键。if (m.contains(2)) { std::cout << "Key 2 exists!" << std::endl; }
(6) 遍历函数
-
iterator begin(): 返回指向容器第一个元素的迭代器auto it = m.begin(); std::cout << it->first << " -> " << it->second << std::endl; -
iterator end(): 返回指向容器最后一个元素之后位置的迭代器 -
reverse_iterator rbegin(): 返回指向容器最后一个元素的反向迭代器 -
reverse_iterator rend(): 返回指向容器第一个元素之前位置的反向迭代器 -
使用基于范围的
for循环:for (const auto& [key, value] : m) { std::cout << key << " -> " << value << std::endl; } -
使用传统的迭代器遍历:
for (auto it = m.begin(); it != m.end(); ++it) { std::cout << it->first << " -> " << it->second << std::endl; }
(7) 哈希相关函数
size_t bucket_count() const: 返回桶的数量size_t bucket_size(size_t n) const: 返回指定桶的元素数量size_t bucket(const key_type& key) const: 返回给定键所落入的桶的索引float load_factor() const: 返回当前负载因子(元素数 / 桶数)void rehash(size_t n): 强制调整桶的数量,避免哈希冲突void reserve(size_t n): 确保容器至少可以容纳n个元素
(8) 其他函数
swap(unordered_multimap& other): 交换当前unordered_multimap和另一个容器的内容std::allocator<T> get_allocator() const: 返回容器的分配器void merge(unordered_multimap& other): 将另一个unordered_multimap中不重复的元素“移动”过来(C++17)hasher hash_function() const: 返回当前使用的哈希函数key_equal_type key_eq() const: 返回用于判断键是否相等的谓词
std::stack
std::stack (栈) 是一个容器适配器 (Container Adapter),它不是一个独立的容器,而是对现有顺序容器(如 std::deque、std::list 或 std::vector)的封装,限制了对其元素的访问,使其遵循 LIFO (Last-In, First-Out) 的数据结构规则。
想象一下堆叠起来的盘子,你只能在顶部放一个新盘子,也只能从顶部取走盘子。在 std::stack 中,这个“顶部”对应于底层容器的尾部。
1. 引入
#include <stack>
2. 存储方式
std::stack 通过封装一个底层容器 (Underlying Container) 来存储数据。它只暴露了符合栈(LIFO)操作的方法,例如:
- 推入 (Push): 插入元素到栈顶(对应底层容器的
push_back())。 - 弹出 (Pop): 移除栈顶元素(对应底层容器的
pop_back())。 - 查看 (Top): 访问栈顶元素(对应底层容器的
back())。
默认底层容器:如果创建 std::stack 时不指定底层容器,它默认使用 std::deque<T>。
可选底层容器:任何满足 LIFO 访问要求的顺序容器都可以作为底层容器,包括:
std::deque<T>(默认):通常是栈操作的最佳选择,因为它在两端操作(push_front/pop_front和push_back/pop_back)都很快。std::vector<T>:如果内存是连续的且不需要在底部(前端)操作,这也是一个高效的选择。std::list<T>:如果需要存储不支持连续存储或拷贝构造/赋值操作的复杂对象,可以使用它。
声明带指定底层容器的栈:
// 使用 vector 作为底层容器
std::stack<int, std::vector<int>> stack_vec;
// 使用 list 作为底层容器
std::stack<int, std::list<int>> stack_list;
3. 方法
由于 std::stack 是一个适配器,它不提供任何迭代器(如 begin()/end()),也不支持随机访问(如 operator[]),只能通过 LIFO 接口进行操作。
(1) 构造方法
-
stack(): 创建一个空的std::stack。std::stack<int> s; // 使用默认的 std::deque 作为底层容器 -
stack(const Container& cont): 使用一个已存在的底层容器副本进行初始化。std::deque<int> d = {1, 2, 3}; std::stack<int> s(d); // s 变为 {1, 2, 3} (1 是底,3 是顶) -
stack(const stack&): 复制构造函数。
(2) 大小函数
-
size_t size() const: 返回std::stack中元素的个数。std::stack<int> s = {1, 2, 3}; std::cout << s.size(); // 输出 3 -
bool empty() const: 检查std::stack是否为空。若为空返回true。std::stack<int> s; if (s.empty()) { std::cout << "Stack is empty." << std::endl; }
(3) 元素访问函数
std::stack 只允许访问顶部元素。
-
reference top(): 返回栈顶元素(即最近push进去的元素)的引用。调用前必须确保栈不为空。std::stack<int> s; s.push(10); s.push(20); std::cout << s.top(); // 输出 20
(4) 增加函数 (推入)
-
void push(const T& value): 将元素value复制或移动到栈的顶部。std::stack<int> s; s.push(10); // 栈变为 {10} s.push(20); // 栈变为 {10, 20} (20 在顶) -
template<class... Args> void emplace(Args&&... args): 在栈的顶部就地构造一个元素,无需拷贝或移动。这是推荐的推入方式,尤其是对于复杂对象。std::stack<std::pair<int, int>> s; s.emplace(1, 2); // 直接在栈顶构造 pair {1, 2}
(5) 删除函数 (弹出)
-
void pop(): 移除栈顶的元素。此函数不返回被移除的元素。调用前必须确保栈不为空。std::stack<int> s; s.push(10); s.push(20); // 栈为 {10, 20} s.pop(); // 移除 20,栈变为 {10} // 注意:要获取并移除元素,需要先调用 top(),再调用 pop()
(6) 其他函数
-
void swap(stack& other): 交换当前std::stack和另一个std::stack的内容。底层容器的内容也会随之交换。std::stack<int> s1; s1.push(1); std::stack<int> s2; s2.push(2); s1.swap(s2); // s1 变为 {2},s2 变为 {1}
std::queue
std::queue (队列) 和 std::stack 类似,也是 C++ 标准库中的一个容器适配器 (Container Adapter)。它将底层容器的功能限制在一个特定的访问模式上,以实现 FIFO (First-In, First-Out,先进先出) 的数据结构。
想象一下排队等候的队伍:第一个进入队伍的人也是第一个离开队伍的人。在 std::queue 中,元素从尾部(Back)进入,从头部(Front)离开。
1. 引入
#include <queue>
2. 存储方式与底层容器
std::queue 通过封装一个底层容器 (Underlying Container) 来存储数据。它只暴露了符合队列(FIFO)操作的方法:
- 入队 (Push): 插入元素到队列的尾部(对应底层容器的
push_back())。 - 出队 (Pop): 移除队列的头部元素(对应底层容器的
pop_front())。 - 查看 (Front/Back): 访问头部/尾部元素。
默认底层容器:如果创建 std::queue 时不指定,它默认使用 std::deque<T>。
可选底层容器:底层容器必须同时支持在头部(前端)移除元素(pop_front())和在尾部(后端)添加元素(push_back())。
满足这些要求的标准容器有:
std::deque<T>(默认):通常是最佳选择,因为它在两端操作(push_back和pop_front)都非常高效且 \(O(1)\)$。std::list<T>:如果元素类型不可复制,或者需要频繁地插入/删除,这也是一个合适的选择,操作复杂度也是 \(O(1)\)$。
注意:std::vector<T> 不能作为 std::queue 的底层容器,因为它不支持高效地从头部移除元素(pop_front),std::vector 的 erase(begin()) 操作是 \(O(N)\) 的。
声明带指定底层容器的队列:
// 使用 list 作为底层容器
std::queue<int, std::list<int>> queue_list;
// 使用默认的 deque 作为底层容器
std::queue<int> queue_deque;
3. 常用方法 (操作)
std::queue 同样不提供迭代器,因此不能像 std::vector 或 std::list 那样遍历所有元素。
(1) 构造方法
-
queue(): 创建一个空的std::queue。std::queue<int> q; -
queue(const Container& cont): 使用一个已存在的底层容器副本进行初始化。std::deque<int> d = {10, 20, 30}; // 10 是 front, 30 是 back std::queue<int> q(d); // q 变为 {10, 20, 30}
(2) 大小函数
size_t size() const: 返回std::queue中元素的个数。bool empty() const: 检查std::queue是否为空。若为空返回true。
(3) 元素访问函数
std::queue 允许访问队列的头部和尾部元素。
-
reference front(): 返回队列头部元素(即最先进入队列的元素)的引用。调用前必须确保队列不为空。 -
reference back(): 返回队列尾部元素(即最近进入队列的元素)的引用。调用前必须确保队列不为空。std::queue<int> q; q.push(10); // q: {10} q.push(20); // q: {10, 20} std::cout << q.front(); // 输出 10 std::cout << q.back(); // 输出 20
(4) 增加函数 (入队)
-
void push(const T& value): 将元素value复制或移动到队列的尾部。 -
template<class... Args> void emplace(Args&&... args): 在队列的尾部就地构造一个元素。这是首选的入队方式。std::queue<std::string> q; q.push("Apple"); // 尾部插入 q.emplace("Banana"); // 尾部就地构造 -
template<container-compatible-range R> void push_range(R&& rg)(C++23): 将一个范围(range)内的所有元素插入到队列的尾部。
(5) 删除函数 (出队)
-
void pop(): 移除队列头部的元素。此函数不返回被移除的元素。调用前必须确保队列不为空。std::queue<int> q; q.push(10); q.push(20); // q: {10, 20} // 要取出元素 10,需要两步操作: int oldest = q.front(); // 访问头部 (10) q.pop(); // 移除头部 (10),q 变为 {20}
(6) 交换
void swap(queue& other): 交换两个std::queue的内容。
4. 示例 (FIFO 原则)
通过一个简单的例子来演示 FIFO 的工作原理:
#include <iostream>
#include <queue>
int main() {
std::queue<int> my_queue;
// 入队 (Enroll)
my_queue.push(100); // 100 先进
my_queue.push(200);
my_queue.push(300); // 300 后进
std::cout << "队列头部 (Front/First-in): " << my_queue.front() << std::endl; // 输出 100
std::cout << "队列尾部 (Back/Last-in): " << my_queue.back() << std::endl; // 输出 300
std::cout << "--------------------" << std::endl;
// 出队 (Serve)
while (!my_queue.empty()) {
std::cout << "服务并移除: " << my_queue.front() << std::endl;
my_queue.pop();
}
// 元素的移除顺序为 100, 200, 300,严格遵循先进先出。
return 0;
}
输出:
队列头部 (Front/First-in): 100
队列尾部 (Back/Last-in): 300
--------------------
服务并移除: 100
服务并移除: 200
服务并移除: 300
std::priority_queue
std::priority_queue 是一个 容器适配器 (Container Adapter),它基于堆 (heap) 实现,能够在对数时间内插入元素,并且始终允许在常数时间内访问到“优先级最高”的元素。
默认情况下,它是一个 最大堆 (Max Heap),即 top() 返回容器中的最大元素。通过自定义比较器,可以变成最小堆或实现任意的优先级规则。
典型应用场景:调度系统、Dijkstra 最短路、Huffman 编码、任务队列等。
1. 引入
#include <queue>
2. 存储方式
std::priority_queue 内部使用一个 底层容器 (Underlying Container) 和一个 比较器 (Compare) 来实现堆的功能。
模板定义
template<
class T,
class Container = std::vector<T>,
class Compare = std::less<typename Container::value_type>
> class priority_queue;
- T:存储的元素类型。
- Container:底层容器,默认为
std::vector<T>。必须支持随机访问迭代器和push_back()/pop_back()。 - Compare:比较器,默认为
std::less<T>(大顶堆)。若改为std::greater<T>,则变为小顶堆。
默认实现
- 大顶堆:
Compare = std::less<T> - 小顶堆:
Compare = std::greater<T>
示例:指定底层容器和比较器
// 默认:大顶堆
std::priority_queue<int> pq1;
// 小顶堆
std::priority_queue<int, std::vector<int>, std::greater<int>> pq2;
// 使用 deque 作为底层容器
std::priority_queue<int, std::deque<int>> pq3;
3. 方法
std::priority_queue 同样不提供迭代器(不能遍历),只能通过专门的接口进行操作。
(1) 构造方法
-
priority_queue():构造一个空的优先队列。std::priority_queue<int> pq; // 空的大顶堆 -
priority_queue(const Compare& comp, const Container& cont):用已有容器构造。std::vector<int> vec = {1, 5, 3}; std::priority_queue<int> pq(std::less<int>(), vec); -
复制构造 / 移动构造。
(2) 大小函数
-
size_t size() const:返回元素个数。std::priority_queue<int> pq; pq.push(10); pq.push(20); std::cout << pq.size(); // 输出 2 -
bool empty() const:检查是否为空。if (pq.empty()) std::cout << "Empty\n";
(3) 元素访问函数
-
const_reference top() const:返回堆顶元素(优先级最高的元素),复杂度 O(1)。 调用前必须确保不为空。pq.push(10); pq.push(30); pq.push(20); std::cout << pq.top(); // 输出 30(大顶堆)
(4) 增加函数 (插入)
-
void push(const T& value):插入元素到队列,自动保持堆序。复杂度 O(log n)。pq.push(15); pq.push(40); -
template<class... Args> void emplace(Args&&... args):原地构造元素并插入。std::priority_queue<std::pair<int,int>> pq; pq.emplace(1, 2); // 构造 pair(1,2)
(5) 删除函数 (移除)
-
void pop():移除堆顶元素,复杂度 O(log n)。调用前必须非空。pq.push(10); pq.push(20); pq.pop(); // 移除 20(大顶堆)
(6) 其他函数
-
void swap(priority_queue& other):交换两个队列的内容。std::priority_queue<int> pq1, pq2; pq1.push(1); pq2.push(2); pq1.swap(pq2);
4. 示例
大顶堆
#include <iostream>
#include <queue>
using namespace std;
int main() {
priority_queue<int> pq;
pq.push(10);
pq.push(5);
pq.push(20);
cout << pq.top() << endl; // 20
pq.pop();
cout << pq.top() << endl; // 10
}
小顶堆
priority_queue<int, vector<int>, greater<int>> min_heap;
min_heap.push(10);
min_heap.push(5);
min_heap.push(20);
cout << min_heap.top() << endl; // 5
自定义比较器(例如:比较 pair 的第二个元素)
struct Compare {
bool operator()(const pair<int,int>& a, const pair<int,int>& b) {
return a.second > b.second; // 小顶堆,按第二个元素比较
}
};
priority_queue<pair<int,int>, vector<pair<int,int>>, Compare> pq;
pq.push({1, 10});
pq.push({2, 5});
cout << pq.top().first << endl; // 输出 2,因为 (2,5) 的 second 更小
Algorithms
C++ 标准库中的算法 (Algorithms) 是一组强大的函数模板,用于对容器或其他范围([first, last))内的元素进行搜索、排序、计数、修改等操作。自 C++20 以来,Ranges (约束算法) 的引入极大地简化了算法的使用,并增强了其通用性。C++17 则引入了执行策略,允许算法进行并行或乱序执行以提升性能。
C++20 约束算法 (Constrained Algorithms / Ranges)
Ranges 是 C++20 引入的一项革命性特性,它极大地简化了 STL 算法的使用,并增强了它们的组合性。
核心概念与优势
| 特性 | 优势和示例 |
|---|---|
| 单一 Range 参数 | 您不再需要手动传递 begin() 和 end() 迭代器。只需将整个容器(或 Range 视图)作为一个参数传递给算法即可。 |
| Projections (投影) | 简化复杂对象操作。 算法可以直接操作容器内元素的某个成员变量或计算结果,而无需使用复杂的 Lambda 表达式。例如,对一个 vector<Person> 按照 Person::age 成员进行排序。 |
| 返回类型增强 | 提供所有有用的信息。 大多数 std::ranges:: 算法不再只返回一个迭代器,而是返回一个包含所有相关结果的结构体(如 std::ranges::sort_result)。例如,ranges::copy 会返回输入范围和输出范围的结束迭代器,方便后续操作。 |
| 更强的约束 | 它们是“约束算法”,这意味着它们使用 C++20 的 Concepts 确保传入的迭代器和 Range 满足算法所需的最低要求,从而提供更清晰的编译错误。 |
示例:
std::vector<int> v{7, 1, 4, 0, -1};
// 经典算法:需要显式传入begin()和end()
std::sort(v.begin(), v.end());
// C++20 Ranges 算法:更简洁
std::ranges::sort(v);
C++17/C++20 执行策略 (Execution Policies)
执行策略 是 C++17 引入的功能,它允许程序员通过向算法传递一个策略对象,来指示算法如何执行——是串行、并行还是乱序执行,从而在多核 CPU 上实现性能优化。
核心策略类型
| 策略类型 | 全局对象 | C++ 版本 | 描述 |
|---|---|---|---|
sequenced_policy | seq | C++17 | 序列化执行:算法按顺序执行,不会使用并行或乱序操作。这是所有算法的默认行为。 |
parallel_policy | par | C++17 | 并行执行:算法可以在不同的线程中并发执行。它保证了并发性,但不保证指令的顺序。 |
parallel_unsequenced_policy | par_unseq | C++17 | 并行且乱序执行:结合了并行和乱序执行,允许并发执行,同时允许编译器进行矢量化 (vectorization) 优化。 |
unsequenced_policy | unseq | C++20 | 乱序执行:不保证并行,但允许编译器对单个线程中的操作进行乱序处理(矢量化),以提高性能。 |
头文件: 所有执行策略都定义在 <execution> 头文件中,并位于 std::execution 命名空间下(尽管全局对象如 std::par 在 std 命名空间)。
示例:
#include <algorithm>
#include <execution>
#include <vector>
std::vector<int> data = ...;
// 使用并行策略对数据进行排序,以利用多核CPU
std::sort(std::execution::par, data.begin(), data.end());
// C++20 Ranges 版本也支持策略(如果算法有重载)
std::ranges::sort(std::execution::par, data);
并行算法的技术限制
在使用执行策略时,有一个关键的安全限制需要注意:
对于大多数并行算法(除了 std::for_each 和 std::for_each_n):
- 如果 Range 中的元素类型 \( T \) 满足 Trivial Copy Construction (
std::is_trivially_copy_constructible_v<T> == true) 和 Trivial Destruction (std::is_trivially_destructible_v<T> == true),库可以对元素进行任意复制,以便在不同的并行线程之间分发数据。
这意味着: 如果你的自定义对象具有非平凡(复杂)的构造函数、析构函数或资源管理,使用并行策略时需要额外小心,以确保你的操作是线程安全的。对于基本类型(如 int, float)和简单的 C 结构体,这通常不是问题。
一、非修改序列操作 (Non-modifying sequence operations)
主要头文件:
<algorithm>:包含绝大多数经典的非修改序列操作。<ranges>:包含所有ranges::版本的算法。
批处理操作 (Batch operations)
| 批处理操作 (Batch operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| for_each / ranges::for_each | (C++20) | <algorithm> / <ranges> | 对范围内的元素应用一个一元函数对象 |
| for_each_n / ranges::for_each_n | (C++17) / (C++20) | <algorithm> / <ranges> | 对序列的前 \( N \) 个元素应用一个函数对象 |
搜索操作 (Search operations)
| 搜索操作 (Search operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| all_of / ranges::all_of | (C++11) / (C++20) | <algorithm> / <ranges> | 检查一个谓词对范围内的所有元素是否为真 |
| any_of / ranges::any_of | (C++11) / (C++20) | <algorithm> / <ranges> | 检查一个谓词对范围内的任一元素是否为真 |
| none_of / ranges::none_of | (C++11) / (C++20) | <algorithm> / <ranges> | 检查一个谓词对范围内的所有元素是否为假 |
| ranges::contains | (C++23) | <ranges> | 检查范围是否包含给定元素 |
| ranges::contains_subrange | (C++23) | <ranges> | 检查范围是否包含给定子范围 |
| find / ranges::find | (C++20) | <algorithm> / <ranges> | 查找满足特定条件的第一个元素 |
| find_if / ranges::find_if | (C++11) / (C++20) | <algorithm> / <ranges> | 查找满足谓词的第一个元素 |
| find_if_not / ranges::find_if_not | (C++11) / (C++20) | <algorithm> / <ranges> | 查找不满足谓词的第一个元素 |
| ranges::find_last | (C++23) | <ranges> | 查找满足特定条件的最后一个元素 |
| ranges::find_last_if | (C++23) | <ranges> | 查找满足谓词的最后一个元素 |
| ranges::find_last_if_not | (C++23) | <ranges> | 查找不满足谓词的最后一个元素 |
| find_end / ranges::find_end | (C++20) | <algorithm> / <ranges> | 查找某一范围内的最后一个序列 |
| find_first_of / ranges::find_first_of | (C++20) | <algorithm> / <ranges> | 搜索一组元素中的任意一个 |
| adjacent_find / ranges::adjacent_find | (C++20) | <algorithm> / <ranges> | 查找第一个两个相邻且相等的项(或满足给定谓词) |
| count / ranges::count | (C++20) | <algorithm> / <ranges> | 返回满足特定条件的元素的数量 |
| count_if / ranges::count_if | (C++20) | <algorithm> / <ranges> | 返回满足谓词的元素的数量 |
| mismatch / ranges::mismatch | (C++20) | <algorithm> / <ranges> | 查找两个范围第一次不同的位置 |
| equal / ranges::equal | (C++20) | <algorithm> / <ranges> | 确定两组元素是否相同 |
| search / ranges::search | (C++20) | <algorithm> / <ranges> | 搜索一个元素范围的第一次出现 |
| search_n / ranges::search_n | (C++20) | <algorithm> / <ranges> | 搜索一个元素在范围中连续出现 \( N \) 次的第一次出现 |
| ranges::starts_with | (C++23) | <ranges> | 检查一个范围是否以另一个范围开始 |
| ranges::ends_with | (C++23) | <ranges> | 检查一个范围是否以另一个范围结束 |
折叠操作 (Fold operations)
| 折叠操作 (Fold operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| ranges::fold_left | (C++23) | <ranges> | 对一系列元素进行左折叠 |
| ranges::fold_left_first | (C++23) | <ranges> | 使用第一个元素作为初始值对一系列元素进行左折叠 |
| ranges::fold_right | (C++23) | <ranges> | 对一系列元素进行右折叠 |
| ranges::fold_right_last | (C++23) | <ranges> | 使用最后一个元素作为初始值对一系列元素进行右折叠 |
| ranges::fold_left_with_iter | (C++23) | <ranges> | 对一系列元素进行左折叠,并返回 (迭代器, 值) 对 |
| ranges::fold_left_first_with_iter | (C++23) | <ranges> | 使用第一个元素作为初始值对一系列元素进行左折叠,并返回 (迭代器, 可选值) 对 |
二、修改序列操作 (Modifying sequence operations)
主要头文件:
<algorithm>:包含绝大多数经典的修改序列操作。<ranges>:包含所有ranges::版本的算法。<utility>:包含std::swap和std::iter_swap。
复制操作 (Copy operations)
| 复制操作 (Copy operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| copy / ranges::copy | (C++11) / (C++20) | <algorithm> / <ranges> | 将一个元素范围复制到一个新位置 |
| copy_if / ranges::copy_if | (C++11) / (C++20) | <algorithm> / <ranges> | 有条件地复制一个元素范围到新位置 |
| copy_n / ranges::copy_n | (C++11) / (C++20) | <algorithm> / <ranges> | 将指定数量的元素复制到一个新位置 |
| copy_backward / ranges::copy_backward | (C++20) | <algorithm> / <ranges> | 以向后顺序复制一个元素范围 |
| move / ranges::move | (C++11) / (C++20) | <algorithm> / <ranges> | 将一个元素范围移动到一个新位置 |
| move_backward / ranges::move_backward | (C++11) / (C++20) | <algorithm> / <ranges> | 以向后顺序移动一个元素范围 |
交换操作 (Swap operations)
| 交换操作 (Swap operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| swap | (C++11) | <utility> | 交换两个对象的值 |
| swap_ranges / ranges::swap_ranges | (C++20) | <algorithm> / <ranges> | 交换两个元素范围 |
| iter_swap | <utility> | 交换两个迭代器所指向的元素 |
变换操作 (Transformation operations)
| 变换操作 (Transformation operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| transform / ranges::transform | (C++20) | <algorithm> / <ranges> | 对一个元素范围应用一个函数,并将结果存储在目标范围中 |
| replace / ranges::replace | (C++20) | <algorithm> / <ranges> | 将所有满足特定条件的值替换为另一个值 |
| replace_if / ranges::replace_if | (C++20) | <algorithm> / <ranges> | 将所有满足谓词的值替换为另一个值 |
| replace_copy / ranges::replace_copy | (C++20) | <algorithm> / <ranges> | 复制一个范围,并将满足特定条件的元素替换为另一个值 |
| replace_copy_if / ranges::replace_copy_if | (C++20) | <algorithm> / <ranges> | 复制一个范围,并将满足谓词的元素替换为另一个值 |
生成操作 (Generation operations)
| 生成操作 (Generation operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| fill / ranges::fill | (C++20) | <algorithm> / <ranges> | 将给定值赋值给范围内的每个元素 |
| fill_n / ranges::fill_n | (C++20) | <algorithm> / <ranges> | 将给定值赋值给范围内的 \( N \) 个元素 |
| generate / ranges::generate | (C++20) | <algorithm> / <ranges> | 将连续函数调用的结果赋值给范围内的每个元素 |
| generate_n / ranges::generate_n | (C++20) | <algorithm> / <ranges> | 将连续 \( N \) 次函数调用的结果赋值给范围内的 \( N \) 个元素 |
移除操作 (Removing operations)
这里的“移除”通常是逻辑移除,通过将未被移除的元素移动到范围的前部来实现,并返回新的逻辑尾部。
| 移除操作 (Removing operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| remove / ranges::remove | (C++20) | <algorithm> / <ranges> | 移除满足特定条件的元素(逻辑移除) |
| remove_if / ranges::remove_if | (C++20) | <algorithm> / <ranges> | 移除满足谓词的元素(逻辑移除) |
| remove_copy / ranges::remove_copy | (C++20) | <algorithm> / <ranges> | 复制一个范围,省略满足特定条件的元素 |
| remove_copy_if / ranges::remove_copy_if | (C++20) | <algorithm> / <ranges> | 复制一个范围,省略满足谓词的元素 |
| unique / ranges::unique | (C++20) | <algorithm> / <ranges> | 移除范围内的连续重复元素(逻辑移除) |
| unique_copy / ranges::unique_copy | (C++20) | <algorithm> / <ranges> | 创建一个不含连续重复元素的范围副本 |
顺序改变操作 (Order-changing operations)
| 顺序改变操作 (Order-changing operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| reverse / ranges::reverse | (C++20) | <algorithm> / <ranges> | 反转范围内的元素顺序 |
| reverse_copy / ranges::reverse_copy | (C++20) | <algorithm> / <ranges> | 创建一个反转后的范围副本 |
| rotate / ranges::rotate | (C++20) | <algorithm> / <ranges> | 旋转范围内的元素顺序 |
| rotate_copy / ranges::rotate_copy | (C++20) | <algorithm> / <ranges> | 复制并旋转一个元素范围 |
| shift_left / ranges::shift_left | (C++20) / (C++23) | <algorithm> / <ranges> | 左移范围内的元素 |
| shift_right / ranges::shift_right | (C++20) / (C++23) | <algorithm> / <ranges> | 右移范围内的元素 |
| shuffle / ranges::shuffle | (C++11) / (C++20) | <algorithm> / <ranges> | 随机重新排序范围内的元素 |
采样操作 (Sampling operations)
| 采样操作 (Sampling operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| sample / ranges::sample | (C++17) / (C++20) | <algorithm> / <ranges> | 从序列中选择 \( N \) 个随机元素 |
三、排序及相关操作 (Sorting and related operations)
主要头文件:
<algorithm>:包含绝大多数经典的排序及相关操作。<ranges>:包含所有ranges::版本的算法。
分区操作 (Partitioning operations)
| 分区操作 (Partitioning operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| is_partitioned / ranges::is_partitioned | (C++11) / (C++20) | <algorithm> / <ranges> | 确定范围是否被给定谓词分区 |
| partition / ranges::partition | (C++20) | <algorithm> / <ranges> | 将一个元素范围划分为两组 |
| partition_copy / ranges::partition_copy | (C++11) / (C++20) | <algorithm> / <ranges> | 复制一个范围,将元素划分为两组 |
| stable_partition / ranges::stable_partition | (C++20) | <algorithm> / <ranges> | 将元素划分为两组,同时保持它们的相对顺序 |
| partition_point / ranges::partition_point | (C++11) / (C++20) | <algorithm> / <ranges> | 定位一个已分区范围的分区点 |
排序操作 (Sorting operations)
| 排序操作 (Sorting operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| sort / ranges::sort | (C++20) | <algorithm> / <ranges> | 将一个范围排序为升序 |
| stable_sort / ranges::stable_sort | (C++20) | <algorithm> / <ranges> | 排序一个元素范围,同时保持相等元素间的相对顺序 |
| partial_sort / ranges::partial_sort | (C++20) | <algorithm> / <ranges> | 排序一个范围的前 \( N \) 个元素 |
| partial_sort_copy / ranges::partial_sort_copy | (C++20) | <algorithm> / <ranges> | 复制并部分排序一个元素范围 |
| is_sorted / ranges::is_sorted | (C++11) / (C++20) | <algorithm> / <ranges> | 检查一个范围是否已排序为升序 |
| is_sorted_until / ranges::is_sorted_until | (C++11) / (C++20) | <algorithm> / <ranges> | 查找最大的已排序子范围 |
| nth_element / ranges::nth_element | (C++20) | <algorithm> / <ranges> | 部分排序给定范围,确保它被给定元素分区 |
二分搜索操作 (Binary search operations)
这些操作要求输入范围必须是已排序的。
| 二分搜索操作 (Binary search operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| lower_bound / ranges::lower_bound | (C++20) | <algorithm> / <ranges> | 返回第一个不小于给定值的元素的迭代器 |
| upper_bound / ranges::upper_bound | (C++20) | <algorithm> / <ranges> | 返回第一个大于给定值的元素的迭代器 |
| equal_range / ranges::equal_range | (C++20) | <algorithm> / <ranges> | 返回匹配特定键的元素的范围 |
| binary_search / ranges::binary_search | (C++20) | <algorithm> / <ranges> | 确定一个元素是否存在于一个部分有序的范围中 |
集合操作 (Set operations)
这些操作要求输入范围必须是已排序的。
| 集合操作 (Set operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| includes / ranges::includes | (C++20) | <algorithm> / <ranges> | 如果一个序列是另一个序列的子序列则返回 true |
| set_union / ranges::set_union | (C++20) | <algorithm> / <ranges> | 计算两个集合的并集 |
| set_intersection / ranges::set_intersection | (C++20) | <algorithm> / <ranges> | 计算两个集合的交集 |
| set_difference / ranges::set_difference | (C++20) | <algorithm> / <ranges> | 计算两个集合的差集 |
| set_symmetric_difference / ranges::set_symmetric_difference | (C++20) | <algorithm> / <ranges> | 计算两个集合的对称差集 |
合并操作 (Merge operations)
| 合并操作 (Merge operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| merge / ranges::merge | (C++20) | <algorithm> / <ranges> | 合并两个已排序的范围 |
| inplace_merge / ranges::inplace_merge | (C++20) | <algorithm> / <ranges> | 在原地合并两个有序范围 |
堆操作 (Heap operations)
这些操作用于维护最大堆 (max heap) 的属性。
| 堆操作 (Heap operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| push_heap / ranges::push_heap | (C++20) | <algorithm> / <ranges> | 向最大堆中添加一个元素 |
| pop_heap / ranges::pop_heap | (C++20) | <algorithm> / <ranges> | 从最大堆中移除最大的元素 |
| make_heap / ranges::make_heap | (C++20) | <algorithm> / <ranges> | 将一个元素范围创建为最大堆 |
| sort_heap / ranges::sort_heap | (C++20) | <algorithm> / <ranges> | 将一个最大堆转换为一个升序排序的元素范围 |
| is_heap / ranges::is_heap | (C++11) / (C++20) | <algorithm> / <ranges> | 检查给定范围是否为最大堆 |
| is_heap_until / ranges::is_heap_until | (C++11) / (C++20) | <algorithm> / <ranges> | 查找作为最大堆的最大子范围 |
最小/最大操作 (Minimum/maximum operations)
| 最小/最大操作 (Minimum/maximum operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| max / ranges::max | (C++20) | <algorithm> / <ranges> | 返回给定值中的较大者 |
| max_element / ranges::max_element | (C++20) | <algorithm> / <ranges> | 返回范围中的最大元素 |
| min / ranges::min | (C++20) | <algorithm> / <ranges> | 返回给定值中的较小者 |
| min_element / ranges::min_element | (C++20) | <algorithm> / <ranges> | 返回范围中的最小元素 |
| minmax / ranges::minmax | (C++11) / (C++20) | <algorithm> / <ranges> | 返回两个元素中的较小者和较大者 |
| minmax_element / ranges::minmax_element | (C++11) / (C++20) | <algorithm> / <ranges> | 返回范围中的最小和最大元素 |
| clamp / ranges::clamp | (C++17) / (C++20) | <algorithm> / <ranges> | 将一个值钳制在一对边界值之间 |
字典序比较操作 (Lexicographical comparison operations)
| 字典序比较操作 (Lexicographical comparison operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| lexicographical_compare / ranges::lexicographical_compare | (C++20) | <algorithm> / <ranges> | 如果一个范围字典序上小于另一个,则返回 true |
| lexicographical_compare_three_way | (C++20) | <algorithm> | 使用三路比较比较两个范围 |
排列操作 (Permutation operations)
| 排列操作 (Permutation operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| next_permutation / ranges::next_permutation | (C++20) | <algorithm> / <ranges> | 生成范围元素的下一个更大的字典序排列 |
| prev_permutation / ranges::prev_permutation | (C++20) | <algorithm> / <ranges> | 生成范围元素的下一个更小的字典序排列 |
| is_permutation / ranges::is_permutation | (C++11) / (C++20) | <algorithm> / <ranges> | 确定一个序列是否是另一个序列的排列 |
四、数值操作 (Numeric operations)
主要头文件:
<numeric>:包含所有传统的数值算法和 C++17 的并行数值算法。<ranges>:包含ranges::iota。
| 数值操作 (Numeric operations) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| iota / ranges::iota | (C++11) / (C++23) | <numeric> / <ranges> | 用递增的起始值填充一个范围 |
| accumulate | <numeric> | 对一个元素范围求和或折叠 | |
| inner_product | <numeric> | 计算两个元素范围的内积 | |
| adjacent_difference | <numeric> | 计算一个范围中相邻元素之间的差值 | |
| partial_sum | <numeric> | 计算一个元素范围的部分和 | |
| reduce | (C++17) | <numeric> | 类似于 std::accumulate,但无序 |
| exclusive_scan | (C++17) | <numeric> | 类似于 std::partial_sum,但不包括第 \( N \) 个输入元素在第 \( N \) 个和中 |
| inclusive_scan | (C++17) | <numeric> | 类似于 std::partial_sum,包括第 \( N \) 个输入元素在第 \( N \) 个和中 |
| transform_reduce | (C++17) | <numeric> | 应用一个可调用对象,然后无序归约 |
| transform_exclusive_scan | (C++17) | <numeric> | 应用一个可调用对象,然后计算独占扫描 |
| transform_inclusive_scan | (C++17) | <numeric> | 应用一个可调用对象,然后计算包含扫描 |
五、未初始化内存操作 (Operations on uninitialized memory)
主要头文件:
<memory>:包含所有未初始化内存操作。
| 未初始化内存操作 (Operations on uninitialized memory) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| uninitialized_copy / ranges::uninitialized_copy | (C++20) | <memory> | 将一系列对象复制到一块未初始化内存区域 |
| uninitialized_copy_n / ranges::uninitialized_copy_n | (C++11) / (C++20) | <memory> | 将指定数量的对象复制到一块未初始化内存区域 |
| uninitialized_fill / ranges::uninitialized_fill | (C++20) | <memory> | 将一个对象复制赋值给一块未初始化内存区域 |
| uninitialized_fill_n / ranges::uninitialized_fill_n | (C++20) | <memory> | 将一个对象复制赋值给指定数量的未初始化内存区域 |
| uninitialized_move / ranges::uninitialized_move | (C++17) / (C++20) | <memory> | 将一系列对象移动到一块未初始化内存区域 |
| uninitialized_move_n / ranges::uninitialized_move_n | (C++17) / (C++20) | <memory> | 将指定数量的对象移动到一块未初始化内存区域 |
| uninitialized_default_construct / ranges::uninitialized_default_construct | (C++17) / (C++20) | <memory> | 在一块未初始化内存区域中默认构造对象 |
| uninitialized_default_construct_n / ranges::uninitialized_default_construct_n | (C++17) / (C++20) | <memory> | 在一块未初始化内存区域中默认构造 \( N \) 个对象 |
| uninitialized_value_construct / ranges::uninitialized_value_construct | (C++17) / (C++20) | <memory> | 在一块未初始化内存区域中值构造对象 |
| uninitialized_value_construct_n / ranges::uninitialized_value_construct_n | (C++17) / (C++20) | <memory> | 在一块未初始化内存区域中值构造 \( N \) 个对象 |
| destroy / ranges::destroy | (C++17) / (C++20) | <memory> | 销毁一系列对象 |
| destroy_n / ranges::destroy_n | (C++17) / (C++20) | <memory> | 销毁范围内的 \( N \) 个对象 |
| destroy_at / ranges::destroy_at | (C++17) / (C++20) | <memory> | 销毁给定地址处的对象 |
| construct_at / ranges::construct_at | (C++20) | <memory> | 在给定地址处创建一个对象 |
六、其他算法和工具
随机数生成 (Random number generation)
主要头文件:
<ranges>
| 随机数生成 (Random number generation) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| ranges::generate_random | (C++26) | <ranges> | 用均匀随机位生成器填充一个随机数范围 |
C 库函数 (C library functions)
主要头文件:
<cstdlib>(或<stdlib.h>)
| C 库函数 (C library functions) | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|
| qsort | <cstdlib> | 排序一个类型未指定的元素范围 | |
| bsearch | <cstdlib> | 搜索一个类型未指定的数组中的元素 |
附注:执行策略 (Execution policies)
主要头文件:
<execution>
| 执行策略类型 (Execution policy types) | 宏/类/对象 | C++ 版本 | 头文件 | 描述 (功能) |
|---|---|---|---|---|
sequenced_policy (seq) | 类 / 全局对象 | (C++17) | <execution> | 序列化执行(无并行) |
parallel_policy (par) | 类 / 全局对象 | (C++17) | <execution> | 并行执行 |
parallel_unsequenced_policy (par\_unseq) | 类 / 全局对象 | (C++17) | <execution> | 并行且乱序执行 |
unsequenced_policy (unseq) | 类 / 全局对象 | (C++20) | <execution> | 乱序执行(允许编译器向量化) |
| is_execution_policy | 类模板 | (C++17) | <execution> | 测试一个类是否表示一个执行策略 |
文件与流(Files and Streams)
在 C++ 中,流(Stream) 是输入输出系统的核心概念。所有数据的读写操作——无论来自键盘、内存、文件还是网络——都被统一抽象为“流”的形式。 从概念上讲,文件是流的一种具体实现:流是一种数据传输通道,而文件流则是通向磁盘文件的通道。
一、流的基本概念
在程序运行过程中,数据在设备之间不断流动。C++ 将数据的输入输出过程抽象为一个“流(stream)”,即:
数据在内存与外部设备之间的有序传输。
流有两种基本方向:
| 类型 | 类名 | 说明 |
|---|---|---|
| 输入流 | istream | 数据从设备流入程序(例如键盘输入、文件读取) |
| 输出流 | ostream | 数据从程序流向设备(例如屏幕输出、文件写入) |
C++ 的标准输入输出(如 cin、cout、cerr、clog)都是基于这套流机制实现的。
二、C++ 流类层次结构
C++ 标准库为不同的数据来源提供了不同种类的流类,这些类共同组成一个继承体系:
ios_base
└── ios
├── istream // 输入流
│ ├── ifstream // 文件输入流
│ └── istringstream// 字符串输入流
├── ostream // 输出流
│ ├── ofstream // 文件输出流
│ └── ostringstream// 字符串输出流
└── iostream // 输入输出流
├── fstream // 文件输入输出流
└── stringstream // 字符串输入输出流
可以看到,无论是文件流、字符串流还是标准流,它们都共享相同的接口和操作方式。 因此,掌握流的基本用法,就能轻松在不同输入输出介质之间迁移代码。
三、流的分类与用途
| 类型 | 头文件 | 主要类 | 典型用途 |
|---|---|---|---|
| 标准输入输出流 | <iostream> | cin, cout, cerr, clog | 控制台输入输出 |
| 文件流 | <fstream> | ifstream, ofstream, fstream | 读取与写入文件 |
| 字符串流 | <sstream> | istringstream, ostringstream, stringstream | 内存中字符串格式化与解析 |
四、文件作为流的体现
在操作文件时,我们使用 ifstream、ofstream、fstream 来打开磁盘文件并执行读写。
但本质上,这些类并没有引入新的 I/O 模型,而是继承自 istream / ostream,仅仅改变了流的来源或去向:
ifstream:从文件读取数据(输入流)ofstream:向文件写入数据(输出流)fstream:既可读也可写(双向流)
这种统一的流模型让文件操作与普通输入输出完全一致:
std::ifstream fin("input.txt");
std::ofstream fout("output.txt");
int x;
fin >> x; // 从文件读取
fout << x * 2; // 写入文件
同时,为了适应多种文件类型,还支持基于二进制操作文件流:
// 二进制写入
std::ofstream fout_bin("data.bin", std::ios::binary);
int x = 42;
fout_bin.write(reinterpret_cast<const char*>(&x), sizeof(x));
五、字符串流的作用
<sstream> 提供了面向内存字符串的流操作。
它们与文件流类似,但数据读写的目标是内存字符串而非磁盘文件,非常适合:
- 格式化文本(如将数值转为字符串)
- 从字符串中提取结构化数据
- 临时缓冲输出内容
std::stringstream ss;
ss << "Result: " << 42;
std::string text = ss.str(); // "Result: 42"
早期 C++ 还提供
<strstream>实现基于字符数组的流,但由于安全性和内存管理问题,现已由<sstream>完全取代。
六、流状态与错误处理
在 C++ 的流系统中,无论是标准输入输出流、文件流还是字符串流,都共享一套统一的状态机制。 每个流对象都维护着一个内部状态,用于反映当前输入输出操作的健康状况。程序可以通过这些状态来判断流是否处于可用、结束或错误状态,从而实现可靠的错误控制。
1. 状态标志(Stream State Flags)
C++ 通过四种主要的状态标志来描述流的状态,这些标志可能同时存在,用于表达复杂情况:
| 状态名 | 成员常量 | 含义 |
|---|---|---|
goodbit | std::ios::goodbit | 一切正常,流处于可用状态 |
eofbit | std::ios::eofbit | 已到达输入结束(End Of File) |
failbit | std::ios::failbit | 输入失败,通常是格式不匹配(如期望数字却读到字符) |
badbit | std::ios::badbit | 流已损坏,通常是严重的系统性错误(如设备失效) |
流的状态存储在 std::ios 基类中,因此所有继承自它的类(如 istream、ostream、fstream、stringstream)都拥有相同的状态接口。
2. 状态检查接口
流对象提供了多种方式用于检查状态:
| 函数 | 返回值 | 说明 |
|---|---|---|
good() | true / false | 流是否处于正常状态 |
eof() | true / false | 是否到达文件或输入末尾 |
fail() | true / false | 是否发生输入失败 |
bad() | true / false | 是否出现系统性错误 |
rdstate() | iostate | 返回全部状态标志的组合 |
例如:
int x;
std::cin >> x;
if (std::cin.fail()) {
std::cerr << "输入错误:类型不匹配。" << std::endl;
}
当用户输入非数字时,cin.fail() 将为 true,表示提取操作失败。
3. 状态恢复与忽略输入
一旦流进入错误状态,后续输入输出操作将被阻塞。要恢复流,需要手动清除错误标志并可能丢弃无效输入:
std::cin.clear(); // 清除所有错误标志
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 忽略当前行
这段代码常用于防止错误输入导致程序陷入死循环,是交互式程序中非常典型的输入修复模式。
4. 状态机制的意义
C++ 的流状态机制让输入输出更具鲁棒性和通用性。 无论是键盘输入、文件读取,还是内存字符串解析,都可以通过相同的方式检测和处理异常。这种设计体现了“统一的流模型”思想:
- 所有流对象共享同一组状态接口;
- 程序可根据状态灵活决定后续逻辑;
- 错误恢复无需依赖具体 I/O 类型。
这种一致性为大型项目中的 I/O 管理提供了强大的可移植性与可扩展性。
总结
“文件与流”既是 C++ I/O 系统的核心概念,也是工程实践中最常用的技术之一:
- 日志系统基于文件流;
- 配置文件解析常通过字符串流;
- 网络传输底层也可抽象为流。
掌握流的思想,意味着可以用同一套接口处理不同数据源,为程序的输入输出设计提供统一模型。
标准输入输出流(iostream)
C++ 的输入输出体系以 <iostream> 头文件为核心。它定义了程序与外部设备(例如键盘与显示器)交互的最基本机制,是所有流操作的基础。标准输入输出流提供了一组通用接口,用于在内存与外部设备之间以流的形式进行数据传输。
一、标准流对象
C++ 预定义了四个标准流对象,分别用于输入、输出与错误处理:
| 对象名 | 所属类 | 方向 | 描述 |
|---|---|---|---|
cin | istream | 输入 | 从标准输入(键盘)读取数据 |
cout | ostream | 输出 | 向标准输出(控制台)写入数据 |
cerr | ostream | 输出 | 向标准错误输出写入数据(不带缓冲) |
clog | ostream | 输出 | 向标准错误输出写入数据(带缓冲) |
cin 和 cout 是最常用的两个流对象,它们对应于 C 语言中的 stdin 与 stdout。而 cerr 与 clog 则用于错误与日志输出:
cerr立即输出,不经过缓冲,适合错误提示;clog使用缓冲区,适合记录日志或调试信息。
二、输入与输出运算符
C++ 使用运算符重载机制,使输入与输出操作更自然直观:
int a;
std::cin >> a; // 输入,将数据流入变量 a
std::cout << a << '\n'; // 输出,将数据流出到控制台
>> 是提取运算符(extraction operator),从输入流中提取数据;
<< 是插入运算符(insertion operator),向输出流中插入数据。
这两个运算符被广泛重载,可用于所有基本类型与标准容器(通过 operator<< 的重载),使得流式编程成为 C++ 的一大特色。例如:
std::string name;
int age;
std::cin >> name >> age;
std::cout << "Name: " << name << ", Age: " << age << std::endl;
每个操作符都返回流对象自身的引用,从而允许链式调用。
三、流缓冲与刷新机制
所有标准输出流都带有缓冲区。输出内容首先被写入缓冲区,当缓冲区满、遇到换行符、调用 flush 或程序结束时,缓冲内容才会真正输出到设备。
常见的刷新方式有:
std::endl:输出换行并刷新缓冲区;std::flush:仅刷新缓冲区;std::ends:输出一个空字符并刷新缓冲区。
std::cout << "Hello, world!" << std::endl; // 换行并刷新
这种延迟输出机制提高了效率,但在交互程序中需要注意及时刷新,否则可能出现输出滞后。
四、格式化输出
C++ 流提供了丰富的格式化控制机制,用于调整输出格式。最常见的控制包括:
-
操纵符(Manipulators) 通过
<iomanip>头文件可以使用setw,setfill,setprecision,fixed,scientific等控制输出格式:#include <iomanip> double pi = 3.14159265; std::cout << std::fixed << std::setprecision(3) << pi; // 输出 3.142 -
流格式标志 可以通过
setf()、unsetf()修改流的格式状态,例如控制对齐方式、进制表示、是否显示符号等:std::cout.setf(std::ios::showpos); std::cout << 42; // 输出 +42
五、ios 与 ios_base
ios_base 是所有流类的根基,提供全局的格式化与状态管理功能,如:
- 格式标志(
fmtflags) - I/O 状态(
iostate) - 用户自定义存储(
xalloc,iword,pword)
而 ios 则在此基础上增加了缓冲区管理与错误处理机制,是 istream 与 ostream 的共同父类。
六、标准流的重定向
C++ 允许将标准输入输出流重定向到文件或其他流对象,实现灵活的数据通道。例如:
std::ofstream fout("log.txt");
std::streambuf* backup = std::cout.rdbuf(fout.rdbuf()); // 重定向 cout 到文件
std::cout << "This will be written to file." << std::endl;
std::cout.rdbuf(backup); // 恢复
这种技术常用于日志系统或单元测试环境,能够让程序在不修改逻辑的情况下改变输出目标。
七、非格式化输入与其他高级输入方法
提取运算符 >>(格式化输入)在读取数据时会跳过开头的空白符,并且遇到空格、制表符或换行符时会停止,这不适用于读取包含空格的字符串或需要精确控制读取字节数的情况。C++ iostream 库提供了一系列非格式化输入函数来满足这些需求。
1. 读取整行:getline()
std::getline() 是最常用的非格式化输入函数之一,用于读取一行文本,包括其中的空格。
| 函数签名 | 描述 |
|---|---|
std::getline(istream& is, std::string& str, char delim) | 从输入流 is 中读取字符,直到遇到指定的分隔符 delim(默认为 \n),并将读取的内容存入 str。分隔符会被读取,但不会存入 str。 |
std::getline(istream& is, std::string& str) | 使用默认的分隔符 \n 读取一行。 |
#include <iostream>
#include <string>
std::string fullName;
std::cout << "Enter your full name: ";
// 读取整行输入,直到遇到换行符
std::getline(std::cin, fullName);
std::cout << "Welcome, " << fullName << std::endl;
2. 读取单个字符:get()
get() 函数用于从流中读取单个字符,且不会跳过空白符。
| 函数签名 | 描述 |
|---|---|
is.get(char& ch) | 将流中的下一个字符存入 ch。 |
is.get() | 返回流中的下一个字符(作为 int 类型),或返回 EOF(文件结束)。 |
char c1, c2;
std::cin.get(c1); // 读取第一个字符(可能是空格)
std::cin.get(c2); // 读取第二个字符
std::cout << "c1: " << c1 << ", c2: " << c2 << std::endl;
3. 忽略流中字符:ignore()
ignore() 函数常用于清除输入缓冲区中残余的字符,特别是在混合使用格式化输入(>>)和 getline() 时。
| 函数签名 | 描述 |
|---|---|
is.ignore(streamsize count, char delim) | 从输入流中丢弃最多 count 个字符,直到遇到指定的分隔符 delim。分隔符也会被丢弃。 |
int age;
char gender;
std::cin >> age;
// 假设用户输入 "25\n"
// `>> age` 读取了 25,但换行符 '\n' 仍留在缓冲区。
// 清除缓冲区中剩余的字符,直到遇到换行符
std::cin.ignore(10000, '\n');
std::cout << "Enter gender (M/F): ";
std::cin.get(gender);
// 现在 `get()` 可以正确读取新的输入,而不是残留在缓冲区的 '\n'
注意:
10000是一个较大的数字,确保能够处理大多数行长度。
4. 窥视下一个字符:peek()
peek() 函数用于查看流中下一个可用的字符,但不会将其从流中移除。
// 假设流中下一个字符是 'H'
char nextChar = std::cin.peek();
// nextChar 是 'H'
// 'H' 仍然在输入流中,可供下次操作读取
5. 高级操作:read() 与 gcount()
read() 是一个底层的非格式化输入函数,用于读取指定数量的原始字节数据,常用于处理二进制文件。
| 函数签名 | 描述 |
|---|---|
is.read(char* s, streamsize n) | 从流中读取 n 个字节到内存地址 s 开始的缓冲区。 |
在调用 read() 或任何非格式化输入函数后,可以使用 gcount() 来获取最近一次非格式化输入操作实际读取的字符数量。
char buffer[10];
// 尝试从流中读取 10 个字节
std::cin.read(buffer, 10);
// 报告实际读取的字节数(可能小于 10,例如遇到文件末尾)
std::streamsize actualRead = std::cin.gcount();
字符串流(sstream)
在 C++ 中,字符串流(String Streams) 是一种特殊的内存流,它将字符串抽象为输入输出流,从而允许程序像操作文件或控制台那样读写字符串。字符串流的核心头文件为 <sstream>,主要用于格式化文本、解析数据或在内存中临时缓存输出内容。相比直接操作字符串,字符串流提供了统一、可扩展且安全的接口。
一、字符串流的分类
字符串流继承自标准流类体系,但其输入输出目标不是物理设备,而是内存中的字符串。C++ 提供三种主要类型的字符串流:
-
istringstream面向输入的字符串流,用于从字符串中提取数据。它将字符串当作数据源,实现类似于从文件读取的行为。 -
ostringstream面向输出的字符串流,用于向字符串写入数据。它提供了格式化输出的能力,将数据以文本形式存储在内存字符串中。 -
stringstream同时支持输入和输出,可以在同一个字符串流对象上进行读写操作。适合需要在内存中反复解析和构建字符串的场景。
这种设计与标准流对象保持一致,使得对内存字符串的操作与文件或控制台的操作具有统一的接口和方法。
二、基本用法
使用字符串流时,通常的步骤包括创建流对象、向流中写入或从流中读取数据,然后获取最终字符串或解析结果。示例:
#include <sstream>
#include <string>
#include <iostream>
std::ostringstream oss;
oss << "Name: " << "Alice" << ", Age: " << 30;
std::string result = oss.str();
std::cout << result << std::endl; // 输出 "Name: Alice, Age: 30"
std::string data = "42 3.14 hello";
std::istringstream iss(data); // 从已有的字符串中建立字符流
int i;
double d;
std::string s;
iss >> i >> d >> s; // 从字符串中提取整数、浮点数和字符串
通过这种方式,程序可以像处理标准流那样操作字符串,利用 >> 和 << 运算符进行格式化输入输出。
三、流的格式化能力
字符串流继承自标准流类,因此支持所有流的格式化特性:
- 可以使用操纵符(
setw,setprecision,fixed等)控制输出格式。 - 可以通过
setf()、unsetf()设置流的格式标志,实现对齐方式、数值进制或符号显示的控制。 - 可以链式调用输入输出操作,使字符串构建或解析更加简洁。
示例:
#include <iomanip>
std::ostringstream oss2;
double pi = 3.14159265;
oss2 << std::fixed << std::setprecision(2) << pi;
std::cout << oss2.str(); // 输出 "3.14"
四、常见操作方法
字符串流提供了丰富的成员函数,用于在内存字符串上实现精细控制:
-
获取内容
str():返回当前流中的字符串内容。str(const std::string&):设置或替换流的内容,为流重新赋值。
-
清空或重置流
clear():重置流的状态,使其可重新进行读写操作。seekg()/seekp():调整读写位置,实现从特定位置开始读取或写入。
-
读取与写入
- 使用
>>从字符串中提取数据,使用<<向流中插入数据。 - 可以使用
getline()从字符串中逐行读取内容,与文件或控制台操作保持一致。
- 使用
示例:
std::stringstream ss("123 456");
int a, b;
ss >> a >> b; // 提取两个整数
ss.str(""); // 清空流内容
ss.clear(); // 重置状态
ss << "New content";
五、应用实例
1. 解析数组
解析"[2,-1,3,0,12]"这样的字符串为数组:
std::string s = "[2,-1,3,0,12]";
std::stringstream ss(s);
char ch;
std::vector<int> nums;
std::string temp;
// 读取第一个字符,应该是 '['
ss >> ch;
if (ch != '[') {
std::cerr << "Invalid input format!" << std::endl;
return 1;
}
while (ss >> ch) {
if (ch == ',' || ch == ']') {
if (!temp.empty()) {
nums.push_back(std::stoi(temp)); // 转换字符串为整数
temp.clear();
}
if (ch == ']') break; // 数组结束
} else {
temp += ch; // 累积数字字符,包括负号
}
}
// 输出结果
std::cout << "Numbers: ";
for (int n : nums) std::cout << n << " ";
std::cout << std::endl;