跳转至

嵌入式现代C++教程——std::array:编译期固定大小数组

你写嵌入式代码时,堆(heap)常常像个不可靠的室友:随时可能把屋子掀翻。std::array 就像那位稳重但不多话的朋友——编译期确定大小、占栈或静态存储、没有动态分配,性能可预测,语义清楚。我们的一个重点就是——他跟传统的C风格数组比起来,怎么样的事情。


什么是 std::array

std::array<T, N> 是一个封装了 C 风格数组的轻量类模板:大小 N 在编译期就确定,提供 STL 风格的接口(.size().begin().data()operator[]、迭代器等),且通常不会比原始数组多多少运行开销。


为什么在嵌入式喜欢它

  • 零动态分配:没有 new / malloc,适合无堆或受限内存环境。
  • 可预测内存布局:编译期大小、连续存储,方便用于 DMA、裸指针接口。
  • STL 友好:可以直接传给算法(std::sortstd::fill)和容器适配器。
  • constexpr 支持:可以用作编译期查表或常量数据。
  • 类型安全与自文档化std::array<uint8_t, 128> 明确表达意图,比 uint8_t buf[128] 更现代。

基本用法(代码示例)

#include <array>
#include <algorithm>
#include <cstdint>
#include <iostream>

int main() {
    std::array<uint8_t, 8> buf{}; // value-initialized -> all zeros
    buf[0] = 0xAA;
    buf.at(1) = 0x55; // .at 会做边界检查(抛异常)

    // 兼容 STL 算法
    std::fill(buf.begin(), buf.end(), 0xFF);

    // 传给 C API(不会隐式退化):使用 data()
    // c_function(buf.data(), buf.size());

    for (auto b : buf) std::cout << int(b) << ' ';
    std::cout << '\n';
}

小提醒:.at() 在异常被禁用或不可用的裸机环境下不适合;用 operator[] 并保持索引正确。


与 C 数组、std::vector 的比较

  • C 数组std::array 是包起来的类,支持 .size()、迭代器、std::get、结构化绑定,且能作为对象被拷贝/赋值。底层仍是连续内存。
  • std::vectorvector 可动态调整大小(需堆),std::array 无堆、大小固定、开销更小、语义更明确,嵌入式通常更倾向 std::array

常见技巧和细节(嵌入式角度)

1. 放静态区还是栈上?

  • 小数组(几十、几百字节)可放在栈上。注意任务/ISR 的栈深度限制。
  • 较大数组应放为 static 或放在 .bss,或放入只读闪存(constexpr 数据)以节省 RAM。

示例:

static std::array<uint8_t, 1024> big_buf; // 在 .bss,程序启动后分配

2. 用于 DMA / 外设

因为 std::array 保证连续内存,你可以安全地传 arr.data() 给 DMA 或 HAL。但确保元素类型是 可复制且没有需要特殊构造的复杂类型(一般使用 POD 或 trivial 类型)。

3. 编译期表与 constexpr

std::array 可用于编译期常量查表(免运行时初始化):

#include <array>

constexpr std::array<int, 5> make_table() {
    return {0, 1, 4, 9, 16};
}

constexpr auto table = make_table(); // 存在于只读段,可放进 flash
static_assert(table[3] == 9);

