Chương 10. Mảng, chuỗi và std::span
Mục tiêu
- Dùng
std::arraythay mảng thô khi độ dài biết trước, vàstd::spankhi cần một "cửa sổ" nhìn vào dữ liệu. - Viết bảng tra và thuật toán CRC tính sẵn lúc biên dịch.
- Xây một chuỗi ký tự dung lượng cố định, không cấp phát động, thay cho
std::stringtrên MCU nhỏ.
Câu chuyện: std::string trên vi điều khiển 8 KB RAM
Một lập trình viên quen lập trình ứng dụng chuyển sang viết firmware, dùng std::string để ghép log. Chương trình chạy được vài giờ rồi treo. Nguyên nhân: std::string cấp phát động; việc ghép chuỗi liên tục làm phân mảnh heap trên một vùng nhớ chỉ vài KB, tới lúc không còn khối nào đủ lớn thì cấp phát thất bại — và mặc định std::string/new khi thất bại sẽ ném ngoại lệ hoặc gọi std::terminate, thường bị tắt bằng -fno-exceptions nên chương trình dừng đột ngột. Giải pháp không phải "không dùng C++" mà là chọn đúng công cụ: chuỗi dung lượng cố định.
Kiến thức
1. std::array<T, N>: mảng có kiểu, biết độ dài
So với mảng thô T arr[N], std::array không tốn thêm byte nào nhưng có size(), hoạt động với thuật toán chuẩn, và không suy biến thành con trỏ khi truyền vào hàm — nên hàm nhận đúng std::array<T, N>& biết chính xác N, còn mảng thô truyền vào hàm suy biến thành T*, mất thông tin độ dài.
2. std::span<T>: cửa sổ nhìn vào dữ liệu
std::span (C++20) là một cặp (con trỏ, độ dài) — không sở hữu, không sao chép dữ liệu. Nó thay thế mẫu (const uint8_t* p, size_t n) bằng một tham số mang cả hai, và còn nhận được từ mảng thô, std::array, hoặc một phần của chúng.
3. CRC-8 với bảng tra sinh lúc biên dịch, nhận std::span
// CRC-8 (đa thức 0x07) với bảng tra sinh lúc biên dịch, nhận dữ liệu qua std::span.
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <span>
constexpr std::array<std::uint8_t, 256> make_crc8_table() {
std::array<std::uint8_t, 256> t{};
for (unsigned i = 0; i < 256; ++i) {
std::uint8_t crc = static_cast<std::uint8_t>(i);
for (int bit = 0; bit < 8; ++bit)
crc = (crc & 0x80) ? static_cast<std::uint8_t>((crc << 1) ^ 0x07) : static_cast<std::uint8_t>(crc << 1);
t[i] = crc;
}
return t;
}
constexpr auto kCrcTable = make_crc8_table();
constexpr std::uint8_t crc8(std::span<const std::uint8_t> data) {
std::uint8_t crc = 0;
for (std::uint8_t b : data) crc = kCrcTable[crc ^ b];
return crc;
}
// Giá trị kiểm tra chuẩn của CRC-8 (poly 0x07, init 0) cho chuỗi "123456789" là 0xF4.
constexpr std::array<std::uint8_t, 9> kCheck{'1', '2', '3', '4', '5', '6', '7', '8', '9'};
static_assert(crc8(kCheck) == 0xF4, "bảng CRC sai");
static_assert(kCrcTable[1] == 0x07);
int main() {
const std::uint8_t frame[] = {0x01, 0x02, 0x03, 0x04};
std::printf("crc8(frame) = 0x%02X\n", static_cast<unsigned>(crc8(frame)));
std::printf("crc8(\"123456789\") = 0x%02X\n", static_cast<unsigned>(crc8(kCheck)));
// Cửa sổ con: bỏ byte đầu, không sao chép dữ liệu.
std::printf("crc8(frame[1..]) = 0x%02X\n", static_cast<unsigned>(crc8(std::span<const std::uint8_t>(frame).subspan(1))));
return 0;
}
crc8(frame) = 0xE3
crc8("123456789") = 0xF4
crc8(frame[1..]) = 0xF5
Ba điều đáng chú ý:
make_crc8_table()là hàmconstexpr: bảng 256 phần tử được tính lúc biên dịch, nằm trong Flash, không tốn chu kỳ CPU lúc chạy.static_assert(crc8(kCheck) == 0xF4, ...): giá trị kiểm tra chuẩn của thuật toán CRC-8/SMBUS (đa thức 0x07, khởi tạo 0) cho chuỗi"123456789"được xác nhận ngay lúc biên dịch. Nếu ai đó sửa sai bảng hoặc thuật toán, build sẽ lỗi trước khi chạy thử.crc8()nhậnstd::span<const std::uint8_t>: gọi được với mảng thô (crc8(frame), nhờ suy luận kiểu tự động dựng span) hoặc vớisubspan()để tính CRC trên một phần dữ liệu mà không sao chép.
Ghi chú về nguồn: giá trị kiểm tra
0xF4cho"123456789"là giá trị chuẩn thường dùng để xác nhận cách cài đặt CRC-8/SMBUS (đa thức 0x07); kiểm chứng lại với bảng CRC tham chiếu nếu bạn dùng biến thể khác (đa thức, giá trị khởi tạo, đảo bit khác nhau cho ra kết quả khác).
4. Chuỗi dung lượng cố định
// Chuỗi dung lượng cố định, không cấp phát động: thay cho std::string trên MCU.
#include <cstddef>
#include <cstdio>
#include <string_view>
template <std::size_t Capacity>
class StaticString {
public:
// Ghi đè bằng nội dung định dạng; cắt bớt nếu quá dài. Trả về false nếu bị cắt.
template <typename... Args>
bool format(const char* fmt, Args... args) {
int n = std::snprintf(buf_, sizeof buf_, fmt, args...);
return n >= 0 && static_cast<std::size_t>(n) < sizeof buf_;
}
[[nodiscard]] std::string_view view() const { return std::string_view(buf_); }
[[nodiscard]] const char* c_str() const { return buf_; }
static constexpr std::size_t capacity() { return Capacity; }
private:
char buf_[Capacity + 1] = {}; // +1 cho ký tự kết thúc
};
int main() {
StaticString<24> line;
bool ok = line.format("T=%d.%d C node=%u", 25, 3, 7u);
std::printf("[%s] ok=%d len=%zu\n", line.c_str(), ok, line.view().size());
StaticString<10> tiny;
ok = tiny.format("qua dai: %d", 123456); // bị cắt
std::printf("[%s] ok=%d\n", tiny.c_str(), ok);
std::string_view v = line.view().substr(0, 7); // không sao chép
std::printf("view=[%.*s]\n", static_cast<int>(v.size()), v.data());
return 0;
}
[T=25.3 C node=7] ok=1 len=15
[qua dai: 1] ok=0
view=[T=25.3 ]
Thiết kế đáng học:
- Bộ nhớ nằm bên trong đối tượng (
char buf_[Capacity + 1]), không cónew/heap; dung lượng cố định lúc biên dịch qua tham số template. format()dùngstd::snprintf— hàm C an toàn với bộ đệm, luôn dừng đúng chỗ, trả về độ dài sẽ cần (có thể lớn hơn bộ đệm) để phát hiện bị cắt.- Trả
std::string_viewđể đọc dữ liệu không sao chép;string_viewchỉ là (con trỏ, độ dài) nhưspan, nhưng dành riêng cho ký tự và có các hàm tiện lợi (substr...). StaticString<10>với nội dung dài hơn bị cắt vàformat()trảfalse— người gọi biết để xử lý, thay vì âm thầm mất dữ liệu.
Ví dụ thực tiễn: gộp bộ đệm nhận UART thành khung
Một bộ nhận UART tích luỹ byte vào std::array<uint8_t, 64> buf cùng biến đếm len; khi đủ khung (theo packet_parse ở chương 9) mới xử lý, rồi dùng std::span để đưa buf (chỉ phần đã nhận) cho hàm tính CRC — không sao chép, không cấp phát, kích thước tối đa cố định ngay từ lúc khai báo.
Góc nhúng
std::array<T, N>có kích thước đúng bằngN * sizeof(T), không có chi phí ẩn — kiểm chứng bằngsizeof.std::spanlà hai từ máy (con trỏ + kích thướcsize_t), thường truyền qua thanh ghi, không cấp phát.- Chọn
CapacitycủaStaticStringđủ dùng cho trường hợp xấu nhất bạn có thể liệt kê (ví dụ độ dài tối đa của một dòng log), không đoán chừng. - Bảng CRC 256 phần tử tốn 256 byte Flash cố định; nếu Flash rất hạn hẹp, có cách tính CRC theo bit không cần bảng (chậm hơn) — đánh đổi tốc độ lấy dung lượng.
Lỗi thường gặp
- Dùng
std::vector/std::stringmặc định trên MCU nhỏ mà không kiểm soát heap. - Truyền mảng thô vào hàm và dùng
sizeof(arr)/sizeof(arr[0])sau khi nó đã suy biến thành con trỏ trong tham số hàm (sizeoflúc đó trả về kích thước con trỏ, không phải mảng) — dùngstd::span/std::arrayđể tránh hẳn lớp lỗi này. - Quên rằng
std::string_view/std::spankhông sở hữu dữ liệu: nếu buffer gốc bị ghi đè hoặc hết phạm vi, view/span trỏ tới dữ liệu không còn hợp lệ. - Không kiểm tra giá trị trả về của
format()/snprintf, bỏ sót trường hợp bị cắt.
Bài tập
- Đổi đa thức CRC trong
make_crc8_table(ví dụ 0x1D thay vì 0x07) và tìm một chuỗi kiểm tra khác cho biến thể đó (đọc tài liệu CRC bạn định dùng); cập nhậtstatic_assert. - Viết
crc16(CCITT, đa thức 0x1021) theo cùng khuôn: bảngconstexpr, hàm nhậnstd::span<const std::uint8_t>. - Thêm hàm
append()vàoStaticStringđể nối thêm nội dung thay vì ghi đè; xử lý trường hợp không đủ chỗ. - Viết hàm
std::size_t count_non_zero(std::span<const std::uint8_t> data)và gọi nó với mộtstd::array, một mảng thô, và mộtsubspan. - Giải thích bằng ví dụ cụ thể vì sao trả về
std::string_viewtrỏ vào mộtStaticStringcục bộ trong hàm là lỗi; sửa lại cho đúng.
Tóm tắt
std::arraythay mảng thô khi độ dài cố định biết trước: không tốn thêm gì, an toàn kiểu hơn.std::spangộp (con trỏ, độ dài) thành một tham số, nhận từ nhiều nguồn, không sao chép.- Bảng tra và các bất biến thuật toán nên là
constexpr+static_assert, kiểm chứng ngay lúc biên dịch. - Chuỗi dung lượng cố định (template hoá theo
Capacity) thaystd::stringkhi cần tránh cấp phát động. string_view/spankhông sở hữu dữ liệu — chú ý vòng đời của dữ liệu gốc.