Skip to content
LIVE FEED

BlogNguyên tắc lập trình C/C++ — Từ cơ bản đến chuyên biệt

Bố cục bài viết

Nguyên tắc lập trình C/C++ — Từ cơ bản đến chuyên biệt (Embedded / RTOS / Viễn thông / An ninh mạng) Mục lục Tầng 1 — Nguyên tắc cơ bản (Nền móng bắt buộc) 1.1. Hiểu “Undefined Behavior” (UB) là gì và vì sao đáng sợ 1.2. Luôn khởi tạo biến 1.3. Một biến, một mục đích, một phạm vi nhỏ nhất có thể 1.4. Luôn kiểm tra giá trị trả về của hàm có thể thất bại 1.5. Biên dịch với cảnh báo (warning) ở mức cao nhất — và coi cảnh báo như lỗi Tầng 2 — Nguyên tắc chung (Áp dụng cho mọi dự án C/C++) 2.1. Quản lý bộ nhớ & tài nguyên RAII (Resource Acquisition Is Initialization) — nguyên tắc số 1 của C++ Rule of Zero / Rule of Five Con trỏ 2.2. An toàn kiểu dữ liệu (Type Safety) 2.3. Const-correctness (đánh dấu const mọi nơi có thể) 2.4. Xử lý lỗi (Error Handling) 2.5. Tối ưu hiệu năng — nhưng đúng cách 2.6. Đa luồng (Concurrency) — nếu dự án có dùng thread 2.7. Viết code dễ đọc (Readability) 2.8. Dùng chuẩn C++ hiện đại (Modern C++) 2.9. Công cụ nên dùng ngay từ đầu dự án Tầng 3 — Nguyên tắc chuyên biệt (Embedded / RTOS / Viễn thông / An ninh mạng) 3.1. Nguyên tắc nền tảng cho Embedded/RTOS Không cấp phát bộ nhớ động sau khi khởi tạo hệ thống Không dùng exception và RTTI trong code lõi thời gian thực Tính xác định (Determinism) là ưu tiên số một Ngắt (Interrupt Service Routine — ISR) Chuẩn MISRA C/C++ và AUTOSAR C++14 3.2. Bộ nhớ & phần cứng cấp thấp 3.3. An ninh mạng / An ninh viễn thông (Secure Coding) Chống tràn bộ đệm (Buffer Overflow) — lỗi bảo mật kinh điển và nguy hiểm nhất Xử lý dữ liệu nhạy cảm (khóa mã hóa, mật khẩu, token) So sánh dữ liệu bảo mật phải dùng constant-time comparison Không tự viết thuật toán mã hóa (crypto) Fail-safe / Fail-secure — nguyên tắc xử lý khi có lỗi Giao thức viễn thông cụ thể (SS7, Diameter, SIP, TLV, ASN.1…) 3.4. Kiểm thử bắt buộc cho hệ thống này 4. Quy tắc đặt tên biến, hàm, kiểu dữ liệu 4.1. Biến 4.2. Hàm 4.3. Type / Class / Struct / Enum 4.4. Macro 4.5. File 4.6. Nguyên tắc chung khi đặt tên (áp dụng cho mọi trường hợp trên) 5. Checklist review code Cơ bản (mọi dự án) Chung (dự án C++ hiện đại) Chuyên biệt (Embedded/RTOS/Viễn thông/An ninh mạng)

Trang này

Sunex 13/08/2026 · 22 phút đọc

Ngày tạo

13/08/2026

Cập nhật

Lượt xem

4

Đọc

22m

Nguyên tắc lập trình C/C++ — Từ cơ bản đến chuyên biệt (Embedded / RTOS / Viễn thông / An ninh mạng)

Tài liệu này dành cho người mới bắt đầu nhưng muốn viết code C/C++ theo chuẩn mực chuyên nghiệp: an toàn – tối ưu – dễ đọc. Đọc theo thứ tự: Phần 1 là nền móng bắt buộc phải nắm, Phần 2 là quy tắc áp dụng cho mọi dự án, Phần 3 dành riêng cho hệ thống nhúng/thời gian thực/viễn thông/an ninh mạng.