如果你需要在编译期生成一个更复杂的表,可以配合 std::index_sequence 做元编程(不赘述复杂实现,这里先给出思路:用 index_sequence 展开索引并在 constexpr 函数中产生元素`)。

4. 结构化绑定与 std::get

std::array 支持 std::get<0>(arr) 和结构化绑定(C++17):

std::array<int, 3> a = {1,2,3};
auto [x,y,z] = a; // nice for small fixed tuples

5. 避免退化为指针的陷阱

C 风格数组在传参时会退化为指针,而 std::array 不会,你必须明确传 .data().size()

void c_api(uint8_t* p, size_t n);
std::array<uint8_t, 16> arr;
c_api(arr.data(), arr.size());

6. 与裸机异常策略的兼容性

某些嵌入式编译链把异常支持关掉,这会影响 .at()(抛异常)的使用。建议在无异常环境下只用 operator[] 并在编译期/开发期做边界检查工具。


高级话题:当元素不是 POD 时

std::array<T, N> 的元素可以是任意类型 T。但在嵌入式里常见注意点:

  • 如果 T 有复杂构造/析构,静态初始化(尤其零初始化)行为会不同,要保证构造成本被接受。
  • 对于需要通过 DMA 读写的缓冲区,T 应该是 trivially copyable。

可以一试——把 std::array 用作编译期 CRC 表

#include <array>
#include <cstdint>

constexpr std::array<uint32_t, 256> make_crc_table() {
    std::array<uint32_t, 256> t{};
    for (size_t i = 0; i < 256; ++i) {
        uint32_t crc = static_cast<uint32_t>(i);
        for (int j = 0; j < 8; ++j)
            crc = (crc & 1) ? (0xEDB88320u ^ (crc >> 1)) : (crc >> 1);
        t[i] = crc;
    }
    return t;
}

constexpr auto crc_table = make_crc_table(); // 编译期计算,放到只读段(若编译器支持)

在支持的工具链上,这样可以把查表数据放进 flash,节省 RAM。

查看完整可编译示例
// basic_usage.cpp - std::array 基本用法演示
#include <array>
#include <algorithm>
#include <cstdint>
#include <iostream>

int main() {
    // value-initialized -> all zeros
    std::array<uint8_t, 8> buf{};
    buf[0] = 0xAA;
    // .at 会做边界检查(抛异常)- 裸机环境慎用
    buf.at(1) = 0x55;

    // 兼容 STL 算法
    std::fill(buf.begin(), buf.end(), 0xFF);

    // 范围 for 遍历
    for (auto b : buf) std::cout << int(b) << ' ';
    std::cout << '\n';

    // 其他有用的成员函数
    std::cout << "size: " << buf.size() << '\n';
    std::cout << "front: " << int(buf.front()) << '\n';
    std::cout << "back: " << int(buf.back()) << '\n';

    // 填充为不同值
    std::fill_n(buf.begin(), 4, 0x11);
    std::fill_n(buf.begin() + 4, 4, 0x22);

    for (auto b : buf) std::cout << std::hex << int(b) << ' ';
    std::cout << std::dec << '\n';

    return 0;
}
查看更多示例:静态存储、DMA、编译期查表等
// static_storage.cpp - 演示静态存储与栈存储的选择
#include <array>
#include <cstdint>
#include <iostream>

// 小数组 - 可以放在栈上
void stack_example() {
    const int stack_size = 64;
    std::array<uint8_t, stack_size> small_buf{};
    small_buf[0] = 0x42;
    std::cout << "Stack array at: " << static_cast<void*>(small_buf.data()) << '\n';
}

// 大数组 - 应放在静态区
static std::array<uint8_t, 1024> big_buf;  // 在 .bss,程序启动后分配

// 只读数据 - 可以放 flash(使用 constexpr)
constexpr std::array<uint8_t, 16> readonly_data = {
    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
    0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F
};

int main() {
    stack_example();

    std::cout << "Static array at: " << static_cast<void*>(big_buf.data()) << '\n';
    std::cout << "Readonly array at: " << static_cast<const void*>(readonly_data.data()) << '\n';

    // 静态数组在程序启动时自动零初始化
    bool all_zero = true;
    for (auto b : big_buf) {
        if (b != 0) {
            all_zero = false;
            break;
        }
    }
    std::cout << "Big array all zero: " << (all_zero ? "yes" : "no") << '\n';

    return 0;
}
// dma_usage.cpp - 演示 std::array 在 DMA 场景的使用
#include <array>
#include <cstdint>
#include <iostream>

// 模拟 HAL 层的 DMA 传输函数
void hal_dma_transmit(const uint8_t* data, size_t length) {
    std::cout << "DMA transmitting " << length << " bytes from " << static_cast<const void*>(data) << ":\n";
    for (size_t i = 0; i < length; ++i) {
        std::cout << std::hex << int(data[i]) << ' ';
        if ((i + 1) % 16 == 0) std::cout << '\n';
    }
    std::cout << std::dec << "\n";
}

// 模拟 HAL 层的 DMA 接收函数
void hal_dma_receive(uint8_t* buffer, size_t length) {
    std::cout << "DMA receiving " << length << " bytes to " << static_cast<void*>(buffer) << '\n';
    // 模拟接收数据
    for (size_t i = 0; i < length; ++i) {
        buffer[i] = static_cast<uint8_t>(i * 2);
    }
}

int main() {
    // DMA 发送场景
    std::array<uint8_t, 32> tx_buffer{};
    for (size_t i = 0; i < tx_buffer.size(); ++i) {
        tx_buffer[i] = static_cast<uint8_t>(i);
    }

    // std::array 保证连续内存,可以安全地传给 DMA
    hal_dma_transmit(tx_buffer.data(), tx_buffer.size());

    // DMA 接收场景
    std::array<uint8_t, 64> rx_buffer{};
    hal_dma_receive(rx_buffer.data(), rx_buffer.size());

    // 验证接收的数据
    std::cout << "First 8 bytes received: ";
    for (size_t i = 0; i < 8; ++i) {
        std::cout << std::hex << int(rx_buffer[i]) << ' ';
    }
    std::cout << std::dec << '\n';

    // 注意:用于 DMA 的数组元素类型应该是 trivially copyable
    static_assert(std::is_trivially_copyable_v<uint8_t>,
                  "DMA buffer type must be trivially copyable");

    return 0;
}
// constexpr_table.cpp - 编译期查表示例
#include <array>
#include <iostream>

// 编译期生成平方表
constexpr std::array<int, 10> make_square_table() {
    std::array<int, 10> table{};
    for (size_t i = 0; i < table.size(); ++i) {
        table[i] = static_cast<int>(i * i);
    }
    return table;
}

// 表在编译期计算,存储在只读段(若编译器支持)
constexpr auto square_table = make_square_table();

// 编译期断言 - 在编译期验证表内容
static_assert(square_table[0] == 0, "Square of 0 should be 0");
static_assert(square_table[5] == 25, "Square of 5 should be 25");
static_assert(square_table[9] == 81, "Square of 9 should be 81");

// 使用查表的函数(编译期优化后可能直接内联常量)
int get_square(int n) {
    if (n >= 0 && n < 10) {
        return square_table[n];
    }
    return -1;  // 错误值
}

int main() {
    std::cout << "Square table (computed at compile time):\n";
    for (size_t i = 0; i < square_table.size(); ++i) {
        std::cout << i << "^2 = " << square_table[i] << '\n';
    }

    std::cout << "\nLookup results:\n";
    for (int i = 0; i < 10; ++i) {
        std::cout << "get_square(" << i << ") = " << get_square(i) << '\n';
    }

    return 0;
}
// constexpr_crc_table.cpp - 编译期 CRC 表生成
#include <array>
#include <cstdint>
#include <iostream>

// 编译期生成 CRC32 查找表
constexpr std::array<uint32_t, 256> make_crc_table() {
    std::array<uint32_t, 256> table{};
    for (size_t i = 0; i < 256; ++i) {
        uint32_t crc = static_cast<uint32_t>(i);
        for (int j = 0; j < 8; ++j) {
            crc = (crc & 1) ? (0xEDB88320u ^ (crc >> 1)) : (crc >> 1);
        }
        table[i] = crc;
    }
    return table;
}

// 表在编译期计算,存储在只读段
constexpr auto crc_table = make_crc_table();

// 编译期验证表中的几个关键值
static_assert(crc_table[0] == 0x00000000, "CRC table[0] mismatch");
static_assert(crc_table[1] == 0x77073096, "CRC table[1] mismatch");
static_assert(crc_table[255] == 0x2D02EF8D, "CRC table[255] mismatch");

// 计算 CRC32 校验和
uint32_t compute_crc(const uint8_t* data, size_t length) {
    uint32_t crc = 0xFFFFFFFFu;
    for (size_t i = 0; i < length; ++i) {
        crc = crc_table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
    }
    return crc ^ 0xFFFFFFFFu;
}

int main() {
    std::cout << "CRC32 Table (first 16 entries, computed at compile time):\n";
    for (size_t i = 0; i < 16; ++i) {
        std::cout << "table[" << i << "] = 0x" << std::hex << crc_table[i] << std::dec << '\n';
    }

    // 计算 CRC32 示例
    const char* test_data = "Hello, World!";
    uint32_t crc = compute_crc(reinterpret_cast<const uint8_t*>(test_data), 13);

    std::cout << "\nCRC32 of \"" << test_data << "\" = 0x" << std::hex << crc << std::dec << '\n';

    return 0;
}
// structured_binding.cpp - 演示 std::get 和结构化绑定
#include <array>
#include <iostream>
#include <utility>  // for std::tuple_size, std::tuple_element

int main() {
    // 基本结构化绑定 (C++17)
    std::array<int, 3> a = {1, 2, 3};
    auto [x, y, z] = a;
    std::cout << "x=" << x << ", y=" << y << ", z=" << z << '\n';

    // std::get<I>(array) 访问
    std::cout << "std::get<0>(a) = " << std::get<0>(a) << '\n';
    std::cout << "std::get<1>(a) = " << std::get<1>(a) << '\n';
    std::cout << "std::get<2>(a) = " << std::get<2>(a) << '\n';

    // 编译期索引访问(模板元编程友好)
    template<size_t I>
    int get_element(const std::array<int, 3>& arr) {
        return std::get<I>(arr);
    }

    std::cout << "get_element<0>(a) = " << get_element<0>(a) << '\n';

    // tuple_size 和 tuple_element 支持
    using ArrayType = std::array<int, 3>;
    constexpr size_t size = std::tuple_size_v<ArrayType>;
    using ElementType = std::tuple_element_t<0, ArrayType>;

    std::cout << "Array size (via tuple_size): " << size << '\n';
    std::cout << "Element type is int: " << std::is_same_v<ElementType, int> << '\n';

    // 结构化绑定用于解构返回值
    auto get_coordinates() -> std::array<double, 3> {
        return {1.5, 2.5, 3.5};
    }

    auto [px, py, pz] = get_coordinates();
    std::cout << "Point: (" << px << ", " << py << ", " << pz << ")\n";

    return 0;
}
// c_api_compatible.cpp - 演示 std::array 与 C API 的兼容性
#include <array>
#include <cstdint>
#include <cstring>
#include <iostream>

// 模拟典型的 C API
extern "C" {
    // 传统 C 风格 API
    void c_api_process(uint8_t* buffer, size_t length);

    // const 正确的 C API
    void c_api_read_only(const uint8_t* data, size_t length);
}

void c_api_process(uint8_t* buffer, size_t length) {
    std::cout << "C API processing " << length << " bytes:\n";
    for (size_t i = 0; i < length; ++i) {
        buffer[i] = static_cast<uint8_t>(buffer[i] * 2);
    }
}

void c_api_read_only(const uint8_t* data, size_t length) {
    std::cout << "C API reading " << length << " bytes:\n";
    for (size_t i = 0; i < length; ++i) {
        std::cout << std::hex << int(data[i]) << ' ';
        if ((i + 1) % 16 == 0) std::cout << '\n';
    }
    std::cout << std::dec << '\n';
}

int main() {
    // C 风格数组隐式退化为指针
    uint8_t c_array[16] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
    c_api_process(c_array, 16);

    // std::array 不会隐式退化,必须使用 .data()
    std::array<uint8_t, 16> cpp_array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

    // 正确方式:使用 .data() 和 .size()
    c_api_process(cpp_array.data(), cpp_array.size());

    // 只读访问:.data() 返回指向元素的指针
    c_api_read_only(cpp_array.data(), cpp_array.size());

    // 对比:std::vector 也使用相同模式
    // std::vector<uint8_t> vec(16, 42);
    // c_api_process(vec.data(), vec.size());

    std::cout << "After processing:\n";
    for (size_t i = 0; i < cpp_array.size(); ++i) {
        std::cout << "cpp_array[" << i << "] = " << int(cpp_array[i]) << '\n';
    }

    return 0;
}