MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 数组用法详解

2024-06-202.6k 阅读

一、C++ 数组基础

1.1 数组的定义

在 C++ 中,数组是一种用于存储多个相同类型数据的集合。其定义语法如下:

dataType arrayName[arraySize];

其中,dataType 是数组中元素的数据类型,arrayName 是数组的名称,arraySize 是一个常量表达式,指定数组中元素的个数。例如,定义一个包含 5 个整数的数组:

int numbers[5];

这里,int 是数据类型,numbers 是数组名,5 是数组大小。

1.2 数组的初始化

数组在定义时可以进行初始化,有以下几种常见方式:

  1. 完全初始化
int numbers[5] = {1, 2, 3, 4, 5};

在这种方式下,花括号内的值按顺序依次赋给数组的每个元素。

  1. 部分初始化
int numbers[5] = {1, 2};

此时,前两个元素被初始化为 12,其余元素会根据数据类型进行默认初始化。对于 int 类型,未初始化的元素默认值为 0

  1. 省略数组大小初始化
int numbers[] = {1, 2, 3, 4, 5};

编译器会根据花括号内元素的个数自动确定数组的大小,这里数组 numbers 的大小为 5

二、访问数组元素

2.1 数组下标访问

数组元素通过下标(索引)来访问,下标从 0 开始。例如,要访问上述 numbers 数组的第一个元素,可以使用:

int firstNumber = numbers[0];

这里,numbers[0] 表示访问数组 numbers 的第一个元素。访问数组元素时,务必确保下标在有效范围内(0arraySize - 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 循环,从 04 遍历数组,并将每个元素输出到控制台。

三、多维数组

3.1 二维数组的定义与初始化

二维数组可以看作是数组的数组,常用于表示矩阵等数据结构。其定义语法如下:

dataType arrayName[rows][columns];

其中,rows 表示行数,columns 表示列数。例如,定义一个 34 列的二维整数数组:

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 使用 newdelete 创建动态数组

在 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] 动态分配了一块能容纳 nint 类型数据的内存,并返回指向这块内存的指针 dynamicArray。使用完动态数组后,必须使用 delete[] dynamicArray 释放内存,以避免内存泄漏。

5.2 使用 std::vector 替代动态数组

std::vector 是 C++ 标准库提供的动态数组容器,它比手动使用 newdelete 管理动态数组更加安全和方便。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 类型的动态数组 vecvec.push_back(1) 方法向数组末尾添加元素。vec.size() 方法返回数组当前的元素个数。std::vector 还提供了许多其他有用的方法,如 eraseinsert 等,方便对数组进行操作。

六、数组的内存布局

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_boundstd::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 指针数组与数组指针

  1. 指针数组:指针数组是一个数组,其元素都是指针。例如:
int num1 = 10;
int num2 = 20;
int* ptrArray[2] = {&num1, &num2};

这里,ptrArray 是一个指针数组,它的两个元素分别是指向 num1num2 的指针。

  1. 数组指针:数组指针是一个指针,它指向一个数组。例如:
int numbers[5] = {1, 2, 3, 4, 5};
int (*arrayPtr)[5] = &numbers;

arrayPtr 是一个指向包含 5int 类型元素的数组的指针。需要注意括号的位置,int (*arrayPtr)[5]int *arrayPtr[5] 是不同的,前者是数组指针,后者是指针数组。

九、数组的限制与注意事项

9.1 数组大小固定

普通数组在定义时大小必须是常量表达式,一旦定义,其大小不能在运行时动态改变。例如,下面的代码是错误的:

int n;
std::cin >> n;
int numbers[n]; // 错误,n 不是常量表达式

要实现动态大小的数组,可以使用 std::vector 或者动态分配内存(如 newdelete)。

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) 返回的是指针的大小(通常为 48 字节,取决于系统的指针大小)。为了在函数中获取数组的大小,需要额外传递一个参数来表示数组的元素个数。