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

C++ 模板类array用法详解

2024-06-192.6k 阅读

C++ 模板类 array 简介

在 C++ 编程中,std::array 是 C++11 引入的标准模板库(STL)中的一个容器。它提供了一种在栈上存储固定大小数组的方式,与传统的 C 风格数组相比,std::array 具有更多的功能和更好的安全性。

从本质上来说,std::array 是一个封装了固定大小数组的类模板。它在编译时就确定了大小,这意味着其大小在运行时不可改变。std::array 定义在 <array> 头文件中,使用时需要包含该头文件。

例如,声明一个包含 5 个 int 类型元素的 std::array

#include <array>
std::array<int, 5> myArray;

这里 std::array<int, 5> 表示一个存储 int 类型数据、大小为 5 的数组。

std::array 的特点

  1. 固定大小std::array 的大小在编译期就确定,一旦声明,运行时无法改变其大小。这与 std::vector 等动态大小的容器形成鲜明对比。例如:
std::array<int, 10> fixedArray;
// 下面这行代码会导致编译错误,因为 std::array 大小不能改变
// fixedArray.resize(20); 
  1. 栈上存储:与传统 C 风格数组类似,std::array 的元素通常存储在栈上(取决于其作用域)。这使得它在性能上对于小尺寸数组有一定优势,因为避免了堆内存分配和释放的开销。
  2. 安全性std::array 提供了一些方法来安全地访问元素,如 at() 方法。at() 方法在访问越界时会抛出 std::out_of_range 异常,而传统 C 风格数组访问越界会导致未定义行为。例如:
std::array<int, 5> arr = {1, 2, 3, 4, 5};
try {
    std::cout << arr.at(10) << std::endl; 
} catch (const std::out_of_range& e) {
    std::cerr << "Out of range exception: " << e.what() << std::endl;
}
  1. 支持 STL 算法std::array 与 STL 算法兼容,因为它满足 STL 容器的要求,如提供了迭代器。这使得我们可以方便地对 std::array 使用各种 STL 算法,例如排序、查找等。

std::array 的声明和初始化

  1. 默认初始化:可以像声明普通变量一样声明 std::array,此时数组元素会进行默认初始化。例如,对于 int 类型,默认初始化为 0。
std::array<int, 3> defaultInitArray; 
for (int i = 0; i < defaultInitArray.size(); ++i) {
    std::cout << defaultInitArray[i] << " "; 
}

输出结果为 0 0 0

  1. 列表初始化:使用花括号 {} 进行列表初始化,可以指定数组元素的值。初始化列表中的元素个数不能超过 std::array 的大小。
std::array<int, 4> listInitArray = {1, 2, 3, 4}; 

如果初始化列表中的元素个数小于 std::array 的大小,剩余元素将进行默认初始化。例如:

std::array<int, 5> partialListInitArray = {1, 2}; 
for (int i = 0; i < partialListInitArray.size(); ++i) {
    std::cout << partialListInitArray[i] << " "; 
}

输出结果为 1 2 0 0 0

  1. 聚合初始化:在 C++17 及以后,std::array 支持聚合初始化,允许省略等号 =
std::array<int, 3> aggregateInitArray {1, 2, 3}; 

std::array 的常用成员函数

  1. at():该函数用于访问指定位置的元素,并进行边界检查。如果索引越界,会抛出 std::out_of_range 异常。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
try {
    std::cout << arr.at(2) << std::endl; 
    std::cout << arr.at(10) << std::endl; 
} catch (const std::out_of_range& e) {
    std::cerr << "Out of range exception: " << e.what() << std::endl;
}
  1. operator[]:提供与传统 C 风格数组相同的方括号访问方式。它不进行边界检查,因此访问越界会导致未定义行为。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::cout << arr[2] << std::endl; 
// 下面这行代码访问越界,会导致未定义行为
// std::cout << arr[10] << std::endl; 
  1. front():返回数组的第一个元素。如果数组为空,调用 front() 会导致未定义行为。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::cout << arr.front() << std::endl; 
  1. back():返回数组的最后一个元素。同样,如果数组为空,调用 back() 会导致未定义行为。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::cout << arr.back() << std::endl; 
  1. data():返回一个指向数组首元素的指针。这使得 std::array 可以与需要 C 风格数组指针的函数进行交互。
std::array<int, 5> arr = {1, 2, 3, 4, 5};
int* ptr = arr.data();
for (int i = 0; i < arr.size(); ++i) {
    std::cout << ptr[i] << " "; 
}
  1. size():返回数组中元素的个数。这个值在编译时就确定,并且不会改变。
std::array<int, 5> arr;
std::cout << arr.size() << std::endl; 
  1. empty():检查数组是否为空。对于 std::array,除非其大小为 0,否则永远不会为空。
std::array<int, 5> arr;
std::cout << (arr.empty()? "Empty" : "Not Empty") << std::endl; 
std::array<int, 0> emptyArr;
std::cout << (emptyArr.empty()? "Empty" : "Not Empty") << std::endl; 

std::array 与 STL 算法

由于 std::array 支持迭代器,它可以与 STL 算法很好地配合使用。

  1. 排序:可以使用 std::sort 算法对 std::array 进行排序。
#include <algorithm>
#include <array>
#include <iostream>

int main() {
    std::array<int, 5> arr = {3, 1, 4, 1, 5};
    std::sort(arr.begin(), arr.end());
    for (int num : arr) {
        std::cout << num << " ";
    }
    return 0;
}
  1. 查找:使用 std::find 算法在 std::array 中查找元素。
#include <algorithm>
#include <array>
#include <iostream>