Mục lục

  1. Tầng 1 — Nguyên tắc cơ bản (Nền móng bắt buộc)
  2. Tầng 2 — Nguyên tắc chung (Áp dụng cho mọi dự án C/C++)
  3. Tầng 3 — Nguyên tắc chuyên biệt (Embedded / RTOS / Viễn thông / An ninh mạng)
  4. Quy tắc đặt tên biến, hàm, kiểu dữ liệu
  5. Checklist review code (in ra dán cạnh bàn)

Tầng 1 — Nguyên tắc cơ bản (Nền móng bắt buộc)

Đây là những điều bắt buộc phải hiểu trước khi viết bất kỳ dòng code C/C++ nào, vì C/C++ không có “lưới an toàn” như Python/Java — sai một chút là chương trình có thể crash hoặc chạy sai âm thầm.

1.1. Hiểu “Undefined Behavior” (UB) là gì và vì sao đáng sợ

Trong C/C++, có những hành vi mà ngôn ngữ không quy định kết quả — chương trình có thể chạy đúng, crash, hoặc cho kết quả sai tùy máy/tùy lần chạy. Đây là nguồn gốc của rất nhiều bug khó tìm nhất.

Các lỗi UB kinh điển cần tránh ngay từ đầu:

LỗiVí dụHậu quả
Truy cập mảng ngoài biênarr[10] khi mảng chỉ có 5 phần tửCrash, đọc/ghi đè dữ liệu khác, lỗ hổng bảo mật
Dùng biến chưa khởi tạoint x; printf("%d", x);Giá trị rác, không đoán trước được
Dùng con trỏ sau khi đã freeUse-after-freeCrash, có thể bị khai thác (exploit)
Dereference con trỏ nullptrint* p = nullptr; *p = 5;Crash ngay lập tức
Chia cho 0 với số nguyênint x = 5 / 0;UB (khác với float, sẽ ra inf)
Tràn số nguyên có dấu (signed overflow)INT_MAX + 1UB (không “quay vòng” một cách an toàn)

Nguyên tắc gốc: Nếu không chắc một hành vi có an toàn hay không, hãy tra cứu, đừng đoán mò hay tin vào “chạy được trên máy mình là được”.

1.2. Luôn khởi tạo biến

// Sai — biến x chứa giá trị rác
int x;

// Đúng
int x = 0;

1.3. Một biến, một mục đích, một phạm vi nhỏ nhất có thể

Khai báo biến càng gần nơi sử dụng càng tốt, không khai báo hàng loạt biến ở đầu hàm rồi dùng rải rác — dễ nhầm lẫn và khó theo dõi vòng đời biến.

1.4. Luôn kiểm tra giá trị trả về của hàm có thể thất bại

FILE* f = fopen("data.txt", "r");
if (f == NULL) {
    // xử lý lỗi, không được bỏ qua!
    return -1;
}

Áp dụng cho: malloc, fopen, hàm đọc/ghi mạng, hàm parse dữ liệu… Rất nhiều lỗi bảo mật nghiêm trọng bắt nguồn từ việc bỏ qua giá trị lỗi trả về.

1.5. Biên dịch với cảnh báo (warning) ở mức cao nhất — và coi cảnh báo như lỗi

g++ -Wall -Wextra -Wpedantic -Werror -std=c++20 main.cpp

Đây là thói quen quan trọng bậc nhất cho người mới: trình biên dịch thường “nhìn thấy” lỗi tiềm ẩn trước khi bạn kịp chạy chương trình.


Tầng 2 — Nguyên tắc chung (Áp dụng cho mọi dự án C/C++)

Khi đã nắm vững Tầng 1, đây là các nguyên tắc kinh điển giúp code an toàn, tối ưu, và dễ đọc ở quy mô dự án thực tế.

2.1. Quản lý bộ nhớ & tài nguyên

RAII (Resource Acquisition Is Initialization) — nguyên tắc số 1 của C++

Ý tưởng: gắn tài nguyên (bộ nhớ, file, khóa mutex…) vào vòng đời của một object. Object được tạo ra thì tài nguyên được cấp; object bị hủy (ra khỏi scope) thì tài nguyên tự động được giải phóng — không cần bạn nhớ gọi free/close thủ công.

// Cách cũ (C-style) — dễ quên free, dễ leak khi có exception
int* arr = new int[100];
// ... nếu có lỗi/exception ở đây, arr sẽ bị leak
delete[] arr;

