C++手写一个轻量级 Array 数组模板类
在 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));
注意事项:
-
对于内置类型(如
int、float)使用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 容器的底层原理。
更多推荐

所有评论(0)