010 《C++ 数组 (Array) 全面深度解析》
🌟🌟🌟本文由Gemini 2.0 Flash Thinking Experimental 01-21生成,用来辅助学习。🌟🌟🌟
书籍大纲
▮▮ 1. 初识数组 (Introduction to Arrays)
▮▮▮▮ 1.1 什么是数组 (What is an Array)
▮▮▮▮▮▮ 1.1.1 数组的定义与特性 (Definition and Characteristics of Arrays)
▮▮▮▮▮▮ 1.1.2 数组的应用场景 (Application Scenarios of Arrays)
▮▮▮▮▮▮ 1.1.3 数组与其它数据结构的比较 (Comparison with Other Data Structures)
▮▮▮▮ 1.2 C++ 中的数组 (Arrays in C++)
▮▮▮▮▮▮ 1.2.1 数组的声明 (Declaration of Arrays)
▮▮▮▮▮▮ 1.2.2 数组的初始化 (Initialization of Arrays)
▮▮▮▮▮▮ 1.2.3 访问数组元素 (Accessing Array Elements)
▮▮ 2. 数组的进阶操作 (Advanced Operations on Arrays)
▮▮▮▮ 2.1 多维数组 (Multi-dimensional Arrays)
▮▮▮▮▮▮ 2.1.1 二维数组的声明与初始化 (Declaration and Initialization of 2D Arrays)
▮▮▮▮▮▮ 2.1.2 访问多维数组元素 (Accessing Elements in Multi-dimensional Arrays)
▮▮▮▮▮▮ 2.1.3 多维数组的应用 (Applications of Multi-dimensional Arrays)
▮▮▮▮ 2.2 数组与指针 (Arrays and Pointers)
▮▮▮▮▮▮ 2.2.1 数组名是指针常量 (Array Name as a Pointer Constant)
▮▮▮▮▮▮ 2.2.2 指针算术与数组 (Pointer Arithmetic and Arrays)
▮▮▮▮▮▮ 2.2.3 数组作为函数参数 (Arrays as Function Arguments)
▮▮▮▮ 2.3 动态数组 (Dynamic Arrays)
▮▮▮▮▮▮ 2.3.1 动态内存分配与 new
运算符 (Dynamic Memory Allocation with new
Operator)
▮▮▮▮▮▮ 2.3.2 动态内存释放与 delete[]
运算符 (Dynamic Memory Deallocation with delete[]
Operator)
▮▮▮▮▮▮ 2.3.3 动态数组的应用与注意事项 (Applications and Considerations of Dynamic Arrays)
▮▮ 3. C++ 数组的深入应用 (In-depth Applications of C++ Arrays)
▮▮▮▮ 3.1 数组与常用算法 (Arrays and Common Algorithms)
▮▮▮▮▮▮ 3.1.1 数组的排序算法 (Sorting Algorithms for Arrays)
▮▮▮▮▮▮ 3.1.2 数组的搜索算法 (Searching Algorithms for Arrays)
▮▮▮▮▮▮ 3.1.3 数组的其他常用算法 (Other Common Algorithms for Arrays)
▮▮▮▮ 3.2 C 风格字符串 (C-style Strings)
▮▮▮▮▮▮ 3.2.1 C 风格字符串的定义与初始化 (Definition and Initialization of C-style Strings)
▮▮▮▮▮▮ 3.2.2 C 风格字符串的常用操作函数 (Common Operation Functions for C-style Strings)
▮▮▮▮▮▮ 3.2.3 C 风格字符串的安全问题与替代方案 (Security Issues and Alternatives of C-style Strings)
▮▮▮▮ 3.3 std::array
容器 (The std::array
Container)
▮▮▮▮▮▮ 3.3.1 std::array
的基本使用 (Basic Usage of std::array
)
▮▮▮▮▮▮ 3.3.2 std::array
的优势 (Advantages of std::array
)
▮▮▮▮▮▮ 3.3.3 何时使用 std::array
(When to Use std::array
)
▮▮ 4. 数组的最佳实践与常见错误 (Best Practices and Common Mistakes with Arrays)
▮▮▮▮ 4.1 数组的最佳实践 (Best Practices for Arrays)
▮▮▮▮▮▮ 4.1.1 清晰的代码风格 (Clear Code Style)
▮▮▮▮▮▮ 4.1.2 合理的内存管理 (Proper Memory Management)
▮▮▮▮▮▮ 4.1.3 性能优化建议 (Performance Optimization Tips)
▮▮▮▮ 4.2 数组的常见错误与陷阱 (Common Mistakes and Pitfalls with Arrays)
▮▮▮▮▮▮ 4.2.1 数组越界访问 (Array Out-of-Bounds Access)
▮▮▮▮▮▮ 4.2.2 数组未初始化 (Uninitialized Arrays)
▮▮▮▮▮▮ 4.2.3 指针与数组的混淆 (Confusion between Pointers and Arrays)
▮▮ 附录A: ASCII 码表 (ASCII Table)
▮▮ 附录B: C++ 数组相关术语表 (Glossary of C++ Array Terms)
▮▮ 附录C: 参考文献 (References)
1. 初识数组 (Introduction to Arrays)
1.1 什么是数组 (What is an Array)
1.1.1 数组的定义与特性 (Definition and Characteristics of Arrays)
数组 (Array) 是一种基础且重要的数据结构 (Data Structure),它 представлять 内存中连续存储的相同类型元素的集合。可以将数组想象成 एक 排整齐的抽屉柜,每个抽屉(元素)存放着相同类型的物品,并且可以通过抽屉的编号(索引 (Index))快速找到对应的物品。
定义:
数组 можно 定义为:在内存中连续分配的一段空间,用于存储相同数据类型的多个元素的集合。
特性:
① 元素类型相同 (Homogeneous Elements):数组中的所有元素都必须是相同的数据类型。例如,可以 всі элементы 都是整数类型 int
, всі элементы 都是浮点数类型 float
,或者 всі элементы 都是字符类型 char
,但不能在一个数组中同时存储整数和字符串等不同类型的数据。
② 内存地址连续 (Contiguous Memory Allocation):数组中的元素在内存中是顺序存储的,彼此之间没有间隙。这意味着一旦知道了数组的首地址 (首地址 (base address) 或 数组名),就可以通过元素的索引 (Index) 和元素的大小,快速计算出任意元素的内存地址。这种连续存储的特性是数组能够实现随机访问 (Random Access) 的关键。
③ 固定大小 (Fixed Size) (静态数组):在 C++ 中,传统的 C 风格数组 (C-style array) 在声明时需要指定数组的大小,且大小在编译时就已确定,运行时无法动态改变。这种数组被称为静态数组 (Static Array)。 (注意:C++11 引入的 std::array
也具有固定大小的特性,但它是一个容器,提供了更多的便利功能,后续章节会详细介绍。)
优势:
⚝ 随机访问高效 (Efficient Random Access):由于数组元素在内存中连续存储,可以通过索引直接计算出元素的地址,实现 \(O(1)\) 时间复杂度的随机访问。这意味着访问数组中任意位置的元素速度都非常快,且与数组的大小无关。
⚝ 内存利用率高 (High Memory Utilization):数组的连续存储特性,使得内存空间能够被充分利用,减少了内存碎片 (Memory Fragmentation) 的产生。
⚝ 结构简单 (Simple Structure):数组的结构相对简单,易于理解和使用,是很多高级数据结构和算法的基础。
限制:
⚝ 大小固定 (Fixed Size) (静态数组):静态数组的大小在声明时就已确定,运行时无法动态扩展或缩小。这在某些需要动态调整数据集合大小的场景下会显得不够灵活。
⚝ 插入和删除效率低 (Inefficient Insertion and Deletion):在数组中插入或删除元素时,为了保持元素的连续性,通常需要移动大量的元素,导致插入和删除操作的时间复杂度较高,平均情况下为 \(O(n)\),其中 \(n\) 是数组的长度。
⚝ 容易产生数组越界 (Array Out-of-Bounds):由于 C++ 不会自动进行严格的数组边界检查 (Bounds Checking) (特别是在使用 C 风格数组时),如果访问了超出数组边界的索引,就可能导致程序崩溃或产生不可预测的错误。这是一个需要程序员特别注意的安全问题。
生活中的类比:
⚝ 书架:书架上的每一层可以看作一个数组,每一层可以存放相同类型的书籍(例如,都是小说,或都是技术书籍)。书架上的书是按顺序排列的(内存地址连续),可以通过书在书架上的位置(索引)快速找到书。
⚝ 电影院的座位:电影院的座位排成多行多列,每一行可以看作一个数组。每个座位都是用来坐人的(相同类型的元素),座位号可以看作索引,通过座位号可以快速找到对应的座位。
理解数组的定义和特性是学习 C++ 数组的基础,这有助于我们更好地理解数组的工作原理,并在程序设计中合理地应用数组这种数据结构。
1.1.2 数组的应用场景 (Application Scenarios of Arrays)
数组作为一种 фундаментальный 数据结构,在程序设计中有着广泛的应用场景。由于其高效的随机访问和连续存储的特点,数组特别适合用于处理需要快速查找和批量操作的数据集合。
① 存储列表数据 (Storing List Data):
数组最常见的应用就是存储同类型元素的列表。例如:
⚝ 学生成绩列表:可以使用浮点数数组 float scores[50]
存储 50 个学生的考试成绩。
⚝ 温度记录列表:可以使用整数数组 int temperatures[31]
存储一个月 31 天的每日最高温度。
⚝ 商品价格列表:可以使用双精度浮点数数组 double prices[100]
存储 100 种商品的价格。
这些列表数据都具有元素类型相同、数量固定 (或在一定范围内) 的特点,非常适合使用数组来存储和管理。
② 实现矩阵 (Implementing Matrices):
多维数组 (Multi-dimensional Array) 特别是二维数组 (Two-dimensional Array) 可以用来表示矩阵。矩阵在数学、物理、工程计算、图像处理等领域有着广泛的应用。例如:
⚝ 图像像素矩阵:可以使用二维数组 int pixels[height][width]
存储图像的像素数据,每个元素代表一个像素的颜色值。
⚝ 游戏地图:可以使用二维数组 char map[rows][cols]
表示游戏地图,每个元素代表地图上的一个单元格,存储单元格的类型 (例如,草地、水、墙壁等)。
⚝ 数值计算中的矩阵:在进行线性代数运算、科学计算时,经常需要使用矩阵来表示数据和进行计算,二维数组是表示矩阵的 естественный 选择。
③ 作为其他数据结构的基础 (Foundation for Other Data Structures):
数组可以作为实现其他更复杂数据结构的基础组件。例如:
⚝ 栈 (Stack) 和队列 (Queue):可以使用数组来实现顺序栈 (Sequential Stack) 和顺序队列 (Sequential Queue),利用数组的连续存储空间来模拟栈和队列的后进先出 (LIFO) 和先进先出 (FIFO) 的特性。
⚝ 哈希表 (Hash Table):哈希表的底层实现 часто 使用数组来存储哈希桶 (Hash Bucket),数组的索引对应哈希值,元素存储哈希冲突 (Hash Collision) 的链表或开放寻址的元素。
⚝ 堆 (Heap):堆数据结构可以使用数组来实现,利用数组的索引关系来维护堆的父节点和子节点之间的关系。
④ 缓存 (Cache):
数组可以被用作 простой 的缓存结构,例如:
⚝ CPU 缓存:CPU 的高速缓存 (Cache) в какой-то мере 就利用了数组的 принцип,存储 недавно 访问过的数据,以提高数据访问速度。
⚝ 简单的程序缓存:在程序中,可以使用数组来缓存 часто 使用的数据,减少重复计算或数据读取的开销。
⑤ 排序和搜索算法 (Sorting and Searching Algorithms):
数组是实现各种排序 (Sorting) 和搜索 (Searching) 算法的 основная 数据结构。许多经典的排序算法,如冒泡排序 (Bubble Sort)、插入排序 (Insertion Sort)、选择排序 (Selection Sort) 以及快速排序 (Quick Sort)、归并排序 (Merge Sort) 等,都 часто 直接基于数组进行操作。同样,线性搜索 (Linear Search)、二分搜索 (Binary Search) 等搜索算法也 часто 应用于数组。
⑥ 字符串 (String) 存储 (C 风格字符串):
在 C 和 C++ 中,字符串 можно 使用字符数组 (Character Array) 来存储,也就是所谓的 C 风格字符串 (C-style string)。虽然 C++ 提供了更 удобный 和安全的 std::string
类,但在很多情况下,仍然需要与 C 风格字符串打交道,例如在与 C 语言库交互时。
总而言之,数组在程序设计中扮演着 многогранный 的角色,其应用场景非常广泛。理解数组的应用场景有助于我们更好地选择合适的数据结构,并有效地解决实际问题。
1.1.3 数组与其它数据结构的比较 (Comparison with Other Data Structures)
数组作为一种 фундаментальный 数据结构,与其他数据结构 (例如链表 (Linked List)、向量 (Vector) 等) 相比,各有优缺点,适用于不同的应用场景。理解数组与其他数据结构的异同,有助于我们根据 конкретные 需求选择最合适的数据结构。
① 数组 vs. 链表 (Linked List)
特性/数据结构 | 数组 (Array) | 链表 (Linked List) |
---|---|---|
内存分配 | 连续内存空间 | 内存空间不连续,通过指针链接 |
大小 | 静态数组大小固定 (编译时确定);动态数组大小可变 (运行时确定) | 大小动态可变 (运行时动态分配和释放节点) |
随机访问 | 支持高效随机访问 \(O(1)\) | 不支持高效随机访问,需要顺序遍历 \(O(n)\) |
插入/删除 | 插入/删除元素效率低 \(O(n)\) (平均情况,需要移动元素) | 插入/删除元素效率高 \(O(1)\) (仅需修改指针) |
内存利用率 | 内存利用率较高,但可能存在空间浪费 (如果预分配空间过大) | 内存利用率相对较低,每个节点需要额外存储指针,但内存使用更灵活 |
缓存友好性 | 缓存友好性好 (连续内存,利于 CPU 缓存) | 缓存友好性较差 (内存不连续,不利于 CPU 缓存) |
总结:
⚝ 数组:适用于频繁随机访问,较少插入和删除,且大小相对固定的数据集合。例如,静态数据查询、矩阵运算、实现顺序栈/队列等。
⚝ 链表:适用于频繁插入和删除,较少随机访问,且大小动态变化的数据集合。例如,实现动态列表、LRU 缓存、图的邻接表表示等。
② 数组 vs. 向量 (Vector) (std::vector
)
std::vector
是 C++ 标准库 (Standard Library) 提供的动态数组容器。与 C 风格数组相比,std::vector
提供了更多便利的功能和更高的安全性。
特性/数据结构 | 数组 (C 风格数组) | 向量 (std::vector ) |
---|---|---|
大小 | 静态数组大小固定 (编译时确定);动态数组大小可变 (运行时确定,但需手动管理内存) | 大小动态可变 (运行时自动扩展和收缩,无需手动管理内存) |
内存管理 | 动态数组需要手动使用 new 和 delete[] 管理内存,容易出现内存泄漏和悬 dangling 指针 (Dangling Pointer) 问题 | 自动管理内存,使用 RAII (Resource Acquisition Is Initialization) 机制,避免内存泄漏 |
边界检查 | 不进行默认边界检查,容易发生数组越界访问错误 | 提供 at() 方法进行边界检查,抛出异常 (Exception) (但默认 [] 运算符不进行边界检查,以提高效率) |
功能 | 功能较少,仅提供基本的数据存储功能 | 功能丰富,提供 множество 方法 (例如 push_back , pop_back , insert , erase , size , capacity 等) 和迭代器 (Iterator),方便数据操作和遍历 |
类型安全 | 类型安全由程序员保证 | 类型安全,编译时进行类型检查 |
与标准库算法兼容性 | 与标准库算法兼容性较好,但使用时需要注意指针和大小问题 | 与标准库算法完美兼容,可以直接作为算法的输入和输出 |
总结:
⚝ C 风格数组: более 底层,效率更高,但需要手动管理内存,安全性较低,功能较少。适用于对性能要求极高,且对内存管理和安全性有充分把握的场景,或者需要与 C 语言代码兼容的场景。
⚝ std::vector
: более 高级,功能丰富,安全性高,自动管理内存,使用方便。推荐在 большинство 情况下使用 std::vector
代替 C 风格数组, особенно 是在现代 C++ 开发中。
③ 数组 vs. 其他数据结构
⚝ 数组 vs. 集合 (Set):集合用于存储唯一元素,并提供高效的查找、插入和删除操作 (基于哈希表或平衡树实现)。数组主要用于存储有序的元素序列,支持索引访问。
⚝ 数组 vs. 映射 (Map):映射用于存储键值对 (Key-Value Pair),并根据键快速查找值 (基于哈希表或平衡树实现)。数组通过索引访问元素,索引本身就是位置信息,不涉及键的概念。
⚝ 数组 vs. 树 (Tree):树是一种层次结构的数据结构,用于表示具有父子关系的数据。数组是线性结构,元素之间是顺序关系。树适用于表示组织结构、文件系统、搜索树等场景,数组适用于表示列表、矩阵等线性数据。
⚝ 数组 vs. 图 (Graph):图用于表示对象之间复杂的网络关系。数组通常用于存储图的邻接矩阵或邻接表表示,作为图数据结构的底层实现。
选择数据结构的原则:
选择合适的数据结构需要综合考虑以下因素:
⚝ 数据特点:数据元素的类型、数量、是否有序、是否唯一等。
⚝ 操作类型:需要频繁进行哪些操作 (例如,随机访问、插入、删除、搜索、排序等)。
⚝ 性能要求:对时间复杂度和空间复杂度的要求。
⚝ 开发效率:数据结构的易用性和开发效率。
在 многих 情况下,std::vector
由于其功能丰富、安全可靠、易于使用,是 C++ 中数组操作的优选方案。但理解 C 风格数组的原理和特性仍然很重要,这有助于我们更好地理解 более 底层的数据结构和内存管理机制。
1.2 C++ 中的数组 (Arrays in C++)
1.2.1 数组的声明 (Declaration of Arrays)
在 C++ 中,声明数组需要指定数组的元素类型 (Element Type)、数组名 (Array Name) 和 数组大小 (Array Size) (对于静态数组)。数组声明的语法形式如下:
1
元素类型 数组名[数组大小];
语法解释:
⚝ 元素类型 (Element Type):指定数组中存储的数据类型。可以是任何有效的 C++ 数据类型,例如 int
, float
, double
, char
, bool
,甚至是自定义的类类型。数组中的所有元素必须具有相同的类型。
⚝ 数组名 (Array Name):遵循 C++ 标识符 (Identifier) 的命名规则。数组名用于在程序中引用和操作该数组。
⚝ 数组大小 (Array Size):指定数组中元素的个数。必须是一个编译时可确定的常量表达式 (对于静态数组)。数组大小决定了数组在内存中分配的空间大小。数组大小需要使用方括号 []
括起来。
示例代码:
① 声明存储 5 个整数的数组:
1
int numbers[5];
这行代码声明了一个名为 numbers
的数组,它可以存储 5 个整数类型的元素。数组的元素类型是 int
,数组大小是 5。
② 声明存储 10 个浮点数的数组:
1
float decimalNumbers[10];
声明了一个名为 decimalNumbers
的数组,可以存储 10 个浮点数类型的元素。元素类型是 float
,数组大小是 10。
③ 声明存储 20 个字符的数组:
1
char letters[20];
声明了一个名为 letters
的数组,可以存储 20 个字符类型的元素。元素类型是 char
,数组大小是 20。这可以用于存储 C 风格字符串 (C-style string)。
④ 声明存储 3 行 4 列的二维整数数组:
1
int matrix[3][4];
声明了一个名为 matrix
的二维数组,可以存储 3 行 4 列的整数矩阵。第一维大小是 3 (行数),第二维大小是 4 (列数)。
注意事项:
⚝ 数组大小必须是常量表达式 (对于静态数组):数组大小在声明时必须是一个编译时可确定的常量值。不能使用变量作为数组大小 (除非使用动态数组,后续章节会介绍)。可以使用字面常量 (Literal Constant) (例如 5
, 10
, 20
) 或 const
常量 (Constant Variable) 或 constexpr
常量 (Constant Expression Variable) 来指定数组大小。
1
const int SIZE = 10;
2
int arr1[SIZE]; // 正确:使用 const 常量
3
constexpr int ARRAY_SIZE = 20;
4
int arr2[ARRAY_SIZE]; // 正确:使用 constexpr 常量
5
6
// int size = 5;
7
// int arr3[size]; // 错误:size 是变量,不是常量表达式 (对于 C 风格的静态数组)
⚝ 数组大小必须是正整数:数组大小必须是大于 0 的正整数。声明大小为 0 或负数的数组是无效的。
⚝ 数组名也是标识符:数组名需要遵循 C++ 标识符的命名规则,例如,以字母或下划线开头,可以包含字母、数字和下划线,不能使用 C++ 关键字 (Keywords) 作为数组名。
⚝ 多维数组声明:声明多维数组时,需要为每个维度指定大小。例如,int matrix[3][4][5];
声明了一个三维数组。
⚝ 未指定大小的数组 (在某些特殊情况下):在函数参数声明中,可以声明未指定大小的数组 (例如 int func(int arr[])
),但这实际上会被编译器解释为指针 (Pointer) (例如 int func(int* arr)
,数组退化为指针)。这将在后续 "数组与指针" 章节详细讨论。
理解数组的声明语法是使用数组的第一步。正确声明数组是后续进行数组初始化和元素访问的基础。
1.2.2 数组的初始化 (Initialization of Arrays)
数组的初始化 (Initialization) 是指在数组声明的同时,为数组元素赋予初始值的过程。C++ 提供了多种数组初始化的方法,可以根据 конкретные 情况选择合适的方式。
① 列表初始化 (List Initialization) (C++11 起):
列表初始化是 C++11 引入的一种通用的初始化方式,也适用于数组。使用花括号 {}
将初始值列表括起来,用逗号 ,
分隔各个初始值。
⚝ 完全初始化 (Full Initialization):初始值列表中的元素个数等于数组的大小。
1
int numbers[5] = {1, 2, 3, 4, 5}; // 初始化 numbers 数组的所有 5 个元素
2
float decimalNumbers[3] = {1.1f, 2.2f, 3.3f}; // 初始化 decimalNumbers 数组的所有 3 个元素
3
char letters[4] = {'a', 'b', 'c', 'd'}; // 初始化 letters 数组的所有 4 个元素
⚝ 部分初始化 (Partial Initialization):初始值列表中的元素个数少于数组的大小。剩余的元素会被默认初始化为 0 (对于数值类型) 或空字符 \0
(对于字符类型) 或 nullptr
(对于指针类型) 等。
1
int partialNumbers[5] = {1, 2, 3}; // 初始化前 3 个元素为 1, 2, 3,后 2 个元素默认初始化为 0
2
float partialDecimals[4] = {1.1f, 2.2f}; // 初始化前 2 个元素为 1.1f, 2.2f,后 2 个元素默认初始化为 0.0f
3
char partialLetters[5] = {'a', 'b'}; // 初始化前 2 个元素为 'a', 'b',后 3 个元素默认初始化为空字符 '\0'
⚝ 零初始化 (Zero Initialization):如果初始值列表为空 {}
,则数组的所有元素都会被默认初始化为 0 (或其类型的默认值)。
1
int zeroNumbers[5] = {}; // 所有元素初始化为 0
2
float zeroDecimals[3] = {}; // 所有元素初始化为 0.0f
3
char zeroLetters[4] = {}; // 所有元素初始化为空字符 '\0'
也可以直接使用 {0}
进行零初始化,效果相同:
1
int zeroNumbers2[5] = {0}; // 所有元素初始化为 0
⚝ 省略数组大小的初始化:在列表初始化时,如果提供了初始值列表,可以省略数组大小,编译器会根据初始值列表的元素个数自动推断数组的大小。
1
int autoSizeNumbers[] = {10, 20, 30, 40}; // 数组大小自动推断为 4
2
float autoSizeDecimals[] = {2.5f, 3.7f}; // 数组大小自动推断为 2
3
char autoSizeLetters[] = {'x', 'y', 'z', 'w', 'v'}; // 数组大小自动推断为 5
注意:只有在声明时初始化的情况下才能省略数组大小,先声明后赋值的方式不能省略数组大小。
② 循环初始化 (Loop Initialization):
可以使用循环 (例如 for
循环) 遍历数组,为每个元素赋值。这种方式适用于需要根据某种规则或计算结果来初始化数组元素的场景。
1
int squaredNumbers[10];
2
for (int i = 0; i < 10; ++i) {
3
squaredNumbers[i] = i * i; // 初始化为平方数
4
}
5
6
float randomNumbers[5];
7
for (int i = 0; i < 5; ++i) {
8
randomNumbers[i] = static_cast<float>(rand()) / RAND_MAX; // 初始化为随机数 (0~1)
9
}
③ 字符数组的特殊初始化 (C 风格字符串):
字符数组 (用于存储 C 风格字符串) 可以使用字符串字面量 (String Literal) 进行初始化。编译器会自动在字符串末尾添加空字符 \0
。
1
char message[] = "Hello"; // 自动在末尾添加 '\0',数组大小为 6 (包括 '\0')
2
char greeting[10] = "World"; // 部分初始化,剩余元素默认初始化为 '\0'
注意:使用字符串字面量初始化字符数组时,数组大小需要足够容纳字符串的所有字符以及结尾的空字符 \0
。
④ 动态初始化 (Dynamic Initialization):
动态数组 (Dynamic Array) 的初始化是在程序运行时进行的,可以使用 new
运算符动态分配内存,并在分配内存后进行初始化。动态数组的初始化更加灵活,可以根据运行时的情况来确定数组的大小和初始值。动态数组的详细内容将在后续章节介绍。
静态初始化 vs. 动态初始化:
⚝ 静态初始化 (Static Initialization):在编译时完成初始化,数组的大小和初始值在编译时就已确定。例如,使用列表初始化、零初始化等方式初始化的数组。静态数组通常分配在栈 (Stack) 或静态存储区。
⚝ 动态初始化 (Dynamic Initialization):在运行时完成初始化,数组的大小和初始值可以在运行时动态确定。例如,使用 new
运算符动态分配内存并初始化的数组。动态数组通常分配在堆 (Heap) 上。
选择合适的初始化方式取决于具体的应用场景和需求。列表初始化简洁方便,适用于静态数组的简单初始化;循环初始化灵活,适用于需要根据规则或计算结果初始化的数组;动态初始化适用于需要在运行时确定数组大小和初始值的场景。
1.2.3 访问数组元素 (Accessing Array Elements)
访问数组元素 (Accessing Array Elements) 是指获取或修改数组中特定位置的元素的值。在 C++ 中,通过索引运算符 []
(Index Operator) 来访问数组元素。
索引 (Index):
数组的每个元素都有一个唯一的索引 (Index),用于标识元素在数组中的位置。C++ 数组的索引从 0 开始。也就是说,数组的第一个元素的索引是 0,第二个元素的索引是 1,依此类推。对于大小为 \(n\) 的数组,其有效索引范围是 \(0\) 到 \(n-1\)。
访问语法:
使用数组名后跟方括号 []
,并在方括号内指定要访问元素的索引。
1
数组名[索引]
示例代码:
假设有以下数组声明和初始化:
1
int numbers[5] = {10, 20, 30, 40, 50};
⚝ 访问第一个元素 (索引为 0):
1
int firstElement = numbers[0]; // 获取索引为 0 的元素的值 (10)
2
cout << "第一个元素: " << firstElement << endl; // 输出:第一个元素: 10
⚝ 访问第三个元素 (索引为 2):
1
int thirdElement = numbers[2]; // 获取索引为 2 的元素的值 (30)
2
cout << "第三个元素: " << thirdElement << endl; // 输出:第三个元素: 30
⚝ 修改第四个元素 (索引为 3):
1
numbers[3] = 45; // 将索引为 3 的元素的值修改为 45
2
cout << "修改后的第四个元素: " << numbers[3] << endl; // 输出:修改后的第四个元素: 45
⚝ 循环遍历数组并访问所有元素:
1
cout << "数组的所有元素: ";
2
for (int i = 0; i < 5; ++i) {
3
cout << numbers[i] << " "; // 依次访问索引为 0, 1, 2, 3, 4 的元素
4
}
5
cout << endl; // 输出:数组的所有元素: 10 20 30 45 50
数组越界 (Array Out-of-Bounds):
非常重要:C++ 不会自动进行严格的数组边界检查 (Bounds Checking) (特别是在使用 C 风格数组时)。这意味着,如果访问了超出数组有效索引范围的索引 (例如,对于大小为 5 的数组,访问索引 5 或更大的索引,或者负数索引),程序不会立即报错,但会发生数组越界访问 (Array Out-of-Bounds Access)。
数组越界访问是一种非常危险的错误,可能导致:
⚝ 程序崩溃 (Crash):访问了无效的内存地址,导致程序运行时错误,甚至崩溃。
⚝ 数据损坏 (Data Corruption):修改了数组以外的内存区域,覆盖了其他变量或数据结构的值,导致程序行为异常或数据错误。
⚝ 安全漏洞 (Security Vulnerability):恶意程序可能利用数组越界漏洞进行缓冲区溢出攻击 (Buffer Overflow Attack),从而控制程序或系统。
示例 (越界访问):
1
int data[3] = {100, 200, 300};
2
3
cout << data[3] << endl; // 越界访问!索引 3 超出了有效范围 (0, 1, 2)
上述代码尝试访问索引为 3 的元素,但 data
数组的大小只有 3,有效索引范围是 0, 1, 2。这是一个典型的数组越界访问错误。程序在编译时可能不会报错,但在运行时会产生未定义行为 (Undefined Behavior),结果不可预测。
如何避免数组越界:
⚝ 始终检查索引的有效性:在访问数组元素之前,务必确保索引值在有效的范围内 (0 到 数组大小 - 1)。
⚝ 使用循环时注意循环条件:在使用循环遍历数组时,循环条件要正确,避免循环次数过多导致越界。
⚝ 使用安全的容器:std::vector
和 std::array
等 C++ 标准库容器提供了一些边界检查的功能 (例如 std::vector::at()
方法),可以 более 安全地访问元素。
⚝ 代码审查和测试:进行充分的代码审查 (Code Review) 和测试 (Testing),尽早发现和修复数组越界错误。
理解数组索引从 0 开始的特性,以及数组越界访问的风险,是正确使用数组的关键。务必养成良好的编程习惯,避免数组越界错误,确保程序的健壮性和安全性。
2. 数组的进阶操作 (Advanced Operations on Arrays)
章节摘要 (Chapter Summary)
本章深入探讨数组的各种进阶操作,包括多维数组 (Multi-dimensional Arrays)、数组与指针 (Pointer) 的关系、动态数组 (Dynamic Arrays) 的创建与管理等,提升读者对数组的运用能力。
2.1 多维数组 (Multi-dimensional Arrays)
章节摘要 (Section Summary)
详细介绍多维数组 (Multi-dimensional Arrays) 的概念,重点讲解二维数组 (Two-dimensional Arrays) 的声明、初始化和应用,并扩展到更高维度的数组。
2.1.1 二维数组的声明与初始化 (Declaration and Initialization of 2D Arrays)
小节摘要 (Subsection Summary)
讲解二维数组 (Two-dimensional Arrays) 的声明语法和多种初始化方法,并通过矩阵 (Matrix) 等实例演示二维数组 (Two-dimensional Arrays) 的应用。
① 二维数组的声明 (Declaration of 2D Arrays)
在 C++ 中,二维数组 (Two-dimensional Array) 本质上是“数组的数组”。你可以将其视为一个表格,其中行和列索引用于访问特定元素。声明二维数组 (Two-dimensional Array) 的基本语法如下:
1
元素类型 数组名[行数][列数];
⚝ 元素类型 (Element Type): 指定数组中存储的数据类型,例如 int
, float
, char
等。
⚝ 数组名 (Array Name): 你为数组选择的标识符名称。
⚝ 行数 (Number of Rows): 二维数组 (Two-dimensional Array) 的行数。
⚝ 列数 (Number of Columns): 二维数组 (Two-dimensional Array) 的列数。
示例:
⚝ 声明一个 3 行 4 列的整型二维数组 (Two-dimensional Array):
1
int matrix[3][4];
这个声明创建了一个名为 matrix
的二维数组 (Two-dimensional Array),它可以存储 3 行 4 列的整数。你可以想象它是一个具有 3 行和 4 列的表格。
⚝ 声明一个 2 行 2 列的浮点型二维数组 (Two-dimensional Array):
1
float identityMatrix[2][2];
这个声明创建了一个名为 identityMatrix
的二维数组 (Two-dimensional Array),用于存储 2x2 的浮点数矩阵 (Matrix)。
内存布局 (Memory Layout)
在内存中,二维数组 (Two-dimensional Array) 仍然是连续存储的。C++ 编译器通常以行优先顺序 (Row-major Order) 存储二维数组 (Two-dimensional Array)。这意味着数组元素按行依次存储,即第一行的所有元素存储在一起,接着是第二行的所有元素,依此类推。
例如,对于 int matrix[3][4];
,内存布局大致如下:
1
matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3],
2
matrix[1][0], matrix[1][1], matrix[1][2], matrix[1][3],
3
matrix[2][0], matrix[2][1], matrix[2][2], matrix[2][3]
理解行优先顺序 (Row-major Order) 对于理解二维数组 (Two-dimensional Array) 的内存访问模式以及与指针 (Pointer) 的关系至关重要。
② 二维数组的初始化 (Initialization of 2D Arrays)
二维数组 (Two-dimensional Array) 的初始化与一维数组 (One-dimensional Array) 类似,但需要考虑行和列的结构。以下是几种常见的初始化方法:
⚝ 列表初始化 (List Initialization)
使用嵌套的大括号 {}
来初始化二维数组 (Two-dimensional Array)。外层 {}
代表行,内层 {}
代表列。
1
int matrix[3][4] = {
2
{1, 2, 3, 4}, // 第 1 行 (Row 1)
3
{5, 6, 7, 8}, // 第 2 行 (Row 2)
4
{9, 10, 11, 12} // 第 3 行 (Row 3)
5
};
这种方式清晰地展示了数组的行和列结构,易于阅读和理解。
你也可以选择性地初始化部分元素。未显式初始化的元素将被默认初始化(对于 int
类型,默认初始化为 0)。
1
int partialMatrix[3][4] = {
2
{1, 2}, // 第 1 行初始化前两列,后两列为 0
3
{5}, // 第 2 行初始化第一列,后三列为 0
4
{} // 第 3 行全部初始化为 0
5
};
⚝ 循环初始化 (Loop Initialization)
使用嵌套循环 (Nested Loop) 遍历二维数组 (Two-dimensional Array) 的每个元素,并为其赋值。这种方法适用于需要根据一定规律初始化数组的情况。
1
int loopMatrix[3][4];
2
for (int i = 0; i < 3; ++i) { // 遍历行 (Iterate through rows)
3
for (int j = 0; j < 4; ++j) { // 遍历列 (Iterate through columns)
4
loopMatrix[i][j] = i * 4 + j + 1; // 赋值 (Assign values)
5
}
6
}
这段代码使用两个 for
循环,外层循环遍历行索引 i
,内层循环遍历列索引 j
。根据行索引和列索引计算出元素的值,并赋值给 loopMatrix[i][j]
。
⚝ 静态初始化 (Static Initialization) 与 动态初始化 (Dynamic Initialization)
与一维数组 (One-dimensional Array) 类似,二维数组 (Two-dimensional Array) 也可以分为静态初始化 (Static Initialization) 和动态初始化 (Dynamic Initialization)。
▮▮▮▮⚝ 静态初始化 (Static Initialization):在声明数组时直接赋值,如上述的列表初始化 (List Initialization) 例子。静态初始化的数组大小在编译时 (Compile Time) 确定。
▮▮▮▮⚝ 动态初始化 (Dynamic Initialization):先声明数组,然后在程序运行时 (Runtime) 通过循环等方式赋值,如上述的循环初始化 (Loop Initialization) 例子。动态初始化可以在运行时根据需要设置数组元素的值。
对于二维数组 (Two-dimensional Array),没有像一维数组 (One-dimensional Array) 那样直接动态分配大小的方式(例如使用 new
直接创建二维数组 (Two-dimensional Array))。动态大小的二维数组 (Two-dimensional Array) 通常需要借助指针 (Pointer) 和动态内存分配 (Dynamic Memory Allocation) 来实现,这将在后续章节中讨论。
③ 矩阵示例 (Matrix Example)
二维数组 (Two-dimensional Array) 非常适合表示矩阵 (Matrix)。矩阵 (Matrix) 在数学、图形学、科学计算等领域广泛应用。
例如,使用二维数组 (Two-dimensional Array) 表示一个 3x3 的单位矩阵 (Identity Matrix):
1
#include <iostream>
2
3
int main() {
4
int identityMatrix[3][3] = {
5
{1, 0, 0},
6
{0, 1, 0},
7
{0, 0, 1}
8
};
9
10
std::cout << "单位矩阵 (Identity Matrix):" << std::endl;
11
for (int i = 0; i < 3; ++i) {
12
for (int j = 0; j < 3; ++j) {
13
std::cout << identityMatrix[i][j] << " ";
14
}
15
std::cout << std::endl; // 换行 (Newline)
16
}
17
18
return 0;
19
}
代码解释:
⚝ int identityMatrix[3][3]
: 声明一个 3x3 的整型二维数组 (Two-dimensional Array) identityMatrix
。
⚝ 初始化: 使用列表初始化 (List Initialization) 将其初始化为单位矩阵 (Identity Matrix)。
⚝ 嵌套循环输出: 使用嵌套循环 (Nested Loop) 遍历并输出矩阵 (Matrix) 的每个元素,模拟矩阵的打印效果。
运行结果:
1
单位矩阵 (Identity Matrix):
2
1 0 0
3
0 1 0
4
0 0 1
这个例子展示了如何使用二维数组 (Two-dimensional Array) 表示和操作矩阵 (Matrix),体现了二维数组 (Two-dimensional Array) 在处理表格数据和矩阵运算方面的优势。
2.1.2 访问多维数组元素 (Accessing Elements in Multi-dimensional Arrays)
小节摘要 (Subsection Summary)
介绍如何通过多个索引 (Index) 访问多维数组 (Multi-dimensional Arrays) 的元素,强调索引 (Index) 的顺序和维度之间的关系。
① 访问二维数组元素 (Accessing 2D Array Elements)
要访问二维数组 (Two-dimensional Array) 中的元素,你需要使用两个索引 (Index):一个用于行 (Row),另一个用于列 (Column)。索引 (Index) 仍然从 0 开始。访问二维数组 (Two-dimensional Array) 元素的语法如下:
1
数组名[行索引][列索引]
⚝ 行索引 (Row Index): 指定要访问的元素所在的行,范围从 0
到 行数 - 1
。
⚝ 列索引 (Column Index): 指定要访问的元素所在的列,范围从 0
到 列数 - 1
。
示例:
对于之前声明的 int matrix[3][4];
和列表初始化的 int identityMatrix[3][3]
,我们可以这样访问元素:
1
#include <iostream>
2
3
int main() {
4
int matrix[3][4] = {
5
{1, 2, 3, 4},
6
{5, 6, 7, 8},
7
{9, 10, 11, 12}
8
};
9
10
int identityMatrix[3][3] = {
11
{1, 0, 0},
12
{0, 1, 0},
13
{0, 0, 1}
14
};
15
16
std::cout << "matrix[0][0] = " << matrix[0][0] << std::endl; // 访问第 1 行第 1 列的元素 (Access element at row 1, column 1)
17
std::cout << "matrix[1][2] = " << matrix[1][2] << std::endl; // 访问第 2 行第 3 列的元素 (Access element at row 2, column 3)
18
std::cout << "matrix[2][3] = " << matrix[2][3] << std::endl; // 访问第 3 行第 4 列的元素 (Access element at row 3, column 4)
19
20
std::cout << "identityMatrix[1][1] = " << identityMatrix[1][1] << std::endl; // 访问单位矩阵的中心元素 (Access center element of identity matrix)
21
22
return 0;
23
}
代码解释:
⚝ matrix[0][0]
: 访问 matrix
数组第 1 行第 1 列的元素,其值为 1。
⚝ matrix[1][2]
: 访问 matrix
数组第 2 行第 3 列的元素,其值为 7。
⚝ matrix[2][3]
: 访问 matrix
数组第 3 行第 4 列的元素,其值为 12。
⚝ identityMatrix[1][1]
: 访问 identityMatrix
数组第 2 行第 2 列的元素 (单位矩阵的中心元素),其值为 1。
运行结果:
1
matrix[0][0] = 1
2
matrix[1][2] = 7
3
matrix[2][3] = 12
4
identityMatrix[1][1] = 1
② 索引顺序的重要性 (Importance of Index Order)
访问二维数组 (Two-dimensional Array) 元素时,索引 (Index) 的顺序至关重要。第一个索引 (Index) 始终是行索引 (Row Index),第二个索引 (Index) 始终是列索引 (Column Index)。 混淆索引顺序会导致访问错误的元素,甚至可能导致程序错误(例如数组越界 (Array Out-of-Bounds))。
类比: 你可以将二维数组 (Two-dimensional Array) 想象成一个坐标系,行索引 (Row Index) 类似于 Y 坐标,列索引 (Column Index) 类似于 X 坐标。要定位到表格中的一个单元格,必须先指定行号,再指定列号。
③ 遍历二维数组 (Traversing 2D Arrays)
通常使用嵌套循环 (Nested Loop) 来遍历二维数组 (Two-dimensional Array) 的所有元素。外层循环遍历行,内层循环遍历列。
1
#include <iostream>
2
3
int main() {
4
int matrix[3][4] = {
5
{1, 2, 3, 4},
6
{5, 6, 7, 8},
7
{9, 10, 11, 12}
8
};
9
10
std::cout << "遍历二维数组 (Traversing 2D Array):" << std::endl;
11
for (int i = 0; i < 3; ++i) { // 遍历行 (Iterate through rows)
12
for (int j = 0; j < 4; ++j) { // 遍历列 (Iterate through columns)
13
std::cout << "matrix[" << i << "][" << j << "] = " << matrix[i][j] << std::endl;
14
}
15
}
16
17
return 0;
18
}
代码解释:
⚝ 外层循环 for (int i = 0; i < 3; ++i)
: 控制行索引 (Row Index) i
从 0 遍历到 2 (共 3 行)。
⚝ 内层循环 for (int j = 0; j < 4; ++j)
: 控制列索引 (Column Index) j
从 0 遍历到 3 (共 4 列)。
⚝ matrix[i][j]
: 在循环内部,使用当前的行索引 i
和列索引 j
访问数组元素。
运行结果:
1
遍历二维数组 (Traversing 2D Array):
2
matrix[0][0] = 1
3
matrix[0][1] = 2
4
matrix[0][2] = 3
5
matrix[0][3] = 4
6
matrix[1][0] = 5
7
matrix[1][1] = 6
8
matrix[1][2] = 7
9
matrix[1][3] = 8
10
matrix[2][0] = 9
11
matrix[2][1] = 10
12
matrix[2][2] = 11
13
matrix[2][3] = 12
通过嵌套循环 (Nested Loop),我们可以按行优先顺序 (Row-major Order) 访问二维数组 (Two-dimensional Array) 的每个元素。
④ 多维数组的索引 (Indexing in Multi-dimensional Arrays)
对于更高维度的数组(例如三维数组 (Three-dimensional Array)、四维数组 (Four-dimensional Array) 等),访问方式类似,只需使用相应数量的索引 (Index)。例如,对于三维数组 (Three-dimensional Array) int cube[X][Y][Z];
,访问元素的语法为 cube[i][j][k]
,其中 i
是第一个维度的索引 (Index),j
是第二个维度,k
是第三个维度。索引 (Index) 的顺序和范围仍然需要根据数组的声明来确定。
2.1.3 多维数组的应用 (Applications of Multi-dimensional Arrays)
小节摘要 (Subsection Summary)
列举多维数组 (Multi-dimensional Arrays) 在图像处理 (Image Processing)、游戏开发 (Game Development)、科学计算 (Scientific Computing) 等领域的应用,展示其在处理复杂数据结构时的优势。
① 图像处理 (Image Processing)
图像 (Image) 可以表示为二维数组 (Two-dimensional Array),其中每个元素代表一个像素 (Pixel) 的颜色值。
⚝ 灰度图像 (Grayscale Image): 可以使用二维数组 (Two-dimensional Array) 存储,每个元素是一个介于 0 (黑色) 到 255 (白色) 之间的整数,表示灰度级别。
⚝ 彩色图像 (Color Image): 可以使用三维数组 (Three-dimensional Array) 存储。例如,RGB 彩色图像 (Color Image) 可以用 [Height][Width][3]
的三维数组 (Three-dimensional Array) 表示,其中第三维的 3 个分量分别代表红色 (Red)、绿色 (Green)、蓝色 (Blue) 的颜色强度。
应用示例:
⚝ 图像滤波 (Image Filtering): 使用二维数组 (Two-dimensional Array) 表示滤波器 (Filter) (例如高斯滤波器 (Gaussian Filter)、均值滤波器 (Mean Filter)),对图像 (Image) 进行卷积运算 (Convolution Operation) 以实现图像平滑 (Image Smoothing)、边缘检测 (Edge Detection) 等效果。
⚝ 图像压缩 (Image Compression): 例如 JPEG 压缩 (JPEG Compression) 中,使用二维离散余弦变换 (Discrete Cosine Transform, DCT) 将图像 (Image) 分块处理,并存储 DCT 系数矩阵 (Matrix)。
② 游戏开发 (Game Development)
多维数组 (Multi-dimensional Arrays) 在游戏开发 (Game Development) 中用于表示游戏世界 (Game World)、地图 (Map)、游戏对象 (Game Object) 的状态等。
⚝ 游戏地图 (Game Map): 可以使用二维数组 (Two-dimensional Array) 表示游戏地图 (Game Map),每个元素代表地图上的一个网格 (Grid) 或瓦片 (Tile),存储地形类型、障碍物、游戏对象等信息。例如,在策略游戏 (Strategy Game) 或角色扮演游戏 (Role-Playing Game) 中,地图网格 (Map Grid) 可以用来表示游戏场景。
⚝ 3D 游戏中的场景 (Scenes in 3D Games): 在 3D 游戏中,可以使用三维数组 (Three-dimensional Array) 或更高维度的数据结构来表示更复杂的场景,例如体素 (Voxel) 游戏中的世界表示。
⚝ 游戏对象的状态 (State of Game Objects): 可以使用多维数组 (Multi-dimensional Arrays) 存储多个游戏对象的属性数据,例如位置、速度、生命值等。
应用示例:
⚝ 碰撞检测 (Collision Detection): 在游戏中,可以使用二维或三维数组 (Multi-dimensional Arrays) 加速碰撞检测 (Collision Detection) 算法,例如空间划分 (Spatial Partitioning) 技术,将游戏世界 (Game World) 划分为网格 (Grid) 或区域,快速查找可能发生碰撞的游戏对象。
⚝ 路径搜索 (Pathfinding): 在寻路算法 (Pathfinding Algorithm) (例如 A* 算法) 中,可以使用二维数组 (Two-dimensional Array) 表示搜索空间,存储节点 (Node) 的状态、代价等信息。
③ 科学计算 (Scientific Computing)
多维数组 (Multi-dimensional Arrays) 在科学计算 (Scientific Computing) 领域中用于表示各种数学和物理模型,例如矩阵 (Matrix)、张量 (Tensor)、离散场 (Discrete Field) 等。
⚝ 矩阵运算 (Matrix Operations): 线性代数 (Linear Algebra) 中的矩阵 (Matrix) 运算 (例如矩阵乘法 (Matrix Multiplication)、矩阵求逆 (Matrix Inversion)、特征值分解 (Eigenvalue Decomposition)) 可以使用二维数组 (Two-dimensional Array) 来实现。
⚝ 有限元分析 (Finite Element Analysis): 在工程和物理学中,有限元方法 (Finite Element Method) 使用网格 (Mesh) 离散化连续物理域,可以使用多维数组 (Multi-dimensional Arrays) 存储网格节点 (Mesh Node) 的坐标、物理量等信息。
⚝ 机器学习 (Machine Learning) 和 深度学习 (Deep Learning): 张量 (Tensor) 是多维数组 (Multi-dimensional Array) 的推广,在机器学习 (Machine Learning) 和深度学习 (Deep Learning) 中广泛应用,例如神经网络 (Neural Network) 的权重 (Weight) 和激活值 (Activation Value) 通常表示为多维数组 (Multi-dimensional Arrays) 或张量 (Tensor)。
应用示例:
⚝ 数值模拟 (Numerical Simulation): 在流体力学 (Fluid Dynamics)、电磁学 (Electromagnetics)、结构力学 (Structural Mechanics) 等领域,可以使用多维数组 (Multi-dimensional Arrays) 存储离散化的物理场 (Physical Field),进行数值求解和模拟。
⚝ 数据分析 (Data Analysis): 在统计学 (Statistics)、金融分析 (Financial Analysis) 等领域,可以使用二维数组 (Two-dimensional Array) 表示表格数据 (Tabular Data),进行数据处理、分析和建模。
④ 其他应用 (Other Applications)
除了上述领域,多维数组 (Multi-dimensional Arrays) 还在很多其他领域有应用:
⚝ 数据库 (Database): 关系型数据库 (Relational Database) 中的表格数据 (Tabular Data) 可以用二维数组 (Two-dimensional Array) 存储。
⚝ 电子表格软件 (Spreadsheet Software): 例如 Excel 等电子表格软件,其工作表 (Worksheet) 本质上就是一个二维数组 (Two-dimensional Array)。
⚝ 控制系统 (Control System): 在控制工程 (Control Engineering) 中,可以使用矩阵 (Matrix) 表示系统状态、控制输入、系统参数等,进行系统建模和控制算法设计。
多维数组 (Multi-dimensional Arrays) 凭借其结构化的数据组织方式和高效的元素访问能力,成为处理复杂数据和实现各种算法的重要工具。理解和熟练运用多维数组 (Multi-dimensional Arrays) 对于进行复杂程序设计至关重要。
2.2 数组与指针 (Arrays and Pointers)
章节摘要 (Section Summary)
深入解析数组名 (Array Name) 与指针 (Pointer) 之间的关系,探讨指针 (Pointer) 在数组操作中的应用,包括指针算术 (Pointer Arithmetic) 和数组作为函数参数 (Function Arguments) 传递等。
2.2.1 数组名是指针常量 (Array Name as a Pointer Constant)
小节摘要 (Subsection Summary)
解释数组名 (Array Name) 在大多数情况下会被隐式转换为指向数组首元素 (First Element) 的指针常量 (Pointer Constant),并通过示例代码验证这一特性。
① 数组名的本质 (Nature of Array Names)
在 C++ 中,数组名 (Array Name) 在绝大多数情况下,会被隐式地转换为指向数组首元素 (First Element) 的指针 (Pointer)。这个指针 (Pointer) 存储的是数组第一个元素 (Element) 的内存地址 (Memory Address)。
关键点:
⚝ 隐式转换 (Implicit Conversion): 这种转换是自动发生的,不需要显式的类型转换 (Type Casting)。
⚝ 指针常量 (Pointer Constant): 数组名 (Array Name) 转换得到的指针 (Pointer) 是一个常量 (Constant),这意味着你不能修改这个指针 (Pointer) 的值,即不能让它指向其他地址。但你可以通过这个指针 (Pointer) 修改它所指向的内存区域的值(即数组元素 (Array Element) 的值)。
例外情况:
在两种情况下,数组名 (Array Name) 不会被转换为指针 (Pointer):
sizeof
运算符: 当数组名 (Array Name) 作为sizeof
运算符的操作数时,sizeof(数组名)
返回的是整个数组 (Array) 的大小(以字节为单位),而不是指针 (Pointer) 的大小。&
运算符: 当对数组名 (Array Name) 使用取地址运算符&
时,&数组名
得到的是整个数组 (Array) 的地址,类型是指向数组的指针 (Pointer to Array),而不是指向数组首元素 (First Element) 的指针 (Pointer)。
② 示例验证 (Example Verification)
通过以下示例代码,可以验证数组名 (Array Name) 作为指针常量 (Pointer Constant) 的特性:
1
#include <iostream>
2
3
int main() {
4
int arr[5] = {10, 20, 30, 40, 50};
5
6
// 数组名 arr 的值 (Value of array name arr)
7
std::cout << "数组名 arr 的值 (Value of arr): " << arr << std::endl;
8
9
// 数组首元素地址 (&arr[0]) (Address of the first element)
10
std::cout << "数组首元素地址 (&arr[0]): " << &arr[0] << std::endl;
11
12
// 数组 arr 的地址 (&arr) (Address of the array arr)
13
std::cout << "数组 arr 的地址 (&arr): " << &arr << std::endl;
14
15
// sizeof 运算符 (sizeof operator)
16
std::cout << "sizeof(arr): " << sizeof(arr) << std::endl; // 整个数组的大小 (Size of the entire array)
17
std::cout << "sizeof(&arr[0]): " << sizeof(&arr[0]) << std::endl; // 指针的大小 (Size of a pointer)
18
19
return 0;
20
}
代码解释:
⚝ std::cout << "数组名 arr 的值 (Value of arr): " << arr << std::endl;
: 直接输出数组名 arr
,你会看到输出的是一个内存地址,这个地址与数组首元素 arr[0]
的地址相同。这表明 arr
被隐式转换为指向 arr[0]
的指针 (Pointer)。
⚝ std::cout << "数组首元素地址 (&arr[0]): " << &arr[0] << std::endl;
: 使用 &
运算符获取数组首元素 arr[0]
的地址,输出结果与直接输出 arr
相同。
⚝ std::cout << "数组 arr 的地址 (&arr): " << &arr << std::endl;
: 使用 &
运算符获取整个数组 arr
的地址。在大多数编译器和架构下,&arr
和 arr
的值(地址)是相同的,但它们的类型不同。arr
是指向 int
的指针 (Pointer to int
),而 &arr
是指向 int[5]
数组的指针 (Pointer to int[5]
)。
⚝ std::cout << "sizeof(arr): " << sizeof(arr) << std::endl;
: sizeof(arr)
返回整个数组 arr
的大小,即 5 * sizeof(int)
字节。
⚝ std::cout << "sizeof(&arr[0]): " << sizeof(&arr[0]) << std::endl;
: sizeof(&arr[0])
返回指针 (Pointer) 的大小,通常在 32 位系统上是 4 字节,在 64 位系统上是 8 字节。
运行结果 (输出的地址值可能因运行环境而异):
1
数组名 arr 的值 (Value of arr): 0x7ffee3081a80
2
数组首元素地址 (&arr[0]): 0x7ffee3081a80
3
数组 arr 的地址 (&arr): 0x7ffee3081a80
4
sizeof(arr): 20
5
sizeof(&arr[0]): 8
结果分析:
⚝ arr
, &arr[0]
, &arr
的值 (地址) 相同,验证了数组名 (Array Name) arr
在大多数情况下被转换为指向数组首元素 (First Element) 的指针 (Pointer)。
⚝ sizeof(arr)
返回整个数组 (Array) 的大小,而 sizeof(&arr[0])
返回指针 (Pointer) 的大小,验证了 sizeof
运算符的例外情况。
③ 数组名作为指针的应用 (Applications of Array Name as Pointer)
由于数组名 (Array Name) 可以隐式转换为指针 (Pointer),因此你可以像使用指针 (Pointer) 一样使用数组名 (Array Name) 进行数组操作,例如:
⚝ 指针算术 (Pointer Arithmetic): 可以使用指针算术 (Pointer Arithmetic) 访问数组元素 (Array Elements)。
⚝ 函数参数 (Function Arguments): 可以将数组名 (Array Name) 作为函数参数 (Function Argument) 传递,实际上传递的是指向数组首元素 (First Element) 的指针 (Pointer)。
这些应用将在后续小节中详细介绍。理解数组名 (Array Name) 作为指针常量 (Pointer Constant) 的本质,是深入理解 C++ 数组 (Array) 和指针 (Pointer) 关系的关键。
2.2.2 指针算术与数组 (Pointer Arithmetic and Arrays)
小节摘要 (Subsection Summary)
讲解指针算术 (Pointer Arithmetic) 运算在数组中的应用,例如通过指针 (Pointer) 的加减运算访问数组的不同元素 (Elements),并强调指针运算的边界问题。
① 指针算术 (Pointer Arithmetic) 的基本概念
指针算术 (Pointer Arithmetic) 允许你对指针 (Pointer) 进行加法和减法运算,以在内存中移动指针 (Pointer) 的位置。当指针 (Pointer) 指向数组元素 (Array Element) 时,指针算术 (Pointer Arithmetic) 可以方便地访问数组中的其他元素 (Elements)。
关键规则:
⚝ 指针加法 (Pointer Addition): 指针 + n
将指针 (Pointer) 向后移动 n * sizeof(指针所指向的类型)
个字节。
⚝ 指针减法 (Pointer Subtraction): 指针 - n
将指针 (Pointer) 向前移动 n * sizeof(指针所指向的类型)
个字节。
⚝ 指针减指针 (Pointer Subtraction between Pointers): 指针1 - 指针2
计算两个指针 (Pointers) 之间相隔多少个元素(而不是字节),前提是 指针1
和 指针2
指向同一数组内的元素。
sizeof(指针所指向的类型)
的重要性: 指针算术 (Pointer Arithmetic) 的步长取决于指针 (Pointer) 所指向的数据类型的大小。例如,如果指针 (Pointer) 指向 int
类型,则指针加 1 会使指针 (Pointer) 指向内存中下一个 int
类型数据的起始地址,移动 sizeof(int)
个字节。
② 通过指针算术访问数组元素 (Accessing Array Elements with Pointer Arithmetic)
由于数组名 (Array Name) 可以转换为指向数组首元素 (First Element) 的指针 (Pointer),我们可以使用指针算术 (Pointer Arithmetic) 结合数组名 (Array Name) 或指向数组的指针 (Pointer to Array) 来访问数组元素 (Array Elements)。
示例:
1
#include <iostream>
2
3
int main() {
4
int arr[5] = {10, 20, 30, 40, 50};
5
int *ptr = arr; // ptr 指向数组首元素 arr[0] (ptr points to arr[0])
6
7
// 使用数组索引访问 (Accessing with array index)
8
std::cout << "arr[0] = " << arr[0] << std::endl;
9
std::cout << "arr[1] = " << arr[1] << std::endl;
10
std::cout << "arr[2] = " << arr[2] << std::endl;
11
12
// 使用指针算术访问 (Accessing with pointer arithmetic)
13
std::cout << "*ptr = " << *ptr << std::endl; // 访问 arr[0] (Access arr[0])
14
std::cout << "*(ptr + 1) = " << *(ptr + 1) << std::endl; // 访问 arr[1] (Access arr[1])
15
std::cout << "*(ptr + 2) = " << *(ptr + 2) << std::endl; // 访问 arr[2] (Access arr[2])
16
17
// 指针加法和赋值 (Pointer addition and assignment)
18
ptr = ptr + 3; // ptr 指向 arr[3] (ptr now points to arr[3])
19
std::cout << "*ptr = " << *ptr << std::endl; // 访问 arr[3] (Access arr[3])
20
21
// 指针减法 (Pointer subtraction)
22
ptr = ptr - 2; // ptr 指向 arr[1] (ptr now points to arr[1])
23
std::cout << "*ptr = " << *ptr << std::endl; // 访问 arr[1] (Access arr[1])
24
25
return 0;
26
}
代码解释:
⚝ int *ptr = arr;
: 将数组名 arr
赋值给指针 ptr
,ptr
指向数组首元素 arr[0]
。
⚝ *ptr
: 解引用指针 ptr
,访问 ptr
当前指向的内存地址的值,即 arr[0]
的值。
⚝ *(ptr + 1)
: ptr + 1
将指针 ptr
向后移动一个 int
的大小,指向 arr[1]
的地址。然后解引用 *(ptr + 1)
,访问 arr[1]
的值。
⚝ ptr = ptr + 3;
: 将 ptr
向后移动 3 个 int
的大小,指向 arr[3]
的地址。
⚝ ptr = ptr - 2;
: 将 ptr
向前移动 2 个 int
的大小,指向 arr[1]
的地址。
运行结果:
1
arr[0] = 10
2
arr[1] = 20
3
arr[2] = 30
4
*ptr = 10
5
*(ptr + 1) = 20
6
*(ptr + 2) = 30
7
*ptr = 40
8
*ptr = 20
等价性: arr[i]
和 *(arr + i)
是等价的。C++ 编译器会将 arr[i]
转换为 *(arr + i)
来处理。 这进一步说明了数组名 (Array Name) 和指针 (Pointer) 之间的紧密联系。
③ 指针运算的边界问题 (Boundary Issues of Pointer Arithmetic)
在使用指针算术 (Pointer Arithmetic) 时,务必注意指针 (Pointer) 的边界 (Boundary),避免访问数组 (Array) 范围之外的内存 (Memory)。
潜在错误:
⚝ 数组越界访问 (Array Out-of-Bounds Access): 如果指针 (Pointer) 移动到数组 (Array) 的起始地址之前或结束地址之后,解引用这样的指针 (Pointer) 会导致未定义行为 (Undefined Behavior),可能造成程序崩溃 (Crash) 或数据损坏 (Data Corruption)。
⚝ 野指针 (Wild Pointer) 和 悬 dangling 指针 (Dangling Pointer): 不恰当的指针算术 (Pointer Arithmetic) 可能导致指针 (Pointer) 指向无效的内存区域,形成野指针 (Wild Pointer) 或悬 dangling 指针 (Dangling Pointer)。
最佳实践:
⚝ 确保指针 (Pointer) 始终在数组 (Array) 的有效范围内。
⚝ 在循环 (Loop) 中使用指针算术 (Pointer Arithmetic) 遍历数组 (Array) 时,需要仔细控制循环条件,防止越界。
⚝ 避免过度复杂的指针运算,保持代码清晰易懂。
示例 (错误的指针运算导致越界):
1
#include <iostream>
2
3
int main() {
4
int arr[5] = {10, 20, 30, 40, 50};
5
int *ptr = arr;
6
7
ptr = ptr - 1; // 指针 ptr 指向数组 arr 之前的位置 (Pointer ptr points before the array arr)
8
std::cout << "*ptr = " << *ptr << std::endl; // 越界访问,未定义行为 (Out-of-bounds access, undefined behavior)
9
10
ptr = arr + 5; // 指针 ptr 指向数组 arr 之后的位置 (Pointer ptr points after the array arr)
11
std::cout << "*ptr = " << *ptr << std::endl; // 越界访问,未定义行为 (Out-of-bounds access, undefined behavior)
12
13
return 0;
14
}
警告: 上述代码示例中的越界访问是错误示范,请勿在实际编程中模仿。 编译器可能不会报错,但程序运行时可能会崩溃或产生不可预测的结果。
④ 指针减指针运算 (Pointer Subtraction between Pointers)
当两个指针 (Pointers) 都指向同一个数组 (Array) 内的元素时,可以进行减法运算。其结果是这两个指针 (Pointers) 之间相隔的元素个数。
示例:
1
#include <iostream>
2
3
int main() {
4
int arr[5] = {10, 20, 30, 40, 50};
5
int *ptr1 = &arr[1]; // ptr1 指向 arr[1] (ptr1 points to arr[1])
6
int *ptr2 = &arr[4]; // ptr2 指向 arr[4] (ptr2 points to arr[4])
7
8
ptrdiff_t diff = ptr2 - ptr1; // 计算 ptr2 和 ptr1 之间相隔的元素个数 (Calculate element difference)
9
10
std::cout << "ptr2 - ptr1 = " << diff << std::endl; // 输出 3 (arr[4] 和 arr[1] 之间相隔 3 个元素)
11
12
return 0;
13
}
代码解释:
⚝ ptrdiff_t diff = ptr2 - ptr1;
: ptrdiff_t
是一种带符号整数类型,用于存储指针 (Pointer) 减法的结果。ptr2 - ptr1
计算 ptr2
和 ptr1
之间相隔的元素个数。
⚝ 结果为 3: 因为 arr[4]
和 arr[1]
之间相隔 arr[2]
, arr[3]
, arr[4]
这三个元素。
注意: 指针减法运算仅在两个指针 (Pointers) 指向同一数组 (Array) 或同一分配块 (Allocation Block) 内的元素时才有意义。 如果两个指针 (Pointers) 指向无关的内存区域,指针减法的结果是未定义的。
指针算术 (Pointer Arithmetic) 是 C++ 中操作数组 (Array) 和内存 (Memory) 的强大工具,但也需要谨慎使用,避免越界访问等错误。
2.2.3 数组作为函数参数 (Arrays as Function Arguments)
小节摘要 (Subsection Summary)
介绍如何将数组 (Array) 作为函数参数 (Function Arguments) 传递,包括传递数组首地址 (First Element Address) 和使用指针形参 (Pointer Parameters),并讨论数组作为函数参数 (Function Arguments) 时的大小退化 (Size Degradation) 问题。
① 数组作为函数参数的传递方式 (Passing Arrays as Function Arguments)
在 C++ 中,当你将数组名 (Array Name) 作为函数参数 (Function Argument) 传递时,实际上传递的是指向数组首元素 (First Element) 的指针 (Pointer),而不是整个数组 (Array) 的副本 (Copy)。
这意味着:
⚝ 形参 (Parameter) 接收的是指针 (Pointer): 函数的形参 (Parameter) 会被视为指向数组元素 (Array Element) 类型的指针 (Pointer)。
⚝ 数组大小信息丢失 (Loss of Array Size Information): 在函数内部,你无法直接通过形参 (Parameter) 获取原始数组 (Array) 的大小。
⚝ 函数内修改会影响原数组 (Modifications in Function Affect Original Array): 由于形参 (Parameter) 指向的是原始数组 (Array) 的内存,因此在函数内部对数组元素 (Array Element) 的修改会直接影响到函数外部的原始数组 (Array)。
② 函数形参的声明方式 (Declaration of Function Parameters for Arrays)
声明接收数组 (Array) 作为参数 (Parameter) 的函数时,有以下几种常见的形参 (Parameter) 声明方式:
⚝ 使用指针形参 (Pointer Parameter)
最常用的方式是使用指针 (Pointer) 作为形参 (Parameter)。这明确表明函数接收的是一个指向数组首元素 (First Element) 的指针 (Pointer)。
1
void processArray(int *arr); // 形参 arr 是指向 int 的指针 (Parameter arr is a pointer to int)
在函数体内,你可以像操作指针 (Pointer) 一样操作 arr
,例如使用指针算术 (Pointer Arithmetic) 访问数组元素 (Array Elements)。
⚝ 使用数组形式的形参 (Array-like Parameter)
你也可以使用数组形式声明形参 (Parameter),但方括号 []
内的大小可以省略。编译器会将这种形式的形参 (Parameter) 仍然解释为指针 (Pointer)。
1
void processArray(int arr[]); // 形参 arr 仍然被视为指向 int 的指针 (Parameter arr is still treated as a pointer to int)
虽然看起来像数组声明,但 arr[]
实际上等同于 *arr
。省略大小是为了强调函数不关心数组的具体大小,只接收一个指向数组起始位置的指针 (Pointer)。
⚝ 使用带有大小的数组形参 (Array Parameter with Size) (不常用,但可以用于特定场景)
你可以在方括号 []
内指定数组的大小。但这仅仅是语法上的提示,编译器不会进行数组边界检查 (Bounds Checking) 或强制要求传入的数组 (Array) 必须是指定大小。 这种形式更多的是为了代码的可读性。
1
void processArray(int arr[5]); // 形参 arr 仍然被视为指向 int 的指针 (Parameter arr is still treated as a pointer to int)
即使你声明了 int arr[5]
,你仍然可以向函数传递大小不是 5 的数组 (Array),程序不会报错,但可能会导致逻辑错误或越界访问 (Out-of-Bounds Access) 等问题。
示例:
1
#include <iostream>
2
3
// 使用指针形参 (Using pointer parameter)
4
void printArray(int *arr, int size) {
5
std::cout << "数组元素 (Array Elements): ";
6
for (int i = 0; i < size; ++i) {
7
std::cout << arr[i] << " "; // 或 *(arr + i) (or *(arr + i))
8
}
9
std::cout << std::endl;
10
}
11
12
// 使用数组形式形参 (Using array-like parameter)
13
void modifyArray(int arr[], int size) {
14
for (int i = 0; i < size; ++i) {
15
arr[i] *= 2; // 修改数组元素 (Modify array elements)
16
}
17
}
18
19
int main() {
20
int myArray[5] = {1, 2, 3, 4, 5};
21
int arraySize = sizeof(myArray) / sizeof(myArray[0]); // 计算数组大小 (Calculate array size)
22
23
std::cout << "原始数组 (Original Array): ";
24
printArray(myArray, arraySize); // 传递数组名 myArray (Pass array name myArray)
25
26
modifyArray(myArray, arraySize); // 传递数组名 myArray (Pass array name myArray)
27
28
std::cout << "修改后的数组 (Modified Array): ";
29
printArray(myArray, arraySize); // 再次打印数组 (Print array again)
30
31
return 0;
32
}
代码解释:
⚝ printArray(int *arr, int size)
和 modifyArray(int arr[], int size)
: 这两个函数都接收一个指向 int
的指针 (Pointer) arr
和一个表示数组大小的 int
型变量 size
。
⚝ printArray
: 遍历并打印数组元素 (Array Elements)。
⚝ modifyArray
: 将数组中每个元素的值乘以 2。
⚝ main
函数:
▮▮▮▮⚝ 声明并初始化数组 myArray
。
▮▮▮▮⚝ 计算数组大小 arraySize
。
▮▮▮▮⚝ 调用 printArray
打印原始数组。
▮▮▮▮⚝ 调用 modifyArray
修改数组。
▮▮▮▮⚝ 再次调用 printArray
打印修改后的数组,验证修改效果。
运行结果:
1
原始数组 (Original Array): 数组元素 (Array Elements): 1 2 3 4 5
2
修改后的数组 (Modified Array): 数组元素 (Array Elements): 2 4 6 8 10
结果分析: 可以看到,modifyArray
函数成功修改了 main
函数中声明的 myArray
数组,验证了函数内对数组的修改会影响原数组。
③ 数组大小退化 (Array Size Degradation)
当数组 (Array) 作为函数参数 (Function Argument) 传递时,会发生数组大小退化 (Array Size Degradation)。这意味着:
⚝ 数组 (Array) 退化为指针 (Pointer): 数组名 (Array Name) 退化为指向数组首元素 (First Element) 的指针 (Pointer)。
⚝ 数组大小信息丢失 (Loss of Array Size Information): 函数无法通过形参 (Parameter) 直接获取原始数组 (Array) 的大小。
后果:
⚝ 必须显式传递数组大小 (Explicitly Pass Array Size): 如果需要在函数内部知道数组的大小,你必须显式地将数组大小作为额外的参数 (Parameter) 传递给函数,就像上述示例中的 size
参数 (Parameter) 一样。
⚝ sizeof
运算符失效 (Ineffectiveness of sizeof
Operator): 在函数内部,对形参 (Parameter) (无论是指针形式还是数组形式) 使用 sizeof
运算符,得到的是指针 (Pointer) 的大小,而不是原始数组 (Array) 的大小。
示例 (sizeof
运算符在函数内失效):
1
#include <iostream>
2
3
void checkArraySize(int arr[]) {
4
std::cout << "函数内部 sizeof(arr): " << sizeof(arr) << std::endl; // 指针的大小 (Size of pointer)
5
}
6
7
int main() {
8
int myArray[5] = {1, 2, 3, 4, 5};
9
std::cout << "main 函数中 sizeof(myArray): " << sizeof(myArray) << std::endl; // 整个数组的大小 (Size of entire array)
10
11
checkArraySize(myArray); // 传递数组名 myArray (Pass array name myArray)
12
13
return 0;
14
}
运行结果 (指针大小可能因系统而异):
1
main 函数中 sizeof(myArray): 20
2
函数内部 sizeof(arr): 8
结果分析:
⚝ 在 main
函数中,sizeof(myArray)
返回 20 字节 (假设 int
占 4 字节),是整个数组 (Array) 的大小。
⚝ 在 checkArraySize
函数中,sizeof(arr)
返回 8 字节 (假设指针 (Pointer) 大小为 8 字节),是指针 (Pointer) 的大小,而不是数组 (Array) 的大小。
总结:
当数组 (Array) 作为函数参数 (Function Argument) 传递时,务必牢记数组大小退化 (Array Size Degradation) 的特性。 如果需要在函数内部处理数组 (Array) 的所有元素,就必须显式地传递数组大小信息,并避免在函数内部使用 sizeof
运算符来获取数组大小。
2.3 动态数组 (Dynamic Arrays)
章节摘要 (Section Summary)
介绍动态数组 (Dynamic Arrays) 的概念,讲解如何使用 new
和 delete[]
运算符在堆 (Heap) 上动态分配 (Dynamic Allocation) 和释放 (Deallocation) 数组内存,以及动态数组 (Dynamic Arrays) 的优势和应用场景。
2.3.1 动态内存分配与 new
运算符 (Dynamic Memory Allocation with new
Operator)
小节摘要 (Subsection Summary)
详细讲解 new
运算符在动态数组 (Dynamic Arrays) 内存分配中的使用方法,包括分配单个元素数组 (Single Element Array) 和多维数组 (Multi-dimensional Arrays),并介绍内存分配失败的处理。
① 动态内存分配的概念 (Concept of Dynamic Memory Allocation)
与静态数组 (Static Arrays) (例如 int arr[10];
) 在栈 (Stack) 内存上分配内存不同,动态数组 (Dynamic Arrays) 的内存分配发生在堆 (Heap) 内存上。
动态内存分配的特点:
⚝ 运行时分配 (Runtime Allocation): 内存分配发生在程序运行时 (Runtime),而不是编译时 (Compile Time)。
⚝ 手动管理 (Manual Management): 程序员需要手动使用 new
运算符分配内存,并使用 delete
或 delete[]
运算符释放内存。
⚝ 大小可变 (Variable Size): 动态数组 (Dynamic Arrays) 的大小可以在运行时根据需要确定,更加灵活。
⚝ 生命周期 (Lifetime): 动态分配的内存的生命周期由程序员控制,直到显式释放为止。
new
运算符的作用:
⚝ 分配内存 (Allocate Memory): new
运算符在堆 (Heap) 上分配指定大小的内存空间。
⚝ 返回指针 (Return Pointer): new
运算符返回一个指向已分配内存起始地址的指针 (Pointer)。
⚝ 类型安全 (Type Safety): new
运算符是类型安全的,它会根据指定的数据类型分配相应大小的内存,并返回正确类型的指针 (Pointer)。
② 分配一维动态数组 (Allocating 1D Dynamic Arrays)
使用 new[]
运算符可以动态分配一维数组 (One-dimensional Dynamic Array)。语法如下:
1
数据类型 *指针变量 = new 数据类型[数组大小];
⚝ 数据类型 (Data Type): 指定数组元素 (Array Element) 的数据类型,例如 int
, float
, char
等。
⚝ 指针变量 (Pointer Variable): 用于存储 new
运算符返回的指针 (Pointer),必须是指向相应数据类型的指针 (Pointer)。
⚝ 数组大小 (Array Size): 指定要分配的数组元素 (Array Element) 的数量,可以是变量,在运行时 (Runtime) 确定。
示例:
1
#include <iostream>
2
3
int main() {
4
int size;
5
std::cout << "请输入数组大小 (Enter array size): ";
6
std::cin >> size;
7
8
// 动态分配整型数组 (Dynamically allocate integer array)
9
int *dynamicArray = new int[size];
10
11
if (dynamicArray == nullptr) { // 检查内存分配是否成功 (Check if allocation succeeded)
12
std::cerr << "内存分配失败 (Memory allocation failed)!" << std::endl;
13
return 1; // 返回错误代码 (Return error code)
14
}
15
16
std::cout << "成功分配大小为 " << size << " 的整型数组 (Successfully allocated integer array of size " << size << ")" << std::endl;
17
18
// 使用动态数组 (Use dynamic array)
19
for (int i = 0; i < size; ++i) {
20
dynamicArray[i] = i + 1; // 初始化数组元素 (Initialize array elements)
21
}
22
23
for (int i = 0; i < size; ++i) {
24
std::cout << dynamicArray[i] << " ";
25
}
26
std::cout << std::endl;
27
28
delete[] dynamicArray; // 释放动态分配的内存 (Deallocate dynamically allocated memory)
29
dynamicArray = nullptr; // 避免悬 dangling 指针 (Avoid dangling pointer)
30
31
return 0;
32
}
代码解释:
⚝ int *dynamicArray = new int[size];
: 使用 new int[size]
在堆 (Heap) 上动态分配一个包含 size
个 int
类型元素的数组 (Array),并将返回的指针 (Pointer) 赋值给 dynamicArray
。
⚝ if (dynamicArray == nullptr)
: 重要: 检查 new
运算符是否成功分配了内存。如果内存分配失败 (例如堆 (Heap) 空间不足),new
运算符会返回空指针 (Null Pointer) nullptr
。 必须检查返回值,处理内存分配失败的情况。
⚝ 使用动态数组 (Use dynamic array): 可以像使用静态数组 (Static Array) 一样,通过索引 (Index) 访问和操作动态数组 (Dynamic Array) 的元素。
⚝ delete[] dynamicArray;
: 重要: 使用 delete[] dynamicArray;
释放之前使用 new[]
分配的动态数组 (Dynamic Array) 的内存。 delete[]
必须与 new[]
配对使用,以正确释放数组内存。
⚝ dynamicArray = nullptr;
: 最佳实践: 在释放内存后,将指针 (Pointer) dynamicArray
设置为 nullptr
,以避免悬 dangling 指针 (Dangling Pointer) 的问题。
③ 分配多维动态数组 (Allocating Multi-dimensional Dynamic Arrays)
动态分配多维数组 (Multi-dimensional Dynamic Arrays) 比一维数组 (One-dimensional Dynamic Arrays) 稍复杂,因为内存需要连续分配,但逻辑上需要组织成多维结构。
常见方法:
⚝ 分配指向指针的指针数组 (Array of Pointers to Pointers) (适用于二维及以上)
对于二维数组 (Two-dimensional Array),可以先动态分配一个指针数组 (Array of Pointers),数组中的每个指针 (Pointer) 再指向动态分配的一行 (Row) 数组。
1
int rows = 3;
2
int cols = 4;
3
4
// 分配指针数组 (Allocate array of pointers)
5
int **dynamicMatrix = new int*[rows];
6
7
if (dynamicMatrix == nullptr) {
8
std::cerr << "指针数组内存分配失败 (Pointer array allocation failed)!" << std::endl;
9
return 1;
10
}
11
12
// 为每一行分配内存 (Allocate memory for each row)
13
for (int i = 0; i < rows; ++i) {
14
dynamicMatrix[i] = new int[cols];
15
if (dynamicMatrix[i] == nullptr) {
16
std::cerr << "第 " << i << " 行内存分配失败 (Memory allocation failed for row " << i << ")!" << std::endl;
17
// 内存分配失败处理,需要释放已分配的内存 (Handle allocation failure, need to deallocate already allocated memory)
18
for (int j = 0; j < i; ++j) {
19
delete[] dynamicMatrix[j];
20
}
21
delete[] dynamicMatrix;
22
return 1;
23
}
24
}
25
26
// 使用 dynamicMatrix (Use dynamicMatrix)
27
for (int i = 0; i < rows; ++i) {
28
for (int j = 0; j < cols; ++j) {
29
dynamicMatrix[i][j] = i * cols + j + 1;
30
}
31
}
32
33
// 释放内存 (Deallocate memory)
34
for (int i = 0; i < rows; ++i) {
35
delete[] dynamicMatrix[i]; // 释放每一行的内存 (Deallocate memory for each row)
36
}
37
delete[] dynamicMatrix; // 释放指针数组的内存 (Deallocate memory for pointer array)
38
dynamicMatrix = nullptr;
代码解释:
▮▮▮▮⚝ int **dynamicMatrix = new int*[rows];
: 动态分配一个包含 rows
个 int*
类型元素的指针数组 (Array of Pointers)。 dynamicMatrix
是一个指向指针的指针 (Pointer to Pointer)。
▮▮▮▮⚝ 循环分配行内存: 外层循环遍历每一行,dynamicMatrix[i] = new int[cols];
为每一行动态分配一个包含 cols
个 int
类型元素的数组 (Array),并将返回的指针 (Pointer) 赋值给 dynamicMatrix[i]
。
▮▮▮▮⚝ 内存分配失败处理: 在分配指针数组 (Array of Pointers) 和每一行内存时,都需要检查 new
运算符的返回值,处理内存分配失败的情况。 重要: 如果在分配某一行内存时失败,需要释放之前已成功分配的所有行内存和指针数组 (Array of Pointers) 的内存,避免内存泄漏 (Memory Leak)。
▮▮▮▮⚝ 释放内存: 释放内存时,需要先释放每一行动态数组 (Dynamic Array) 的内存,然后再释放指针数组 (Array of Pointers) 的内存。 顺序不能颠倒。
⚝ 连续分配一维数组模拟多维数组 (Contiguous 1D Allocation Simulating Multi-dimensional Array) (适用于二维及以上,内存连续,效率更高)
将多维数组 (Multi-dimensional Array) 的所有元素连续存储在一维数组 (One-dimensional Array) 中,然后通过索引计算来模拟多维数组 (Multi-dimensional Array) 的访问。
对于二维数组 (Two-dimensional Array) matrix[rows][cols]
,元素 matrix[i][j]
在一维数组中的索引 (Index) 可以计算为 i * cols + j
(行优先顺序 (Row-major Order)) 或 j * rows + i
(列优先顺序 (Column-major Order))。
1
int rows = 3;
2
int cols = 4;
3
4
// 连续分配一维数组 (Contiguously allocate 1D array)
5
int *dynamicMatrix = new int[rows * cols];
6
7
if (dynamicMatrix == nullptr) {
8
std::cerr << "连续内存分配失败 (Contiguous memory allocation failed)!" << std::endl;
9
return 1;
10
}
11
12
// 使用 dynamicMatrix 模拟二维数组访问 (Simulate 2D array access)
13
for (int i = 0; i < rows; ++i) {
14
for (int j = 0; j < cols; ++j) {
15
dynamicMatrix[i * cols + j] = i * cols + j + 1; // 行优先顺序索引计算 (Row-major order index calculation)
16
}
17
}
18
19
// 访问元素 (Access element)
20
std::cout << "dynamicMatrix[1][2] = " << dynamicMatrix[1 * cols + 2] << std::endl; // 访问逻辑上的 matrix[1][2] (Access logical matrix[1][2])
21
22
delete[] dynamicMatrix; // 释放内存 (Deallocate memory)
23
dynamicMatrix = nullptr;
代码解释:
▮▮▮▮⚝ int *dynamicMatrix = new int[rows * cols];
: 动态分配一个大小为 rows * cols
的一维整型数组 (One-dimensional Integer Array),足以存储二维数组 (Two-dimensional Array) 的所有元素。
▮▮▮▮⚝ 索引计算 i * cols + j
: 使用行优先顺序 (Row-major Order) 计算二维索引 [i][j]
对应的一维数组 (One-dimensional Array) 索引。
▮▮▮▮⚝ dynamicMatrix[i * cols + j]
: 通过计算出的索引访问和操作一维数组 (One-dimensional Array),模拟二维数组 (Two-dimensional Array) 的访问。
▮▮▮▮⚝ 内存释放: 只需要使用 delete[] dynamicMatrix;
释放整个一维数组 (One-dimensional Array) 的内存即可。
优势: 内存连续分配,访问速度更快,内存管理更简单 (只需一次 new[]
和 delete[]
)。 适用于对性能有较高要求的场景。
缺点: 索引计算相对复杂,代码可读性稍差。
④ 内存分配失败的处理 (Handling Memory Allocation Failure)
务必检查 new
运算符的返回值,判断内存分配是否成功。 如果 new
运算符返回 nullptr
,表示内存分配失败。
处理方法:
⚝ 检查 nullptr
: 使用 if (指针变量 == nullptr)
或 if (!指针变量)
检查指针 (Pointer) 是否为空。
⚝ 错误处理 (Error Handling): 如果内存分配失败,应该进行适当的错误处理,例如:
▮▮▮▮⚝ 输出错误信息 (Output error message) 到标准错误流 std::cerr
。
▮▮▮▮⚝ 返回错误代码 (Return error code) 从函数中,告知调用者内存分配失败。
▮▮▮▮⚝ 抛出异常 (Throw exception) (更高级的错误处理机制,超出本书当前章节范围)。
▮▮▮▮⚝ 优雅退出程序 (Gracefully exit program)。
⚝ 避免程序崩溃 (Prevent Program Crash): 绝对不能在内存分配失败的情况下,继续使用未成功分配的指针 (Pointer) 进行操作。 这样做会导致程序崩溃 (Crash) 或未定义行为 (Undefined Behavior)。
示例 (内存分配失败处理): 见上述分配一维动态数组 (Allocating 1D Dynamic Arrays) 和分配指向指针的指针数组 (Array of Pointers to Pointers) 的示例代码。
动态内存分配 (Dynamic Memory Allocation) 提供了在运行时 (Runtime) 灵活管理内存的能力,但同时也增加了程序员的责任,需要仔细处理内存分配和释放,避免内存泄漏 (Memory Leak) 和其他内存管理问题。
2.3.2 动态内存释放与 delete[]
运算符 (Dynamic Memory Deallocation with delete[]
Operator)
小节摘要 (Subsection Summary)
讲解 delete[]
运算符在动态数组 (Dynamic Arrays) 内存释放中的使用方法,强调 new
和 delete[]
配对使用的重要性,避免内存泄漏 (Memory Leak)。
① 动态内存释放的重要性 (Importance of Dynamic Memory Deallocation)
使用 new
运算符动态分配的内存不会自动释放。 如果动态分配的内存不再使用,但没有被显式释放,就会发生内存泄漏 (Memory Leak)。
内存泄漏的后果:
⚝ 程序可用内存减少 (Reduced Available Memory): 每次内存泄漏都会占用一部分内存,长期运行的程序如果持续发生内存泄漏,会导致可用内存逐渐减少。
⚝ 程序性能下降 (Performance Degradation): 操作系统 (Operating System) 需要花费更多时间管理有限的内存资源,程序运行速度可能变慢。
⚝ 程序崩溃 (Program Crash): 极端情况下,如果内存泄漏非常严重,耗尽所有可用内存,可能导致程序崩溃 (Crash) 甚至系统崩溃 (System Crash)。
因此,动态内存释放是动态内存管理中至关重要的一环。 必须确保每一块使用 new
分配的动态内存,最终都通过 delete
或 delete[]
运算符正确释放。
② delete[]
运算符 (The delete[]
Operator)
delete[]
运算符用于释放使用 new[]
动态分配的数组内存。 语法如下:
1
delete[] 指针变量;
⚝ 指针变量 (Pointer Variable): 必须是指向之前使用 new[]
分配的内存起始地址的指针 (Pointer)。
关键点:
⚝ 配对使用 (Paired Usage): delete[]
必须与 new[]
配对使用。 如果使用 new
分配单个对象 (Object),则应使用 delete
释放; 如果使用 new[]
分配数组 (Array),则应使用 delete[]
释放。 混用 delete
和 delete[]
会导致未定义行为 (Undefined Behavior)。
⚝ 释放数组内存 (Deallocate Array Memory): delete[]
运算符会释放整个数组所占用的内存空间。
⚝ 只能释放堆内存 (Only Deallocate Heap Memory): delete[]
只能用于释放使用 new[]
在堆 (Heap) 上分配的内存。 不能用于释放栈 (Stack) 内存或静态内存 (Static Memory)。
⚝ 释放后指针变为悬 dangling 指针 (Pointer Becomes Dangling Pointer After Deallocation): 释放内存后,指向已释放内存的指针 (Pointer) 变为悬 dangling 指针 (Dangling Pointer)。 访问悬 dangling 指针 (Dangling Pointer) 会导致未定义行为 (Undefined Behavior)。 建议在释放内存后,立即将指针 (Pointer) 设置为 nullptr
,避免悬 dangling 指针 (Dangling Pointer) 问题。
③ 释放一维动态数组内存 (Deallocating 1D Dynamic Array Memory)
释放一维动态数组 (One-dimensional Dynamic Array) 的内存非常简单,只需使用 delete[]
运算符,并将指向数组起始地址的指针 (Pointer) 作为操作数即可。
示例: 见 2.3.1 小节中分配一维动态数组 (Allocating 1D Dynamic Arrays) 的示例代码。
1
delete[] dynamicArray; // 释放动态分配的内存 (Deallocate dynamically allocated memory)
2
dynamicArray = nullptr; // 避免悬 dangling 指针 (Avoid dangling pointer)
④ 释放多维动态数组内存 (Deallocating Multi-dimensional Dynamic Array Memory)
释放多维动态数组 (Multi-dimensional Dynamic Arrays) 的内存,需要根据其分配方式采取相应的释放策略。
⚝ 释放指向指针的指针数组 (Array of Pointers to Pointers)
如果使用“指针数组 (Array of Pointers)”方式分配多维动态数组 (Multi-dimensional Dynamic Array),则释放内存时,需要逆序执行分配操作:
- 循环释放每一行动态数组 (Dynamic Array) 的内存: 对外层指针数组 (Array of Pointers) 中的每个指针 (Pointer) (指向每一行数组) 使用
delete[]
释放。 - 释放指针数组 (Array of Pointers) 自身的内存: 对指针数组 (Array of Pointers) 的起始地址指针 (Pointer) 使用
delete[]
释放。
示例: 见 2.3.1 小节中分配指向指针的指针数组 (Array of Pointers to Pointers) 的示例代码。
1
// 释放内存 (Deallocate memory)
2
for (int i = 0; i < rows; ++i) {
3
delete[] dynamicMatrix[i]; // 释放每一行的内存 (Deallocate memory for each row)
4
}
5
delete[] dynamicMatrix; // 释放指针数组的内存 (Deallocate memory for pointer array)
6
dynamicMatrix = nullptr;
⚝ 释放连续分配的一维数组模拟的多维数组 (Contiguous 1D Allocation Simulating Multi-dimensional Array)
如果使用“连续分配一维数组 (One-dimensional Array) 模拟多维数组 (Multi-dimensional Array)”方式分配多维动态数组 (Multi-dimensional Dynamic Array),则释放内存非常简单,只需释放整个一维数组 (One-dimensional Array) 的内存即可。
示例: 见 2.3.1 小节中连续分配一维数组模拟多维数组 (Contiguous 1D Allocation Simulating Multi-dimensional Array) 的示例代码。
1
delete[] dynamicMatrix; // 释放内存 (Deallocate memory)
2
dynamicMatrix = nullptr;
⑤ 避免内存泄漏 (Avoiding Memory Leaks)
为了避免内存泄漏 (Memory Leak),需要遵循以下最佳实践:
⚝ new
和 delete
(或 new[]
和 delete[]
) 配对使用: 每次使用 new
(或 new[]
) 分配内存后,都必须确保在不再使用时,使用 delete
(或 delete[]
) 释放相应的内存。
⚝ 在合适的时机释放内存 (Deallocate Memory at the Right Time): 在动态分配的内存不再需要使用时,立即释放。 避免长时间持有不再需要的内存。
⚝ 使用智能指针 (Smart Pointers) (更高级的内存管理技术,超出本书当前章节范围): C++ 提供了智能指针 (Smart Pointers) (例如 std::unique_ptr
, std::shared_ptr
) 等工具,可以自动管理动态内存的释放,减少手动内存管理的错误。 智能指针 (Smart Pointers) 会在对象生命周期结束时自动释放所管理的内存,有效防止内存泄漏 (Memory Leak)。
⚝ 资源获取即初始化 (RAII, Resource Acquisition Is Initialization): RAII 是一种 C++ 编程范式,将资源的获取 (例如内存分配) 与对象的生命周期绑定,利用对象的析构函数 (Destructor) 在对象生命周期结束时自动释放资源。 智能指针 (Smart Pointers) 就是 RAII 范式的典型应用。
动态内存释放是动态内存管理的关键环节,必须养成良好的内存管理习惯,避免内存泄漏 (Memory Leak),保证程序的健壮性和性能。
2.3.3 动态数组的应用与注意事项 (Applications and Considerations of Dynamic Arrays)
小节摘要 (Subsection Summary)
列举动态数组 (Dynamic Arrays) 的应用场景,例如需要运行时 (Runtime) 确定数组大小的情况,并强调动态内存管理的风险和注意事项,例如内存泄漏 (Memory Leak) 和悬 dangling 指针 (Dangling Pointer)。
① 动态数组的应用场景 (Application Scenarios of Dynamic Arrays)
动态数组 (Dynamic Arrays) 在以下场景中非常有用:
⚝ 运行时确定数组大小 (Runtime Array Size Determination): 当数组 (Array) 的大小在编译时 (Compile Time) 未知,需要在程序运行时 (Runtime) 根据用户输入、数据读取或计算结果等动态确定时,必须使用动态数组 (Dynamic Arrays)。
示例: 读取用户输入的学生数量,然后动态创建一个数组 (Array) 来存储学生成绩。
⚝ 需要大尺寸数组 (Large Size Arrays): 静态数组 (Static Arrays) 的大小通常受到栈 (Stack) 内存大小的限制,不能创建过大的数组 (Arrays)。 而动态数组 (Dynamic Arrays) 在堆 (Heap) 内存上分配,堆 (Heap) 内存空间通常比栈 (Stack) 内存大得多,可以创建更大尺寸的数组 (Arrays),例如用于存储大型数据集、图像数据、矩阵 (Matrix) 等。
示例: 图像处理 (Image Processing) 程序中,需要加载和处理高分辨率图像,图像像素数据通常需要使用动态数组 (Dynamic Arrays) 存储。
⚝ 灵活的内存管理 (Flexible Memory Management): 动态数组 (Dynamic Arrays) 允许程序员在程序运行过程中,根据需要动态分配和释放内存,更加灵活地管理内存资源。 可以根据实际数据量动态调整数组 (Array) 的大小,提高内存利用率。
示例: 在某些算法中,可能需要在运行时 (Runtime) 创建临时数组 (Temporary Arrays) 存储中间结果,算法结束后,这些临时数组 (Temporary Arrays) 就不再需要,可以及时释放内存。
⚝ 动态调整数组大小 (Dynamically Resizing Arrays) (间接实现): C++ 标准库中的 std::vector
容器可以动态调整大小 (Dynamically Resizing),但原始的 C++ 数组 (Array) (包括动态数组 (Dynamic Arrays)) 本身不能直接调整大小。 如果需要动态调整数组 (Array) 大小,通常需要:
1. 分配新的更大 (或更小) 的动态数组 (Dynamic Array)。
2. 将原数组 (Original Array) 的数据复制到新数组 (New Array)。
3. 释放原数组 (Original Array) 的内存。
4. 使用新数组 (New Array) 代替原数组 (Original Array)。
这种“重新分配和复制”的操作效率较低,如果频繁调整大小,建议使用 std::vector
等动态容器。
② 动态内存管理的风险与注意事项 (Risks and Considerations of Dynamic Memory Management)
动态内存管理虽然灵活,但也伴随着一些风险和需要注意的问题:
⚝ 内存泄漏 (Memory Leak): 最常见的风险。 如果动态分配的内存没有被及时释放,就会发生内存泄漏 (Memory Leak),长期运行的程序会消耗大量内存资源,最终可能导致程序崩溃 (Crash)。
避免方法: new
和 delete
(或 new[]
和 delete[]
) 配对使用,在不再需要使用动态内存时,及时释放。 使用智能指针 (Smart Pointers) 等工具辅助内存管理。
⚝ 悬 dangling 指针 (Dangling Pointer): 释放动态内存后,指向已释放内存的指针 (Pointer) 变为悬 dangling 指针 (Dangling Pointer)。 如果程序继续访问悬 dangling 指针 (Dangling Pointer),会导致未定义行为 (Undefined Behavior),可能造成程序崩溃 (Crash) 或数据损坏 (Data Corruption)。
避免方法: 在释放内存后,立即将指针 (Pointer) 设置为 nullptr
。 在访问指针 (Pointer) 之前,检查其是否为 nullptr
,确保指针 (Pointer) 有效。
⚝ 重复释放 (Double Free): 对同一块动态内存进行多次释放 (例如多次调用 delete
或 delete[]
),会导致未定义行为 (Undefined Behavior),通常会导致程序崩溃 (Crash)。
避免方法: 确保每块动态内存只释放一次。 使用智能指针 (Smart Pointers) 可以避免重复释放的问题。
⚝ 内存碎片 (Memory Fragmentation): 频繁地分配和释放不同大小的动态内存,可能会导致堆 (Heap) 内存中出现许多小的、不连续的空闲块,即内存碎片 (Memory Fragmentation)。 内存碎片 (Memory Fragmentation) 会降低内存利用率,有时可能导致即使总的空闲内存足够,也无法分配一块大的连续内存块。
缓解方法: 尽量分配和释放大小相近的内存块。 避免频繁地进行小块内存的分配和释放。 操作系统 (Operating System) 的内存管理算法也在不断改进,以减少内存碎片 (Memory Fragmentation)。
⚝ 内存分配失败 (Memory Allocation Failure): 当堆 (Heap) 内存空间不足时,new
运算符可能会分配内存失败,返回空指针 (Null Pointer) nullptr
。 程序必须检查 new
运算符的返回值,处理内存分配失败的情况,避免程序崩溃 (Crash)。
处理方法: 检查 new
运算符的返回值是否为 nullptr
,如果为 nullptr
,进行错误处理,例如输出错误信息、返回错误代码、优雅退出程序等。
③ 动态数组 vs. 静态数组 (Dynamic Arrays vs. Static Arrays)
特性 (Feature) | 动态数组 (Dynamic Arrays) | 静态数组 (Static Arrays) |
---|---|---|
内存分配位置 (Memory Allocation Location) | 堆 (Heap) 内存 | 栈 (Stack) 内存 |
大小确定时间 (Size Determination Time) | 运行时 (Runtime) 确定,可使用变量指定大小 | 编译时 (Compile Time) 确定,大小必须是常量表达式 |
大小灵活性 (Size Flexibility) | 大小可变,运行时动态确定 | 大小固定,编译时确定 |
内存管理 (Memory Management) | 手动管理 (使用 new 和 delete 或 new[] 和 delete[] ),需要程序员负责内存分配和释放 | 自动管理 (由编译器和运行时系统自动管理),无需手动释放内存 |
生命周期 (Lifetime) | 由程序员控制,直到显式释放 | 与数组声明所在的作用域 (Scope) 绑定,作用域结束时自动释放内存 |
大小限制 (Size Limit) | 受堆 (Heap) 内存大小限制,通常较大 | 受栈 (Stack) 内存大小限制,通常较小 |
性能 (Performance) | 内存分配和释放有一定开销,访问速度略慢 (堆内存访问通常比栈内存慢) | 内存分配和释放开销小,访问速度较快 (栈内存访问通常比堆内存快) |
安全性 (Safety) | 手动内存管理容易出错 (内存泄漏、悬 dangling 指针、重复释放等),需要谨慎处理 | 自动内存管理,相对安全,不易出错 |
适用场景 (Application Scenarios) | 运行时确定大小、需要大尺寸数组、需要灵活内存管理的场景 | 大小固定、小尺寸数组、性能敏感、安全性要求高的场景 |
选择建议:
⚝ 如果数组 (Array) 大小在编译时 (Compile Time) 就可以确定,且大小不大,优先使用静态数组 (Static Arrays),性能更好,更安全。
⚝ 如果数组 (Array) 大小需要在运行时 (Runtime) 才能确定,或者需要创建大尺寸数组 (Arrays),或者需要灵活地管理内存,则使用动态数组 (Dynamic Arrays)。
⚝ 在现代 C++ 编程中,更推荐使用 C++ 标准库提供的容器 (Containers) (例如 std::vector
, std::array
, std::string
等) 来代替原始的 C 风格数组 (C-style Arrays) (包括静态数组 (Static Arrays) 和动态数组 (Dynamic Arrays))。 容器 (Containers) 提供了更高级的功能、更好的类型安全 (Type Safety) 和内存管理机制,可以有效减少程序错误,提高开发效率。
动态数组 (Dynamic Arrays) 是 C++ 中重要的内存管理工具,理解其原理、应用和注意事项,对于编写高效、健壮的 C++ 程序至关重要。
3. C++ 数组的深入应用 (In-depth Applications of C++ Arrays)
本章深入探讨 C++ 数组在实际编程中的高级应用,包括数组与算法、C 风格字符串 (C-style string)、以及 std::array
的使用,帮助读者提升解决实际问题的能力。
3.1 数组与常用算法 (Arrays and Common Algorithms)
本节介绍如何使用数组实现常见的算法,例如排序 (Sorting)、搜索 (Searching)、查找最大值/最小值等,并通过示例代码演示算法的具体实现。数组作为最基本的数据结构之一,是许多算法实现的基础。掌握如何利用数组高效地实现算法,对于提升程序性能至关重要。
3.1.1 数组的排序算法 (Sorting Algorithms for Arrays)
排序算法 (Sorting Algorithm) 是计算机科学中最基础且最重要的算法之一。其目标是将一组数据按照特定的顺序进行排列。对于数组而言,排序意味着重新排列数组中的元素,使其按照升序或降序排列。
以下介绍几种常见的数组排序算法:
① 冒泡排序 (Bubble Sort)
冒泡排序是一种简单直观的排序算法。它重复地遍历要排序的数组,每次比较相邻的两个元素,如果它们的顺序错误就交换它们的位置。遍历数组的工作重复进行直到没有再需要交换,也就是说该数组已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1
#include <iostream>
2
#include <vector>
3
4
void bubbleSort(std::vector<int>& arr) {
5
int n = arr.size();
6
for (int i = 0; i < n - 1; ++i) {
7
for (int j = 0; j < n - i - 1; ++j) {
8
if (arr[j] > arr[j + 1]) {
9
std::swap(arr[j], arr[j + 1]);
10
}
11
}
12
}
13
}
14
15
int main() {
16
std::vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
17
bubbleSort(arr);
18
std::cout << "冒泡排序后的数组: \n";
19
for (int i = 0; i < arr.size(); ++i)
20
std::cout << arr[i] << " ";
21
std::cout << std::endl;
22
return 0;
23
}
⚝ 时间复杂度 (Time Complexity):
▮▮▮▮⚝ 最好情况 (Best Case): \(O(n)\) (数组已经有序)
▮▮▮▮⚝ 平均情况 (Average Case): \(O(n^2)\)
▮▮▮▮⚝ 最坏情况 (Worst Case): \(O(n^2)\) (数组逆序)
⚝ 空间复杂度 (Space Complexity): \(O(1)\) (原地排序)
⚝ 适用场景 (Application Scenarios): 冒泡排序实现简单,但效率较低,通常不适用于大型数据集。它适合教学示例或小型数据集的排序。
② 选择排序 (Selection Sort)
选择排序是一种简单直观的排序算法。它的工作原理是每次从待排序的数据元素中选出最小(或最大)的一个元素存放在序列的起始位置,然后再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。
1
#include <iostream>
2
#include <vector>
3
#include <algorithm> // std::min_element, std::iter_swap
4
5
void selectionSort(std::vector<int>& arr) {
6
int n = arr.size();
7
for (int i = 0; i < n - 1; ++i) {
8
auto min_it = std::min_element(arr.begin() + i, arr.end());
9
std::iter_swap(arr.begin() + i, min_it);
10
}
11
}
12
13
int main() {
14
std::vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
15
selectionSort(arr);
16
std::cout << "选择排序后的数组: \n";
17
for (int i = 0; i < arr.size(); ++i)
18
std::cout << arr[i] << " ";
19
std::cout << std::endl;
20
return 0;
21
}
⚝ 时间复杂度 (Time Complexity):
▮▮▮▮⚝ 最好情况 (Best Case): \(O(n^2)\)
▮▮▮▮⚝ 平均情况 (Average Case): \(O(n^2)\)
▮▮▮▮⚝ 最坏情况 (Worst Case): \(O(n^2)\)
⚝ 空间复杂度 (Space Complexity): \(O(1)\) (原地排序)
⚝ 适用场景 (Application Scenarios): 选择排序的性能略优于冒泡排序,但仍然是 \(O(n^2)\) 复杂度,不适合大型数据集。它在数据量较小时表现尚可,并且其交换次数少于冒泡排序。
③ 插入排序 (Insertion Sort)
插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到 \(O(1)\) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
1
#include <iostream>
2
#include <vector>
3
4
void insertionSort(std::vector<int>& arr) {
5
int n = arr.size();
6
for (int i = 1; i < n; ++i) {
7
int key = arr[i];
8
int j = i - 1;
9
while (j >= 0 && arr[j] > key) {
10
arr[j + 1] = arr[j];
11
j = j - 1;
12
}
13
arr[j + 1] = key;
14
}
15
}
16
17
int main() {
18
std::vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
19
insertionSort(arr);
20
std::cout << "插入排序后的数组: \n";
21
for (int i = 0; i < arr.size(); ++i)
22
std::cout << arr[i] << " ";
23
std::cout << std::endl;
24
return 0;
25
}
⚝ 时间复杂度 (Time Complexity):
▮▮▮▮⚝ 最好情况 (Best Case): \(O(n)\) (数组已经有序)
▮▮▮▮⚝ 平均情况 (Average Case): \(O(n^2)\)
▮▮▮▮⚝ 最坏情况 (Worst Case): \(O(n^2)\) (数组逆序)
⚝ 空间复杂度 (Space Complexity): \(O(1)\) (原地排序)
⚝ 适用场景 (Application Scenarios): 插入排序对于小型数据集或基本有序的数据集非常高效。在实际应用中,当数据量较小或者部分有序时,插入排序通常优于冒泡排序和选择排序。它也常用于混合排序算法的优化部分,例如在快速排序 (Quick Sort) 或归并排序 (Merge Sort) 中,当子问题规模足够小时,切换到插入排序以提高效率。
3.1.2 数组的搜索算法 (Searching Algorithms for Arrays)
搜索算法 (Searching Algorithm) 用于在一个数据集合中查找特定的元素。对于数组,搜索算法旨在确定数组中是否存在目标元素,并可能返回其索引位置。
以下介绍两种常见的数组搜索算法:
① 线性搜索 (Linear Search)
线性搜索,也称为顺序搜索 (Sequential Search),是最简单的搜索算法之一。它从数组的第一个元素开始,逐个比较数组中的元素与目标值,直到找到目标元素或搜索完整个数组。
1
#include <iostream>
2
#include <vector>
3
4
int linearSearch(const std::vector<int>& arr, int target) {
5
int n = arr.size();
6
for (int i = 0; i < n; ++i) {
7
if (arr[i] == target) {
8
return i; // 找到目标,返回索引
9
}
10
}
11
return -1; // 未找到目标,返回 -1
12
}
13
14
int main() {
15
std::vector<int> arr = {2, 3, 4, 10, 40};
16
int target = 10;
17
int result = linearSearch(arr, target);
18
if (result == -1) {
19
std::cout << "元素不在数组中" << std::endl;
20
} else {
21
std::cout << "元素在数组中的索引为: " << result << std::endl;
22
}
23
return 0;
24
}
⚝ 时间复杂度 (Time Complexity):
▮▮▮▮⚝ 最好情况 (Best Case): \(O(1)\) (目标元素是数组的第一个元素)
▮▮▮▮⚝ 平均情况 (Average Case): \(O(n)\)
▮▮▮▮⚝ 最坏情况 (Worst Case): \(O(n)\) (目标元素是数组的最后一个元素或不存在于数组中)
⚝ 空间复杂度 (Space Complexity): \(O(1)\)
⚝ 适用场景 (Application Scenarios): 线性搜索适用于任何数组,无论数组是否有序。由于其简单性,它常用于小型数据集或当数据无需排序时。然而,对于大型数据集,线性搜索的效率较低。
② 二分搜索 (Binary Search)
二分搜索,也称为折半搜索 (Half-interval Search) 或对数搜索 (Logarithmic Search),是一种高效的搜索算法,但它要求被搜索的数组必须是有序的。二分搜索的工作原理是,每次将搜索区域减半。它从数组的中间元素开始比较,如果中间元素正好是目标值,则搜索结束;如果目标值大于或小于中间元素,则在数组大于或小于中间元素的那一半中搜索,并且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到目标值。
1
#include <iostream>
2
#include <vector>
3
4
int binarySearch(const std::vector<int>& arr, int target) {
5
int left = 0;
6
int right = arr.size() - 1;
7
while (left <= right) {
8
int mid = left + (right - left) / 2; // 防止 (left + right) 溢出
9
if (arr[mid] == target) {
10
return mid;
11
} else if (arr[mid] < target) {
12
left = mid + 1;
13
} else {
14
right = mid - 1;
15
}
16
}
17
return -1; // 未找到目标,返回 -1
18
}
19
20
int main() {
21
std::vector<int> arr = {2, 3, 4, 10, 40}; // 有序数组
22
int target = 10;
23
int result = binarySearch(arr, target);
24
if (result == -1) {
25
std::cout << "元素不在数组中" << std::endl;
26
} else {
27
std::cout << "元素在数组中的索引为: " << result << std::endl;
28
}
29
return 0;
30
}
⚝ 时间复杂度 (Time Complexity):
▮▮▮▮⚝ 最好情况 (Best Case): \(O(1)\) (目标元素是数组的中间元素)
▮▮▮▮⚝ 平均情况 (Average Case): \(O(\log n)\)
▮▮▮▮⚝ 最坏情况 (Worst Case): \(O(\log n)\)
⚝ 空间复杂度 (Space Complexity): \(O(1)\)
⚝ 适用场景 (Application Scenarios): 二分搜索非常高效,适用于大型有序数据集。在需要频繁进行搜索操作且数据量大的场景下,二分搜索是首选算法。但前提是数组必须预先排序。
3.1.3 数组的其他常用算法 (Other Common Algorithms for Arrays)
除了排序和搜索,数组还可以用于实现其他许多常用算法,例如:
① 查找最大值和最小值
查找数组中的最大值和最小值是最基本的操作之一。可以通过遍历数组,同时维护当前已知的最大值和最小值来实现。
1
#include <iostream>
2
#include <vector>
3
#include <limits> // std::numeric_limits
4
5
std::pair<int, int> findMinMax(const std::vector<int>& arr) {
6
if (arr.empty()) {
7
return {std::numeric_limits<int>::min(), std::numeric_limits<int>::max()}; // 空数组情况处理
8
}
9
int minVal = arr[0];
10
int maxVal = arr[0];
11
for (size_t i = 1; i < arr.size(); ++i) {
12
if (arr[i] < minVal) {
13
minVal = arr[i];
14
}
15
if (arr[i] > maxVal) {
16
maxVal = arr[i];
17
}
18
}
19
return {minVal, maxVal};
20
}
21
22
int main() {
23
std::vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
24
std::pair<int, int> minMax = findMinMax(arr);
25
std::cout << "最小值: " << minMax.first << std::endl;
26
std::cout << "最大值: " << minMax.second << std::endl;
27
return 0;
28
}
⚝ 时间复杂度 (Time Complexity): \(O(n)\) (需要遍历数组一次)
⚝ 空间复杂度 (Space Complexity): \(O(1)\)
② 计算平均值 (Average) 和求和 (Summation)
计算数组元素的平均值和总和也是常见的操作。求和可以通过遍历数组并将所有元素累加来实现,平均值则是在求和的基础上除以数组元素的个数。
1
#include <iostream>
2
#include <vector>
3
#include <numeric> // std::accumulate
4
5
double calculateAverage(const std::vector<int>& arr) {
6
if (arr.empty()) {
7
return 0.0; // 空数组平均值为 0
8
}
9
double sum = std::accumulate(arr.begin(), arr.end(), 0.0); // 使用 accumulate 求和
10
return sum / arr.size();
11
}
12
13
int calculateSum(const std::vector<int>& arr) {
14
return std::accumulate(arr.begin(), arr.end(), 0);
15
}
16
17
18
int main() {
19
std::vector<int> arr = {1, 2, 3, 4, 5};
20
double average = calculateAverage(arr);
21
int sum = calculateSum(arr);
22
std::cout << "平均值: " << average << std::endl;
23
std::cout << "总和: " << sum << std::endl;
24
return 0;
25
}
⚝ 时间复杂度 (Time Complexity): \(O(n)\) (需要遍历数组一次)
⚝ 空间复杂度 (Space Complexity): \(O(1)\)
③ 数组元素反转 (Reverse)
反转数组元素顺序也是一个基本操作。可以通过双指针法,从数组的两端向中间遍历,并交换对应位置的元素来实现。
1
#include <iostream>
2
#include <vector>
3
#include <algorithm> // std::swap
4
5
void reverseArray(std::vector<int>& arr) {
6
int start = 0;
7
int end = arr.size() - 1;
8
while (start < end) {
9
std::swap(arr[start], arr[end]);
10
start++;
11
end--;
12
}
13
}
14
15
int main() {
16
std::vector<int> arr = {1, 2, 3, 4, 5};
17
reverseArray(arr);
18
std::cout << "反转后的数组: \n";
19
for (int i = 0; i < arr.size(); ++i)
20
std::cout << arr[i] << " ";
21
std::cout << std::endl;
22
return 0;
23
}
⚝ 时间复杂度 (Time Complexity): \(O(n)\) (需要遍历数组一半的元素)
⚝ 空间复杂度 (Space Complexity): \(O(1)\) (原地反转)
3.2 C 风格字符串 (C-style Strings)
C 风格字符串 (C-style String) 是 C 语言中处理文本数据的一种方式,在 C++ 中仍然被广泛使用,尤其是在与 C 语言库互操作或需要兼容旧代码时。C 风格字符串本质上是字符数组 (Character Array),以空字符 \0
结尾。
3.2.1 C 风格字符串的定义与初始化 (Definition and Initialization of C-style Strings)
C 风格字符串的定义和初始化方式与普通字符数组类似,但关键在于必须以空字符 \0
结尾。空字符 \0
(ASCII 码值为 0) 用来标记字符串的结束位置。
① 定义 (Definition)
C 风格字符串定义为一个字符数组:
1
char str[6]; // 声明一个可以容纳 5 个字符 + 1 个空字符的字符数组
为了确保字符串可以完整存储,字符数组的大小必须至少比实际存储的字符串长度大 1,以容纳结尾的空字符 \0
。
② 初始化 (Initialization)
C 风格字符串可以通过多种方式初始化:
⚝ 字面量初始化 (Literal Initialization):最常用的方式是使用字符串字面量直接初始化。编译器会自动在字符串末尾添加空字符 \0
。
1
char str1[] = "Hello"; // 编译器自动计算大小并添加 '\0'
2
char str2[6] = "World"; // 显式指定大小,确保包含 '\0' 的空间
⚝ 逐字符初始化 (Character-by-character Initialization):可以逐个字符初始化数组,但必须手动添加空字符 \0
。
1
char str3[6] = {'C', '+', '+', '\0'}; // 手动添加 '\0'
2
char str4[6] = {'C', '+', '+', ' ', '!', '\0'};
⚝ 使用 strcpy
函数 (Using strcpy
function):可以使用 strcpy
函数从一个字符串复制内容到字符数组。
1
#include <cstring> // 引入 cstring 头文件,for strcpy
2
3
char str5[6];
4
strcpy(str5, "Test"); // 将 "Test" 复制到 str5,strcpy 会自动添加 '\0'
⚠️ 注意事项 (Important Notes):
⚝ 务必确保字符串以 \0
结尾。如果 C 风格字符串没有以 \0
结尾,很多字符串处理函数 (如 strlen
, strcpy
, strcmp
等) 将无法正确工作,可能会导致读取越界,引发程序错误甚至崩溃。
⚝ 数组大小要足够。在定义字符数组时,要确保其大小足以容纳要存储的字符串 (包括结尾的 \0
),否则可能发生缓冲区溢出 (Buffer Overflow) 的安全问题。
3.2.2 C 风格字符串的常用操作函数 (Common Operation Functions for C-style Strings)
C 标准库 <cstring>
(在 C++ 中通常使用 <cstring>
而非 <string.h>
) 提供了一系列函数来操作 C 风格字符串。以下是一些常用的函数:
① strlen(str)
: 计算字符串长度 (String Length)。返回字符串 str
的长度,不包括结尾的空字符 \0
。
1
#include <iostream>
2
#include <cstring>
3
4
int main() {
5
char str[] = "Hello";
6
size_t len = strlen(str); // 返回 5
7
std::cout << "字符串 '" << str << "' 的长度是: " << len << std::endl;
8
return 0;
9
}
② strcpy(dest, src)
: 字符串复制 (String Copy)。将源字符串 src
复制到目标字符数组 dest
中,包括结尾的空字符 \0
。目标数组 dest
必须有足够的空间容纳 src
,否则可能导致缓冲区溢出。
1
#include <iostream>
2
#include <cstring>
3
4
int main() {
5
char src[] = "Source";
6
char dest[10];
7
strcpy(dest, src);
8
std::cout << "复制后的字符串: " << dest << std::endl; // 输出 "Source"
9
return 0;
10
}
③ strcat(dest, src)
: 字符串连接 (String Concatenation)。将源字符串 src
追加到目标字符串 dest
的末尾,目标数组 dest
必须有足够的空间容纳连接后的字符串,包括结尾的空字符 \0
。同样,存在缓冲区溢出的风险。
1
#include <iostream>
2
#include <cstring>
3
4
int main() {
5
char dest[20] = "Hello, ";
6
char src[] = "World!";
7
strcat(dest, src);
8
std::cout << "连接后的字符串: " << dest << std::endl; // 输出 "Hello, World!"
9
return 0;
10
}
④ strcmp(str1, str2)
: 字符串比较 (String Compare)。比较字符串 str1
和 str2
的字典顺序。
⚝ 返回值 (Return Value):
▮▮▮▮⚝ 如果 str1
小于 str2
,返回值小于 0。
▮▮▮▮⚝ 如果 str1
等于 str2
,返回值等于 0。
▮▮▮▮⚝ 如果 str1
大于 str2
,返回值大于 0。
1
#include <iostream>
2
#include <cstring>
3
4
int main() {
5
char str1[] = "apple";
6
char str2[] = "banana";
7
char str3[] = "apple";
8
9
int result1 = strcmp(str1, str2); // str1 < str2
10
int result2 = strcmp(str1, str3); // str1 == str3
11
int result3 = strcmp(str2, str1); // str2 > str1
12
13
std::cout << "strcmp(str1, str2): " << result1 << std::endl; // 输出负数
14
std::cout << "strcmp(str1, str3): " << result2 << std::endl; // 输出 0
15
std::cout << "strcmp(str2, str1): " << result3 << std::endl; // 输出正数
16
return 0;
17
}
⑤ strncmp(str1, str2, n)
: 限定长度字符串比较 (Limited Length String Compare)。与 strcmp
类似,但只比较字符串 str1
和 str2
的前 n
个字符。
1
#include <iostream>
2
#include <cstring>
3
4
int main() {
5
char str1[] = "applepie";
6
char str2[] = "applejuice";
7
8
int result = strncmp(str1, str2, 5); // 比较前 5 个字符 "apple"
9
std::cout << "strncmp(str1, str2, 5): " << result << std::endl; // 输出 0,因为前 5 个字符相同
10
return 0;
11
}
⚠️ 安全风险 (Security Risks):
strcpy
和 strcat
函数存在严重的缓冲区溢出 (Buffer Overflow) 风险。如果源字符串的长度超过了目标数组的剩余空间,这些函数会继续写入超出数组边界的内存,可能覆盖其他数据,导致程序崩溃或安全漏洞。因此,强烈建议避免直接使用 strcpy
和 strcat
,而应使用更安全的替代方案,例如 strncpy
和 strncat
(可以限制复制或连接的字符数),或者使用 C++ 标准库提供的 std::string
类,后者可以自动管理内存,避免缓冲区溢出问题。
3.2.3 C 风格字符串的安全问题与替代方案 (Security Issues and Alternatives of C-style Strings)
C 风格字符串虽然在 C 语言和早期 C++ 代码中广泛使用,但由于其固有的安全问题,现代 C++ 编程中应尽量避免使用,尤其是在处理用户输入或外部数据时。
① 缓冲区溢出 (Buffer Overflow)
如前所述,strcpy
, strcat
等函数不进行边界检查,如果目标缓冲区空间不足,就会发生缓冲区溢出。这是一种严重的安全漏洞,攻击者可以利用缓冲区溢出覆盖程序的关键数据或执行恶意代码。
② 手动内存管理 (Manual Memory Management)
C 风格字符串的内存管理是手动的,需要程序员自己分配和释放字符数组的内存。这容易导致内存泄漏 (Memory Leak) 或悬 dangling 指针 (Dangling Pointer) 等问题。
③ 缺乏类型安全 (Lack of Type Safety)
C 风格字符串本质上是字符数组,类型检查相对较弱,容易发生类型相关的错误。
替代方案:std::string
类 (Alternatives: std::string
Class)
C++ 标准库提供了 std::string
类,作为 C 风格字符串的现代替代方案。std::string
类具有以下优势:
⚝ 自动内存管理 (Automatic Memory Management):std::string
自动管理字符串的内存,无需手动分配和释放,避免内存泄漏和悬 dangling 指针。
⚝ 动态大小 (Dynamic Size):std::string
可以根据字符串内容自动调整大小,避免缓冲区溢出风险。
⚝ 类型安全 (Type Safety):std::string
是一个类,提供了更强的类型检查,减少类型错误。
⚝ 丰富的功能 (Rich Functionality):std::string
提供了丰富的成员函数,例如字符串查找、子串操作、插入、删除等,操作方便且安全。
⚝ 与标准库算法兼容 (Compatibility with Standard Library Algorithms):std::string
可以方便地与 C++ 标准库的算法 (如 std::sort
, std::find
等) 配合使用。
示例:使用 std::string
(Example: Using std::string
)
1
#include <iostream>
2
#include <string> // 引入 string 头文件
3
4
int main() {
5
std::string str1 = "Hello"; // 初始化 std::string
6
std::string str2 = " World!";
7
8
std::string str3 = str1 + str2; // 字符串连接,使用 '+' 运算符
9
std::cout << "连接后的字符串: " << str3 << std::endl; // 输出 "Hello World!"
10
11
size_t len = str3.length(); // 获取字符串长度,使用 length() 方法
12
std::cout << "字符串长度: " << len << std::endl; // 输出 12
13
14
if (str1 == "Hello") { // 字符串比较,使用 '==' 运算符
15
std::cout << "str1 等于 'Hello'" << std::endl;
16
}
17
18
return 0;
19
}
在现代 C++ 编程中,强烈推荐使用 std::string
类来处理字符串,以提高代码的安全性、可靠性和可维护性。只有在需要与 C 语言库互操作或处理旧代码时,才需要考虑使用 C 风格字符串。即使在这种情况下,也应尽量使用安全的 C 风格字符串操作函数 (如 strncpy
, strncat
),并仔细进行边界检查。
3.3 std::array
容器 (The std::array
Container)
std::array
是 C++ 标准库 <array>
中提供的固定大小数组容器 (Fixed-size Array Container)。它封装了 C 风格数组,提供了类型安全、边界检查 (可选) 以及与标准库算法的兼容性等优势,是 C++ 中推荐使用的数组类型之一。
3.3.1 std::array
的基本使用 (Basic Usage of std::array
)
std::array
的基本使用包括声明、初始化、访问元素等操作。
① 声明 (Declaration)
声明 std::array
需要指定元素类型 (Element Type) 和 数组大小 (Array Size),这两个信息都作为模板参数 (Template Parameters) 传递给 std::array
。
1
#include <array> // 引入 array 头文件
2
3
std::array<int, 5> arr1; // 声明一个包含 5 个 int 元素的 std::array,未初始化
4
std::array<double, 10> arr2; // 声明一个包含 10 个 double 元素的 std::array
与 C 风格数组不同,std::array
的大小是类型的一部分,在编译时就确定了,不能动态改变。
② 初始化 (Initialization)
std::array
可以使用多种方式初始化:
⚝ 列表初始化 (List Initialization):使用花括号 {}
初始化。
1
std::array<int, 5> arr3 = {1, 2, 3, 4, 5}; // 初始化所有元素
2
std::array<int, 5> arr4 = {1, 2, 3}; // 初始化前 3 个元素,剩余元素默认为 0
3
std::array<int, 5> arr5{}; // 初始化所有元素为 0
⚝ 构造函数初始化 (Constructor Initialization):使用 std::array
的构造函数。
1
std::array<int, 5> arr6; // 默认构造,元素值未定义
2
std::array<int, 5> arr7 = {0}; // 初始化第一个元素为 0,其余元素未定义 (注意,这不等同于全部初始化为 0)
⚝ std::fill
算法初始化 (Using std::fill
algorithm):可以使用 std::fill
算法将所有元素初始化为相同的值。
1
#include <algorithm> // 引入 algorithm 头文件,for std::fill
2
3
std::array<int, 5> arr8;
4
std::fill(arr8.begin(), arr8.end(), 10); // 将所有元素初始化为 10
③ 访问元素 (Accessing Elements)
std::array
提供了多种方式访问元素:
⚝ 下标运算符 []
(Subscript Operator []
): 与 C 风格数组类似,可以使用下标运算符 []
访问元素。[]
运算符不进行边界检查,访问越界索引会导致未定义行为 (Undefined Behavior)。
1
std::array<int, 5> arr9 = {1, 2, 3, 4, 5};
2
int firstElement = arr9[0]; // 访问第一个元素
3
int thirdElement = arr9[2]; // 访问第三个元素
⚝ at()
方法 ( at()
Method): std::array
提供了 at()
方法来访问元素。at()
方法会进行边界检查,如果索引越界,会抛出 std::out_of_range
异常 (Exception)。
1
std::array<int, 5> arr10 = {1, 2, 3, 4, 5};
2
int firstElementAt = arr10.at(0); // 访问第一个元素
3
try {
4
int outOfBoundElement = arr10.at(5); // 尝试访问越界索引
5
} catch (const std::out_of_range& e) {
6
std::cerr << "越界访问异常: " << e.what() << std::endl; // 捕获并处理越界异常
7
}
⚝ 迭代器 (Iterators):std::array
提供了迭代器,可以使用迭代器遍历数组元素。
1
std::array<int, 5> arr11 = {1, 2, 3, 4, 5};
2
for (auto it = arr11.begin(); it != arr11.end(); ++it) {
3
std::cout << *it << " "; // 使用迭代器遍历并输出元素
4
}
5
std::cout << std::endl;
6
7
// 使用范围 for 循环 (Range-based for loop) 更简洁
8
for (int element : arr11) {
9
std::cout << element << " ";
10
}
11
std::cout << std::endl;
④ 其他常用方法 (Other Useful Methods)
std::array
还提供了一些其他有用的方法,例如:
⚝ size()
: 返回数组的大小 (元素个数)。
1
std::array<int, 5> arr12;
2
size_t arraySize = arr12.size(); // 返回 5
⚝ empty()
: 检查数组是否为空。对于 std::array
,由于大小固定,该方法始终返回 false
。
1
std::array<int, 5> arr13;
2
bool isEmpty = arr13.empty(); // 始终返回 false
⚝ fill(value)
: 将所有元素设置为指定的值。
1
std::array<int, 5> arr14 = {1, 2, 3, 4, 5};
2
arr14.fill(0); // 将所有元素设置为 0
⚝ front()
: 返回第一个元素的引用。
1
std::array<int, 5> arr15 = {1, 2, 3, 4, 5};
2
int& firstElementRef = arr15.front(); // 返回第一个元素的引用
⚝ back()
: 返回最后一个元素的引用。
1
std::array<int, 5> arr16 = {1, 2, 3, 4, 5};
2
int& lastElementRef = arr16.back(); // 返回最后一个元素的引用
⚝ data()
: 返回指向数组首元素的指针 (Raw Pointer)。可以用于与接受 C 风格数组的函数互操作。
1
std::array<int, 5> arr17 = {1, 2, 3, 4, 5};
2
int* rawPointer = arr17.data(); // 获取指向首元素的裸指针
3.3.2 std::array
的优势 (Advantages of std::array
)
std::array
相对于 C 风格数组,具有以下主要优势:
① 类型安全 (Type Safety)
std::array
是一个模板类,其元素类型和大小都是类型信息的一部分。这提供了更强的类型安全,编译器可以在编译时进行更多类型检查,减少运行时错误。
1
std::array<int, 5> intArray;
2
std::array<double, 5> doubleArray;
3
4
// 编译时错误:类型不匹配
5
// intArray = doubleArray;
② 边界检查 (Bounds Checking)
通过 at()
方法,std::array
提供了可选的边界检查。可以避免 C 风格数组常见的数组越界访问问题,提高程序的安全性。
1
std::array<int, 5> arr18;
2
// arr18[5] = 10; // 越界访问,未定义行为,可能不会报错
3
4
// arr18.at(5) = 10; // 越界访问,抛出 std::out_of_range 异常,更容易发现错误
③ 与标准库算法兼容 (Compatibility with Standard Library Algorithms)
std::array
提供了迭代器 (begin(), end()),可以方便地与 C++ 标准库的各种算法 (如 std::sort
, std::find
, std::copy
等) 配合使用。
1
#include <algorithm> // 引入 algorithm 头文件,for std::sort
2
3
std::array<int, 5> arr19 = {5, 2, 1, 4, 3};
4
std::sort(arr19.begin(), arr19.end()); // 使用 std::sort 排序 std::array
④ 封装性 (Encapsulation)
std::array
是一个类,封装了数组的数据和操作,提供了更好的封装性。例如,通过 size()
方法可以方便地获取数组大小,而无需像 C 风格数组那样需要手动传递数组大小。
⑤ 避免数组退化 (No Array Decay)
在 C++ 中,C 风格数组作为函数参数传递时会退化为指针,丢失数组大小信息。而 std::array
作为函数参数传递时,不会发生退化,数组大小信息仍然保留。
1
#include <iostream>
2
3
void processCStyleArray(int arr[]) { // C 风格数组退化为指针
4
// 无法在函数内部获取数组大小
5
// size_t size = sizeof(arr) / sizeof(arr[0]); // 错误,sizeof(arr) 只是指针的大小
6
std::cout << "处理 C 风格数组" << std::endl;
7
}
8
9
void processStdArray(std::array<int, 5> arr) { // std::array 不会退化
10
size_t size = arr.size(); // 可以获取数组大小
11
std::cout << "处理 std::array,大小为: " << size << std::endl;
12
}
13
14
int main() {
15
int cArr[5] = {1, 2, 3, 4, 5};
16
std::array<int, 5> stdArr = {1, 2, 3, 4, 5};
17
18
processCStyleArray(cArr); // 数组退化为指针
19
processStdArray(stdArr); // 数组大小信息保留
20
return 0;
21
}
3.3.3 何时使用 std::array
(When to Use std::array
)
std::array
适用于以下场景:
⚝ 固定大小数组 (Fixed-size Arrays):当数组的大小在编译时已知且不会改变时,std::array
是一个理想的选择。例如,存储固定数量的元素,如矩阵、固定长度的缓冲区等。
⚝ 需要类型安全和边界检查的场景 (Scenarios Requiring Type Safety and Bounds Checking):std::array
提供了类型安全和可选的边界检查,可以提高代码的健壮性和安全性。
⚝ 需要与标准库算法配合使用的场景 (Scenarios Requiring Compatibility with Standard Library Algorithms):std::array
提供了迭代器,可以方便地与 C++ 标准库的算法配合使用,提高代码的效率和可读性。
⚝ 替代 C 风格数组 (Replacing C-style Arrays):在大多数情况下,std::array
可以作为 C 风格数组的更安全、更现代的替代品。
不适用场景 (When not to use std::array
):
⚝ 动态大小数组 (Dynamic-size Arrays):std::array
的大小在编译时固定,无法动态改变。如果需要运行时动态调整大小的数组,应使用 std::vector
。
⚝ 性能敏感且不需要边界检查的底层代码 (Performance-sensitive low-level code where bounds checking is not needed):虽然 at()
方法提供了边界检查,但会带来一定的性能开销。如果对性能要求非常高,且确信不会发生越界访问,可以使用 []
运算符或直接使用 C 风格数组以获得更高的性能。但在大多数应用场景下,std::array
的性能损失可以忽略不计,安全性优势更为重要。
总而言之,std::array
是 C++ 中处理固定大小数组的首选容器。它结合了 C 风格数组的效率和 C++ 类的类型安全、封装性等优点,是现代 C++ 编程中值得推荐使用的数组类型。对于动态大小的数组,则应选择 std::vector
。
4. 数组的最佳实践与常见错误 (Best Practices and Common Mistakes with Arrays)
4.1 数组的最佳实践 (Best Practices for Arrays)
4.1.1 清晰的代码风格 (Clear Code Style)
本节旨在强调在 C++ 数组 (Array) 编程中采用清晰代码风格的重要性。良好的代码风格不仅能提高代码的可读性,还能减少错误,方便维护和团队协作。对于数组而言,清晰的代码风格尤为重要,因为数组操作常常涉及到索引 (Index)、循环 (Loop) 和内存管理 (Memory Management),任何疏忽都可能导致难以追踪的错误。
① 有意义的命名 (Meaningful Naming):
▮▮▮▮ⓑ 数组变量命名:使用能够清晰表达数组用途的名称。例如,存储学生分数的数组可以命名为 studentScores
而不是 scores
或更糟糕的 a
。对于多维数组 (Multi-dimensional Array),命名应能反映其维度含义,例如 pixelMatrix
表示像素矩阵,dailyTemperaturesOfYear
表示一年中每日温度的数组。
1
// Bad example
2
int a[10];
3
for (int i = 0; i < 10; ++i) {
4
a[i] = i * 2;
5
}
6
7
// Good example
8
int studentScores[100];
9
for (int i = 0; i < 100; ++i) {
10
studentScores[i] = 0; // Initialize scores to 0
11
}
▮▮▮▮ⓑ 循环计数器命名:在使用循环遍历数组时,循环计数器 (Loop Counter) 的命名也应具有描述性。常见的做法是使用 i
, j
, k
等,但当循环嵌套较深或逻辑复杂时,使用更具意义的名称(如 rowIndex
, colIndex
)可以提高代码可读性。
1
// Bad example
2
for (int i = 0; i < rows; ++i) {
3
for (int j = 0; j < cols; ++j) {
4
matrix[i][j] = i + j;
5
}
6
}
7
8
// Good example
9
for (int rowIndex = 0; rowIndex < numRows; ++rowIndex) {
10
for (int colIndex = 0; colIndex < numCols; ++colIndex) {
11
pixelMatrix[rowIndex][colIndex] = 0; // Initialize pixel to black
12
}
13
}
② 恰当的注释 (Appropriate Comments):
▮▮▮▮ⓑ 解释数组的用途:在声明数组时,添加注释说明数组的用途和存储的数据类型。特别是对于全局数组 (Global Array) 或在复杂函数中使用的数组,注释能够帮助其他开发者快速理解代码意图。
1
// 存储学生考试成绩的数组,最多存储 100 个学生的分数
2
int studentScores[100];
3
4
// 函数:计算数组中所有元素的平均值
5
double calculateAverage(const int arr[], int size) {
6
// ... function body ...
7
}
▮▮▮▮ⓑ 解释复杂的数组操作:对于涉及复杂索引计算或算法的数组操作,添加注释解释代码的逻辑和步骤。例如,在实现排序算法 (Sorting Algorithm) 或搜索算法 (Searching Algorithm) 时,注释可以帮助理解算法的实现细节。
1
// 冒泡排序算法实现
2
void bubbleSort(int arr[], int size) {
3
for (int i = 0; i < size - 1; ++i) {
4
for (int j = 0; j < size - i - 1; ++j) {
5
// 如果前一个元素大于后一个元素,则交换它们
6
if (arr[j] > arr[j + 1]) {
7
std::swap(arr[j], arr[j + 1]);
8
}
9
}
10
}
11
}
③ 一致的缩进和格式 (Consistent Indentation and Formatting):
▮▮▮▮ⓑ 循环和条件语句的缩进:对于涉及数组的循环和条件语句,保持一致的缩进风格,使代码结构清晰可见。通常使用 4 个空格或一个制表符 (Tab) 进行缩进。
1
// 良好的缩进风格
2
for (int i = 0; i < size; ++i) {
3
if (arr[i] > 0) {
4
// 处理正数元素
5
processPositiveElement(arr[i]);
6
} else {
7
// 处理非正数元素
8
processNonPositiveElement(arr[i]);
9
}
10
}
11
12
// 不良的缩进风格 (难以阅读)
13
for (int i = 0; i < size; ++i) {
14
if (arr[i] > 0) {
15
// 处理正数元素
16
processPositiveElement(arr[i]);
17
} else {
18
// 处理非正数元素
19
processNonPositiveElement(arr[i]);
20
}
21
}
▮▮▮▮ⓑ 数组初始化格式:对于数组的初始化 (Initialization),采用清晰的格式,特别是列表初始化 (List Initialization) 时,使其易于阅读和理解。
1
// 清晰的列表初始化
2
int numbers[] = {1, 2, 3, 4, 5};
3
int matrix[3][3] = {
4
{1, 0, 0},
5
{0, 1, 0},
6
{0, 0, 1}
7
};
8
9
// 不清晰的列表初始化 (当元素较多时难以阅读)
10
int numbers[] = {1,2,3,4,5};
11
int matrix[3][3] = {{1,0,0},{0,1,0},{0,0,1}};
通过遵循以上最佳实践,可以显著提高数组相关代码的质量和可维护性,减少错误发生的可能性,并提升团队协作效率。清晰的代码风格是专业编程的重要组成部分,尤其在处理复杂数据结构如数组时,更显得至关重要。
4.1.2 合理的内存管理 (Proper Memory Management)
在 C++ 中,尤其是在使用动态数组 (Dynamic Array) 时,合理的内存管理 (Memory Management) 至关重要。不当的内存管理可能导致内存泄漏 (Memory Leak) 和悬 dangling 指针 (Dangling Pointer) 等严重问题,进而影响程序的稳定性甚至安全性。本节将重点讨论动态数组的内存管理最佳实践。
① 动态数组的分配与释放 (Allocation and Deallocation of Dynamic Arrays):
▮▮▮▮ⓑ 使用 new
和 delete[]
配对:对于通过 new
运算符 (operator) 分配的动态数组,必须使用 delete[]
运算符进行释放。new
用于分配内存,而 delete[]
用于释放为数组分配的内存。两者必须配对使用,否则会导致内存泄漏。
1
// 动态分配 int 数组
2
int* dynamicArray = new int[100];
3
4
// ... 使用数组 ...
5
6
// 释放动态数组内存
7
delete[] dynamicArray;
8
dynamicArray = nullptr; // 避免悬 dangling 指针
注意:如果使用 new
分配单个对象,则使用 delete
释放;如果使用 new[]
分配数组,则必须使用 delete[]
释放。混用 delete
和 delete[]
会导致未定义行为 (Undefined Behavior)。
▮▮▮▮ⓑ 多维动态数组的释放:对于多维动态数组,例如通过 new
分配的二维数组,释放时需要逐层释放。以二维数组为例,先释放每一行的内存,然后再释放存储行指针的内存。
1
int** dynamic2DArray = new int*[rows];
2
for (int i = 0; i < rows; ++i) {
3
dynamic2DArray[i] = new int[cols];
4
}
5
6
// ... 使用二维数组 ...
7
8
// 释放二维数组内存
9
for (int i = 0; i < rows; ++i) {
10
delete[] dynamic2DArray[i]; // 释放每一行的内存
11
}
12
delete[] dynamic2DArray; // 释放存储行指针的内存
13
dynamic2DArray = nullptr;
② 避免内存泄漏 (Avoiding Memory Leaks):
▮▮▮▮ⓑ 及时释放不再使用的内存:当动态分配的数组不再需要使用时,应立即使用 delete[]
释放其占用的内存。避免在循环 (Loop)、函数 (Function) 或程序的不同部分多次分配内存而忘记释放,导致内存泄漏。
1
void processData() {
2
int* dataArray = new int[10000];
3
// ... 使用 dataArray ...
4
delete[] dataArray; // 及时释放内存
5
dataArray = nullptr;
6
} // 函数结束时,dataArray 占用的内存已被释放
▮▮▮▮ⓑ 资源获取即初始化 (RAII):RAII (Resource Acquisition Is Initialization) 是一种 C++ 编程技术,它将资源的生命周期与对象的生命周期绑定。虽然 C 风格数组 (C-style Array) 本身不直接支持 RAII,但可以使用标准库容器,如 std::vector
和 std::array
,它们实现了 RAII,可以自动管理内存,减少内存泄漏的风险。
1
#include <vector>
2
3
void processDataWithVector() {
4
std::vector<int> dataVector(10000); // 使用 vector,自动管理内存
5
// ... 使用 dataVector ...
6
} // 函数结束时,dataVector 的内存自动释放,无需手动 delete
③ 避免悬 dangling 指针 (Avoiding Dangling Pointers):
▮▮▮▮ⓑ 释放内存后将指针置为 nullptr
:在使用 delete[]
释放动态数组的内存后,应立即将指向该内存的指针设置为 nullptr
。这可以避免悬 dangling 指针,即指针仍然指向已被释放的内存,访问悬 dangling 指针会导致未定义行为。
1
int* dynamicArray = new int[10];
2
// ...
3
delete[] dynamicArray;
4
dynamicArray = nullptr; // 将指针置为 nullptr,避免悬 dangling 指针
5
6
if (dynamicArray != nullptr) {
7
// 安全检查,避免访问空指针
8
// ...
9
}
▮▮▮▮ⓑ 注意指针的作用域 (Scope of Pointers):确保指针的作用域与其指向的内存的生命周期相匹配。避免返回指向局部动态数组的指针,因为当局部作用域结束时,指针仍然有效,但其指向的内存可能已被释放。
1
int* createArray() {
2
int* localArray = new int[5];
3
// ...
4
return localArray; // 返回局部动态数组的指针 (潜在问题)
5
} // 函数结束,但 localArray 指向的内存仍然存在,需要调用者负责释放
6
7
void example() {
8
int* myArrayPtr = createArray();
9
// ... 使用 myArrayPtr ...
10
delete[] myArrayPtr; // 调用者需要负责释放内存
11
myArrayPtr = nullptr;
12
}
合理的内存管理是编写健壮 C++ 程序的关键。对于动态数组,务必遵循 new
和 delete[]
配对使用、及时释放内存、避免内存泄漏和悬 dangling 指针等最佳实践。在现代 C++ 编程中,推荐优先使用如 std::vector
和 std::array
等标准库容器来管理内存,以减少手动内存管理的复杂性和风险。
4.1.3 性能优化建议 (Performance Optimization Tips)
数组 (Array) 作为一种基本的数据结构 (Data Structure),在追求程序性能 (Performance) 的场景中经常被使用。然而,不恰当的数组操作也可能成为性能瓶颈 (Performance Bottleneck)。本节将提供一些关于数组性能优化的建议,帮助开发者编写更高效的数组相关代码。
① 减少不必要的拷贝 (Minimize Unnecessary Copies):
▮▮▮▮ⓑ 避免值传递 (Pass-by-Value) 大型数组:当数组作为函数参数传递时,如果使用值传递,会发生数组的完整拷贝,这对于大型数组来说会消耗大量时间和内存。应尽量使用引用传递 (Pass-by-Reference) 或指针传递 (Pass-by-Pointer) 来避免不必要的拷贝。
1
// Bad example: 值传递,会拷贝整个数组
2
void processArrayByValue(int arr[1000]) {
3
// ...
4
}
5
6
// Good example: 引用传递,避免拷贝
7
void processArrayByReference(int (&arr)[1000]) {
8
// ...
9
}
10
11
// Good example: 指针传递,避免拷贝
12
void processArrayByPointer(int* arr, int size) {
13
// ...
14
}
15
16
int main() {
17
int largeArray[1000];
18
processArrayByValue(largeArray); // 性能较差,拷贝数组
19
processArrayByReference(largeArray); // 性能较好,引用传递
20
processArrayByPointer(largeArray, 1000); // 性能较好,指针传递
21
return 0;
22
}
▮▮▮▮ⓑ 使用 const
引用避免修改时的拷贝:如果函数只需要读取数组内容,而不需要修改数组,可以使用 const
引用 (Const Reference) 参数,这样既避免了拷贝,又保证了数组内容不会被意外修改。
1
// 使用 const 引用,避免拷贝,同时保证数组内容不被修改
2
double calculateSum(const int (&arr)[1000]) {
3
double sum = 0;
4
for (int i = 0; i < 1000; ++i) {
5
sum += arr[i];
6
}
7
return sum;
8
}
② 利用缓存 (Cache) 局部性原理 (Locality of Reference):
▮▮▮▮ⓑ 顺序访问数组元素:数组在内存中是连续存储的,顺序访问数组元素 (例如,通过循环按索引递增的顺序访问) 可以充分利用 CPU 缓存 (CPU Cache) 的局部性原理。顺序访问可以提高缓存命中率 (Cache Hit Rate),减少从主内存 (Main Memory) 读取数据的次数,从而提高性能。
1
// 顺序访问数组,利用缓存局部性
2
for (int i = 0; i < size; ++i) {
3
processElement(arr[i]); // 顺序访问 arr[0], arr[1], arr[2], ...
4
}
▮▮▮▮ⓑ 多维数组的访问顺序:对于多维数组,访问顺序也会影响性能。在 C++ 中,多维数组按行优先 (Row-major) 顺序存储。这意味着在二维数组 matrix[rows][cols]
中,matrix[0][0]
, matrix[0][1]
, ..., matrix[0][cols-1]
, matrix[1][0]
, ... 这样的顺序访问在内存中是连续的,可以更好地利用缓存。
1
// 二维数组按行优先顺序访问,利用缓存局部性
2
for (int i = 0; i < rows; ++i) {
3
for (int j = 0; j < cols; ++j) {
4
processElement(matrix[i][j]); // 先遍历完一行,再遍历下一行
5
}
6
}
7
8
// 避免列优先顺序访问 (如果可能),可能导致缓存效率降低
9
// for (int j = 0; j < cols; ++j) {
10
// for (int i = 0; i < rows; ++i) {
11
// processElement(matrix[i][j]); // 列优先访问,可能导致缓存效率降低
12
// }
13
// }
③ 避免在循环中重复计算数组长度:
▮▮▮▮ⓑ 预先计算数组长度:在循环遍历数组时,如果数组长度在循环过程中不会改变,应在循环外部预先计算好数组长度,避免在循环条件中重复计算,这可以节省 CPU 时间。
1
// Bad example: 在循环条件中重复计算数组长度 (如果 size() 是一个耗时操作)
2
for (int i = 0; i < getArraySize(); ++i) {
3
// ...
4
}
5
6
// Good example: 预先计算数组长度
7
int arraySize = getArraySize();
8
for (int i = 0; i < arraySize; ++i) {
9
// ...
10
}
注意:对于 C 风格数组,其大小在声明时固定,获取大小通常是直接访问已存储的值,开销很小。但对于某些自定义的数组类或动态获取数组大小的情况,预先计算长度可能带来性能提升。对于 std::array
和 std::vector
,.size()
方法通常是常数时间复杂度 \(O(1)\) 的操作,重复调用开销很小,但在极端性能敏感的循环中,预先计算仍然是一个好的习惯。
④ 选择合适的数据结构 (Choose Appropriate Data Structures):
▮▮▮▮ⓑ 根据需求选择数据结构:虽然数组在许多场景下高效,但并非所有问题都适合使用数组。例如,如果需要频繁地在序列中间插入或删除元素,链表 (Linked List) 或 std::list
可能更高效。如果需要快速查找元素,哈希表 (Hash Table) 或 std::unordered_map
可能更适合。在性能优化的早期阶段,应评估是否数组是解决问题的最佳数据结构。
1
// 示例:如果需要频繁插入和删除元素,std::list 可能比数组更高效
2
#include <list>
3
#include <vector>
4
5
void testListInsertion() {
6
std::list<int> myList;
7
for (int i = 0; i < 10000; ++i) {
8
myList.insert(std::next(myList.begin(), myList.size() / 2), i); // 中间插入
9
}
10
}
11
12
void testVectorInsertion() {
13
std::vector<int> myVector;
14
for (int i = 0; i < 10000; ++i) {
15
myVector.insert(myVector.begin() + myVector.size() / 2, i); // 中间插入 (效率较低)
16
}
17
}
18
19
int main() {
20
// 性能测试表明,std::list 在中间插入操作上比 std::vector 更高效
21
return 0;
22
}
通过采纳以上性能优化建议,开发者可以编写出更高效的数组相关代码,提升程序的整体性能。性能优化是一个持续的过程,需要根据具体的应用场景和性能瓶颈进行分析和调整。
4.2 数组的常见错误与陷阱 (Common Mistakes and Pitfalls with Arrays)
4.2.1 数组越界访问 (Array Out-of-Bounds Access)
数组越界访问 (Array Out-of-Bounds Access) 是 C++ 数组编程中最常见且最危险的错误之一。它指的是程序试图访问数组边界之外的内存位置。由于 C++ 不会自动进行数组边界检查 (Bounds Checking) (对于 C 风格数组和 std::array
的默认访问方式),越界访问通常不会在编译时 (Compile Time) 或运行时 (Runtime) 立即报错,而是可能导致程序行为未定义 (Undefined Behavior),产生难以预测的后果。
① 越界访问的危害 (Dangers of Out-of-Bounds Access):
▮▮▮▮ⓑ 数据损坏 (Data Corruption):越界写入 (Out-of-Bounds Write) 可能会覆盖数组相邻内存区域的数据,导致程序中其他变量或数据结构的值被意外修改,引发逻辑错误或程序崩溃 (Crash)。
▮▮▮▮ⓑ 程序崩溃 (Program Crash):越界读写 (Out-of-Bounds Read/Write) 可能访问到程序没有权限访问的内存区域,导致操作系统 (Operating System) 终止程序运行,发生段错误 (Segmentation Fault) 或其他类型的运行时错误。
▮▮▮▮ⓒ 安全漏洞 (Security Vulnerabilities):在某些情况下,恶意用户可以利用数组越界漏洞进行缓冲区溢出 (Buffer Overflow) 攻击,执行恶意代码或获取系统控制权,造成严重的安全风险。
② 常见的越界访问场景 (Common Scenarios of Out-of-Bounds Access):
▮▮▮▮ⓑ 循环索引错误:循环条件设置错误,导致循环索引 (Loop Index) 超出数组边界。例如,循环条件使用 <=
而不是 <
,或者循环次数计算错误。
1
int arr[5];
2
for (int i = 0; i <= 5; ++i) { // 错误:循环条件应为 i < 5,此处会访问 arr[5],越界
3
arr[i] = i;
4
}
▮▮▮▮ⓑ 索引计算错误:在计算数组索引时出现逻辑错误,导致索引值超出有效范围。例如,使用错误的公式或变量进行索引计算。
1
int arr[10];
2
int index = calculateIndex(); // 假设 calculateIndex() 返回的值可能超出 [0, 9] 范围
3
arr[index] = 10; // 如果 index < 0 或 index >= 10,则越界
▮▮▮▮ⓒ 多维数组索引错误:在访问多维数组 (Multi-dimensional Array) 时,维度索引使用错误,导致访问到非法的内存位置。例如,行列索引混淆或索引值超出维度范围。
1
int matrix[3][4]; // 3 行 4 列
2
matrix[4][2] = 1; // 错误:行索引 4 越界,有效行索引为 0, 1, 2
3
matrix[2][5] = 2; // 错误:列索引 5 越界,有效列索引为 0, 1, 2, 3
③ 避免数组越界访问的方法 (Methods to Avoid Out-of-Bounds Access):
▮▮▮▮ⓑ 仔细检查循环条件和索引计算:编写数组相关代码时,务必仔细检查循环条件、索引计算公式和变量,确保索引值始终在数组的有效范围内。
▮▮▮▮ⓑ 使用边界检查 (Bounds Checking):在开发和调试阶段,可以使用断言 (Assertion) 或条件判断等方法,显式地进行数组边界检查。这可以在越界访问发生时及时发现错误。
1
#include <cassert>
2
3
int arr[5];
4
for (int i = 0; i < 5; ++i) {
5
assert(i >= 0 && i < 5); // 添加边界检查断言
6
arr[i] = i;
7
}
8
9
int getArrayElement(int arr[], int size, int index) {
10
if (index < 0 || index >= size) {
11
// 处理越界情况,例如抛出异常或返回错误值
12
return -1; // 返回错误值
13
}
14
return arr[index];
15
}
▮▮▮▮ⓒ 使用 std::array::at()
进行安全访问:对于 std::array
容器,可以使用 .at()
方法进行元素访问。.at()
方法会进行边界检查,如果索引越界,会抛出 std::out_of_range
异常,可以捕获并处理异常,提高程序的健壮性。但 注意:.at()
方法的边界检查会带来一定的性能开销,在性能敏感的代码中应权衡使用。
1
#include <array>
2
#include <stdexcept>
3
#include <iostream>
4
5
int main() {
6
std::array<int, 5> myStdArray = {1, 2, 3, 4, 5};
7
try {
8
int element = myStdArray.at(10); // 尝试越界访问,会抛出 std::out_of_range 异常
9
std::cout << element << std::endl;
10
} catch (const std::out_of_range& e) {
11
std::cerr << "Error: Array index out of range: " << e.what() << std::endl;
12
}
13
return 0;
14
}
▮▮▮▮ⓓ 使用静态分析工具和代码审查 (Static Analysis Tools and Code Review):使用静态分析工具可以帮助在编译时检测潜在的数组越界访问错误。代码审查 (Code Review) 也是一种有效的方法,通过多人 review 代码,可以发现代码中潜在的越界访问风险。
数组越界访问是一个需要高度重视的问题。开发者应养成良好的编程习惯,时刻注意数组边界,采用合适的检查方法,以最大程度地避免这类错误,提高程序的可靠性和安全性。
4.2.2 数组未初始化 (Uninitialized Arrays)
数组未初始化 (Uninitialized Arrays) 是另一个常见的 C++ 数组编程错误。当声明一个数组但没有显式地为其赋初值时,数组中的元素将包含垃圾值 (Garbage Values)。这些垃圾值是内存中遗留下来的随机数据,其值是不确定的。使用未初始化的数组可能导致程序行为不可预测,产生逻辑错误或运行时错误。
① 未初始化数组的危害 (Dangers of Uninitialized Arrays):
▮▮▮▮ⓑ 不可预测的结果 (Unpredictable Results):未初始化数组中的元素值是不确定的,每次程序运行,这些值都可能不同。因此,依赖于未初始化数组的程序可能会产生不可预测的结果,使程序行为难以理解和调试 (Debugging)。
▮▮▮▮ⓑ 逻辑错误 (Logical Errors):程序逻辑可能基于数组元素的值进行判断或计算。如果数组未初始化,使用其中的垃圾值进行判断或计算,可能导致程序逻辑错误,产生错误的输出或行为。
▮▮▮▮ⓒ 运行时错误 (Runtime Errors):在某些情况下,未初始化的数组可能包含一些特殊的值,例如非常大的数或非常小的数,这些值在后续的运算中可能导致溢出 (Overflow)、下溢 (Underflow) 或其他运行时错误。
② 常见的未初始化数组场景 (Common Scenarios of Uninitialized Arrays):
▮▮▮▮ⓑ 局部数组未显式初始化:在函数 (Function) 内部声明的局部数组,如果没有显式地初始化,则默认为未初始化状态。
1
void processArray() {
2
int localArray[10]; // 局部数组,未初始化
3
for (int i = 0; i < 10; ++i) {
4
std::cout << localArray[i] << " "; // 访问未初始化的数组元素,输出垃圾值
5
}
6
std::cout << std::endl;
7
}
▮▮▮▮ⓑ 动态分配的数组未初始化:使用 new
运算符动态分配的数组,默认情况下也是未初始化的。
1
int* dynamicArray = new int[10]; // 动态数组,未初始化
2
for (int i = 0; i < 10; ++i) {
3
std::cout << dynamicArray[i] << " "; // 访问未初始化的数组元素,输出垃圾值
4
}
5
std::cout << std::endl;
6
delete[] dynamicArray;
7
dynamicArray = nullptr;
注意:使用 new int[10]()
可以进行值初始化 (Value Initialization),将数组元素初始化为 0。
1
int* valueInitializedArray = new int[10](); // 动态数组,值初始化为 0
2
for (int i = 0; i < 10; ++i) {
3
std::cout << valueInitializedArray[i] << " "; // 输出 0 0 0 0 0 0 0 0 0 0
4
}
5
std::cout << std::endl;
6
delete[] valueInitializedArray;
7
valueInitializedArray = nullptr;
▮▮▮▮ⓒ 结构体或类中的数组成员未初始化:如果结构体 (Struct) 或类 (Class) 中包含数组类型的成员,且在构造对象时没有显式地初始化这些数组成员,则它们也是未初始化的。
1
struct Data {
2
int values[5]; // 数组成员,未初始化
3
Data() {
4
// 构造函数没有初始化 values 数组
5
}
6
};
7
8
int main() {
9
Data myData; // 创建 Data 对象,values 数组未初始化
10
for (int i = 0; i < 5; ++i) {
11
std::cout << myData.values[i] << " "; // 访问未初始化的数组元素,输出垃圾值
12
}
13
std::cout << std::endl;
14
return 0;
15
}
③ 避免数组未初始化的方法 (Methods to Avoid Uninitialized Arrays):
▮▮▮▮ⓑ 显式初始化数组:在声明数组时,始终显式地为其赋初值。可以使用列表初始化 (List Initialization)、循环初始化 (Loop Initialization) 或值初始化等方法。
1
// 列表初始化
2
int initializedArray1[5] = {0, 0, 0, 0, 0}; // 初始化为 0
3
int initializedArray2[5] = {1, 2, 3}; // 初始化前 3 个元素,剩余元素值初始化为 0
4
int initializedArray3[] = {1, 2, 3, 4, 5}; // 自动推导数组大小并初始化
5
6
// 循环初始化
7
int initializedArray4[10];
8
for (int i = 0; i < 10; ++i) {
9
initializedArray4[i] = 0; // 循环初始化为 0
10
}
11
12
// 值初始化 (动态数组)
13
int* initializedDynamicArray = new int[10](); // 值初始化为 0
▮▮▮▮ⓑ 使用初始化列表 (Initializer List) 初始化结构体/类数组成员:对于结构体或类中的数组成员,在构造函数 (Constructor) 中使用初始化列表进行初始化。
1
struct Data {
2
int values[5];
3
Data() : values{0, 0, 0, 0, 0} { // 使用初始化列表初始化数组成员
4
}
5
};
6
7
int main() {
8
Data myData; // 创建 Data 对象,values 数组已初始化为 0
9
for (int i = 0; i < 5; ++i) {
10
std::cout << myData.values[i] << " "; // 输出 0 0 0 0 0
11
}
12
std::cout << std::endl;
13
return 0;
14
}
▮▮▮▮ⓒ 使用 std::array
或 std::vector
:std::array
和 std::vector
在创建时会自动进行值初始化 (对于数值类型,初始化为 0)。使用这些标准库容器可以减少未初始化数组错误的发生。
1
#include <array>
2
#include <vector>
3
#include <iostream>
4
5
int main() {
6
std::array<int, 5> stdArray; // std::array 会进行值初始化
7
std::vector<int> stdVector(5); // std::vector 会进行值初始化
8
for (int i = 0; i < 5; ++i) {
9
std::cout << stdArray[i] << " "; // 输出 0 0 0 0 0
10
}
11
std::cout << std::endl;
12
for (int i = 0; i < 5; ++i) {
13
std::cout << stdVector[i] << " "; // 输出 0 0 0 0 0
14
}
15
std::cout << std::endl;
16
return 0;
17
}
始终牢记显式初始化数组的重要性。良好的初始化习惯可以避免许多潜在的错误,提高程序的可靠性和可维护性。
4.2.3 指针与数组的混淆 (Confusion between Pointers and Arrays)
在 C++ 中,指针 (Pointer) 和数组 (Array) 之间存在密切的关系,但也容易引起混淆。理解指针和数组的区别与联系,避免在使用指针操作数组时产生错误,是 C++ 数组编程中的一个重要方面。
① 数组名与指针 (Array Name and Pointer):
▮▮▮▮ⓑ 数组名退化为指针 (Array Name Decay to Pointer):在大多数情况下,数组名 (Array Name) 会被隐式地转换为指向数组首元素 (First Element) 的指针 (Pointer)。例如,当数组名作为函数参数传递、赋值给指针变量或进行指针运算时,会发生数组名退化为指针的现象。
1
int arr[5] = {1, 2, 3, 4, 5};
2
int* ptr = arr; // 数组名 arr 退化为指向 arr[0] 的指针
3
std::cout << *ptr << std::endl; // 输出 1 (arr[0])
4
std::cout << ptr == &arr[0] << std::endl; // 输出 1 (true)
5
6
void processArray(int* arrayPtr) { // 函数参数为指针
7
std::cout << *arrayPtr << std::endl; // 输出数组首元素
8
}
9
10
processArray(arr); // 数组名 arr 退化为指针传递给函数
▮▮▮▮ⓑ 数组名是指针常量 (Array Name as Pointer Constant):虽然数组名在很多情况下表现得像指针,但数组名本质上是一个指针常量 (Pointer Constant),它存储的是数组首元素的地址,并且这个地址在数组的生命周期内是不可改变的。因此,不能对数组名进行赋值操作,例如 arr++
或 arr = anotherArray
是非法的。
1
int arr1[5];
2
int arr2[5];
3
// arr1 = arr2; // 错误:数组名是指针常量,不能被赋值
4
// arr1++; // 错误:数组名是指针常量,不能进行自增运算
5
6
int* ptr = arr1;
7
ptr = arr2; // 正确:指针变量可以被赋值
8
ptr++; // 正确:指针变量可以进行自增运算
② 指针运算与数组索引 (Pointer Arithmetic and Array Indexing):
▮▮▮▮ⓑ 指针运算访问数组元素:可以使用指针运算 (Pointer Arithmetic) 来访问数组中的元素。对于指向数组元素的指针 ptr
,ptr + i
表示指向数组中索引为 i
的元素的地址,*(ptr + i)
表示访问该地址所存储的值,等价于数组索引 arr[i]
(假设 ptr
指向 arr[0]
)。
1
int arr[5] = {10, 20, 30, 40, 50};
2
int* ptr = arr; // ptr 指向 arr[0]
3
4
std::cout << *(ptr + 0) << std::endl; // 输出 10 (arr[0])
5
std::cout << *(ptr + 1) << std::endl; // 输出 20 (arr[1])
6
std::cout << *(ptr + 2) << std::endl; // 输出 30 (arr[2])
7
8
for (int i = 0; i < 5; ++i) {
9
std::cout << *(ptr + i) << " "; // 使用指针运算遍历数组
10
}
11
std::cout << std::endl;
▮▮▮▮ⓑ 数组索引本质是指针运算:实际上,数组索引操作 arr[i]
在底层就是通过指针运算实现的。arr[i]
等价于 *(arr + i)
,编译器 (Compiler) 会将数组索引操作转换为指针运算。
1
int arr[5] = {10, 20, 30, 40, 50};
2
std::cout << arr[2] << std::endl; // 数组索引访问
3
std::cout << *(arr + 2) << std::endl; // 指针运算访问,结果相同
③ 指针与数组作为函数参数 (Pointers and Arrays as Function Parameters):
▮▮▮▮ⓑ 数组作为函数参数退化为指针:当数组作为函数参数传递时,会发生数组名退化为指针的现象。因此,在函数内部,数组参数实际上是指针,而不是真正的数组。这意味着在函数内部无法直接获取数组的大小 (对于 C 风格数组)。
1
void printArray(int arr[]) { // 数组作为函数参数,退化为指针 int* arr
2
// sizeof(arr) 在函数内部是指针的大小,而不是数组的大小
3
std::cout << "Size of arr in function: " << sizeof(arr) << std::endl; // 通常输出指针的大小 (例如 4 或 8 字节)
4
for (int i = 0; i < 5; ++i) { // 错误:此处假设数组大小为 5,但函数无法知道实际大小
5
std::cout << arr[i] << " "; // 可能越界访问,如果实际数组大小小于 5
6
}
7
std::cout << std::endl;
8
}
9
10
int main() {
11
int myArray[3] = {1, 2, 3};
12
std::cout << "Size of myArray in main: " << sizeof(myArray) << std::endl; // 输出数组的实际大小 (例如 12 字节,如果 int 为 4 字节)
13
printArray(myArray); // 数组名退化为指针传递
14
return 0;
15
}
▮▮▮▮ⓑ 显式传递数组大小:由于数组作为函数参数会退化为指针,为了在函数内部正确处理数组,通常需要显式地将数组的大小作为额外的参数传递给函数。
1
void printArrayWithSize(int arr[], int size) { // 显式传递数组大小
2
std::cout << "Size of array in function: " << size << std::endl; // 输出传递的数组大小
3
for (int i = 0; i < size; ++i) {
4
std::cout << arr[i] << " "; // 使用传递的大小作为循环边界,避免越界
5
}
6
std::cout << std::endl;
7
}
8
9
int main() {
10
int myArray[3] = {1, 2, 3};
11
printArrayWithSize(myArray, 3); // 显式传递数组大小 3
12
return 0;
13
}
理解指针和数组之间的关系,特别是数组名退化为指针的特性,以及指针运算和数组索引的联系,有助于避免在使用指针操作数组时产生混淆和错误。在函数参数传递数组时,务必注意数组大小退化的问题,并采取合适的措施 (例如显式传递大小) 来确保程序的正确性。在现代 C++ 编程中,推荐优先使用 std::vector
和 std::array
等容器,它们可以更好地管理数组的大小和内存,并提供更安全和方便的接口。
Appendix A: ASCII 码表 (ASCII Table)
Appendix A1: ASCII 码表概述 (Overview of ASCII Table)
ASCII (American Standard Code for Information Interchange) 码表,即美国信息交换标准代码表,是计算机发展史上一个里程碑式的字符编码标准。在早期的计算机系统中,为了实现不同设备和系统之间文本信息的可靠交换,迫切需要一套统一的字符编码方案。ASCII 码应运而生,并迅速成为全球通用的标准,为现代计算机信息处理奠定了基础。
ASCII 码使用 7 位 (bits) 二进制数来表示 128 个不同的字符,包括:
① 控制字符 (Control Characters):编码范围为 0-31 (十进制),主要用于控制计算机设备和通信,例如换行符 (newline character, LF)、回车符 (carriage return, CR)、制表符 (tab) 等。这些字符在文本格式化和数据传输中扮演着重要角色。虽然在现代文本处理中,部分控制字符的使用频率有所降低,但在特定的应用场景,如终端控制、网络协议等方面,仍然发挥着不可替代的作用。
② 可打印字符 (Printable Characters):编码范围为 32-127 (十进制),包括:
▮▮▮▮ⓐ 数字 (Digits):'0' 到 '9',用于表示数值。
▮▮▮▮ⓑ 大写字母 (Uppercase Letters):'A' 到 'Z',用于表示英文大写字母。
▮▮▮▮ⓒ 小写字母 (Lowercase Letters):'a' 到 'z',用于表示英文小写字母。
▮▮▮▮ⓓ 标点符号和特殊符号 (Punctuation Marks and Special Symbols):例如空格 (space)、感叹号 (!)、逗号 (,)、句号 (.)、分号 (;)、冒号 (:)、问号 (?)、引号 (")、单引号 (')、括号 ()、花括号 {}、方括号 []、加号 (+)、减号 (-)、乘号 (*)、除号 (/)、百分号 (%)、和号 (&)、竖线 (|)、反斜杠 (\)、波浪号 (~)、尖号 (^)、下划线 (_)、美元符号 ($)、井号 (#)、at 符号 (@)、小于号 (<)、大于号 (>)、等于号 (=) 等。这些符号用于构建文本、表达式和程序代码。
ASCII 码的设计简洁高效,覆盖了英语文本和基本控制需求,在计算机发展的早期阶段极大地促进了信息技术的普及和应用。尽管随着全球化的发展和多语言环境的需求,ASCII 码的局限性日益显现,例如无法表示非英文字符,但它作为字符编码的鼻祖,其影响深远。现代字符编码标准,如 Unicode (统一码),在设计时也借鉴了 ASCII 码的思想,并保持了对 ASCII 码的兼容性。理解 ASCII 码,有助于我们更好地理解字符编码的基本原理,以及计算机文本处理的底层机制。
Appendix A2: 常用 ASCII 字符 (Commonly Used ASCII Characters)
以下列出一些在编程和日常文本处理中经常遇到的 ASCII 字符及其十进制、十六进制编码和简要说明:
① 控制字符 (Control Characters)
▮▮▮▮ⓐ NUL (Null character):十进制 0,十六进制 0x00。表示空字符,在 C 风格字符串 (C-style string) 中用作字符串的结束符。
▮▮▮▮ⓑ LF (Line Feed):十进制 10,十六进制 0x0A。换行符,在文本中表示另起一行。在 Unix-like 系统中,常用于表示行尾。
▮▮▮▮ⓒ CR (Carriage Return):十进制 13,十六进制 0x0D。回车符,在早期的打印机中,表示将打印头移动到行首。在 Windows 系统中,CRLF
(回车换行) 一起用于表示行尾。
▮▮▮▮ⓓ TAB (Tab character):十进制 9,十六进制 0x09。制表符,用于在文本中产生水平制表效果,通常用于对齐文本。
▮▮▮▮ⓔ ESC (Escape character):十进制 27,十六进制 0x1B。转义字符,常用于控制序列的起始,例如在终端控制和某些文本格式中。
▮▮▮▮ⓕ DEL (Delete character):十进制 127,十六进制 0x7F。删除字符,用于删除文本中的字符。
② 空格 (Space):十进制 32,十六进制 0x20。用于分隔单词和字符,是文本中重要的组成部分。
③ 数字 (Digits)
▮▮▮▮ⓐ '0' 到 '9':十进制 48-57,十六进制 0x30-0x39。分别对应数字 0 到 9 的字符表示。注意数字字符的 ASCII 码值与其数值大小之间存在偏移,例如 '0' 的 ASCII 码是 48,而不是 0。
④ 大写字母 (Uppercase Letters)
▮▮▮▮ⓐ 'A' 到 'Z':十进制 65-90,十六进制 0x41-0x5A。分别对应大写字母 A 到 Z。
⑤ 小写字母 (Lowercase Letters)
▮▮▮▮ⓐ 'a' 到 'z':十进制 97-122,十六进制 0x61-0x7A。分别对应小写字母 a 到 z。注意大小写字母的 ASCII 码值之间存在固定的偏移量,这在字符的大小写转换中非常有用。
⑥ 常用标点符号和特殊符号
▮▮▮▮ⓐ ! (Exclamation mark):十进制 33,十六进制 0x21。
▮▮▮▮ⓑ " (Double quote):十进制 34,十六进制 0x22。
▮▮▮▮ⓒ # (Number sign):十进制 35,十六进制 0x23。
▮▮▮▮ⓓ $ (Dollar sign):十进制 36,十六进制 0x24。
▮▮▮▮ⓔ % (Percent sign):十进制 37,十六进制 0x25。
▮▮▮▮ⓕ & (Ampersand):十进制 38,十六进制 0x26。
▮▮▮▮ⓖ ' (Single quote / Apostrophe):十进制 39,十六进制 0x27。
▮▮▮▮ⓗ ( (Left parenthesis):十进制 40,十六进制 0x28。
▮▮▮▮ⓘ ) (Right parenthesis):十进制 41,十六进制 0x29。
▮▮▮▮ⓙ (Asterisk)*:十进制 42,十六进制 0x2A。
▮▮▮▮ⓚ + (Plus sign):十进制 43,十六进制 0x2B。
▮▮▮▮ⓛ , (Comma):十进制 44,十六进制 0x2C。
▮▮▮▮ⓜ - (Hyphen / Minus sign):十进制 45,十六进制 0x2D。
▮▮▮▮ⓝ . (Period / Dot):十进制 46,十六进制 0x2E。
▮▮▮▮ⓞ / (Slash):十进制 47,十六进制 0x2F。
▮▮▮▮ⓟ : (Colon):十进制 58,十六进制 0x3A。
▮▮▮▮ⓠ ; (Semicolon):十进制 59,十六进制 0x3B。
▮▮▮▮ⓡ < (Less-than sign):十进制 60,十六进制 0x3C。
▮▮▮▮ⓢ = (Equals sign):十进制 61,十六进制 0x3D。
▮▮▮▮ⓣ > (Greater-than sign):十进制 62,十六进制 0x3E。
▮▮▮▮ⓤ ? (Question mark):十进制 63,十六进制 0x3F。
▮▮▮▮ⓥ @ (At sign):十进制 64,十六进制 0x40。
▮▮▮▮ⓦ [ (Left square bracket):十进制 91,十六进制 0x5B。
▮▮▮▮ⓧ \ (Backslash):十进制 92,十六进制 0x5C。
▮▮▮▮ⓨ ] (Right square bracket):十进制 93,十六进制 0x5D。
▮▮▮▮ⓩ ^ (Caret):十进制 94,十六进制 0x5E。
▮▮▮▮{aa} _ (Underscore):十进制 95,十六进制 0x5F。
▮▮▮▮{bb} ` (Grave accent):十进制 96,十六进制 0x60。
▮▮▮▮{cc} { (Left curly brace):十进制 123,十六进制 0x7B。
▮▮▮▮{dd} | (Vertical bar):十进制 124,十六进制 0x7C。
▮▮▮▮{ee} } (Right curly brace):十进制 125,十六进制 0x7D。
▮▮▮▮{ff} ~ (Tilde)**:十进制 126,十六进制 0x7E。
完整的 ASCII 码表可以查阅相关的在线资源或参考书籍。了解常用 ASCII 字符的编码,有助于在程序设计中进行字符处理和文本操作。
Appendix A3: 扩展 ASCII 码 (Extended ASCII Codes)
随着计算机应用的扩展,7 位 ASCII 码的 128 个字符逐渐显得不足以满足需求,尤其是在处理欧洲语言中常见的带重音符号的字符时。为了扩展 ASCII 码的表示能力,出现了 8 位 (bits) 的扩展 ASCII 码。
扩展 ASCII 码利用了字节 (byte) 的全部 8 位,将编码范围从 0-127 扩展到 0-255 (十进制)。其中,0-127 仍然与标准的 7 位 ASCII 码兼容,而 128-255 (十进制) 的范围则被用来表示各种扩展字符,例如:
① 特殊符号 (Special Symbols):例如货币符号 (如 £, ¥, €)、数学符号 (如 ±, ×, ÷)、图形符号等。
② 带重音符号的欧洲字符 (European Characters with Accents):例如 á, é, í, ó, ú, à, è, ì, ò, ù, â, ê, î, ô, û, ä, ë, ï, ö, ü, ç, ñ 等。
然而,需要注意的是,扩展 ASCII 码并非统一的标准。不同的系统和地区可能使用不同的扩展 ASCII 码表,导致同一个编码值 (128-255 范围内) 可能表示不同的字符,这就是所谓的代码页 (Code Page) 问题。例如,常见的扩展 ASCII 码表包括 ISO-8859-1 (Latin-1) 、Windows-1252 等。
由于扩展 ASCII 码的碎片化和不统一性,以及全球化和多语言环境的日益普及,为了实现更广泛的字符表示和跨平台、跨语言的文本信息交换,Unicode (统一码) 应运而生,并逐渐取代了扩展 ASCII 码,成为现代计算机系统中最主要的字符编码标准。Unicode 旨在为世界上所有字符提供唯一的编码,包括各种语言的文字、符号、图形等,从而彻底解决了字符编码的兼容性问题。
尽管如此,了解扩展 ASCII 码的历史和局限性,有助于我们理解字符编码发展的演进过程,以及在处理历史遗留系统或特定应用场景时可能遇到的字符编码问题。在现代 C++ 编程中,推荐使用 Unicode 编码 (例如 UTF-8, UTF-16, UTF-32) 和相关的标准库工具 (例如 std::wstring
, std::u8string
, std::u16string
, std::u32string
) 来处理文本,以获得更好的跨平台兼容性和更广泛的字符支持。
Appendix B: C++ 数组相关术语表 (Glossary of C++ Array Terms)
收录本书中涉及的 C++ 数组相关术语,并提供简明解释,方便读者查阅和理解。
Appendix B1: 核心概念术语 (Core Concept Terms)
① 数组 (Array):是一种基本的数据结构 (Data Structure),用于存储相同类型元素的集合,这些元素在内存中连续存储,并通过索引 (Index) 访问。
② 数据结构 (Data Structure):计算机存储、组织数据的方式。数组是其中一种最基础的数据结构,其他常见的数据结构包括链表 (Linked List)、树 (Tree)、图 (Graph) 等。
③ 元素 (Element):构成数组的个体,每个元素都存储着相同类型的数据。可以通过数组的索引 (Index) 来访问特定的元素。
④ 索引 (Index):用于定位数组中特定元素的整数值。在 C++ 中,数组的索引从 0 开始,即第一个元素的索引为 0,第二个元素的索引为 1,依此类推。
⑤ 多维数组 (Multi-dimensional Array):具有多个维度的数组。最常见的是二维数组 (2D Arrays),可以看作是数组的数组,用于表示表格或矩阵等数据结构。
⑥ 维度 (Dimension):描述数组结构复杂程度的属性。一维数组只有一个索引,二维数组有两个索引(例如行和列),依此类推。
⑦ 声明 (Declaration):在 C++ 中,声明数组意味着指定数组的名称、元素类型和大小,但不一定分配内存空间。例如,int arr[5];
声明了一个可以存储 5 个整数的数组 arr
。
⑧ 初始化 (Initialization):在声明数组的同时或之后,为数组元素赋予初始值的过程。C++ 提供了多种初始化数组的方法,例如列表初始化、循环初始化等。
⑨ 静态数组 (Static Array):在编译时确定大小的数组。其大小在声明时指定,并且在程序运行期间不可更改。
⑩ 动态数组 (Dynamic Array):在运行时根据需要动态分配内存的数组。其大小可以在程序运行时确定,并且可以根据需要调整大小。动态数组通常在堆 (Heap) 内存中分配。
Appendix B2: 内存与指针术语 (Memory and Pointer Terms)
① 内存 (Memory):计算机中用于存储数据的硬件组件。程序运行时,数据和指令都存储在内存中。
② 连续内存 (Contiguous Memory):数组的元素在内存中一个接一个地存储,没有空隙。这是数组的重要特性,使得可以通过索引快速访问元素。
③ 堆 (Heap):一种动态内存分配区域。程序可以在运行时从堆中申请内存(例如使用 new
运算符),并在不再需要时释放内存(例如使用 delete
运算符)。动态数组通常在堆上分配。
④ 栈 (Stack):一种自动内存管理区域。函数调用和局部变量通常存储在栈上。栈内存的分配和释放是自动的,由编译器管理。静态数组如果作为局部变量声明,通常分配在栈上。
⑤ 内存分配 (Memory Allocation):在内存中预留空间以供程序存储数据的过程。可以是静态的(编译时分配)或动态的(运行时分配)。
⑥ 动态内存分配 (Dynamic Memory Allocation):在程序运行时根据需要分配内存。C++ 中使用 new
运算符进行动态内存分配。
⑦ 内存释放 (Memory Deallocation):将已分配的动态内存归还给系统,使其可以被重新使用的过程。C++ 中使用 delete
运算符(或 delete[]
运算符对于数组)进行动态内存释放。
⑧ 内存泄漏 (Memory Leak):已动态分配的内存在不再使用后没有被正确释放,导致这部分内存无法被再次使用,长期积累可能导致程序性能下降甚至崩溃。
⑨ 指针 (Pointer):一种变量,存储的是内存地址。指针可以指向变量、数组、函数等数据所在的内存位置。
⑩ 指针常量 (Pointer Constant):指针本身的值(即存储的内存地址)不可改变的指针。数组名在大多数情况下会被隐式转换为指向数组首元素的指针常量。
⑪ 指针算术 (Pointer Arithmetic):对指针进行加减运算。当指针指向数组元素时,指针算术可以用于访问数组中的其他元素。例如,将指针加 1 会使其指向数组的下一个元素。
⑫ 悬 dangling 指针 (Dangling Pointer):指向已被释放或无效内存的指针。访问悬 dangling 指针会导致未定义行为,通常是程序错误。
Appendix B3: C 风格字符串术语 (C-style String Terms)
① C 风格字符串 (C-style String):在 C 和 C++ 中,使用字符数组 (Character Array) 来表示字符串。C 风格字符串以空字符 (Null Character) \0
结尾作为字符串结束的标志。
② 字符数组 (Character Array):元素类型为 char
的数组。当用于存储字符串时,通常会在末尾添加一个空字符 \0
。
③ 空字符 (Null Character) \0
:ASCII 码值为 0 的字符。在 C 风格字符串中,用于标记字符串的结束位置。字符串处理函数通常通过查找空字符来确定字符串的长度。
④ 缓冲区溢出 (Buffer Overflow):一种安全漏洞,当程序向缓冲区 (Buffer) 写入数据时,写入的数据超出了缓冲区的大小,覆盖了缓冲区之外的内存区域,可能导致程序崩溃或被恶意利用。C 风格字符串操作不当容易导致缓冲区溢出。
⑤ 字符串操作函数 (String Operation Functions):C 标准库提供的一系列用于操作 C 风格字符串的函数,例如 strcpy
(字符串复制)、strcat
(字符串连接)、strlen
(计算字符串长度)、strcmp
(字符串比较)等。这些函数通常定义在 <cstring>
头文件中。
Appendix B4: std::array
和其他术语 (std::array
and Other Terms)
① std::array
容器 (std::array
Container):C++ 标准库提供的固定大小数组的容器。与 C 风格数组相比,std::array
提供了类型安全 (Type Safety)、边界检查 (Bounds Checking) 等优势,并且可以与标准库算法更好地兼容。定义在 <array>
头文件中。
② 类型安全 (Type Safety):编程语言的一种特性,强制类型检查,以防止类型错误。std::array
是类型安全的,因为它在编译时知道元素类型和大小,有助于发现类型相关的错误。
③ 边界检查 (Bounds Checking):在访问数组元素时,检查索引是否在有效范围内。C 风格数组默认不进行边界检查,可能导致数组越界访问。std::array
提供了 at()
方法进行带边界检查的访问。
④ 算法 (Algorithm):解决特定问题的步骤或方法。在计算机科学中,算法通常指解决特定计算问题的明确指令序列。排序算法 (Sorting Algorithm) 和搜索算法 (Searching Algorithm) 是常见的数组算法。
⑤ 排序算法 (Sorting Algorithm):将数组元素按照特定顺序排列的算法。常见的排序算法包括冒泡排序 (Bubble Sort)、选择排序 (Selection Sort)、插入排序 (Insertion Sort)、快速排序 (Quick Sort)、归并排序 (Merge Sort) 等。
⑥ 搜索算法 (Searching Algorithm):在数组中查找特定元素的算法。常见的搜索算法包括线性搜索 (Linear Search)、二分搜索 (Binary Search) (适用于有序数组) 等。
⑦ 代码风格 (Code Style):编写代码的规范和约定,包括命名规则、缩进、注释等。良好的代码风格可以提高代码的可读性和可维护性。
⑧ 性能优化 (Performance Optimization):改进程序性能,使其运行得更快或更高效的过程。数组的性能优化可以包括减少不必要的拷贝、利用缓存 (Cache) 局部性原理等。
⑨ 缓存 (Cache):一种高速缓冲存储器,用于临时存储频繁访问的数据,以提高数据访问速度。合理利用缓存局部性原理可以提高数组操作的性能。
⑩ 局部性原理 (Locality Principle):程序访问数据的局部性规律,包括时间局部性(最近访问的数据很可能在不久的将来再次被访问)和空间局部性(最近访问的数据附近的数据也很可能在不久的将来被访问)。数组的连续存储特性有利于利用空间局部性。
⑪ 数组越界 (Array Out-of-Bounds):访问数组时使用的索引超出了数组的有效范围(例如,对于大小为 5 的数组,访问索引 5 或更大的索引)。数组越界访问是常见的程序错误,可能导致程序崩溃或安全漏洞。
⑫ 未初始化 (Uninitialized):变量或数组在声明后没有被赋予初始值的状态。访问未初始化的数组元素会得到不确定的值,可能导致程序行为异常。
⑬ 智能指针 (Smart Pointer):C++ 标准库提供的自动管理动态内存的对象。智能指针可以在对象不再使用时自动释放其管理的内存,有助于避免内存泄漏。常见的智能指针类型包括 std::unique_ptr
、std::shared_ptr
等。
Appendix C: 参考文献 (References)
Appendix C: 参考文献 (References)
列出本书编写过程中参考的书籍、文档和在线资源,为读者提供进一步学习的线索。
① 书籍 (Books)
▮▮▮▮ⓐ C++ Primer (第5版) (C++ Primer, 5th Edition)
⚝▮▮▮▮▮▮▮- 作者:Stanley B. Lippman, Josée Lajoie, Barbara E. Moo
⚝▮▮▮▮▮▮▮- 出版社:人民邮电出版社
⚝▮▮▮▮▮▮▮- 描述:C++ 编程的经典入门教材,内容全面、深入,覆盖 C++ 语言的各个方面,包括数组、指针、内存管理等。对于初学者和有经验的程序员都极具参考价值。
▮▮▮▮ⓑ Effective C++: 改善程序与设计的55个具体做法 (Effective C++: 55 Specific Ways to Improve Your Programs and Designs)
⚝▮▮▮▮▮▮▮- 作者:Scott Meyers
⚝▮▮▮▮▮▮▮- 出版社:电子工业出版社
⚝▮▮▮▮▮▮▮- 描述:C++ 实践经验的总结,提出了 55 个改善 C++ 程序设计和效率的具体建议,其中多项建议与数组、内存管理、以及高效编程密切相关。
▮▮▮▮ⓒ More Effective C++: 35个改善编程与设计的有效方法 (More Effective C++: 35 New Ways to Improve Your Programs and Designs)
⚝▮▮▮▮▮▮▮- 作者:Scott Meyers
⚝▮▮▮▮▮▮▮- 出版社:电子工业出版社
⚝▮▮▮▮▮▮▮- 描述:Effective C++ 的续作,进一步深入探讨了 C++ 编程中的高级主题和技巧,对于想要精通 C++ 数组和相关技术的读者非常有帮助。
▮▮▮▮ⓓ C++程序设计原理与实践 (Programming: Principles and Practice Using C++)
⚝▮▮▮▮▮▮▮- 作者:Bjarne Stroustrup
⚝▮▮▮▮▮▮▮- 出版社:机械工业出版社
⚝▮▮▮▮▮▮▮- 描述:由 C++ 语言的设计者 Bjarne Stroustrup 亲自撰写,系统地介绍了 C++ 编程的基础知识和高级概念,强调编程原理和实践,适合作为 C++ 课程的教材或自学读物。
② 在线文档 (Online Documents)
▮▮▮▮ⓐ cppreference.com
⚝▮▮▮▮▮▮▮- 网址:https://zh.cppreference.com/
⚝▮▮▮▮▮▮▮- 描述:权威的 C++ 在线参考手册,提供了 C++ 标准库和语言特性的详细文档,包括数组、std::array
容器、以及相关的算法和函数。是学习和查阅 C++ 知识的必备网站。
▮▮▮▮ⓑ cplusplus.com
⚝▮▮▮▮▮▮▮- 网址:http://www.cplusplus.com/
⚝▮▮▮▮▮▮▮- 描述:另一个重要的 C++ 在线资源网站,提供了 C++ 教程、参考文档、以及代码示例,涵盖了 C++ 数组的各个方面,适合不同 уровней 的读者学习和参考。
▮▮▮▮ⓒ C++ Standard ISO/IEC 14882
⚝▮▮▮▮▮▮▮- 描述:C++ 语言的国际标准文档,是了解 C++ 语言规范的最权威来源。虽然内容较为技术化,但对于深入理解 C++ 数组的底层原理和标准细节至关重要。可以从 ISO 官网或相关标准组织获取。
③ 在线教程与课程 (Online Tutorials and Courses)
▮▮▮▮ⓐ Coursera 和 edX 等在线教育平台
⚝▮▮▮▮▮▮▮- 描述:Coursera、edX 等平台上有大量优质的 C++ 编程课程,涵盖从入门到高级的各个 уровней。可以通过搜索 "C++ programming" 或 "C++ array" 等关键词找到相关课程,系统学习 C++ 数组的知识和应用。
▮▮▮▮ⓑ YouTube 和 B站 (bilibili) 等视频平台
⚝▮▮▮▮▮▮▮- 描述:YouTube 和 B站 (bilibili) 上有许多 C++ 教学视频,涵盖了 C++ 数组的各个主题,例如 "C++ array tutorial" 等。可以通过观看视频教程,更直观地学习 C++ 数组的用法和技巧。
④ 技术博客与论坛 (Technical Blogs and Forums)
▮▮▮▮ⓐ Stack Overflow
⚝▮▮▮▮▮▮▮- 网址:https://stackoverflow.com/
⚝▮▮▮▮▮▮▮- 描述:程序员问答社区,可以在 Stack Overflow 上搜索关于 C++ 数组的问题,或者提问自己遇到的问题,获取来自全球程序员的解答和帮助。
▮▮▮▮ⓑ CSDN, 知乎等中文技术社区
⚝▮▮▮▮▮▮▮- 描述:CSDN、知乎等中文技术社区也有大量的 C++ 数组相关的文章和讨论,可以从中学习 C++ 数组的使用技巧,解决实际问题。
⑤ 其他参考资料 (Other References)
▮▮▮▮ⓐ MSDN (Microsoft Developer Network)
⚝▮▮▮▮▮▮▮- 描述:如果使用 Microsoft Visual C++ 编译器,MSDN 提供了非常详细的 C++ 文档和示例代码,包括数组和相关库函数的说明。
▮▮▮▮ⓑ GCC (GNU Compiler Collection) 和 Clang 文档
⚝▮▮▮▮▮▮▮- 描述:如果使用 GCC 或 Clang 编译器,它们的官方文档也提供了关于 C++ 语言特性和标准库的详细信息,是深入了解 C++ 数组的重要参考资料。