array

ZaynPei Lv6

std::array 是在 C++11 中引入的一个容器模板,它封装了一个固定大小的数组。它结合了C风格数组的性能优势(静态分配、无额外开销)和STL容器的便利性与安全性。

1
2
#include <array>
std::array<T, N> array_name;
  • T: 存储的元素类型 (例如 int, double, std::string)。
  • N: 数组的大小,必须是一个编译时常量

主要特性

  1. 空间固定且内存连续:一旦声明之后大小就不能改变。这与 std::vector 不同,后者是动态大小的。并且, std::array<T, N> 的内存布局与 T[N] 完全相同。它不包含任何额外的元数据,比如虚函数表指针、大小变量等。它在内存中就是一块连续的、大小为 N * sizeof(T) 的空间。

  2. 栈上分配:和C风格数组一样,如果作为局部变量声明,它通常在栈上分配内存,速度非常快。

  3. 完整的容器接口:它表现得像一个标准的STL容器,提供了许多方便的成员函数:

    • at(pos): 访问指定位置的元素,会进行边界检查。如果越界,会抛出 std::out_of_range 异常。

    • operator[]: 访问指定位置的元素,不进行边界检查(为了性能,与C风格数组行为一致)。

    • front() / back(): 访问第一个/最后一个元素。

    • size() / max_size(): 返回数组的大小。

    • empty(): 检查数组是否为空 (对于 std::array 来说,如果 N > 0 则永远不为空)。

    • fill(value): 用一个指定的值填充整个数组

    • begin() / end(): 提供迭代器支持,可以轻松与STL算法(如 std::sort)配合使用。

底层实现

std::array 的底层实现非常简单,它在内部只包含一个公开的、C风格的普通数组作为其唯一的非静态数据成员。标准库围绕这个内置数组提供了一系列成员函数(如 size(), at(), begin() 等),以赋予它现代容器的行为和安全性。

这个设计的关键在于,所有这些“包装”工作都在编译时完成,几乎不会产生任何运行时的性能开销。

以下是一个简化的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <cstddef> // for size_t
#include <stdexcept> // for std::out_of_range
#include <algorithm> // for std::fill

template<typename T, std::size_t N>
struct MyArray {
// 核心:内部就是一个公开的C风格数组
// 命名为 _data 以避免与用户代码冲突(标准库实现有自己的命名规则)
T _data[N];

// --- 成员函数实现 ---

// size():返回大小。因为N是编译时常量,所以这个函数可以标记为 constexpr
constexpr std::size_t size() const noexcept {
return N;
}

// operator[]:直接访问内部数组,不进行边界检查
T& operator[](std::size_t index) noexcept {
return _data[index];
}
const T& operator[](std::size_t index) const noexcept {
return _data[index];
}

// at():访问内部数组,但带有边界检查
T& at(std::size_t index) {
if (index >= N) {
throw std::out_of_range("MyArray::at() index out of range");
}
return _data[index];
}
const T& at(std::size_t index) const {
if (index >= N) {
throw std::out_of_range("MyArray::at() index out of range");
}
return _data[index];
}

// begin() 和 end():返回指向内部数组的指针,实现迭代器支持
T* begin() noexcept {
return _data; // 或者 &_data[0]
}
const T* begin() const noexcept {
return _data;
}

T* end() noexcept {
return _data + N; // 指向数组末尾的后一个位置
}
const T* end() const noexcept {
return _data + N;
}

// fill():填充数组
void fill(const T& value) {
std::fill(begin(), end(), value);
}
};

// ------------------- 使用示例 -------------------
#include <iostream>

int main() {
MyArray<int, 5> arr = {1, 2, 3, 4, 5};
std::cout << "Size: " << arr.size() << std::endl;
arr[0] = 100;

for(int val : arr) { // 可以使用范围for循环,因为有 begin() 和 end()
std::cout << val << " ";
}
std::cout << std::endl;
}

聚合类型 (Aggregate Type) 与初始化

std::array 被设计成一个聚合类型。在C++中,聚合类型大致是指没有用户定义的构造函数没有私有或保护的非静态数据成员没有基类没有虚函数的类或结构体。

因为 std::array 内部只有一个公开的C风格数组 T _data[N];,它符合聚合类型的定义。

这使得我们可以使用大括号 {} 进行聚合初始化,就像初始化一个普通的C风格数组一样,非常直观。

1
std::array<int, 3> arr = {10, 20, 30}; // 直接初始化内部的C风格数组

零成本抽象 (Zero-Cost Abstraction)

这是 std::array 最重要的特性。这意味着你获得了更高的安全性(at())、便利性(size()、迭代器)和类型安全,在开启编译器优化后,其性能与手写的C风格数组代码完全相同

如何实现?下面是几个关键点:

  • size(): size() 函数返回的是模板参数 N,它是一个编译时常量。编译器在编译时就可以直接将 arr.size() 替换为具体数字(例如 5)。这个函数调用在最终的机器码中不存在(也就是说根本不存在这样一个size变量或者函数调用)

  • operator[], begin(), end(): 这些函数非常简单,通常只有一条返回语句。编译器可以轻易地进行内联 (inlining),即把函数调用替换为函数体本身。最终生成的汇编代码与直接操作C风格数组的指针或索引完全一样(没有额外的函数调用开销)。

  • 范围for循环: for (auto& element : arr)之所以能工作,是因为编译器会将其转换为基于迭代器的代码 for (auto it = arr.begin(); it != arr.end(); ++it)。由于 begin() 和 end() 会被内联,最终的循环代码与操作C风格数组的指针循环效率相同。

类型系统与模板元编程

td::array 的强大之处在于它将数组的大小 N 融入了类型系统。std::array<int, 5> 和 std::array<int, 10> 是完全不同的、不兼容的类型, 因为 N 是一个非类型模板参数 (non-type template parameter)。

非类型模板参数是指模板参数不是一个类型,而是一个值(如整数、枚举值、指针等)。在 std::array 中,N 是一个 std::size_t 类型的非类型模板参数,表示数组的大小。

这使得我们可以在编译时捕获许多错误。例如,试图将一个 std::array<int, 5> 赋值给 std::array<int, 10> 会导致编译错误,而不是运行时错误。

同时, 还可以防止数组退化:当把 std::array 传递给函数时,传递的是一个完整的对象,函数签名中包含了确切的大小信息。编译器可以检查类型匹配,不会再出现数组退化为指针、丢失大小信息的问题。