int main() {
    std::array<int, 5> arr = {1, 2, 3, 4, 5};
    auto it = std::find(arr.begin(), arr.end(), 3);
    if (it != arr.end()) {
        std::cout << "Element found at position: " << std::distance(arr.begin(), it) << std::endl;
    } else {
        std::cout << "Element not found" << std::endl;
    }
    return 0;
}

std::array 的嵌套使用

std::array 可以嵌套使用,形成多维数组。例如,创建一个二维数组:

std::array<std::array<int, 3>, 2> twoDArray = {
    {1, 2, 3},
    {4, 5, 6}
};
for (const auto& innerArray : twoDArray) {
    for (int num : innerArray) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
}

这里外层 std::array 的每个元素又是一个包含 3 个 intstd::array,从而形成了一个 2x3 的二维数组。

std::array 与传统 C 风格数组的比较

  1. 安全性:如前文所述,std::arrayat() 方法提供了边界检查,而传统 C 风格数组没有。这使得 std::array 在访问元素时更安全,减少了越界访问导致的错误。
  2. 功能std::array 提供了一系列成员函数,如 size()empty() 等,方便获取数组的信息。传统 C 风格数组则需要额外的代码来实现类似功能。
  3. 性能:在小尺寸数组的情况下,std::array 由于在栈上存储,性能与传统 C 风格数组相近甚至更好,因为避免了堆内存分配。但对于大尺寸数组,动态内存分配的 std::vector 可能更具优势,因为栈空间有限。
  4. 类型安全性std::array 是类型安全的,而传统 C 风格数组本质上是指针,容易出现类型错误,例如指针类型不匹配。

std::array 的内存布局

std::array 的内存布局与传统 C 风格数组类似,元素在内存中是连续存储的。这使得 std::array 在性能上有一定优势,特别是在需要顺序访问元素的场景下,因为连续的内存布局有利于提高缓存命中率。

例如,假设我们有一个 std::array<int, 10>,其元素在内存中是依次排列的,从第一个元素的地址开始,每个 int 类型元素占用 4 个字节(假设 int 为 4 字节),第二个元素的地址就是第一个元素地址加上 4 字节,以此类推。

这种连续的内存布局也使得 std::array 可以方便地与需要 C 风格数组指针的函数进行交互,通过 data() 方法获取的指针指向的内存布局与传统 C 风格数组完全一致。

std::array 在函数参数传递中的应用

  1. 按值传递:可以将 std::array 作为函数参数按值传递。此时函数会得到数组的一份拷贝。
void printArray(std::array<int, 5> arr) {
    for (int num : arr) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::array<int, 5> arr = {1, 2, 3, 4, 5};
    printArray(arr);
    return 0;
}

按值传递的缺点是如果数组较大,拷贝的开销会比较大。

  1. 按引用传递:为了避免拷贝开销,可以按引用传递 std::array
void modifyArray(std::array<int, 5>& arr) {
    for (auto& num : arr) {
        num *= 2;
    }
}

int main() {
    std::array<int, 5> arr = {1, 2, 3, 4, 5};
    modifyArray(arr);
    for (int num : arr) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;
}

按引用传递可以直接修改原数组,同时避免了拷贝开销。

  1. 传递指针:还可以通过 data() 方法获取指针,将 std::array 以指针的形式传递给函数。这种方式在与需要 C 风格数组指针的函数交互时很有用。
void processArray(int* arr, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        arr[i] += 1;
    }
}

int main() {
    std::array<int, 5> arr = {1, 2, 3, 4, 5};
    processArray(arr.data(), arr.size());
    for (int num : arr) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
    return 0;
}

std::array 在模板元编程中的应用

std::array 在模板元编程中也有一定的应用。由于其大小在编译期确定,结合模板特化等技术,可以实现一些编译期的计算和操作。

例如,我们可以定义一个模板函数,计算 std::array 所有元素的和,并且在编译期完成计算。

template <typename T, size_t N>
constexpr T sumArray(const std::array<T, N>& arr) {
    T result = 0;
    for (size_t i = 0; i < N; ++i) {
        result += arr[i];
    }
    return result;
}

int main() {
    std::array<int, 5> arr = {1, 2, 3, 4, 5};
    constexpr int sum = sumArray(arr);
    std::cout << "Sum: " << sum << std::endl;
    return 0;
}

这里 sumArray 函数使用 constexpr 修饰,使得其可以在编译期执行,从而提高了程序的性能。

std::array 的局限性

  1. 大小固定:虽然固定大小在某些场景下是优点,但在需要动态改变数组大小的情况下,std::array 就无法满足需求,此时需要使用 std::vector 等动态大小的容器。
  2. 缺乏一些高级功能:与 std::vector 相比,std::array 缺乏一些高级功能,如自动内存管理策略的调整、更灵活的内存分配器支持等。

总结 std::array 的适用场景

  1. 已知固定大小的数组:当数组大小在编译期就确定,并且不会改变时,std::array 是一个很好的选择,例如表示 RGB 颜色值的数组(通常是 3 个元素)。
  2. 性能敏感的场景:对于小尺寸数组,由于 std::array 在栈上存储,避免了堆内存分配的开销,在性能敏感的场景下可以提高效率。
  3. 需要安全性和 STL 兼容性:如果既需要数组的安全性(如边界检查),又希望能方便地使用 STL 算法,std::array 能很好地满足这些需求。

综上所述,std::array 作为 C++ 标准库中的一个重要容器,在很多场景下都能发挥其独特的优势,合理使用 std::array 可以提高程序的安全性、性能和代码的可读性。在实际编程中,应根据具体需求选择合适的容器,以达到最佳的编程效果。