array
std::array 是在 C++11 中引入的一个容器模板,它封装了一个固定大小的数组。它结合了C风格数组的性能优势(静态分配、无额外开销)和STL容器的便利性与安全性。
1 |
|
- T: 存储的元素类型 (例如 int, double, std::string)。
- N: 数组的大小,必须是一个编译时常量。
主要特性
空间固定且内存连续:一旦声明之后大小就不能改变。这与 std::vector 不同,后者是动态大小的。并且, std::array<T, N> 的内存布局与 T[N] 完全相同。它不包含任何额外的元数据,比如虚函数表指针、大小变量等。它在内存中就是一块连续的、大小为 N * sizeof(T) 的空间。
栈上分配:和C风格数组一样,如果作为局部变量声明,它通常在栈上分配内存,速度非常快。
完整的容器接口:它表现得像一个标准的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 |
|
聚合类型 (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 传递给函数时,传递的是一个完整的对象,函数签名中包含了确切的大小信息。编译器可以检查类型匹配,不会再出现数组退化为指针、丢失大小信息的问题。