第三十二天:数组
C++ 数组
一、数组基础概念
- 定义:
- 数组是一种在 C++ 中用于存储多个相同类型数据元素的数据结构,这些元素在内存中连续存储。
- 语法:
数据类型 数组名[数组大小];
例如:int numbers[5];
这里定义了一个名为numbers
的数组,它可以存储 5 个int
类型的数据。
- 数组的特点:
- 类型一致性:数组中所有元素的数据类型必须相同。这确保了在内存中每个元素占用相同大小的空间,方便系统进行管理和访问。
- 内存连续性:数组元素在内存中是按顺序连续存储的。这使得通过索引访问元素时效率很高,因为可以根据数组起始地址和元素偏移量快速定位到目标元素。
二、数组的初始化
- 静态初始化:
- 完全初始化:在定义数组时,直接为每个元素指定初始值。例如:
int numbers[5] = {1, 2, 3, 4, 5};
这里将数组numbers
的 5 个元素依次初始化为 1、2、3、4、5。 - 部分初始化:当初始化列表中的值少于数组元素个数时,剩余元素会根据其数据类型进行默认初始化。对于数值类型(如
int
、float
等),剩余元素会初始化为 0;对于字符类型(char
),会初始化为空字符'\0'
。例如:int numbers[5] = {1, 2};
此时numbers[0] = 1
,numbers[1] = 2
,numbers[2] = 0
,numbers[3] = 0
,numbers[4] = 0
。
- 完全初始化:在定义数组时,直接为每个元素指定初始值。例如:
- 动态初始化(C++11 及以后):
- 使用花括号进行初始化,编译器可以根据初始化列表的元素个数自动推断数组大小。例如:
int numbers[]{1, 2, 3, 4, 5};
编译器会自动确定numbers
数组的大小为 5。 - 这种方式更加灵活,也有助于减少一些潜在的错误,比如数组大小与初始化列表元素个数不匹配的问题。
- 使用花括号进行初始化,编译器可以根据初始化列表的元素个数自动推断数组大小。例如:
三、数组元素的访问
- 索引(下标):
- 数组元素通过索引(也称为下标)来访问,索引从 0 开始计数。对于一个具有
n
个元素的数组,其有效索引范围是从 0 到n - 1
。例如,对于数组int numbers[5];
,可以通过numbers[0]
访问第一个元素,numbers[4]
访问第五个元素。 - 访问数组元素的语法:
数组名[索引];
例如:int firstNumber = numbers[0];
这里将numbers
数组的第一个元素赋值给firstNumber
变量。
- 数组元素通过索引(也称为下标)来访问,索引从 0 开始计数。对于一个具有
- 越界访问:
- 访问数组元素时,必须确保索引在有效范围内。如果访问的索引小于 0 或者大于等于数组大小,就会发生越界访问。例如,对于
int numbers[5];
,访问numbers[5]
就是越界访问。 - 越界访问会导致未定义行为,这意味着程序的行为是不确定的。可能会导致程序崩溃、数据损坏或产生看似随机的结果。因此,在编写代码时,一定要仔细检查索引值,避免越界访问。
- 访问数组元素时,必须确保索引在有效范围内。如果访问的索引小于 0 或者大于等于数组大小,就会发生越界访问。例如,对于
四、数组的常见操作
- 遍历数组:
- 使用
for
循环:这是最常见的遍历数组的方式。例如:
- 使用
int numbers[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {std::cout << numbers[i] << " ";
}
在这个例子中,for
循环从 i = 0
开始,每次循环 i
自增 1,直到 i
达到数组大小 5 时停止。在每次循环中,输出数组元素 numbers[i]
。
- 使用
while
循环:同样可以实现数组的遍历。例如:
int numbers[5] = {1, 2, 3, 4, 5};
int i = 0;
while (i < 5) {std::cout << numbers[i] << " ";i++;
}
这里通过 while
循环,在 i
小于 5 的条件下,不断输出数组元素并将 i
自增 1。
2. 修改数组元素:
- 可以通过索引直接修改数组元素的值。例如:
int numbers[5] = {1, 2, 3, 4, 5}; numbers[2] = 10;
此时,数组numbers
的第三个元素(索引为 2)的值从 3 被修改为 10。
- 查找数组元素:
- 可以通过遍历数组,将每个元素与目标值进行比较,来查找数组中是否存在特定元素。例如,查找数组
numbers
中是否存在值为 3 的元素:
- 可以通过遍历数组,将每个元素与目标值进行比较,来查找数组中是否存在特定元素。例如,查找数组
int numbers[5] = {1, 2, 3, 4, 5};
bool found = false;
for (int i = 0; i < 5; i++) {if (numbers[i] == 3) {found = true;break;}
}
if (found) {std::cout << "元素 3 存在于数组中。" << std::endl;
} else {std::cout << "元素 3 不存在于数组中。" << std::endl;
}
- 数组排序:
- 常见的排序算法如冒泡排序、选择排序、插入排序等都可以用于对数组进行排序。以冒泡排序为例:
int numbers[5] = {5, 4, 3, 2, 1};
for (int i = 0; i < 5 - 1; i++) {for (int j = 0; j < 5 - i - 1; j++) {if (numbers[j] > numbers[j + 1]) {int temp = numbers[j];numbers[j] = numbers[j + 1];numbers[j + 1] = temp;}}
}
for (int i = 0; i < 5; i++) {std::cout << numbers[i] << " ";
}
冒泡排序通过多次比较相邻元素并交换位置,将最大(或最小)的元素逐步“冒泡”到数组末尾。
五、多维数组
- 二维数组:
- 定义:二维数组可以看作是数组的数组,常用于表示具有行和列结构的数据,如矩阵。语法:
数据类型 数组名[行数][列数];
例如:int matrix[3][4];
定义了一个 3 行 4 列的二维整数数组。 - 初始化:
- 逐行初始化:
int matrix[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
这种方式将二维数组按行进行初始化。 - 按顺序初始化:
int matrix[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
这种方式按顺序为二维数组的元素赋值,编译器会按行填充元素。
- 逐行初始化:
- 访问元素:通过两个索引来访问二维数组的元素,第一个索引表示行,第二个索引表示列。例如:
int element = matrix[1][2];
这里获取了matrix
数组第二行第三列的元素(值为 7)。
- 定义:二维数组可以看作是数组的数组,常用于表示具有行和列结构的数据,如矩阵。语法:
- 多维数组的拓展:
- 除了二维数组,还可以定义三维、四维等多维数组。例如,三维数组可以用于表示立体空间中的数据。定义三维数组的语法:
数据类型 数组名[第一维大小][第二维大小][第三维大小];
例如:int cube[2][3][4];
表示一个具有 2 层、每层 3 行、每行 4 列的三维数组。
- 除了二维数组,还可以定义三维、四维等多维数组。例如,三维数组可以用于表示立体空间中的数据。定义三维数组的语法:
六、数组与函数
- 数组作为函数参数:
- 当数组作为函数参数传递时,数组会自动退化为指针。函数定义的语法:
返回类型 函数名(数据类型 数组名[], 数组大小参数);
例如:
- 当数组作为函数参数传递时,数组会自动退化为指针。函数定义的语法:
void printArray(int arr[], int size) {for (int i = 0; i < size; i++) {std::cout << arr[i] << " ";}
}
在调用函数时,可以将数组名作为参数传递,同时传递数组的大小。例如:int numbers[5] = {1, 2, 3, 4, 5}; printArray(numbers, 5);
2. 返回数组的函数:
- 在 C++ 中,函数不能直接返回数组,但可以返回指向数组的指针。不过,使用指针返回数组时需要注意内存管理问题,避免悬空指针等错误。一种更安全的方式是返回
std::vector
(C++ 标准库中的动态数组)。例如:
#include <vector>
std::vector<int> createArray() {std::vector<int> arr = {1, 2, 3, 4, 5};return arr;
}
然后可以这样调用函数:std::vector<int> result = createArray();
七、数组的内存管理
- 栈上的数组(自动数组):
- 当数组定义在函数内部时,它是在栈上分配内存的,也称为自动数组。例如:
void function() { int numbers[5]; }
当函数function
执行结束时,数组numbers
所占用的内存会自动被释放,无需手动管理。
- 当数组定义在函数内部时,它是在栈上分配内存的,也称为自动数组。例如:
- 堆上的数组(动态数组):
- 使用
new
关键字可以在堆上动态分配数组内存。例如:int* dynamicArray = new int[5];
这里在堆上分配了一块可以存储 5 个int
类型数据的内存,并将这块内存的地址赋值给指针dynamicArray
。 - 动态分配的数组在使用完毕后,必须使用
delete[]
来释放内存,以避免内存泄漏。例如:delete[] dynamicArray;
如果不释放动态分配的数组内存,这块内存将一直占用,直到程序结束,从而导致内存泄漏问题。
- 使用
八、数组的优缺点
- 优点:
- 高效的随机访问:由于数组元素在内存中连续存储,可以通过索引快速定位到任意元素,时间复杂度为 O(1)。这使得数组在需要频繁随机访问元素的场景下表现出色,如查找表。
- 简单直观:数组的概念简单,易于理解和使用。在处理一些基本的数据集合时,数组是一种很自然的选择。
- 缺点:
- 大小固定:数组的大小在编译时就必须确定,运行时不能动态改变。这在需要动态调整数据规模的场景下会带来不便,例如在程序运行过程中需要不断添加或删除元素的情况。
- 插入和删除操作效率低:在数组中间插入或删除元素时,需要移动大量的后续元素,时间复杂度为 O(n),效率较低。相比之下,链表等数据结构在插入和删除操作上更具优势。
通过对 C++ 数组的详细学习,我们了解了数组的基本概念、初始化、访问、常见操作、与函数的交互以及内存管理等方面的知识。在实际编程中,应根据具体需求合理选择和使用数组,充分发挥其优势,同时注意避免其缺点带来的问题。