C++ 数组用法详解
一、C++ 数组基础
1.1 数组的定义
在 C++ 中,数组是一种用于存储多个相同类型数据的集合。其定义语法如下:
dataType arrayName[arraySize];
其中,dataType
是数组中元素的数据类型,arrayName
是数组的名称,arraySize
是一个常量表达式,指定数组中元素的个数。例如,定义一个包含 5 个整数的数组:
int numbers[5];
这里,int
是数据类型,numbers
是数组名,5
是数组大小。
1.2 数组的初始化
数组在定义时可以进行初始化,有以下几种常见方式:
- 完全初始化:
int numbers[5] = {1, 2, 3, 4, 5};
在这种方式下,花括号内的值按顺序依次赋给数组的每个元素。
- 部分初始化:
int numbers[5] = {1, 2};
此时,前两个元素被初始化为 1
和 2
,其余元素会根据数据类型进行默认初始化。对于 int
类型,未初始化的元素默认值为 0
。
- 省略数组大小初始化:
int numbers[] = {1, 2, 3, 4, 5};
编译器会根据花括号内元素的个数自动确定数组的大小,这里数组 numbers
的大小为 5
。
二、访问数组元素
2.1 数组下标访问
数组元素通过下标(索引)来访问,下标从 0
开始。例如,要访问上述 numbers
数组的第一个元素,可以使用:
int firstNumber = numbers[0];
这里,numbers[0]
表示访问数组 numbers
的第一个元素。访问数组元素时,务必确保下标在有效范围内(0
到 arraySize - 1
),否则会导致未定义行为。例如,访问 numbers[5]
就是越界访问,可能会引发程序崩溃或产生错误结果。
2.2 使用循环访问数组
通常使用循环来遍历数组,对每个元素进行操作。例如,打印数组 numbers
的所有元素:
#include <iostream>
int main() {
int numbers[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
std::cout << numbers[i] << " ";
}
return 0;
}
上述代码使用 for
循环,从 0
到 4
遍历数组,并将每个元素输出到控制台。
三、多维数组
3.1 二维数组的定义与初始化
二维数组可以看作是数组的数组,常用于表示矩阵等数据结构。其定义语法如下:
dataType arrayName[rows][columns];
其中,rows
表示行数,columns
表示列数。例如,定义一个 3
行 4
列的二维整数数组:
int matrix[3][4];
二维数组的初始化方式有多种,例如:
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
这里,外层花括号内的每一个内层花括号表示一行的数据。
3.2 访问二维数组元素
访问二维数组元素需要使用两个下标,第一个下标表示行,第二个下标表示列。例如,访问上述 matrix
数组中第 2
行第 3
列的元素:
int element = matrix[1][2];
注意,行和列的下标都是从 0
开始的,所以 matrix[1][2]
实际上是第二行第三列的元素。
3.3 多维数组的扩展
除了二维数组,C++ 还支持三维及更高维度的数组。例如,三维数组的定义如下:
dataType arrayName[depth][rows][columns];
三维数组常用于表示三维空间的数据,如立体图像的像素数据等。其初始化和访问方式与二维数组类似,只是需要更多的下标。例如:
int threeDArray[2][3][4];
初始化时:
int threeDArray[2][3][4] = {
{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
},
{
{13, 14, 15, 16},
{17, 18, 19, 20},
{21, 22, 23, 24}
}
};
访问元素时:
int element = threeDArray[1][2][3];
四、数组与函数
4.1 数组作为函数参数
在 C++ 中,可以将数组作为函数的参数传递。例如,定义一个函数来计算数组元素的总和:
#include <iostream>
int sumArray(int arr[], int size) {
int sum = 0;
for (int i = 0; i < size; i++) {
sum += arr[i];
}
return sum;
}
int main() {
int numbers[5] = {1, 2, 3, 4, 5};
int total = sumArray(numbers, 5);
std::cout << "Sum of array elements: " << total << std::endl;
return 0;
}
在函数声明 int sumArray(int arr[], int size)
中,arr
是一个数组参数,size
用于指定数组的大小。需要注意的是,当数组作为函数参数传递时,实际上传递的是数组的首地址,而不是整个数组的副本。这意味着在函数内部对数组的修改会影响到原数组。
4.2 多维数组作为函数参数
多维数组也可以作为函数参数传递。以二维数组为例,函数声明如下:
void printMatrix(int matrix[][4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
std::cout << matrix[i][j] << " ";
}
std::cout << std::endl;
}
}
这里,int matrix[][4]
表示一个二维数组参数,其中列数必须明确指定,行数可以通过另一个参数 rows
来传递。
五、动态数组
5.1 使用 new
和 delete
创建动态数组
在 C++ 中,可以使用 new
运算符动态分配数组内存,使用 delete
运算符释放内存。例如,动态创建一个包含 n
个整数的数组:
#include <iostream>
int main() {
int n = 5;
int* dynamicArray = new int[n];
for (int i = 0; i < n; i++) {
dynamicArray[i] = i * 2;
}
for (int i = 0; i < n; i++) {
std::cout << dynamicArray[i] << " ";
}
delete[] dynamicArray;
return 0;
}
在上述代码中,new int[n]
动态分配了一块能容纳 n
个 int
类型数据的内存,并返回指向这块内存的指针 dynamicArray
。使用完动态数组后,必须使用 delete[] dynamicArray
释放内存,以避免内存泄漏。
5.2 使用 std::vector
替代动态数组
std::vector
是 C++ 标准库提供的动态数组容器,它比手动使用 new
和 delete
管理动态数组更加安全和方便。std::vector
能够自动管理内存,在需要时动态调整大小。例如:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
for (size_t i = 0; i < vec.size(); i++) {
std::cout << vec[i] << " ";
}
return 0;
}
这里,std::vector<int> vec
创建了一个 int
类型的动态数组 vec
。vec.push_back(1)
方法向数组末尾添加元素。vec.size()
方法返回数组当前的元素个数。std::vector
还提供了许多其他有用的方法,如 erase
、insert
等,方便对数组进行操作。
六、数组的内存布局
6.1 一维数组的内存布局
在内存中,一维数组的元素是连续存储的。例如,对于数组 int numbers[5] = {1, 2, 3, 4, 5};
,假设数组首地址为 address
,那么 numbers[0]
存储在 address
处,numbers[1]
存储在 address + sizeof(int)
处,numbers[2]
存储在 address + 2 * sizeof(int)
处,以此类推。这种连续存储的方式使得数组的访问效率较高,通过简单的指针运算就可以快速定位到每个元素。
6.2 二维数组的内存布局
二维数组在内存中也是按顺序连续存储的,通常采用行优先(Row - Major)的存储方式。以 int matrix[3][4]
为例,先存储第一行的所有元素,接着存储第二行的所有元素,最后存储第三行的所有元素。假设数组首地址为 address
,每个 int
类型占 4
个字节,那么 matrix[0][0]
存储在 address
处,matrix[0][1]
存储在 address + 4
处,matrix[1][0]
存储在 address + 4 * 4
处(因为第一行有 4
个元素,每个元素占 4
个字节)。这种存储方式对于按行遍历二维数组非常高效。
6.3 内存布局对性能的影响
了解数组的内存布局对于编写高效的代码很重要。例如,在遍历二维数组时,按行优先遍历通常比按列优先遍历更高效,因为按行优先遍历更符合内存的连续存储方式,减少了内存访问的跳跃,提高了缓存命中率。下面是一个简单的性能对比示例:
#include <iostream>
#include <chrono>
const int rows = 1000;
const int cols = 1000;
void traverseByRow(int matrix[rows][cols]) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j]++;
}
}
}
void traverseByColumn(int matrix[rows][cols]) {
for (int j = 0; j < cols; j++) {
for (int i = 0; i < rows; i++) {
matrix[i][j]++;
}
}
}
int main() {
int matrix[rows][cols];
auto start = std::chrono::high_resolution_clock::now();
traverseByRow(matrix);
auto end = std::chrono::high_resolution_clock::now();
auto durationRow = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
start = std::chrono::high_resolution_clock::now();
traverseByColumn(matrix);
end = std::chrono::high_resolution_clock::now();
auto durationColumn = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "Traverse by row time: " << durationRow << " ms" << std::endl;
std::cout << "Traverse by column time: " << durationColumn << " ms" << std::endl;
return 0;
}
在这个示例中,traverseByRow
函数按行遍历二维数组,traverseByColumn
函数按列遍历二维数组。通过计时可以发现,按行遍历通常会比按列遍历花费更少的时间。
七、数组的常见操作与应用
7.1 数组排序
排序是数组常见的操作之一。C++ 标准库提供了 std::sort
函数来对数组进行排序。例如,对一个整数数组进行排序:
#include <iostream>
#include <algorithm>
int main() {
int numbers[5] = {3, 1, 4, 1, 5};
std::sort(numbers, numbers + 5);
for (int i = 0; i < 5; i++) {
std::cout << numbers[i] << " ";
}
return 0;
}
std::sort
函数默认使用快速排序算法,对指定范围内的元素进行升序排序。这里,numbers
是数组首地址,numbers + 5
表示数组末尾地址(不包含该地址处的元素)。
7.2 数组查找
查找数组中的元素也是常见需求。可以使用线性查找或二分查找。线性查找简单地遍历数组,逐个比较元素。二分查找则要求数组已经排序,通过不断将数组分成两半来缩小查找范围。下面是线性查找的示例:
#include <iostream>
bool linearSearch(int arr[], int size, int target) {
for (int i = 0; i < size; i++) {
if (arr[i] == target) {
return true;
}
}
return false;
}
int main() {
int numbers[5] = {1, 2, 3, 4, 5};
int target = 3;
if (linearSearch(numbers, 5, target)) {
std::cout << "Element found." << std::endl;
} else {
std::cout << "Element not found." << std::endl;
}
return 0;
}
二分查找可以使用 C++ 标准库中的 std::lower_bound
和 std::upper_bound
函数。例如:
#include <iostream>
#include <algorithm>
int main() {
int numbers[5] = {1, 2, 3, 4, 5};
int target = 3;
auto it = std::lower_bound(numbers, numbers + 5, target);
if (it != numbers + 5 && *it == target) {
std::cout << "Element found." << std::endl;
} else {
std::cout << "Element not found." << std::endl;
}
return 0;
}
std::lower_bound
函数返回指向第一个大于或等于目标值的元素的迭代器。
7.3 数组在实际项目中的应用
数组在实际项目中有广泛应用。例如,在图形处理中,图像数据可以存储在二维数组中,每个元素表示一个像素的颜色值。在游戏开发中,地图数据可以用二维数组表示,每个元素表示地图上的一个方块类型。在数据分析中,数组可以存储数据样本,方便进行统计和计算。
八、数组与指针的关系
8.1 数组名作为指针
在 C++ 中,数组名可以看作是一个指向数组首元素的常量指针。例如,对于数组 int numbers[5]
,numbers
等同于 &numbers[0]
,都是指向数组第一个元素的地址。可以通过指针运算来访问数组元素,例如:
#include <iostream>
int main() {
int numbers[5] = {1, 2, 3, 4, 5};
int* ptr = numbers;
for (int i = 0; i < 5; i++) {
std::cout << *(ptr + i) << " ";
}
return 0;
}
这里,ptr
是一个指向 numbers
数组首元素的指针,*(ptr + i)
等同于 numbers[i]
,通过指针运算访问数组元素。
8.2 指针与动态数组
指针在动态数组的创建和管理中起着重要作用。如前文所述,使用 new
运算符创建动态数组时,返回的是一个指针。例如:
int* dynamicArray = new int[5];
dynamicArray
是一个指向动态分配的数组首元素的指针。在释放动态数组内存时,也需要使用这个指针:
delete[] dynamicArray;
8.3 指针数组与数组指针
- 指针数组:指针数组是一个数组,其元素都是指针。例如:
int num1 = 10;
int num2 = 20;
int* ptrArray[2] = {&num1, &num2};
这里,ptrArray
是一个指针数组,它的两个元素分别是指向 num1
和 num2
的指针。
- 数组指针:数组指针是一个指针,它指向一个数组。例如:
int numbers[5] = {1, 2, 3, 4, 5};
int (*arrayPtr)[5] = &numbers;
arrayPtr
是一个指向包含 5
个 int
类型元素的数组的指针。需要注意括号的位置,int (*arrayPtr)[5]
与 int *arrayPtr[5]
是不同的,前者是数组指针,后者是指针数组。
九、数组的限制与注意事项
9.1 数组大小固定
普通数组在定义时大小必须是常量表达式,一旦定义,其大小不能在运行时动态改变。例如,下面的代码是错误的:
int n;
std::cin >> n;
int numbers[n]; // 错误,n 不是常量表达式
要实现动态大小的数组,可以使用 std::vector
或者动态分配内存(如 new
和 delete
)。
9.2 数组越界访问
如前文所述,访问数组元素时必须确保下标在有效范围内,否则会导致未定义行为。例如:
int numbers[5] = {1, 2, 3, 4, 5};
int outOfBounds = numbers[5]; // 越界访问,未定义行为
在编写代码时,务必仔细检查数组下标,避免越界访问。
9.3 数组传递的陷阱
当数组作为函数参数传递时,实际上传递的是数组的首地址,而不是整个数组的副本。这可能会导致一些误解,例如在函数内部无法通过 sizeof
运算符获取数组的真实大小。例如:
void printArraySize(int arr[]) {
std::cout << "Size of array in function: " << sizeof(arr) << std::endl;
}
int main() {
int numbers[5] = {1, 2, 3, 4, 5};
std::cout << "Size of array in main: " << sizeof(numbers) << std::endl;
printArraySize(numbers);
return 0;
}
在 main
函数中,sizeof(numbers)
返回整个数组的大小(5 * sizeof(int)
),而在 printArraySize
函数中,sizeof(arr)
返回的是指针的大小(通常为 4
或 8
字节,取决于系统的指针大小)。为了在函数中获取数组的大小,需要额外传递一个参数来表示数组的元素个数。