// Cách RAII (khuyến nghị)
std::vector<int> arr(100); // tự động giải phóng khi ra khỏi scope
  • Không dùng new/delete thô trong code nghiệp vụ. Dùng std::unique_ptr, std::shared_ptr, std::vector, std::string.
  • std::unique_ptr: sở hữu tài nguyên duy nhất, không copy được (chỉ move). Dùng khi chỉ một nơi “sở hữu” đối tượng.
  • std::shared_ptr: nhiều nơi cùng sở hữu, tự đếm tham chiếu. Dùng khi thực sự cần chia sẻ quyền sở hữu (cẩn thận vòng lặp tham chiếu → dùng std::weak_ptr để phá vòng lặp).

Rule of Zero / Rule of Five

  • Rule of Zero: Nếu class của bạn không trực tiếp quản lý tài nguyên thô (chỉ chứa các thành viên là std::vector, std::string, smart pointer…), đừng viết destructor/copy/move — để compiler tự sinh đúng và đủ.
  • Rule of Five: Nếu class quản lý tài nguyên thô (con trỏ raw, file handle…), phải định nghĩa đủ cả 5: destructor, copy constructor, copy assignment, move constructor, move assignment (hoặc đánh dấu = delete để cấm nếu không hợp lý).

Con trỏ

  • Ưu tiên tham chiếu (&) hoặc smart pointer hơn con trỏ thô (raw pointer).
  • Con trỏ thô chỉ nên dùng để “quan sát” (không sở hữu), không bao giờ dùng nó để sở hữu tài nguyên cần giải phóng.
  • Không bao giờ trả về con trỏ/tham chiếu tới biến cục bộ (local) — biến đó đã “chết” khi hàm kết thúc.
// SAI — con trỏ trỏ đến vùng nhớ đã bị hủy
int* create() {
    int x = 5;
    return &x; // x là biến local, sẽ bị hủy khi hàm return
}

// ĐÚNG
std::unique_ptr<int> create() {
    return std::make_unique<int>(5);
}

2.2. An toàn kiểu dữ liệu (Type Safety)

  • Không dùng ép kiểu kiểu C (int)x. Dùng các cast của C++ vì mỗi loại có mục đích rõ ràng, dễ tìm kiếm khi review code:
    • static_cast<T>(): ép kiểu thông thường, an toàn khi kiểu tương thích.
    • dynamic_cast<T>(): ép kiểu đa hình (polymorphism), có kiểm tra runtime.
    • const_cast<T>(): bỏ const (hiếm khi cần, cẩn trọng).
    • reinterpret_cast<T>(): ép kiểu cấp thấp (bit pattern), rủi ro cao, dùng tối thiểu.
  • Dùng enum class thay vì enum thường — tránh tự động chuyển đổi ngầm sang int và tránh xung đột tên giữa các enum khác nhau.
  • Dùng constexpr/const thay vì macro #define cho hằng số — vì có kiểm tra kiểu, dễ debug hơn.
// Không tốt
#define MAX_SIZE 100

// Tốt hơn
constexpr int kMaxSize = 100;

2.3. Const-correctness (đánh dấu const mọi nơi có thể)

  • Biến không đổi giá trị → khai báo const.
  • Tham số truyền vào hàm nhưng không sửa đổi bên trong → dùng const T& (tránh copy tốn kém với object lớn, đồng thời thông báo rõ ý định “hàm này không sửa dữ liệu của bạn”).
  • Method của class không làm thay đổi trạng thái object → đánh dấu const.
class Buffer {
public:
    size_t size() const { return size_; } // không sửa gì, nên const
    void print(const std::string& name) const; // tham số const&, method const
private:
    size_t size_;
};

Lợi ích: compiler tối ưu tốt hơn, code tự “kể” ý định của người viết, và compiler sẽ báo lỗi ngay nếu bạn lỡ tay sửa nhầm giá trị không nên sửa.

