在 C++ 编程中,我们大多数时候都会直接使用 std::vector 来创建数组,因为 vector 支持动态扩容、封装完善、性能优秀。
但在某些情况下,我们只需要一个静态大小的数组,而此时使用 vector 反而会引入额外的堆内存分配开销,不如直接在栈上分配的静态数组高效。

这时候,一个轻量的 Array 模板类就派上用场了。
本文将从 API设计 → 实现 → 示例 → 原理分析 全流程,手把手教你写出属于你自己的 Array 类。


一、为什么要自己实现 Array?

在需要一个固定大小的数组时:

  • std::vector 会在堆上分配内存;

  • Array 可以直接在栈上分配,效率更高;

  • 模板参数让我们在编译期就能确定大小。

因此,在某些场合(如嵌入式系统、实时渲染、性能关键代码)中,手写一个 Array 是非常有用的。


二、Array 数组要实现的功能(API 设计)

我们先看一眼它应该提供的接口和能力:

功能 描述
支持创建指定类型与大小的数组 例如:Array<int, 5>
支持下标运算符 [] 可以读写元素
Size() 返回数组大小 且在编译期可确定
Data() 返回原始数据指针 可用于与 C 风格接口兼容(如 memset
兼容 const 对象 支持 const 修饰的版本

我们直接用一个小例子来展示目标效果:

int main() {
    constexpr int size = 5;
    Array<int, size> data;

    static_assert(data.Size() < 10, "Size is too large");

    data[0] = 2;
    data[1] = 3;
    data[2] = 5;
    for (size_t i = 0; i < data.Size(); ++i)
        std::cout << data[i] << std::endl;

    std::cout << "-----------------" << std::endl;

    memset(data.Data(), 0, data.Size() * sizeof(int));
    for (size_t i = 0; i < data.Size(); ++i)
        std::cout << data[i] << std::endl;

    std::cout << "-----------------" << std::endl;

    Array<std::string, size> data2;
    data2[0] = "Cherno";
    data2[1] = "C++";
    for (size_t i = 0; i < data2.Size(); ++i)
        std::cout << data2[i] << std::endl;

    return 0;
}

输出结果如下:

2
3
5
0
336165216
-----------------
0
0
0
0
0
-----------------
Cherno
C++

三、Array 模板类完整实现

#include <iostream>
#include <cstring>

template<typename T, size_t S>
class Array
{
private:
    T m_Data[S]; // 内部固定大小数组

public:
    // 返回数组大小(constexpr 确保在编译期即可求值)
    constexpr size_t Size() const { return S; }

    // 下标运算符重载(支持读写)
    T& operator[](size_t index) { return m_Data[index]; }
    const T& operator[](size_t index) const { return m_Data[index]; }

    // 获取底层原始数据指针
    T* Data() { return m_Data; }
    const T* Data() const { return m_Data; }
};

四、关键特性与设计细节解析

1. const 成员函数的双版本设计

你可能注意到:

T& operator[](size_t index);
const T& operator[](size_t index) const;

为什么要写两个版本?
因为我们希望:

  • Array 是非常量对象时,可以修改元素;

  • Array 是常量对象时,只能读取而不能修改。

例如:

const Array<int, 5> arr;
arr[0] = 10; // 错误:返回 const 引用,禁止修改

2. constexpr 的使用

我们希望在编译期检查数组大小,例如:

static_assert(data.Size() < 10, "Size too large!");

这要求 Size() 在编译时即可求值。
因此我们加上 constexpr

constexpr size_t Size() const { return S; }

这样编译器在编译期即可使用该值,而无需运行时计算。


3. Data() 与 memset 的使用

Data() 方法返回数组首地址:

T* Data() { return m_Data; }

这样我们可以方便地使用 memset 等 C 风格函数:

memset(data.Data(), 0, data.Size() * sizeof(int));

注意事项:

  • 对于内置类型(如 intfloat)使用 memset 是安全的;

  • 对于复杂类型(如 std::string),使用 memset 会导致未定义行为。


五、完整示例(含注释)

#include <iostream>   // 引入标准输入输出头文件
#include <cstring>    // 引入 C 风格内存操作函数,如 memset

// 定义一个模板类 Array
// 模板参数:
// T 表示存储的数据类型(如 int、float、std::string 等)
// S 表示数组的固定大小(在编译期确定)
template<typename T, size_t S>
class Array
{
private:
    // 在类内部定义一个固定大小的 C 风格数组,用于存储数据
    T m_Data[S];

public:
    // 返回数组大小
    // constexpr 关键字表示这个函数可以在编译期求值
    constexpr size_t Size() const { return S; }

    // 下标运算符重载(非 const 版本)
    // 当 Array 是普通对象时,通过该函数可以修改数组元素
    T& operator[](size_t index) { return m_Data[index]; }

    // 下标运算符重载(const 版本)
    // 当 Array 是常量对象时,只能读取元素,不能修改
    const T& operator[](size_t index) const { return m_Data[index]; }

    // 返回底层数据的首地址(非 const 版本)
    // 可与 C 风格函数(如 memset、memcpy)兼容
    T* Data() { return m_Data; }

    // 返回底层数据的首地址(const 版本)
    const T* Data() const { return m_Data; }
};

// 主函数示例
int main()
{
    // 创建一个存储 5 个 int 类型元素的 Array 对象
    Array<int, 5> data;

    // 使用传统的 C 风格方式,通过 &data[0] 获取数组首地址
    // memset 用于将内存的前 n 个字节设置为指定的值
    // 这里将整个数组内容清零(每个 int 的所有字节都置 0)
    memset(&data[0], 0, data.Size() * sizeof(int));

    // 等价写法(推荐使用 Data())
    // 使用类中提供的 Data() 方法更符合现代 C++ 风格
    // memset(data.Data(), 0, data.Size() * sizeof(int));

    // 遍历并输出数组内容
    // 因为上面使用 memset 清零了内存,所以输出结果都是 0
    for (int i = 0; i < data.Size(); i++)
        std::cout << data[i] << std::endl;

    // 程序结束
    return 0;
}

运行结果:

0
0
0
0
0

constexpr 的作用

constexpr 是 C++11 引入的关键字,用于表示“可以在编译期计算的常量表达式”。
它与 const 的区别在于:const 只是“只读变量”,而 constexpr 要求值必须在编译时就能确定

在我们的 Array 类中:

constexpr size_t Size() const { return S; }

这意味着:

  • 支持编译期检查
    比如下面的代码只有在 Size() 是编译期常量时才合法:

    static_assert(data.Size() < 10, "Size is too large");

    这能在编译阶段直接发现错误,而不是等到运行时。

  • 提高执行效率
    编译器在编译时就能计算出返回值,不需要在运行时再执行函数。

  • 符合现代 C++ 的设计理念
    对于确定不变的值(如固定大小数组的长度),用 constexpr 明确表达“这是编译期常量”,可读性更高,也更安全。

简单来说:

constexpr = 可以在编译期求值的 const
编译器在编译时就能“算出它的值”,而 const 只是告诉编译器“运行时别改它”。


六、memset 的类型安全警告

当模板参数为 std::string 等非平凡类型时,下面代码是危险的:

Array<std::string, 5> data;
memset(data.Data(), 0, data.Size() * sizeof(std::string)); // 未定义行为

此时应使用:

for (auto& s : data)
    s = T{}; // 安全地重置对象

七、Array 与 vector 的区别总结

特性 Array<T, N> std::vector<T>
内存分配 栈上 堆上
大小 编译期固定 运行期可变
初始化 自动构造元素 自动构造元素
性能 无动态分配,快 动态分配,稍慢
适用场景 固定容量、高性能 动态变化、通用应用

八、总结

  • 当你需要固定大小、高性能、无需扩容的数组时,使用 Array 是更好的选择;

  • 当你需要动态扩容时,使用 std::vector

  • Array 的实现非常简单,只需掌握模板、constexpr、引用返回即可完成。


九、结语

Array 看似简单,但却体现了 C++ 模板、内存管理和类型系统的核心思想。
理解它,不仅能让你写出更高效的代码,也能帮助你更深入地理解 STL 容器的底层原理。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