2.4. Xử lý lỗi (Error Handling)

  • Exception dùng cho lỗi thực sự ngoại lệ, không dùng để điều khiển luồng chương trình thông thường (ví dụ không nên dùng exception để thoát vòng lặp).
  • Tuyệt đối không throw exception trong destructor — có thể khiến chương trình gọi std::terminate() và crash ngay lập tức nếu đang trong quá trình “unwind” do một exception khác.
  • Với C thuần hoặc code hiệu năng cao, dùng mã lỗi trả về (error code / status enum) thay vì exception.

2.5. Tối ưu hiệu năng — nhưng đúng cách

  • Đo trước khi tối ưu (profiling). Câu nói kinh điển của Donald Knuth: “tối ưu sớm là nguồn gốc của mọi cái ác” — chỉ tối ưu đúng chỗ đang thực sự chậm (hotspot), không đoán mò.
  • Tránh copy dữ liệu thừa: dùng std::move khi chuyển quyền sở hữu, truyền tham số lớn bằng const&.
  • Gọi vector.reserve(n) trước khi thêm nhiều phần tử để tránh cấp phát lại bộ nhớ nhiều lần.
  • Hiểu độ phức tạp thuật toán (Big-O) trước khi “vặn vẹo” từng dòng code — thuật toán tốt hơn luôn thắng micro-optimization.
  • Code đơn giản, rõ ràng thường được trình biên dịch tối ưu tốt hơn code “khôn lỏi” viết tay — đừng cố qua mặt compiler.

2.6. Đa luồng (Concurrency) — nếu dự án có dùng thread

  • Dữ liệu chia sẻ giữa các thread luôn cần đồng bộ hóa: std::mutex kết hợp std::lock_guard/std::unique_lock (áp dụng RAII cho khóa — tự động unlock khi ra khỏi scope), hoặc std::atomic cho biến đơn giản.
  • Luôn lock các mutex theo cùng một thứ tự ở mọi nơi trong code để tránh deadlock; dùng std::lock() khi cần khóa nhiều mutex cùng lúc.
  • Dùng công cụ -fsanitize=thread khi test để tự động phát hiện race condition.

2.7. Viết code dễ đọc (Readability)

  • Đặt tên rõ ràng, nhất quán trong toàn dự án (xem Mục 4).
  • Mỗi hàm chỉ nên làm một việc (Single Responsibility). Nếu hàm dài quá một màn hình, cân nhắc tách nhỏ.
  • Tránh số “ma thuật” (magic number) — luôn đặt tên cho hằng số.
  • Hạn chế lồng nhau (nested if/else) quá sâu — ưu tiên “early return” để code phẳng, dễ đọc.
  • Comment nên giải thích “tại sao” làm vậy, không phải “cái gì” đang xảy ra (code rõ ràng đã tự nói lên “cái gì”).
// Không tốt — lồng nhau sâu, khó đọc
void process(int x) {
    if (x > 0) {
        if (x < 100) {
            // xử lý
        }
    }
}

// Tốt hơn — early return, phẳng, dễ đọc
void process(int x) {
    if (x <= 0) return;
    if (x >= 100) return;
    // xử lý
}

2.8. Dùng chuẩn C++ hiện đại (Modern C++)

  • Ưu tiên C++17/20 trở lên: auto, structured bindings, std::optional, std::variant, ranges.
  • Tận dụng thư viện chuẩn (STL) — không tự viết lại container/thuật toán cơ bản (linked list, sort…) trừ khi có lý do đặc biệt.
  • Dùng #pragma once (hoặc include guard đầy đủ) ở đầu mọi file header để tránh include lặp.

2.9. Công cụ nên dùng ngay từ đầu dự án

Công cụMục đích
-Wall -Wextra -Wpedantic -WerrorBắt lỗi tiềm ẩn ngay lúc biên dịch
clang-formatTự động định dạng code, tránh tranh cãi style
clang-tidy, cppcheckPhân tích tĩnh, phát hiện bug/code smell
-fsanitize=address,undefinedBắt lỗi bộ nhớ, UB khi chạy test
Google Test / Catch2Viết unit test cho logic quan trọng

Tầng 3 — Nguyên tắc chuyên biệt (Embedded / RTOS / Viễn thông / An ninh mạng)

Đây là những quy tắc bắt buộc khi làm việc trong lĩnh vực nhúng, hệ thống thời gian thực, viễn thông, hoặc bất cứ nơi nào xử lý dữ liệu từ nguồn không tin cậy (mạng, RF…). Sai sót ở đây không chỉ gây crash mà có thể gây sự cố an toàn (safety) hoặc lỗ hổng bảo mật (security) nghiêm trọng.

3.1. Nguyên tắc nền tảng cho Embedded/RTOS

Không cấp phát bộ nhớ động sau khi khởi tạo hệ thống

Vì sao? malloc/new có thời gian thực thi không xác định trước (có thể nhanh, có thể chậm tùy tình trạng heap), gây phân mảnh bộ nhớ theo thời gian. Điều này vi phạm yêu cầu “thời gian thực” (real-time) — hệ thống phải phản hồi trong thời gian đảm bảo.

  • Cấm malloc/free/new/delete trong vòng lặp chính hoặc trong ngắt (interrupt).
  • Cấp phát toàn bộ bộ nhớ cần thiết tĩnh hoặc dùng memory pool đã chuẩn bị sẵn từ lúc khởi động.

Không dùng exception và RTTI trong code lõi thời gian thực

  • try/catch/throw có overhead không dự đoán được, làm tăng kích thước binary và khó tính toán WCET (Worst-Case Execution Time — thời gian thực thi tệ nhất).
  • Nhiều toolchain nhúng build với cờ -fno-exceptions -fno-rtti.
  • Thay vào đó: trả về mã lỗi (error code) hoặc enum trạng thái.

Tính xác định (Determinism) là ưu tiên số một

  • Tránh đệ quy không giới hạn độ sâu → dùng vòng lặp hoặc giới hạn số lần đệ quy rõ ràng (tránh tràn stack).
  • Tránh std::vector/std::string (có thể tự resize, thời gian không xác định) trong đường xử lý thời gian thực cứng — dùng mảng cố định kích thước (std::array) hoặc buffer đã cấp phát sẵn.

Ngắt (Interrupt Service Routine — ISR)

  • ISR phải cực ngắn: không gọi hàm blocking, không cấp phát bộ nhớ, không dùng mutex thông thường.
  • Biến được chia sẻ giữa ISR và chương trình chính phải khai báo volatile, và đồng bộ bằng cơ chế an toàn cho ngắt (ví dụ thao tác atomic, tắt/bật ngắt trong đoạn code ngắn — gọi là “critical section”).

Lưu ý quan trọng: volatile không phải công cụ đồng bộ đa luồng — nó chỉ báo cho compiler “đừng tối ưu/cache giá trị này”. Với đa luồng thật sự, phải dùng std::atomic.

Chuẩn MISRA C/C++ và AUTOSAR C++14

  • Nếu làm trong lĩnh vực an toàn cao (automotive, hàng không, viễn thông hạ tầng), dự án thường phải tuân thủ chuẩn MISRA (cấm goto, cấm biểu thức có side-effect trong điều kiện if, yêu cầu ép kiểu tường minh…).
  • Dùng công cụ kiểm tra tuân thủ tự động trong quy trình CI: Polyspace, Coverity, PC-lint Plus.

3.2. Bộ nhớ & phần cứng cấp thấp

  • Alignment (căn chỉnh bộ nhớ): khi struct ánh xạ tới thanh ghi phần cứng hoặc gói tin mạng, phải hiểu rõ cách compiler sắp xếp bộ nhớ (padding). Kiểm tra bằng static_assert(sizeof(MyStruct) == kExpectedSize).
  • Endianness (thứ tự byte): dữ liệu qua mạng/giao thức viễn thông phải xử lý rõ ràng thứ tự byte (htons/ntohs hoặc serialize thủ công từng byte) — không bao giờ giả định máy gửi và máy nhận dùng cùng thứ tự byte.
  • Không ép kiểu con trỏ trực tiếp lên struct khi đọc dữ liệu từ buffer mạng/cảm biến — vi phạm “strict aliasing rule” của C++, gây UB. Thay vào đó, dùng memcpy để sao chép an toàn vào biến đã khai báo đúng kiểu.
// SAI — vi phạm strict aliasing, có thể UB hoặc sai do alignment
Header* h = reinterpret_cast<Header*>(raw_buffer);

// ĐÚNG — sao chép an toàn
Header h;
std::memcpy(&h, raw_buffer, sizeof(Header));
h.length = ntohs(h.length); // chuyển đúng thứ tự byte

3.3. An ninh mạng / An ninh viễn thông (Secure Coding)

Nguyên tắc bao trùm: Không bao giờ tin dữ liệu đến từ bên ngoài (mạng, RF, người dùng, cảm biến…). Mọi input phải được coi là “thù địch” cho đến khi được kiểm chứng.

Chống tràn bộ đệm (Buffer Overflow) — lỗi bảo mật kinh điển và nguy hiểm nhất

  • Cấm tuyệt đối: strcpy, strcat, sprintf, gets — các hàm này không kiểm tra biên bộ nhớ đích.
  • Dùng thay thế: snprintf (có giới hạn kích thước), std::string, hoặc std::span (C++20, mang theo thông tin độ dài khi làm việc với buffer thô).
  • Luôn kiểm tra độ dài khai báo trong header của gói tin có khớp với độ dài thực tế nhận được trước khi đọc payload — tấn công giả mạo độ dài là kỹ thuật phổ biến.

Xử lý dữ liệu nhạy cảm (khóa mã hóa, mật khẩu, token)

  • Không dùng std::string thông thường để chứa secret — dữ liệu có thể bị copy nhiều lần trong bộ nhớ và không bị xóa sạch khi “xóa” (vì std::string không đảm bảo ghi đè vùng nhớ cũ).
  • Xóa vùng nhớ chứa secret bằng hàm chuyên dụng như explicit_bzero/memset_s — không dùng memset thường, vì compiler có thể tối ưu bỏ qua lệnh ghi đè “vô nghĩa” (dữ liệu không được đọc lại sau đó) này.
  • Không bao giờ log secret/khóa/mật khẩu dưới bất kỳ hình thức nào, kể cả trong log debug.

So sánh dữ liệu bảo mật phải dùng constant-time comparison

Vì sao? Hàm so sánh thông thường (==, memcmp) dừng lại ngay khi phát hiện byte đầu tiên khác nhau — kẻ tấn công có thể đo thời gian phản hồi để đoán dần từng byte của mã bí mật (gọi là timing attack).

// SAI — dễ bị timing attack (so sánh dừng sớm khi gặp byte sai)
if (memcmp(computed_mac, received_mac, len) == 0) { ... }

// ĐÚNG — so sánh với thời gian không đổi bất kể dữ liệu
if (CRYPTO_memcmp(computed_mac, received_mac, len) == 0) { ... } // hàm của OpenSSL

Không tự viết thuật toán mã hóa (crypto)

  • Luôn dùng thư viện đã được kiểm định kỹ lưỡng: OpenSSL, mbedTLS, BoringSSL (cho embedded: wolfSSL, mbedTLS).
  • Không tự implement AES/RSA/hash — kể cả khi hiểu rõ thuật toán trên lý thuyết, việc cài đặt an toàn (chống side-channel, timing attack…) đòi hỏi chuyên môn sâu.
  • Sinh số ngẫu nhiên cho mục đích bảo mật (khóa, token, nonce) phải dùng bộ sinh số ngẫu nhiên an toàn về mật mã (CSPRNG) như RAND_bytes (OpenSSL) hoặc bộ sinh ngẫu nhiên phần cứng — không bao giờ dùng rand() thông thường cho việc này (dễ đoán được).

Fail-safe / Fail-secure — nguyên tắc xử lý khi có lỗi

  • Khi phát hiện lỗi parse, xác thực thất bại, hoặc dữ liệu bất thường → hệ thống phải chuyển ngay về trạng thái an toàn (từ chối gói tin, ngắt kết nối) — không bao giờ “cố xử lý tiếp” với dữ liệu chưa được xác nhận hợp lệ.
  • Giới hạn tốc độ xử lý gói tin bất thường (rate limiting) ở tầng ứng dụng để chống tấn công từ chối dịch vụ (DoS).

Giao thức viễn thông cụ thể (SS7, Diameter, SIP, TLV, ASN.1…)

  • Ưu tiên dùng bộ decode đã được kiểm định hoặc sinh tự động bằng compiler ASN.1, thay vì tự parse tay (rất dễ lỗi và là nguồn gốc nhiều lỗ hổng thực tế trong hạ tầng viễn thông).
  • Với cấu trúc dữ liệu lồng nhau (TLV lồng TLV…), luôn đặt giới hạn cứng về độ sâu lồng nhau và số lượng phần tử lặp để chống kiểu tấn công gây bùng nổ tài nguyên khi parse (tương tự kiểu tấn công “billion laughs” trong XML).

3.4. Kiểm thử bắt buộc cho hệ thống này

Công cụMục đíchGhi chú
-fsanitize=address,undefinedPhát hiện overflow, UBDùng khi test, không dùng trên bản production
clang-tidy, cppcheck, PVS-StudioPhân tích tĩnhChạy tự động trong CI
Polyspace, CoverityChứng nhận an toàn (safety-critical)Bắt buộc với automotive/hàng không
Fuzzing (libFuzzer, AFL++)Ném dữ liệu ngẫu nhiên/bất thường vào hàm parseBắt buộc, không phải tùy chọn, cho mọi hàm xử lý dữ liệu từ mạng
-fsanitize=threadPhát hiện race conditionDùng khi có đa luồng

Ghi nhớ: Với bất kỳ hàm nào nhận dữ liệu từ bên ngoài (mạng, giao thức viễn thông, file cấu hình không tin cậy), fuzzing là yêu cầu bắt buộc, không phải “nice to have”.


4. Quy tắc đặt tên biến, hàm, kiểu dữ liệu

Không có chuẩn “đúng tuyệt đối” — điều quan trọng nhất là nhất quán trong toàn bộ dự án. Dưới đây là convention phổ biến, khuyến nghị cho hệ thống nhúng/lớn (theo phong cách Google C++ Style Guide kết hợp thực tiễn embedded).

4.1. Biến

Loại biếnConventionVí dụ
Biến cục bộ (local)snake_casepacket_length, retry_count
Biến thành viên của classsnake_case_ (gạch dưới ở cuối)buffer_size_, is_connected_
Biến toàn cục (hạn chế tối đa việc dùng)prefix g_g_system_state
Biến static trong phạm vi fileprefix s_s_instance_count
Hằng số / constexprkPascalCase hoặc UPPER_SNAKE_CASEkMaxRetries, MAX_PACKET_SIZE
Con trỏ (một số chuẩn MISRA/embedded)prefix p_p_header, p_next
Cờ/bit trạng thái phần cứngmô tả rõ ý nghĩa, dạng câu hỏiis_tx_ready, has_crc_error

4.2. Hàm

  • Dùng snake_case() hoặc camelCase() — chọn một kiểu và dùng xuyên suốt dự án, không trộn lẫn.
  • Tên hàm nên là động từ + danh từ, mô tả rõ hành động hàm thực hiện:
    • parse_packet(), validate_checksum(), send_heartbeat()
  • Hàm trả về kiểu bool nên đặt tên dạng câu hỏi để dễ đọc tại nơi gọi:
    • is_valid(), has_timeout(), can_retry()
  • Phân biệt rõ hàm chỉ đọc dữ liệu (không side-effect) và hàm có làm thay đổi trạng thái:
    • get_state() (chỉ đọc, không đổi gì) khác với update_state() (có thay đổi trạng thái).

4.3. Type / Class / Struct / Enum

  • Dùng PascalCase cho tên kiểu dữ liệu: class PacketParser, struct FrameHeader, enum class ConnectionState.
  • Tổ chức theo namespace rõ ràng theo module, tránh tên chung chung dễ đụng độ:
    • Tốt: telecom::sip::MessageParser
    • Không tốt: Parser (tên quá chung, dễ trùng khi dự án lớn dần)

4.4. Macro

  • Chỉ dùng macro cho include guard hoặc biên dịch có điều kiện (conditional compilation) — hạn chế tối đa cho việc khác (ưu tiên constexpr/inline/template).
  • Luôn viết UPPER_SNAKE_CASE, và luôn prefix theo tên module để tránh xung đột tên toàn cục:
    • TELECOM_SIP_MAX_HEADER_LEN thay vì chỉ MAX_HEADER_LEN

4.5. File

  • Đặt tên file theo snake_case.h / snake_case.cpp.
  • Tên file nên khớp với tên class/module chính bên trong nó:
    • File packet_parser.h nên chứa class PacketParser.

4.6. Nguyên tắc chung khi đặt tên (áp dụng cho mọi trường hợp trên)

  • Không viết tắt mơ hồ: tên như tmp, data, val, flag không nói lên điều gì cả — ngoại trừ biến đếm vòng lặp ngắn quen thuộc (i, j, k) hoặc các từ viết tắt phổ biến trong ngành (ctx = context, cfg = config).
  • Luôn ghi rõ đơn vị đo trong tên biến — đây là quy tắc cực kỳ quan trọng với embedded/real-time, vì nhầm đơn vị là một trong những lỗi gây hậu quả nghiêm trọng nhất trong lịch sử kỹ thuật (ví dụ nổi tiếng: tàu vũ trụ Mars Climate Orbiter của NASA bị mất do một đội dùng đơn vị Anh, đội kia dùng đơn vị mét mà không thống nhất).
    • Tốt: timeout_ms, delay_us, buffer_size_bytes, frequency_hz
    • Không tốt: timeout, delay, size (không rõ đơn vị gì)
  • Biến chứa dữ liệu nhạy cảm nên có tên rõ ràng để dễ rà soát bảo mật (audit) và giúp công cụ tự động phát hiện, tránh vô tình log ra:
    • secret_key, auth_token, private_key
  • Với hàm xử lý dữ liệu từ nguồn không tin cậy, có thể thêm hậu tố nhắc nhở người đọc code cẩn trọng, ví dụ đặt trong namespace untrusted_input:: hoặc hậu tố _from_network.

5. Checklist review code

In ra hoặc lưu lại, dùng để tự kiểm tra trước khi commit code hoặc khi review code người khác trong dự án embedded/viễn thông/an ninh mạng.

Cơ bản (mọi dự án)

  •  Mọi biến đều được khởi tạo trước khi dùng?
  •  Đã kiểm tra giá trị trả về của mọi hàm có thể thất bại (malloc, fopen, hàm mạng…)?
  •  Code biên dịch sạch với -Wall -Wextra -Wpedantic -Werror?
  •  Không còn con trỏ nào bị dangling (trỏ tới vùng nhớ đã giải phóng/đã hết scope)?

Chung (dự án C++ hiện đại)

  •  Có dùng RAII/smart pointer thay vì new/delete thô không?
  •  Tham số truyền vào hàm đã dùng const& khi không cần sửa đổi chưa?
  •  Class quản lý tài nguyên thô đã tuân thủ Rule of Five (hoặc Rule of Zero nếu không cần)?
  •  Tên biến/hàm có rõ ràng, nhất quán theo convention của dự án không?

Chuyên biệt (Embedded/RTOS/Viễn thông/An ninh mạng)

  •  Có cấp phát bộ nhớ động (malloc/new) trong đường xử lý thời gian thực không? → phải loại bỏ.
  •  Dữ liệu nhận từ mạng/giao thức đã được validate độ dài và kiểu trước khi xử lý chưa?
  •  Có dùng hàm string không an toàn (strcpy, sprintf, gets…) ở đâu không?
  •  So sánh dữ liệu bảo mật (MAC, token, hash) đã dùng constant-time comparison chưa?
  •  Secret/khóa/mật khẩu có bị log ra ở đâu không? Có được xóa an toàn khỏi bộ nhớ sau khi dùng xong không?
  •  ISR (interrupt handler) có gọi hàm blocking hoặc cấp phát bộ nhớ không?
  •  Biến chia sẻ giữa ISR và task chính đã khai báo volatile và đồng bộ đúng cách chưa?
  •  Hàm parse dữ liệu từ bên ngoài đã được fuzz test chưa?
  •  Có ép kiểu con trỏ trực tiếp lên struct từ buffer nhận từ mạng không? (vi phạm alignment/strict aliasing)
  •  Tên biến thời gian/kích thước có ghi rõ đơn vị đo (ms/us/bytes/Hz) không?

Tài liệu này mang tính tổng hợp nguyên tắc kinh điển, nên được điều chỉnh theo coding style guide cụ thể của từng công ty/dự án (ví dụ: Google C++ Style Guide, MISRA C:2012, AUTOSAR C++14) khi áp dụng thực tế.

Contact
Facebook
TOP