💻 Lập trình C++ cơ bản → nâng cao (hướng nhúng)
Từ biến, vòng lặp tới con trỏ, hướng đối tượng, template, đa luồng — học C++ để lập trình vi điều khiển, robot và IoT.
?Khóa học này dành cho ai?
Học sinh, sinh viên muốn học C++ bài bản từ con số 0 để lập trình vi điều khiển (Arduino, ESP32, STM32), robot, IoT — hoặc làm nền tảng cho thi Tin học, Khoa học kỹ thuật. Không cần biết lập trình trước.
Khác với các khóa C++ thông thường, mọi ví dụ đều gắn với phần cứng: đọc cảm biến, điều khiển LED, động cơ, phân tích lệnh UART, thao tác thanh ghi, lọc nhiễu, đóng gói dữ liệu truyền thông.
📖 Lý thuyết đầy đủ
Mỗi bài có mục tiêu, giải thích, bảng so sánh, lỗi thường gặp và tóm tắt.
🖼️ Hình minh hoạ
Bộ nhớ, con trỏ, thanh ghi, endian, vtable, data race… được vẽ trực quan.
▶ Code chạy thật
45 chương trình hoàn chỉnh, kèm kết quả chạy thật — tải về chạy ngay được.
⚙Chuẩn bị
- Máy tính cài g++ (MinGW-w64/MSYS2 trên Windows, sẵn trên Linux/macOS) và VS Code. Xem chi tiết ở Bài 1.
- Chạy một ví dụ:
g++ -std=c++17 01-hello.cpp -o app && ./app(Windows:app.exe). Chữ tiếng Việt lỗi trong cửa sổ lệnh Windows: chạychcp 65001trước. - Không có máy? Dán code vào godbolt.org hoặc onlinegdb.com.
- Các ví dụ in bằng
std::coutđể chạy trên máy tính; khi đưa lên board, thay bằngSerial.print().
≡Đối chiếu với giáo trình (50 chủ đề)
⌚Lộ trình gợi ý — 17 buổi × 2 giờ
Hiểu C++ là gì, vì sao dùng cho lập trình nhúng, cài môi trường và viết – biên dịch – chạy chương trình đầu ti…
Biết chọn đúng kiểu dữ liệu cho từng loại giá trị, hiểu phạm vi sống của biến và cách khai báo hằng.
Hiểu static/extern/volatile và dùng thành thạo mọi nhóm toán tử, nhất là thao tác bit trên thanh ghi.
Điều khiển luồng chạy của chương trình bằng if/else, switch và các loại vòng lặp — kể cả vòng lặp không chặn k…
Chia chương trình thành các hàm rõ ràng; dùng thư viện toán và ép kiểu số đúng cách.
Lưu nhiều giá trị cùng kiểu bằng mảng, xử lý chuỗi kiểu C và std::string, phân tích lệnh văn bản.
Hiểu bản chất con trỏ — biến chứa địa chỉ — và dùng để truy cập mảng, truyền tham số, sửa con trỏ của nơi khác…
Dùng con trỏ để nhìn dữ liệu ở mức byte (gói tin, thanh ghi, endian) và để truyền hàm như một giá trị (callbac…
Dùng tham chiếu thay con trỏ khi phù hợp, gom dữ liệu bằng struct, nhập/xuất có định dạng và làm việc với thời…
Mô hình hoá thiết bị phần cứng thành lớp có dữ liệu riêng tư và phương thức công khai; hiểu vòng đời đối tượng…
Tái sử dụng code bằng kế thừa; dùng nạp chồng hàm và toán tử để viết code tự nhiên hơn.
Viết code làm việc với “bất kỳ loại thiết bị nào” qua lớp cha/interface — nền tảng của mọi thiết kế mềm dẻo.
Dùng con trỏ tới đối tượng để xây các kỹ thuật thiết kế thường gặp trong firmware.
Ghi/đọc dữ liệu ra file, xử lý lỗi bằng ngoại lệ hoặc std::optional, và quản lý bộ nhớ động an toàn.
Tổ chức code lớn bằng namespace, viết code tổng quát bằng template, cấu hình biên dịch bằng tiền xử lý và hiểu…
Chạy nhiều việc song song an toàn, dùng thành thạo container và thuật toán STL, biết C++ làm web thế nào.
Rèn tư duy thuật toán qua 12 bài kinh điển (thường gặp trong kiểm tra và phỏng vấn) — tự làm trước, rồi mở lời…
→Học tiếp
⏱ FreeRTOS cho lập trình nhúng
Đa nhiệm thời gian thực trên ESP32/STM32: task, queue, semaphore, mutex, timer.
🧩 Design Pattern
24 mẫu thiết kế với C++ hướng nhúng — tổ chức firmware lớn cho gọn và dễ mở rộng.
🚀 Giới thiệu C++, cài đặt và quá trình build
Hiểu C++ là gì, vì sao dùng cho lập trình nhúng, cài môi trường và viết – biên dịch – chạy chương trình đầu tiên.
- Biết C++ ra đời thế nào và được dùng ở đâu
- Cài được trình biên dịch và chạy chương trình đầu tiên
- Hiểu 4 bước biến mã nguồn thành chương trình chạy được
- Nắm cú pháp cơ bản: câu lệnh, khối lệnh, chú thích, hàm main
1C++ là gì?
C++ do Bjarne Stroustrup phát triển từ năm 1979 tại Bell Labs, ban đầu tên là “C with Classes” — tức ngôn ngữ C cộng thêm lập trình hướng đối tượng. C++ là ngôn ngữ biên dịch (compiled): mã nguồn được dịch một lần thành mã máy, nên chạy rất nhanh và kiểm soát được từng byte bộ nhớ.
| Phiên bản | Năm | Điểm mới đáng chú ý |
|---|---|---|
| C++98/03 | 1998 | Chuẩn đầu tiên, STL |
| C++11 | 2011 | auto, lambda, nullptr, con trỏ thông minh, std::thread — “C++ hiện đại” |
| C++14 | 2014 | Số nhị phân 0b1010, dấu phân cách 1'000'000 |
| C++17 | 2017 | std::optional, std::variant, structured binding — khóa học dùng chuẩn này |
| C++20/23 | 2020+ | concepts, ranges, modules |
🔌 Lập trình nhúng
Arduino, ESP32, STM32, Raspberry Pi Pico đều lập trình bằng C/C++.
🎮 Game & đồ hoạ
Unreal Engine, các engine game, phần mềm đồ hoạ 3D.
⚙️ Hệ thống
Trình duyệt, hệ điều hành, cơ sở dữ liệu, robot (ROS), AI (TensorFlow lõi C++).
2Cài đặt môi trường
| Hệ điều hành | Trình biên dịch | Kiểm tra |
|---|---|---|
| Windows | MSYS2 → pacman -S mingw-w64-ucrt-x86_64-gcc, hoặc Visual Studio Community | g++ --version |
| macOS | xcode-select --install (clang++) | |
| Linux | sudo apt install g++ |
Trình soạn thảo gợi ý: VS Code + extension C/C++. Với vi điều khiển: Arduino IDE hoặc PlatformIO (trong VS Code). Không muốn cài gì: dùng godbolt.org hoặc onlinegdb.com.
3Chương trình đầu tiên
// Chương trình C++ đầu tiên.
// Biên dịch: g++ -std=c++17 01-hello.cpp -o hello
// Chạy: ./hello (Windows: hello.exe)
#include <iostream> // chỉ thị tiền xử lý: chèn thư viện vào/ra (cout, cin)
/*
Chú thích nhiều dòng.
Mọi chương trình C++ bắt đầu chạy từ hàm main().
*/
int main() {
std::cout << "Xin chào, C++!" << std::endl; // in ra màn hình rồi xuống dòng
// Mỗi câu lệnh kết thúc bằng dấu chấm phẩy ;
// Khối lệnh được bao bởi cặp ngoặc nhọn { }
int namHoc = 2026;
std::cout << "Năm học: " << namHoc << "\n";
std::cout << "C++ chạy trên: máy tính, Arduino, ESP32, STM32..." << '\n';
return 0; // 0 = chương trình kết thúc thành công
}
g++ -std=c++17 01-hello.cpp -o app && ./appXin chào, C++! Năm học: 2026 C++ chạy trên: máy tính, Arduino, ESP32, STM32...
Giải thích
#include <iostream>chèn thư viện vào/ra. Dòng bắt đầu bằng#do bộ tiền xử lý xử lý trước khi biên dịch.int main()là điểm bắt đầu của mọi chương trình C++ trên máy tính. Trên Arduino,main()được viết sẵn và gọisetup()một lần rồiloop()mãi mãi.std::cout << ...in ra màn hình;std::endlhoặc"\n"để xuống dòng.std::là namespace của thư viện chuẩn.- Mỗi câu lệnh kết thúc bằng
;. Chú thích một dòng//, nhiều dòng/* ... */. return 0;báo cho hệ điều hành chương trình kết thúc thành công.
4C++ được build như thế nào?
- Tiền xử lý (preprocessor): thay
#includebằng nội dung file, thay các#define, xử lý#if. - Biên dịch (compile): kiểm tra cú pháp, kiểu dữ liệu, dịch sang hợp ngữ. Lỗi biên dịch (thiếu
;, sai kiểu…) xuất hiện ở bước này. - Hợp dịch (assemble): hợp ngữ → mã máy, tạo file đối tượng
.o. - Liên kết (link): ghép các file
.ovà thư viện thành một chương trình. Lỗi “undefined reference” xuất hiện ở bước này (khai báo hàm nhưng quên định nghĩa).
Dự án lớn có nhiều file: g++ main.cpp cambien.cpp -o app. Mỗi file .cpp được biên dịch riêng rồi liên kết lại; file .h chứa khai báo để các file khác biết hàm/lớp tồn tại.
🔧Áp dụng trong lập trình nhúng
- Arduino IDE thực hiện đúng 4 bước trên khi bạn bấm Verify/Upload, dùng trình biên dịch chéo
avr-g++(Uno) hoặcxtensa-esp32-elf-g++(ESP32). - Kết quả là file
.hex/.binnạp vào bộ nhớ Flash của chip qua USB-UART. - Khác máy tính: chương trình nhúng không bao giờ kết thúc —
loop()chạy mãi tới khi mất điện.
⚠Lỗi thường gặp
- Quên dấu
;cuối câu lệnh → lỗi biên dịch báo ở dòng KẾ TIẾP. - Viết
Main()hoặcmainsai chính tả → lỗi liên kết “undefined reference to main”. - Quên
std::trướccoutmà không cóusing.
✎Bài tập về nhà
🔢 Kiểu dữ liệu, biến, phạm vi, hằng và modifier
Biết chọn đúng kiểu dữ liệu cho từng loại giá trị, hiểu phạm vi sống của biến và cách khai báo hằng.
- Phân biệt các kiểu số nguyên, số thực, ký tự, logic và kích thước của chúng
- Dùng kiểu cố định độ rộng
uint8_t,int16_t… như dân nhúng - Hiểu biến cục bộ, toàn cục, phạm vi khối
- Khai báo hằng bằng
const/constexpr, viết literal hệ 2/8/16
1Các kiểu dữ liệu cơ bản
| Kiểu | Dùng cho | Ví dụ |
|---|---|---|
bool | đúng/sai | bool denBat = true; |
char | một ký tự (thực chất là số 8 bit) | char lenh = 'F'; |
int, short, long, long long | số nguyên | int dem = 0; |
float, double | số thực (có sai số) | float t = 28.5f; |
void | “không có kiểu” (hàm không trả về) | void setup() |
int phụ thuộc phần cứng: 2 byte trên Arduino Uno (AVR 8 bit), 4 byte trên ESP32/PC. Vì vậy code nhúng dùng kiểu trong <cstdint>: uint8_t, int16_t, uint32_t… luôn đúng số bit trên mọi chip.2Ví dụ: kích thước, giới hạn, tràn số
// Kiểu dữ liệu cơ bản và kiểu số nguyên cố định độ rộng (rất quan trọng trong lập trình nhúng)
#include <cstdint>
#include <iostream>
#include <limits>
int main() {
std::cout << "== Kích thước (byte) trên máy này ==\n";
std::cout << "bool : " << sizeof(bool) << "\n";
std::cout << "char : " << sizeof(char) << "\n";
std::cout << "short : " << sizeof(short) << "\n";
std::cout << "int : " << sizeof(int) << " (Arduino Uno: 2 byte!)\n";
std::cout << "long : " << sizeof(long) << "\n";
std::cout << "float : " << sizeof(float) << "\n";
std::cout << "double : " << sizeof(double) << " (Arduino Uno: 4 byte!)\n";
std::cout << "\n== Kiểu cố định độ rộng <cstdint> – giống nhau trên mọi vi điều khiển ==\n";
std::cout << "uint8_t : 0 .. " << +std::numeric_limits<uint8_t>::max() << "\n"; // dấu + để in số, không in ký tự
std::cout << "int8_t : " << +std::numeric_limits<int8_t>::min() << " .. " << +std::numeric_limits<int8_t>::max() << "\n";
std::cout << "uint16_t : 0 .. " << std::numeric_limits<uint16_t>::max() << "\n";
std::cout << "int16_t : " << std::numeric_limits<int16_t>::min() << " .. " << std::numeric_limits<int16_t>::max() << "\n";
std::cout << "uint32_t : 0 .. " << std::numeric_limits<uint32_t>::max() << "\n";
std::cout << "\n== Tràn số (overflow) ==\n";
uint8_t dem = 255;
dem = dem + 1; // 8 bit không chứa nổi 256 -> quay vòng về 0
std::cout << "uint8_t 255 + 1 = " << +dem << "\n";
uint16_t adc = 4095; // ADC 12 bit của ESP32 tối đa 4095
std::cout << "ADC 12 bit tối đa: " << adc << " (vừa trong uint16_t)\n";
std::cout << "\n== Số thực có sai số ==\n";
double a = 0.1 + 0.2;
std::cout.precision(17);
std::cout << "0.1 + 0.2 = " << a << "\n";
std::cout << "a == 0.3 ? " << std::boolalpha << (a == 0.3) << " -> đừng so sánh số thực bằng ==\n";
std::cout << "\n== Ký tự là một số ==\n";
char c = 'A';
std::cout << "'A' có mã ASCII " << int(c) << ", 'A' + 2 = " << char(c + 2) << "\n";
bool denBat = true;
std::cout << "bool true in ra: " << std::noboolalpha << denBat << "\n";
}
g++ -std=c++17 02a-kieu-du-lieu.cpp -o app && ./app== Kích thước (byte) trên máy này == bool : 1 char : 1 short : 2 int : 4 (Arduino Uno: 2 byte!) long : 4 float : 4 double : 8 (Arduino Uno: 4 byte!) == Kiểu cố định độ rộng <cstdint> – giống nhau trên mọi vi điều khiển == uint8_t : 0 .. 255 int8_t : -128 .. 127 uint16_t : 0 .. 65535 int16_t : -32768 .. 32767 uint32_t : 0 .. 4294967295 == Tràn số (overflow) == uint8_t 255 + 1 = 0 ADC 12 bit tối đa: 4095 (vừa trong uint16_t) == Số thực có sai số == 0.1 + 0.2 = 0.30000000000000004 a == 0.3 ? false -> đừng so sánh số thực bằng == == Ký tự là một số == 'A' có mã ASCII 65, 'A' + 2 = C bool true in ra: 1
Giải thích
std::numeric_limits<T>::max()cho biết giá trị lớn nhất của kiểuT.- Dấu
+trước biếnuint8_tđể in ra số; nếu không,coutcoi nó là ký tự. - Tràn số:
uint8_tchỉ chứa 0–255, nên 255 + 1 quay vòng về 0 — lỗi kinh điển khi đếm xung, đếm thời gian. - Số thực lưu dạng nhị phân nên
0.1 + 0.2không đúng bằng0.3. So sánh số thực bằng sai số nhỏ (xem bài 5).
3Biến, phạm vi, hằng, literal và modifier
- Biến là một vùng nhớ có tên và kiểu. Khai báo:
kiểu tên = giá_trị;. Luôn khởi tạo — biến cục bộ chưa khởi tạo chứa “rác”. - Phạm vi (scope): biến khai báo trong
{ }chỉ sống trong khối đó (cục bộ); khai báo ngoài mọi hàm là toàn cục. - Hằng:
const(không đổi sau khi khởi tạo) vàconstexpr(tính được ngay lúc biên dịch → không tốn RAM, rất hợp cho số chân, tốc độ baud). - Literal:
255,0xFF(hệ 16),0b11111111(hệ 2),0377(hệ 8),3.14f,'A',"chuỗi",1'000'000UL. - Modifier:
signed/unsigned(có/không dấu),short/long(ngắn/dài).
// Biến, phạm vi biến, hằng số, literal và modifier
#include <cstdint>
#include <iostream>
int soLanKhoiDong = 0; // biến TOÀN CỤC: sống suốt chương trình, mọi hàm thấy được
const int LED_PIN = 13; // hằng: không thể gán lại
constexpr uint32_t BAUD = 115200; // hằng tính lúc BIÊN DỊCH
constexpr int TONG_CHAN = 2 * 20 + 4; // biểu thức hằng – trình biên dịch tính sẵn = 44
void tangBien() {
int cucBo = 0; // biến CỤC BỘ: tạo mới mỗi lần gọi hàm
cucBo++;
soLanKhoiDong++;
std::cout << " cucBo = " << cucBo << ", soLanKhoiDong = " << soLanKhoiDong << "\n";
}
int main() {
std::cout << "== Phạm vi biến ==\n";
tangBien();
tangBien();
int x = 10;
{ // khối lệnh mới = phạm vi mới
int x = 99; // biến che (shadow) x bên ngoài – nên tránh!
int y = 5;
std::cout << " trong khối: x = " << x << ", y = " << y << "\n";
}
// std::cout << y; // LỖI: y đã ra khỏi phạm vi
std::cout << " ngoài khối: x = " << x << "\n";
std::cout << "\n== Hằng ==\n";
std::cout << " LED_PIN = " << LED_PIN << ", BAUD = " << BAUD << ", TONG_CHAN = " << TONG_CHAN << "\n";
// LED_PIN = 2; // LỖI biên dịch: không gán được hằng
std::cout << "\n== Literal (cách viết hằng số) ==\n";
int thapPhan = 255;
int thapLucPhan = 0xFF; // hệ 16 – hay dùng cho thanh ghi, mã màu
int nhiPhan = 0b11111111; // hệ 2 (C++14) – dễ nhìn từng bit
int batPhan = 0377; // hệ 8 – số 0 đứng đầu! dễ nhầm
std::cout << " 255 = 0xFF = 0b11111111 = 0377 ? "
<< (thapPhan == thapLucPhan && thapLucPhan == nhiPhan && nhiPhan == batPhan) << "\n";
unsigned long trieu = 1'000'000UL; // dấu ' phân cách chữ số (C++14), hậu tố UL
float pi = 3.14159f; // hậu tố f = float
std::cout << " trieu = " << trieu << ", pi = " << pi << ", ký tự xuống dòng '\\n', tab '\\t'\n";
std::cout << "\n== Modifier: signed / unsigned / short / long ==\n";
unsigned int u = 0;
u = u - 1; // không âm được -> quay vòng thành số rất lớn
std::cout << " unsigned 0 - 1 = " << u << "\n";
signed char sc = -128;
short s = 32767;
long long ll = 9'000'000'000LL;
std::cout << " signed char = " << int(sc) << ", short = " << s << ", long long = " << ll << "\n";
std::cout << "\n== auto: để trình biên dịch tự suy ra kiểu ==\n";
auto nhietDo = 28.5; // double
auto soCamBien = 4; // int
std::cout << " sizeof(nhietDo) = " << sizeof(nhietDo) << ", sizeof(soCamBien) = " << sizeof(soCamBien) << "\n";
}
g++ -std=c++17 02b-bien-hang.cpp -o app && ./app== Phạm vi biến == cucBo = 1, soLanKhoiDong = 1 cucBo = 1, soLanKhoiDong = 2 trong khối: x = 99, y = 5 ngoài khối: x = 10 == Hằng == LED_PIN = 13, BAUD = 115200, TONG_CHAN = 44 == Literal (cách viết hằng số) == 255 = 0xFF = 0b11111111 = 0377 ? 1 trieu = 1000000, pi = 3.14159, ký tự xuống dòng '\n', tab '\t' == Modifier: signed / unsigned / short / long == unsigned 0 - 1 = 4294967295 signed char = -128, short = 32767, long long = 9000000000 == auto: để trình biên dịch tự suy ra kiểu == sizeof(nhietDo) = 8, sizeof(soCamBien) = 4
Giải thích
- Biến
cucBođược tạo lại mỗi lần gọi hàm nên luôn bằng 1; biến toàn cụcsoLanKhoiDonggiữ giá trị. - Đặt tên biến trong khối trùng với biến bên ngoài (shadowing) là hợp lệ nhưng rất dễ gây nhầm lẫn.
unsigned 0 - 1thành 4 294 967 295 — đây là lỗi hay gặp khi đếm lùi với biến không dấu.autođể trình biên dịch tự suy kiểu:28.5làdouble(8 byte), muốnfloatphải viết28.5f.
🔧Áp dụng trong lập trình nhúng
- Dùng
uint8_tcho giá trị 0–255 (PWM, độ sáng, byte giao tiếp),uint16_tcho ADC (10–12 bit),uint32_tchomillis(). - Trên AVR,
doublechỉ làfloat4 byte; ESP32 có FPU chofloatnhưngdoublechạy chậm hơn nhiều. - Hằng nên khai báo
constexpr/constthay vì biến thường để trình biên dịch đặt vào Flash, tiết kiệm RAM.
⚠Lỗi thường gặp
- Dùng
intđể chứamillis()trên Arduino Uno → tràn sau 32 giây. - So sánh
floatbằng==. - Viết
0123tưởng là 123 nhưng thực ra là số hệ 8 (= 83).
✎Bài tập về nhà
➗ Lớp lưu trữ và toán tử (đặc biệt là toán tử bit)
Hiểu static/extern/volatile và dùng thành thạo mọi nhóm toán tử, nhất là thao tác bit trên thanh ghi.
- Phân biệt biến tự động,
static,extern,volatile,mutable - Nắm toán tử số học, so sánh, logic, gán, ba ngôi
- Đặt, xoá, đảo, đọc bit bằng
| & ^ ~ << >>
1Lớp lưu trữ (storage class)
| Từ khoá | Ý nghĩa | Khi nào dùng |
|---|---|---|
| (mặc định) | biến tự động, sống trong khối | hầu hết biến cục bộ |
static trong hàm | khởi tạo 1 lần, giữ giá trị giữa các lần gọi | đếm số lần gọi, trạng thái trước của nút nhấn |
static ngoài hàm | chỉ thấy trong file .cpp hiện tại | giấu biến/hàm nội bộ của một module |
extern | khai báo biến được định nghĩa ở file khác | chia sẻ biến toàn cục giữa nhiều file |
mutable | cho phép sửa trong hàm const | bộ đếm, cache bên trong lớp |
thread_local | mỗi luồng một bản | chương trình đa luồng |
// Lớp lưu trữ (storage class): auto, static, extern, mutable, thread_local – và từ khoá volatile
#include <iostream>
// static ở phạm vi file: biến/hàm chỉ thấy được trong file .cpp này (internal linkage)
static int demNoiBo = 0;
// extern: KHAI BÁO biến được ĐỊNH NGHĨA ở nơi khác (thường ở file .cpp khác).
// Ở đây định nghĩa ngay bên dưới để chương trình chạy được trong một file.
extern int soLanNhanNut;
int soLanNhanNut = 0;
int docCamBien() {
static int lanGoi = 0; // static CỤC BỘ: khởi tạo 1 lần, GIỮ giá trị giữa các lần gọi
int tam = 0; // biến tự động (automatic): tạo lại mỗi lần gọi
lanGoi++;
tam++;
std::cout << " docCamBien(): lanGoi = " << lanGoi << ", tam = " << tam << "\n";
return lanGoi * 10;
}
class Led {
public:
void bat() const {
soLanTruyCap++; // được phép sửa trong hàm const vì là mutable
std::cout << " LED bật (lần truy cập " << soLanTruyCap << ")\n";
}
private:
mutable int soLanTruyCap = 0;
};
// volatile: báo trình biên dịch "giá trị có thể bị thay đổi từ bên ngoài" (ngắt, phần cứng)
// -> không được tối ưu hoá bằng cách đọc từ thanh ghi CPU cũ, phải đọc lại bộ nhớ mỗi lần.
volatile bool coNgat = false;
void isrNutNhan() { // giả lập hàm phục vụ ngắt (ISR)
coNgat = true;
soLanNhanNut++;
}
int main() {
std::cout << "== static cục bộ ==\n";
docCamBien();
docCamBien();
docCamBien();
std::cout << "\n== static toàn cục & extern ==\n";
demNoiBo += 5;
isrNutNhan();
isrNutNhan();
std::cout << " demNoiBo = " << demNoiBo << ", soLanNhanNut = " << soLanNhanNut << "\n";
std::cout << "\n== mutable ==\n";
const Led led;
led.bat();
led.bat();
std::cout << "\n== volatile ==\n";
if (coNgat) {
coNgat = false;
std::cout << " Phát hiện ngắt từ nút nhấn -> xử lý trong loop()\n";
}
std::cout << "\n== thread_local ==\n";
thread_local int moiLuongMotBan = 7; // mỗi luồng có một bản riêng (xem bài Đa luồng)
std::cout << " moiLuongMotBan = " << moiLuongMotBan << "\n";
}
g++ -std=c++17 03a-lop-luu-tru.cpp -o app && ./app== static cục bộ == docCamBien(): lanGoi = 1, tam = 1 docCamBien(): lanGoi = 2, tam = 1 docCamBien(): lanGoi = 3, tam = 1 == static toàn cục & extern == demNoiBo = 5, soLanNhanNut = 2 == mutable == LED bật (lần truy cập 1) LED bật (lần truy cập 2) == volatile == Phát hiện ngắt từ nút nhấn -> xử lý trong loop() == thread_local == moiLuongMotBan = 7
Giải thích
lanGoilàstaticcục bộ: lần gọi thứ 3 đã là 3, trong khitamluôn là 1.- Biến dùng chung giữa hàm ngắt (ISR) và
loop()phải khai báovolatile.
2Toán tử
| Nhóm | Toán tử | Ghi chú |
|---|---|---|
| Số học | + - * / % ++ -- | chia hai số nguyên sẽ bỏ phần lẻ |
| So sánh | == != < > <= >= | kết quả là bool |
| Logic | && || ! | tính “ngắn mạch” (short-circuit) |
| Bit | & | ^ ~ << >> | làm việc trên từng bit |
| Gán | = += -= *= /= |= &= ^= <<= | |
| Khác | ?: sizeof , -> :: | ba ngôi: đk ? a : b |
// Toán tử: số học, so sánh, logic, gán, bit (thao tác thanh ghi), ba ngôi
#include <bitset>
#include <cstdint>
#include <iostream>
void inBit(const char* ten, uint8_t v) {
std::cout << " " << ten << " = 0b" << std::bitset<8>(v) << " (" << +v << ")\n";
}
int main() {
std::cout << "== Số học ==\n";
int a = 17, b = 5;
std::cout << " 17 + 5 = " << a + b << ", 17 - 5 = " << a - b << ", 17 * 5 = " << a * b << "\n";
std::cout << " 17 / 5 = " << a / b << " (chia SỐ NGUYÊN bỏ phần lẻ!)\n";
std::cout << " 17 % 5 = " << a % b << " (chia lấy dư)\n";
std::cout << " 17.0 / 5 = " << 17.0 / b << "\n";
int i = 5;
int x = i++; // dùng giá trị cũ rồi mới tăng
int y = ++i; // tăng trước rồi mới dùng
std::cout << " x = i++ -> " << x << ", y = ++i -> " << y << ", i = " << i << "\n";
std::cout << "\n== So sánh & logic ==\n";
float nhietDo = 31.5f;
bool troiNong = nhietDo > 30;
bool coNguoi = true;
std::cout << std::boolalpha;
std::cout << " troiNong && coNguoi = " << (troiNong && coNguoi) << "\n";
std::cout << " troiNong || false = " << (troiNong || false) << "\n";
std::cout << " !troiNong = " << !troiNong << "\n";
// Short-circuit: vế phải KHÔNG được tính nếu vế trái đã quyết định kết quả
int* p = nullptr;
if (p != nullptr && *p > 0) std::cout << "không bao giờ tới đây\n";
std::cout << " p != nullptr && *p > 0 -> an toàn nhờ short-circuit\n";
std::cout << "\n== Toán tử gán kết hợp ==\n";
int tong = 10;
tong += 5; tong *= 2; tong -= 4; tong /= 2;
std::cout << " ((10 + 5) * 2 - 4) / 2 = " << tong << "\n";
std::cout << "\n== Toán tử BIT – thao tác thanh ghi trong vi điều khiển ==\n";
uint8_t PORTB = 0b00000000; // giả lập thanh ghi 8 bit điều khiển 8 chân
PORTB |= (1 << 5); // ĐẶT bit 5 = 1 (bật LED chân 13 Arduino Uno)
inBit("Đặt bit 5 ", PORTB);
PORTB |= (1 << 0) | (1 << 2); // đặt nhiều bit cùng lúc
inBit("Đặt bit 0,2 ", PORTB);
PORTB &= ~(1 << 0); // XOÁ bit 0
inBit("Xoá bit 0 ", PORTB);
PORTB ^= (1 << 5); // ĐẢO bit 5 (nhấp nháy LED)
inBit("Đảo bit 5 ", PORTB);
bool bit2 = PORTB & (1 << 2); // ĐỌC bit 2
std::cout << " Bit 2 đang = " << bit2 << "\n";
uint16_t giaTri = 0xABCD;
uint8_t byteCao = giaTri >> 8; // lấy byte cao
uint8_t byteThap = giaTri & 0xFF; // lấy byte thấp
std::cout << std::hex << std::uppercase;
std::cout << " 0xABCD -> byte cao 0x" << +byteCao << ", byte thấp 0x" << +byteThap << "\n";
std::cout << std::dec;
std::cout << " 1 << 4 = " << (1 << 4) << " (nhân 2^4), 160 >> 3 = " << (160 >> 3) << " (chia 2^3)\n";
std::cout << "\n== Ba ngôi & thứ tự ưu tiên ==\n";
int pin = 87;
std::cout << " Pin " << pin << "%: " << (pin < 20 ? "SẮP HẾT" : "ổn") << "\n";
std::cout << " 2 + 3 * 4 = " << 2 + 3 * 4 << ", (2 + 3) * 4 = " << (2 + 3) * 4 << "\n";
uint8_t reg = 0b0100;
// Bẫy: == ưu tiên cao hơn & -> phải có ngoặc
std::cout << " (reg & 4) == 4 -> " << ((reg & 4) == 4) << "\n";
}
g++ -std=c++17 03b-toan-tu.cpp -o app && ./app== Số học == 17 + 5 = 22, 17 - 5 = 12, 17 * 5 = 85 17 / 5 = 3 (chia SỐ NGUYÊN bỏ phần lẻ!) 17 % 5 = 2 (chia lấy dư) 17.0 / 5 = 3.4 x = i++ -> 5, y = ++i -> 7, i = 7 == So sánh & logic == troiNong && coNguoi = true troiNong || false = true !troiNong = false p != nullptr && *p > 0 -> an toàn nhờ short-circuit == Toán tử gán kết hợp == ((10 + 5) * 2 - 4) / 2 = 13 == Toán tử BIT – thao tác thanh ghi trong vi điều khiển == Đặt bit 5 = 0b00100000 (32) Đặt bit 0,2 = 0b00100101 (37) Xoá bit 0 = 0b00100100 (36) Đảo bit 5 = 0b00000100 (4) Bit 2 đang = true 0xABCD -> byte cao 0xAB, byte thấp 0xCD 1 << 4 = 16 (nhân 2^4), 160 >> 3 = 20 (chia 2^3) == Ba ngôi & thứ tự ưu tiên == Pin 87%: ổn 2 + 3 * 4 = 14, (2 + 3) * 4 = 20 (reg & 4) == 4 -> true
Giải thích
i++dùng giá trị cũ rồi mới tăng;++ităng trước. Tránh dùng cả hai trên cùng biến trong một biểu thức.(1 << 5)tạo “mặt nạ” chỉ có bit 5 bằng 1.|=bật,&= ~tắt,^=đảo.giaTri >> 8vàgiaTri & 0xFFtách một số 16 bit thành 2 byte để gửi qua UART/I2C.- Toán tử
==ưu tiên cao hơn&, nên phải viết(reg & 4) == 4.
🔧Áp dụng trong lập trình nhúng
- Trên AVR:
PORTB |= (1 << PB5);nhanh hơndigitalWrite(13, HIGH)hàng chục lần. - Trên STM32:
GPIOA->BSRR = (1 << 5);đặt chân PA5 lên mức cao. - Gói dữ liệu cảm biến 16 bit thường được gửi thành 2 byte:
hi = v >> 8; lo = v & 0xFF;và ghép lại:v = (hi << 8) | lo;.
⚠Lỗi thường gặp
- Viết
&thay vì&&(và ngược lại) trong điều kiện if. - Quên
volatilecho biến cờ ngắt → vòng lặp chờ cờ không bao giờ thoát khi bật tối ưu. - Dịch bit vượt độ rộng kiểu (
1 << 32với int 32 bit) là hành vi không xác định.
✎Bài tập về nhà
🔀 Câu lệnh rẽ nhánh và vòng lặp
Điều khiển luồng chạy của chương trình bằng if/else, switch và các loại vòng lặp — kể cả vòng lặp không chặn kiểu Arduino.
- Viết if / else if / else và switch đúng cách
- Chọn đúng for / while / do-while / range-for
- Dùng break, continue
- Viết vòng
loop()không chặn bằngmillis()thay chodelay()
1Rẽ nhánh: if – else if – else và switch
if kiểm tra điều kiện bất kỳ; các nhánh else if được xét lần lượt từ trên xuống, gặp nhánh đúng đầu tiên thì dừng. switch so sánh một giá trị nguyên/ký tự/enum với nhiều hằng — gọn và nhanh hơn chuỗi if khi có nhiều lựa chọn.
case cần break;. Thiếu break, chương trình “rơi” xuống case tiếp theo (fall-through) — đôi khi cố ý (nhiều case chung xử lý), nhưng thường là lỗi.// Câu lệnh rẽ nhánh: if, else if, else, switch
#include <iostream>
void xuLyNhietDo(float t) {
std::cout << " " << t << "°C -> ";
if (t < 18) {
std::cout << "lạnh: bật máy sưởi\n";
} else if (t <= 28) {
std::cout << "dễ chịu: tắt hết\n";
} else if (t <= 35) {
std::cout << "nóng: bật quạt\n";
} else {
std::cout << "quá nóng: bật quạt + còi cảnh báo\n";
}
}
// Lệnh nhận qua Serial thường là 1 ký tự -> switch rất hợp
void xuLyLenh(char lenh) {
std::cout << " Lệnh '" << lenh << "': ";
switch (lenh) {
case 'F': std::cout << "robot đi thẳng\n"; break;
case 'B': std::cout << "robot đi lùi\n"; break;
case 'L':
case 'l': // nhiều case dùng chung một xử lý (fall-through)
std::cout << "rẽ trái\n"; break;
case 'S': std::cout << "dừng\n"; break;
default: std::cout << "không hiểu lệnh\n"; break;
}
}
enum class CheDo { TuDong, ThuCong, TietKiem };
int main() {
std::cout << "== if / else if / else ==\n";
for (float t : {15.0f, 25.0f, 31.5f, 40.0f}) xuLyNhietDo(t);
std::cout << "\n== switch ==\n";
for (char c : {'F', 'l', 'S', 'X'}) xuLyLenh(c);
std::cout << "\n== switch với enum class ==\n";
CheDo cheDo = CheDo::TietKiem;
switch (cheDo) {
case CheDo::TuDong: std::cout << " Tự động\n"; break;
case CheDo::ThuCong: std::cout << " Thủ công\n"; break;
case CheDo::TietKiem: std::cout << " Tiết kiệm pin: giảm độ sáng màn hình\n"; break;
}
std::cout << "\n== if có khởi tạo (C++17) ==\n";
if (int adc = 3100; adc > 3000) { // biến adc chỉ sống trong if/else này
std::cout << " ADC = " << adc << " > 3000: đất khô, bật bơm\n";
}
std::cout << "\n== Bẫy thường gặp ==\n";
int x = 0;
if (x = 5) { // GÁN chứ không phải SO SÁNH! luôn đúng (g++ -Wall sẽ cảnh báo)
std::cout << " if (x = 5) luôn chạy vào đây, x giờ = " << x << "\n";
}
}
g++ -std=c++17 04a-re-nhanh.cpp -o app && ./app== if / else if / else == 15°C -> lạnh: bật máy sưởi 25°C -> dễ chịu: tắt hết 31.5°C -> nóng: bật quạt 40°C -> quá nóng: bật quạt + còi cảnh báo == switch == Lệnh 'F': robot đi thẳng Lệnh 'l': rẽ trái Lệnh 'S': dừng Lệnh 'X': không hiểu lệnh == switch với enum class == Tiết kiệm pin: giảm độ sáng màn hình == if có khởi tạo (C++17) == ADC = 3100 > 3000: đất khô, bật bơm == Bẫy thường gặp == if (x = 5) luôn chạy vào đây, x giờ = 5
Giải thích
enum classđặt tên cho các trạng thái — dễ đọc hơn số 0, 1, 2.- C++17 cho phép
if (khởi_tạo; điều_kiện)để biến chỉ sống trong if. if (x = 5)là phép GÁN, luôn đúng. Bật-Wallđể trình biên dịch cảnh báo (xem dòng cảnh báo khi biên dịch).
2Vòng lặp
| Vòng lặp | Dùng khi |
|---|---|
for (khởi tạo; điều kiện; bước) | biết trước số lần lặp |
while (điều kiện) | lặp tới khi điều kiện sai, có thể không chạy lần nào |
do { } while (điều kiện); | chạy ít nhất 1 lần (thử kết nối, đọc lại) |
for (auto x : mang) | duyệt mọi phần tử của mảng/container |
break thoát hẳn vòng lặp, continue bỏ qua phần còn lại của lượt hiện tại.
// Vòng lặp: for, while, do-while, range-for, break/continue – và vòng loop() "không chặn" kiểu Arduino
#include <iostream>
// Giả lập millis() của Arduino: mỗi lần gọi thời gian trôi thêm 100 ms
unsigned long thoiGian = 0;
unsigned long millis() { return thoiGian += 100; }
int main() {
std::cout << "== for: đếm biết trước số lần ==\n ";
for (int i = 1; i <= 5; i++) std::cout << i << " ";
std::cout << "\n";
std::cout << "\n== while: lặp khi điều kiện còn đúng ==\n";
int pin = 100;
int phut = 0;
while (pin > 20) { // dùng tới khi pin xuống 20%
pin -= 15;
phut += 30;
}
std::cout << " Sau " << phut << " phút pin còn " << pin << "%\n";
std::cout << "\n== do-while: chạy ít nhất 1 lần ==\n";
int lanThu = 0;
bool ketNoi = false;
do {
lanThu++;
ketNoi = (lanThu == 3); // giả sử lần thứ 3 kết nối WiFi thành công
std::cout << " Thử kết nối WiFi lần " << lanThu << (ketNoi ? ": OK\n" : ": thất bại\n");
} while (!ketNoi && lanThu < 5);
std::cout << "\n== range-for: duyệt mọi phần tử ==\n ";
int docCamBien[] = {512, 530, 498, 1023, 505};
long tong = 0;
for (int v : docCamBien) tong += v;
std::cout << "Trung bình = " << tong / 5 << "\n";
std::cout << "\n== break & continue ==\n ";
for (int v : docCamBien) {
if (v == 1023) { std::cout << "[nhiễu, bỏ qua] "; continue; } // bỏ qua lượt này
if (v < 500) { std::cout << v << " [< 500, dừng] "; break; } // thoát hẳn vòng lặp
std::cout << v << " ";
}
std::cout << "\n";
std::cout << "\n== Lồng nhau: vẽ ma trận LED 3x5 ==\n";
for (int hang = 0; hang < 3; hang++) {
std::cout << " ";
for (int cot = 0; cot < 5; cot++) std::cout << ((hang + cot) % 2 ? "○ " : "● ");
std::cout << "\n";
}
std::cout << "\n== Vòng loop() không chặn (non-blocking) bằng millis() ==\n";
// Thay vì delay(500) làm treo cả chương trình, ta kiểm tra "đã đủ thời gian chưa?"
unsigned long lanCuoiNhay = 0;
bool led = false;
for (int vong = 0; vong < 25; vong++) { // giả lập 25 lần chạy loop()
unsigned long now = millis();
if (now - lanCuoiNhay >= 500) {
lanCuoiNhay = now;
led = !led;
std::cout << " t=" << now << "ms: LED " << (led ? "BẬT" : "TẮT") << "\n";
}
// ... các việc khác (đọc nút, cảm biến) vẫn chạy mỗi vòng
}
}
g++ -std=c++17 04b-vong-lap.cpp -o app && ./app== for: đếm biết trước số lần == 1 2 3 4 5 == while: lặp khi điều kiện còn đúng == Sau 180 phút pin còn 10% == do-while: chạy ít nhất 1 lần == Thử kết nối WiFi lần 1: thất bại Thử kết nối WiFi lần 2: thất bại Thử kết nối WiFi lần 3: OK == range-for: duyệt mọi phần tử == Trung bình = 613 == break & continue == 512 530 498 [< 500, dừng] == Lồng nhau: vẽ ma trận LED 3x5 == ● ○ ● ○ ● ○ ● ○ ● ○ ● ○ ● ○ ● == Vòng loop() không chặn (non-blocking) bằng millis() == t=500ms: LED BẬT t=1000ms: LED TẮT t=1500ms: LED BẬT t=2000ms: LED TẮT t=2500ms: LED BẬT
Giải thích
- Kỹ thuật non-blocking: lưu thời điểm lần cuối làm việc, mỗi vòng kiểm tra
now - lanCuoi >= chuKy. - Phép trừ
now - lanCuoivớiunsigned longvẫn đúng ngay cả khimillis()tràn số sau 49 ngày. - Vòng lặp lồng nhau: vòng trong chạy hết cho mỗi lượt của vòng ngoài — dùng cho ma trận LED, bản đồ.
🔧Áp dụng trong lập trình nhúng
loop()của Arduino chính là một vòngwhile(true). Không bao giờ dùngdelay()dài khi cần đọc nút nhấn hay cảm biến đồng thời.- Máy trạng thái (state machine) =
switchtrên biến trạng thái bên trongloop(). - Vòng chờ cờ:
while (!coNgat) {}— biến cờ phải làvolatile.
⚠Lỗi thường gặp
- Dấu
;ngay sauif (...)hoặcfor (...)làm khối lệnh luôn chạy / chỉ chạy một lần. - Vòng
whilequên cập nhật biến điều kiện → lặp vô hạn, chip bị watchdog reset. - Quên
breaktrongswitch.
✎Bài tập về nhà
🧮 Hàm và xử lý số (Number)
Chia chương trình thành các hàm rõ ràng; dùng thư viện toán và ép kiểu số đúng cách.
- Khai báo, định nghĩa, gọi hàm; tách khai báo vào file .h
- Truyền tham trị, tham chiếu, con trỏ; tham số mặc định; nạp chồng
- Viết hàm đệ quy, lambda, constexpr
- Dùng
<cmath>, ép kiểu, số ngẫu nhiên,map()/constrain()
1Hàm
Hàm gom một nhóm lệnh thành một đơn vị có tên để tái sử dụng và làm code dễ đọc. Cấu trúc: kiểu_trả_về tên(danh_sách_tham_số) { thân hàm }.
| Cách truyền | Cú pháp | Hàm sửa được biến gốc? | Khi nào dùng |
|---|---|---|---|
| Tham trị | void f(int x) | Không (nhận bản sao) | kiểu nhỏ: int, float, char |
| Tham chiếu | void f(int& x) | Có | cần sửa biến, trả nhiều kết quả |
| Tham chiếu hằng | void f(const T& x) | Không | đối tượng lớn (string, struct) — không sao chép |
| Con trỏ | void f(int* x) | Có | có thể truyền nullptr; giao tiếp với thư viện C |
// Hàm: khai báo, định nghĩa, truyền tham trị / tham chiếu / con trỏ, mặc định, nạp chồng, đệ quy, inline
#include <iostream>
// 1. KHAI BÁO (prototype) trước – ĐỊNH NGHĨA ở cuối file. Thường đặt khai báo trong file .h
float celsiusToF(float c);
// 2. Truyền THAM TRỊ: hàm nhận BẢN SAO, sửa không ảnh hưởng biến gốc
void tangThamTri(int x) { x++; }
// 3. Truyền THAM CHIẾU: hàm làm việc trên CHÍNH biến gốc
void tangThamChieu(int& x) { x++; }
// 4. Truyền CON TRỎ: giống tham chiếu nhưng phải dùng * và có thể là nullptr
void tangConTro(int* x) { if (x) (*x)++; }
// 5. "Trả về nhiều giá trị" bằng tham chiếu đầu ra
void minMax(const int a[], int n, int& mn, int& mx) {
mn = mx = a[0];
for (int i = 1; i < n; i++) {
if (a[i] < mn) mn = a[i];
if (a[i] > mx) mx = a[i];
}
}
// 6. Tham số MẶC ĐỊNH (phải nằm ở cuối danh sách)
void nhayLed(int pin, int soLan = 3, int msMoiLan = 200) {
std::cout << " Nháy LED chân " << pin << " x" << soLan << " lần, mỗi lần " << msMoiLan << "ms\n";
}
// 7. NẠP CHỒNG hàm: cùng tên, khác kiểu/số tham số
int binhPhuong(int x) { return x * x; }
double binhPhuong(double x) { return x * x; }
// 8. ĐỆ QUY: hàm tự gọi chính nó – phải có điều kiện dừng
unsigned long giaiThua(unsigned n) { return n <= 1 ? 1 : n * giaiThua(n - 1); }
// 9. inline / constexpr: gợi ý chèn thân hàm tại chỗ gọi, tránh chi phí gọi hàm
constexpr int adcToMilliVolt(int adc) { return adc * 3300 / 4095; }
int main() {
std::cout << "== Gọi hàm đã khai báo trước ==\n";
std::cout << " 30°C = " << celsiusToF(30) << "°F\n";
std::cout << "\n== Tham trị / tham chiếu / con trỏ ==\n";
int a = 10;
tangThamTri(a); std::cout << " sau tangThamTri: a = " << a << "\n";
tangThamChieu(a); std::cout << " sau tangThamChieu: a = " << a << "\n";
tangConTro(&a); std::cout << " sau tangConTro: a = " << a << "\n";
int mau[] = {23, 7, 91, 45, 12};
int mn, mx;
minMax(mau, 5, mn, mx);
std::cout << " min = " << mn << ", max = " << mx << "\n";
std::cout << "\n== Tham số mặc định ==\n";
nhayLed(13);
nhayLed(13, 5);
nhayLed(2, 1, 1000);
std::cout << "\n== Nạp chồng ==\n";
std::cout << " binhPhuong(7) = " << binhPhuong(7) << ", binhPhuong(1.5) = " << binhPhuong(1.5) << "\n";
std::cout << "\n== Đệ quy ==\n";
for (unsigned n : {0u, 5u, 10u}) std::cout << " " << n << "! = " << giaiThua(n) << "\n";
std::cout << "\n== constexpr: tính sẵn lúc biên dịch ==\n";
constexpr int mv = adcToMilliVolt(2048);
std::cout << " ADC 2048 ≈ " << mv << " mV\n";
std::cout << "\n== Lambda: hàm không tên, viết ngay tại chỗ ==\n";
auto loc = [](int v) { return v > 50; };
for (int v : mau) if (loc(v)) std::cout << " " << v << " > 50\n";
}
// ĐỊNH NGHĨA hàm đã khai báo ở đầu file
float celsiusToF(float c) {
return c * 9 / 5 + 32;
}
g++ -std=c++17 05a-ham.cpp -o app && ./app== Gọi hàm đã khai báo trước == 30°C = 86°F == Tham trị / tham chiếu / con trỏ == sau tangThamTri: a = 10 sau tangThamChieu: a = 11 sau tangConTro: a = 12 min = 7, max = 91 == Tham số mặc định == Nháy LED chân 13 x3 lần, mỗi lần 200ms Nháy LED chân 13 x5 lần, mỗi lần 200ms Nháy LED chân 2 x1 lần, mỗi lần 1000ms == Nạp chồng == binhPhuong(7) = 49, binhPhuong(1.5) = 2.25 == Đệ quy == 0! = 1 5! = 120 10! = 3628800 == constexpr: tính sẵn lúc biên dịch == ADC 2048 ≈ 1650 mV == Lambda: hàm không tên, viết ngay tại chỗ == 91 > 50
Giải thích
- Khai báo (prototype) ở đầu file cho phép gọi hàm trước khi định nghĩa. Dự án thật đặt khai báo trong file
.h. - Tham số mặc định phải nằm ở cuối danh sách tham số.
- Hàm đệ quy cần điều kiện dừng; trên MCU stack nhỏ nên tránh đệ quy sâu.
constexprcho phép trình biên dịch tính sẵn kết quả —adcToMilliVolt(2048)không tốn thời gian chạy.- Dòng cảnh báo “parameter set but not used” là do hàm
tangThamTricố ý sửa bản sao vô ích — đúng thứ ví dụ muốn chứng minh.
2Number: thư viện toán và ép kiểu
// Number: thư viện toán <cmath>, ép kiểu số, số ngẫu nhiên, các hàm tiện ích hay dùng trong Arduino
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <random>
// Tự viết lại map() và constrain() của Arduino
long mapRange(long x, long inMin, long inMax, long outMin, long outMax) {
return (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
}
int main() {
std::cout << std::fixed << std::setprecision(3);
std::cout << "== <cmath> ==\n";
std::cout << " sqrt(2) = " << std::sqrt(2.0) << "\n";
std::cout << " pow(2, 10) = " << std::pow(2, 10) << "\n";
std::cout << " sin(30°) = " << std::sin(30 * 3.14159265358979 / 180) << "\n";
std::cout << " hypot(3, 4) = " << std::hypot(3, 4) << " (khoảng cách)\n";
std::cout << " fabs(-2.5) = " << std::fabs(-2.5) << ", abs(-7) = " << std::abs(-7) << "\n";
std::cout << " floor(2.7) = " << std::floor(2.7) << ", ceil(2.1) = " << std::ceil(2.1)
<< ", round(2.5) = " << std::round(2.5) << "\n";
std::cout << " log10(1000) = " << std::log10(1000) << ", exp(1) = " << std::exp(1) << "\n";
std::cout << "\n== Ép kiểu số ==\n";
int adc = 2047;
float volt1 = adc * 3.3 / 4095; // 3.3 là double -> phép tính số thực
float volt2 = adc * 33 / 40950; // toàn số nguyên -> mất phần thập phân!
float volt3 = static_cast<float>(adc) * 33 / 40950;
std::cout << " " << volt1 << " V (đúng) | " << volt2 << " V (sai: chia nguyên) | " << volt3 << " V (ép kiểu)\n";
double d = 9.99;
int cat = static_cast<int>(d); // cắt phần lẻ, KHÔNG làm tròn
int lamTron = static_cast<int>(std::lround(d));
std::cout << " (int)9.99 = " << cat << ", lround(9.99) = " << lamTron << "\n";
std::cout << "\n== map() và constrain() kiểu Arduino ==\n";
for (int bienTro : {0, 512, 1023}) {
long pwm = mapRange(bienTro, 0, 1023, 0, 255); // biến trở 10 bit -> PWM 8 bit
std::cout << " biến trở " << std::setw(4) << bienTro << " -> PWM " << pwm << "\n";
}
int tocDo = 300;
std::cout << " constrain(300, 0, 255) = " << std::clamp(tocDo, 0, 255) << "\n";
std::cout << "\n== Số ngẫu nhiên ==\n";
std::mt19937 gen(2026); // seed cố định -> lần nào chạy cũng giống nhau (dễ kiểm tra)
std::uniform_int_distribution<int> xucXac(1, 6);
std::cout << " Tung xúc xắc: ";
for (int i = 0; i < 8; i++) std::cout << xucXac(gen) << " ";
std::cout << "\n (Arduino: randomSeed(analogRead(A0)); random(1, 7);)\n";
std::cout << "\n== Giới hạn & đặc biệt ==\n";
std::cout << " 1.0 / 0 = " << 1.0 / 0 << ", sqrt(-1) = " << std::sqrt(-1.0)
<< ", isnan? " << std::boolalpha << std::isnan(std::sqrt(-1.0)) << "\n";
float a = 0.1f + 0.2f;
std::cout << " So sánh số thực đúng cách: |a - 0.3| < 1e-6 ? " << (std::fabs(a - 0.3f) < 1e-6f) << "\n";
}
g++ -std=c++17 05b-number.cpp -o app && ./app== <cmath> == sqrt(2) = 1.414 pow(2, 10) = 1024.000 sin(30°) = 0.500 hypot(3, 4) = 5.000 (khoảng cách) fabs(-2.5) = 2.500, abs(-7) = 7 floor(2.7) = 2.000, ceil(2.1) = 3.000, round(2.5) = 3.000 log10(1000) = 3.000, exp(1) = 2.718 == Ép kiểu số == 1.650 V (đúng) | 1.000 V (sai: chia nguyên) | 1.650 V (ép kiểu) (int)9.99 = 9, lround(9.99) = 10 == map() và constrain() kiểu Arduino == biến trở 0 -> PWM 0 biến trở 512 -> PWM 127 biến trở 1023 -> PWM 255 constrain(300, 0, 255) = 255 == Số ngẫu nhiên == Tung xúc xắc: 2 5 3 6 6 6 1 5 (Arduino: randomSeed(analogRead(A0)); random(1, 7);) == Giới hạn & đặc biệt == 1.0 / 0 = inf, sqrt(-1) = nan, isnan? true So sánh số thực đúng cách: |a - 0.3| < 1e-6 ? true
Giải thích
adc * 33 / 40950toàn số nguyên nên bị cắt phần lẻ trước khi gán vàofloat— lỗi rất phổ biến khi đổi ADC sang điện áp.static_cast<int>(9.99)cắt bỏ phần lẻ (9), muốn làm tròn dùngstd::lround.std::mt19937với seed cố định cho dãy giống nhau mỗi lần chạy — tiện kiểm thử. Trên Arduino:randomSeed(analogRead(A0))lấy nhiễu làm seed.- Cảnh báo “division by zero” là do ví dụ cố ý tính
1.0 / 0để minh hoạ giá trịinf.
🔧Áp dụng trong lập trình nhúng
- Hàm
map()của Arduino dùng số nguyên nên có sai số làm tròn;constrain()tương đươngstd::clamp. - Trên chip không có FPU (Arduino Uno), phép tính
floatrất chậm — ưu tiên số nguyên “nhân 10” (285 thay cho 28.5°C). - Hàm ngắn gọi rất nhiều lần nên khai báo
inline/constexpr.
⚠Lỗi thường gặp
- Hàm khai báo trả về giá trị nhưng quên
return→ kết quả rác. - Truyền
std::string/struct lớn bằng tham trị làm chậm và tốn RAM. - Nhầm
abs()(số nguyên) vớifabs()(số thực).
✎Bài tập về nhà
📚 Mảng (Array) và chuỗi (String)
Lưu nhiều giá trị cùng kiểu bằng mảng, xử lý chuỗi kiểu C và std::string, phân tích lệnh văn bản.
- Khai báo, khởi tạo, duyệt mảng 1 và 2 chiều; truyền mảng vào hàm
- Dùng
std::arrayvà kỹ thuật bộ đệm vòng - Thao tác chuỗi kiểu C an toàn (
snprintf) vàstd::string - Phân tích lệnh dạng
TEN:GIA_TRI
1Mảng
Mảng là dãy các phần tử cùng kiểu, nằm liên tiếp trong bộ nhớ, đánh chỉ số từ 0. Kích thước mảng tĩnh phải biết lúc biên dịch.
- Số phần tử:
sizeof(a) / sizeof(a[0])(chỉ đúng khialà mảng thật, không phải con trỏ). - Khi truyền vào hàm, mảng “suy biến” thành con trỏ tới phần tử đầu → phải truyền kèm số phần tử.
std::array<T, N>là mảng tĩnh “có văn hoá”: biết kích thước, có.at()kiểm tra chỉ số.
// Mảng: một chiều, hai chiều, truyền vào hàm, std::array, bộ đệm vòng (ring buffer)
#include <array>
#include <iostream>
// Mảng truyền vào hàm sẽ "suy biến" thành con trỏ -> phải truyền kèm số phần tử
int tinhTong(const int a[], int n) {
int s = 0;
for (int i = 0; i < n; i++) s += a[i];
return s;
}
// std::array giữ được kích thước, có .size(), an toàn hơn
template <size_t N>
float trungBinh(const std::array<int, N>& a) {
long s = 0;
for (int v : a) s += v;
return float(s) / a.size();
}
// Bộ lọc trung bình trượt dùng bộ đệm vòng – kỹ thuật kinh điển để lọc nhiễu cảm biến
class MovingAverage {
public:
float them(int giaTri) {
tong_ -= buf_[viTri_];
buf_[viTri_] = giaTri;
tong_ += giaTri;
viTri_ = (viTri_ + 1) % N; // quay vòng về 0 khi tới cuối mảng
if (dem_ < N) dem_++;
return float(tong_) / dem_;
}
private:
static constexpr int N = 4;
int buf_[N] = {0};
int viTri_ = 0, dem_ = 0;
long tong_ = 0;
};
int main() {
std::cout << "== Mảng một chiều ==\n";
int nhietDo[7] = {28, 30, 31, 29, 27, 33, 32}; // nhiệt độ 7 ngày
int n = sizeof(nhietDo) / sizeof(nhietDo[0]); // số phần tử = tổng byte / byte mỗi phần tử
std::cout << " Số phần tử: " << n << ", phần tử đầu: " << nhietDo[0] << ", cuối: " << nhietDo[n - 1] << "\n";
std::cout << " Tổng: " << tinhTong(nhietDo, n) << "\n";
int chanLed[] = {2, 4, 5, 18}; // khởi tạo không ghi kích thước
std::cout << " Các chân LED: ";
for (int p : chanLed) std::cout << p << " ";
std::cout << "\n";
int day[5] = {1, 2}; // phần còn lại tự = 0
std::cout << " day[4] = " << day[4] << "\n";
// nhietDo[7] = 0; // LỖI NGUY HIỂM: truy cập ngoài mảng -> ghi đè bộ nhớ khác, không báo lỗi!
std::cout << "\n== Mảng hai chiều: bản đồ 4x6 cho robot ==\n";
const int H = 4, W = 6;
char banDo[H][W] = {
{'.', '.', '#', '.', '.', '.'},
{'.', '#', '#', '.', '#', '.'},
{'.', '.', '.', '.', '#', '.'},
{'#', '#', '.', '.', '.', 'G'},
};
int soVatCan = 0;
for (int r = 0; r < H; r++) {
std::cout << " ";
for (int c = 0; c < W; c++) {
std::cout << banDo[r][c] << ' ';
if (banDo[r][c] == '#') soVatCan++;
}
std::cout << "\n";
}
std::cout << " Số ô vật cản: " << soVatCan << "\n";
std::cout << "\n== std::array ==\n";
std::array<int, 5> adc = {510, 520, 505, 515, 500};
std::cout << " size = " << adc.size() << ", trung bình = " << trungBinh(adc) << "\n";
std::cout << " adc.at(2) = " << adc.at(2) << " (at() kiểm tra chỉ số, an toàn hơn [])\n";
std::cout << "\n== Lọc nhiễu bằng trung bình trượt (ring buffer) ==\n";
MovingAverage loc;
for (int v : {500, 502, 498, 900, 501, 499, 503}) { // 900 là nhiễu
std::cout << " đọc " << v << " -> sau lọc " << loc.them(v) << "\n";
}
}
g++ -std=c++17 06a-mang.cpp -o app && ./app== Mảng một chiều == Số phần tử: 7, phần tử đầu: 28, cuối: 32 Tổng: 210 Các chân LED: 2 4 5 18 day[4] = 0 == Mảng hai chiều: bản đồ 4x6 cho robot == . . # . . . . # # . # . . . . . # . # # . . . G Số ô vật cản: 7 == std::array == size = 5, trung bình = 510 adc.at(2) = 505 (at() kiểm tra chỉ số, an toàn hơn []) == Lọc nhiễu bằng trung bình trượt (ring buffer) == đọc 500 -> sau lọc 500 đọc 502 -> sau lọc 501 đọc 498 -> sau lọc 500 đọc 900 -> sau lọc 600 đọc 501 -> sau lọc 600.25 đọc 499 -> sau lọc 599.5 đọc 503 -> sau lọc 600.75
Giải thích
- Khởi tạo thiếu phần tử (
int day[5] = {1, 2};) thì phần còn lại tự bằng 0. - Mảng 2 chiều
banDo[H][W]duyệt bằng 2 vòng lặp lồng nhau (hàng rồi cột). - Lớp
MovingAveragedùng bộ đệm vòng: thay phần tử cũ nhất bằng phần tử mới, cập nhật tổng trong O(1).
2Chuỗi
Chuỗi kiểu C — char[]
Mảng ký tự kết thúc bằng '\0'. Nhẹ, không cấp phát động, dùng nhiều trong nhúng. Hàm trong <cstring>: strlen, strcpy, strcat, strcmp, strchr. Dễ tràn bộ đệm — dùng snprintf.
std::string
Tự quản lý bộ nhớ, có +, find, substr, replace, to_string, stoi. Tiện nhưng cấp phát heap → cẩn thận trên MCU RAM nhỏ (Arduino có lớp String tương tự).
// Chuỗi: chuỗi kiểu C (char[]) và std::string – kèm ví dụ phân tích lệnh nhận từ Serial
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
int main() {
std::cout << "== Chuỗi kiểu C: mảng char kết thúc bằng '\\0' ==\n";
char ten[16] = "ESP32"; // 5 ký tự + '\0' = 6 byte được dùng
std::cout << " ten = " << ten << ", strlen = " << std::strlen(ten) << ", sizeof = " << sizeof(ten) << "\n";
std::strcat(ten, "-S3"); // nối chuỗi (PHẢI đủ chỗ, nếu không sẽ tràn bộ đệm!)
std::cout << " sau strcat: " << ten << "\n";
std::cout << " strcmp(\"abc\", \"abd\") = " << std::strcmp("abc", "abd") << " (âm: abc đứng trước)\n";
char buf[32];
int nhiet = 28, am = 65;
// snprintf: định dạng chuỗi AN TOÀN, không bao giờ ghi quá kích thước buf
std::snprintf(buf, sizeof(buf), "T=%dC H=%d%%", nhiet, am);
std::cout << " snprintf -> \"" << buf << "\"\n";
char nho[8];
int canGhi = std::snprintf(nho, sizeof(nho), "Xin chao cac ban");
std::cout << " Bộ đệm 8 byte chỉ giữ: \"" << nho << "\" (cần " << canGhi << " ký tự)\n";
std::cout << "\n== std::string: tự quản lý bộ nhớ, dễ dùng ==\n";
std::string s = "Hello";
s += ", BKSTAR"; // nối
std::cout << " s = " << s << ", length = " << s.length() << "\n";
std::cout << " s.substr(7, 6) = " << s.substr(7, 6) << "\n";
std::cout << " s.find(\"BK\") = " << s.find("BK") << "\n";
s.replace(0, 5, "Xin chao");
std::cout << " sau replace: " << s << "\n";
std::cout << " so sánh == : " << std::boolalpha << (std::string("on") == "on") << "\n";
std::cout << "\n== Chuyển đổi số <-> chuỗi ==\n";
std::string soChuoi = std::to_string(3.5);
int so = std::stoi("123") + 1;
float f = std::stof("27.75");
std::cout << " to_string(3.5) = " << soChuoi << ", stoi(\"123\")+1 = " << so << ", stof = " << f << "\n";
std::cout << "\n== Phân tích lệnh dạng \"TEN:GIA_TRI\" nhận qua Serial ==\n";
for (std::string lenh : {"LED:1", "SERVO:90", "SPEED:255", "SAI_CU_PHAP"}) {
size_t pos = lenh.find(':');
if (pos == std::string::npos) {
std::cout << " \"" << lenh << "\" -> lỗi cú pháp\n";
continue;
}
std::string ten = lenh.substr(0, pos);
int giaTri = std::stoi(lenh.substr(pos + 1));
std::cout << " \"" << lenh << "\" -> thiết bị " << ten << ", giá trị " << giaTri << "\n";
}
std::cout << "\n== Lưu ý tiếng Việt: length() đếm BYTE (UTF-8), không phải số chữ ==\n";
std::string viet = "Việt";
std::cout << " \"Việt\" có 4 chữ nhưng length() = " << viet.length() << "\n";
}
g++ -std=c++17 06b-chuoi.cpp -o app && ./app== Chuỗi kiểu C: mảng char kết thúc bằng '\0' ==
ten = ESP32, strlen = 5, sizeof = 16
sau strcat: ESP32-S3
strcmp("abc", "abd") = -1 (âm: abc đứng trước)
snprintf -> "T=28C H=65%"
Bộ đệm 8 byte chỉ giữ: "Xin cha" (cần 16 ký tự)
== std::string: tự quản lý bộ nhớ, dễ dùng ==
s = Hello, BKSTAR, length = 13
s.substr(7, 6) = BKSTAR
s.find("BK") = 7
sau replace: Xin chao, BKSTAR
so sánh == : true
== Chuyển đổi số <-> chuỗi ==
to_string(3.5) = 3.500000, stoi("123")+1 = 124, stof = 27.75
== Phân tích lệnh dạng "TEN:GIA_TRI" nhận qua Serial ==
"LED:1" -> thiết bị LED, giá trị 1
"SERVO:90" -> thiết bị SERVO, giá trị 90
"SPEED:255" -> thiết bị SPEED, giá trị 255
"SAI_CU_PHAP" -> lỗi cú pháp
== Lưu ý tiếng Việt: length() đếm BYTE (UTF-8), không phải số chữ ==
"Việt" có 4 chữ nhưng length() = 6Giải thích
snprintfkhông bao giờ ghi quá kích thước bộ đệm và trả về số ký tự cần — dùng để phát hiện bị cắt.findtrả vềstd::string::nposkhi không tìm thấy.- Chuỗi tiếng Việt UTF-8: một chữ có dấu chiếm 2–3 byte, nên
length()lớn hơn số chữ nhìn thấy.
🔧Áp dụng trong lập trình nhúng
- Đọc lệnh từ Serial vào bộ đệm
char buf[32]tới khi gặp'\n', rồi phân tích — tránh dùngStringliên tục gây phân mảnh heap trên Arduino Uno. - Lưu mẫu ADC vào mảng tĩnh để lọc nhiễu, tính trung bình, tìm đỉnh.
- Bảng tra (lookup table)
const— ví dụ bảng sin cho PWM — được đặt trong Flash.
⚠Lỗi thường gặp
- Truy cập
a[n]với mảng n phần tử (chỉ số hợp lệ 0..n-1) — không báo lỗi nhưng ghi đè bộ nhớ khác. - Mảng
charthiếu chỗ cho'\0':char s[5] = "Hello";là lỗi. - So sánh chuỗi C bằng
==(so địa chỉ!) thay vìstrcmp.
✎Bài tập về nhà
👉 Con trỏ: địa chỉ, con trỏ và con trỏ tới con trỏ
Hiểu bản chất con trỏ — biến chứa địa chỉ — và dùng để truy cập mảng, truyền tham số, sửa con trỏ của nơi khác.
- Dùng
&lấy địa chỉ và*truy cập giá trị - Hiểu số học con trỏ và mối liên hệ con trỏ – mảng
- Dùng
nullptrvàconstvới con trỏ - Hiểu và dùng con trỏ tới con trỏ
int**
1Con trỏ là gì?
Mỗi biến nằm ở một địa chỉ trong bộ nhớ. Con trỏ là biến dùng để lưu địa chỉ của biến khác.
| Cú pháp | Ý nghĩa |
|---|---|
int* p; | p là con trỏ tới int |
p = &x; | & lấy địa chỉ của x |
*p | * “đi tới” địa chỉ, lấy giá trị ở đó (dereference) |
p + 1 | địa chỉ phần tử kế tiếp: tăng sizeof(int) byte, không phải 1 byte |
int** pp = &p; | con trỏ tới con trỏ: **pp == x |
2Ví dụ đầy đủ
// Con trỏ với địa chỉ – địa chỉ với con trỏ – con trỏ tới con trỏ
// (Địa chỉ thật thay đổi mỗi lần chạy nên ví dụ chỉ in KHOẢNG CÁCH và kết quả so sánh)
#include <cstdint>
#include <iostream>
void hoanDoi(int* a, int* b) { // đổi chỗ hai biến qua con trỏ
int tam = *a;
*a = *b;
*b = tam;
}
void capPhatLai(int** pp, int* moi) { // con trỏ tới con trỏ: hàm SỬA ĐƯỢC chính con trỏ của bên gọi
*pp = moi;
}
int main() {
std::cout << std::boolalpha;
std::cout << "== & lấy địa chỉ, * truy cập giá trị tại địa chỉ ==\n";
int nhietDo = 28;
int* p = &nhietDo; // p chứa ĐỊA CHỈ của nhietDo
std::cout << " *p = " << *p << "\n";
*p = 35; // ghi qua con trỏ = ghi vào nhietDo
std::cout << " sau *p = 35: nhietDo = " << nhietDo << "\n";
std::cout << " p == &nhietDo ? " << (p == &nhietDo) << "\n";
std::cout << " sizeof(p) = " << sizeof(p) << " byte (máy 64 bit; ESP32/STM32 là 4 byte)\n";
std::cout << "\n== nullptr: con trỏ không trỏ vào đâu ==\n";
int* q = nullptr;
if (q == nullptr) std::cout << " q rỗng – luôn kiểm tra trước khi dùng *q\n";
std::cout << "\n== Con trỏ và mảng: tên mảng là địa chỉ phần tử đầu ==\n";
int adc[5] = {100, 200, 300, 400, 500};
int* pa = adc; // tương đương &adc[0]
std::cout << " *pa = " << *pa << ", *(pa + 2) = " << *(pa + 2) << ", pa[3] = " << pa[3] << "\n";
std::cout << " (pa + 1) cách pa bao nhiêu byte? "
<< (reinterpret_cast<uintptr_t>(pa + 1) - reinterpret_cast<uintptr_t>(pa))
<< " = sizeof(int)\n";
std::cout << " Duyệt bằng con trỏ: ";
for (int* it = adc; it != adc + 5; ++it) std::cout << *it << " ";
std::cout << "\n Số phần tử giữa hai con trỏ: " << (&adc[4] - &adc[0]) << "\n";
std::cout << "\n== Truyền con trỏ vào hàm ==\n";
int a = 1, b = 2;
hoanDoi(&a, &b);
std::cout << " sau hoanDoi: a = " << a << ", b = " << b << "\n";
std::cout << "\n== Con trỏ tới con trỏ (int**) ==\n";
int x = 10, y = 20;
int* px = &x;
int** ppx = &px; // ppx trỏ tới biến con trỏ px
std::cout << " **ppx = " << **ppx << "\n";
capPhatLai(&px, &y); // hàm đổi px sang trỏ y
std::cout << " sau capPhatLai: *px = " << *px << ", px == &y ? " << (px == &y) << "\n";
std::cout << "\n== Mảng con trỏ: bảng tên chế độ ==\n";
const char* cheDo[] = {"TẮT", "TỰ ĐỘNG", "THỦ CÔNG"};
for (int i = 0; i < 3; i++) std::cout << " cheDo[" << i << "] = " << cheDo[i] << "\n";
std::cout << "\n== const với con trỏ ==\n";
const int* chiDoc = &x; // không sửa được GIÁ TRỊ qua con trỏ: *chiDoc = 5 -> lỗi
int* const coDinh = &x; // không đổi được ĐỊA CHỈ: coDinh = &y -> lỗi
*coDinh = 11;
std::cout << " *chiDoc = " << *chiDoc << "\n";
}
g++ -std=c++17 07-con-tro-dia-chi.cpp -o app && ./app== & lấy địa chỉ, * truy cập giá trị tại địa chỉ == *p = 28 sau *p = 35: nhietDo = 35 p == &nhietDo ? true sizeof(p) = 8 byte (máy 64 bit; ESP32/STM32 là 4 byte) == nullptr: con trỏ không trỏ vào đâu == q rỗng – luôn kiểm tra trước khi dùng *q == Con trỏ và mảng: tên mảng là địa chỉ phần tử đầu == *pa = 100, *(pa + 2) = 300, pa[3] = 400 (pa + 1) cách pa bao nhiêu byte? 4 = sizeof(int) Duyệt bằng con trỏ: 100 200 300 400 500 Số phần tử giữa hai con trỏ: 4 == Truyền con trỏ vào hàm == sau hoanDoi: a = 2, b = 1 == Con trỏ tới con trỏ (int**) == **ppx = 10 sau capPhatLai: *px = 20, px == &y ? true == Mảng con trỏ: bảng tên chế độ == cheDo[0] = TẮT cheDo[1] = TỰ ĐỘNG cheDo[2] = THỦ CÔNG == const với con trỏ == *chiDoc = 11
Giải thích
- Địa chỉ thật thay đổi mỗi lần chạy nên ví dụ chỉ in khoảng cách và kết quả so sánh — kết quả luôn giống nhau.
- Tên mảng
adcchính là địa chỉ phần tử đầu;pa[3]tương đương*(pa + 3). - Hàm
hoanDoi(int*, int*)sửa được biến của nơi gọi vì nhận địa chỉ của chúng. capPhatLai(int** pp, ...)sửa được chính con trỏ của nơi gọi — kỹ thuật này dùng nhiều trong thư viện C (ví dụ hàm cấp phát trả kết quả qua tham số).const int* p: không sửa giá trị qua p.int* const p: không đổi được địa chỉ p trỏ tới. Đọc từ phải sang trái.
🔧Áp dụng trong lập trình nhúng
- Driver phần cứng truy cập thanh ghi bằng con trỏ tới địa chỉ cố định (xem bài 8).
- Hàm thư viện nhận con trỏ tới bộ đệm:
Serial.readBytes(buf, n),Wire.write(data, len). - DMA (truyền dữ liệu không cần CPU) được cấu hình bằng địa chỉ nguồn và đích — chính là con trỏ.
⚠Lỗi thường gặp
- Dùng con trỏ chưa khởi tạo (trỏ lung tung) → treo chip / HardFault.
- Dereference
nullptr. - Trả về địa chỉ biến cục bộ từ hàm — biến đã chết khi hàm kết thúc (dangling pointer).
✎Bài tập về nhà
🎯 Ép kiểu con trỏ và con trỏ hàm
Dùng con trỏ để nhìn dữ liệu ở mức byte (gói tin, thanh ghi, endian) và để truyền hàm như một giá trị (callback).
- Chuyển đổi int ↔ char, xem số như dãy byte
- Hiểu little/big endian
- Đóng gói/giải gói struct để truyền qua UART
- Truy cập thanh ghi qua con trỏ
- Dùng con trỏ hàm: callback, bảng xử lý lệnh
1Ép kiểu con trỏ
Ép uint32_t* thành uint8_t* cho phép đọc từng byte của một số — nền tảng của việc đóng gói dữ liệu truyền thông. C++ có 4 toán tử ép kiểu rõ ràng thay cho kiểu C (T)x: static_cast, reinterpret_cast, const_cast, dynamic_cast.
// Ép kiểu con trỏ: xem một số nguyên như dãy byte, int <-> char, endian, đóng gói gói tin, thanh ghi
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <iostream>
void inByte(const char* ten, const void* p, size_t n) {
const uint8_t* b = static_cast<const uint8_t*>(p); // void* -> uint8_t* để đọc từng byte
std::printf(" %-12s:", ten);
for (size_t i = 0; i < n; i++) std::printf(" %02X", b[i]);
std::printf("\n");
}
// Mô phỏng một khối thanh ghi ngoại vi (thực tế nằm ở địa chỉ cố định, vd 0x40020000 trên STM32)
struct GPIO_TypeDef {
volatile uint32_t MODER;
volatile uint32_t ODR;
};
uint32_t vungNhoGiaLap[2] = {0, 0};
int main() {
std::cout << "== int <-> char ==\n";
char c = 'B';
int maAscii = c; // char -> int: lấy mã
char chuSo = '0' + 7; // int -> ký tự chữ số
int giaTri = '9' - '0'; // ký tự chữ số -> int
std::cout << " 'B' = " << maAscii << ", '0'+7 = '" << chuSo << "', '9'-'0' = " << giaTri << "\n";
int lon = 300;
char cat = static_cast<char>(lon); // chỉ giữ 8 bit thấp: 300 = 0x12C -> 0x2C
std::cout << " (char)300 = " << int(static_cast<unsigned char>(cat)) << " (mất dữ liệu!)\n";
std::cout << "\n== Xem số nguyên 32 bit như 4 byte (ép uint32_t* -> uint8_t*) ==\n";
uint32_t so = 0x12345678;
uint8_t* pb = reinterpret_cast<uint8_t*>(&so);
std::printf(" pb[0..3] = %02X %02X %02X %02X\n", pb[0], pb[1], pb[2], pb[3]);
std::cout << " -> " << (pb[0] == 0x78 ? "LITTLE endian (x86, ARM, ESP32: byte thấp nằm trước)" : "BIG endian") << "\n";
std::cout << "\n== Đóng gói dữ liệu cảm biến thành mảng byte để gửi UART/LoRa ==\n";
#pragma pack(push, 1) // bỏ byte đệm giữa các trường
struct GoiTin {
uint8_t id;
int16_t nhietDoX10; // 28.5°C gửi dưới dạng 285
uint16_t doAm;
float apSuat;
};
#pragma pack(pop)
GoiTin g{0x01, 285, 65, 1013.25f};
inByte("GoiTin", &g, sizeof(g));
std::cout << " sizeof(GoiTin) = " << sizeof(g) << " byte (không pack sẽ là 12)\n";
// Phía nhận: chép mảng byte trở lại struct bằng memcpy (an toàn hơn ép con trỏ)
uint8_t nhan[sizeof(GoiTin)];
std::memcpy(nhan, &g, sizeof(g));
GoiTin g2;
std::memcpy(&g2, nhan, sizeof(g2));
std::cout << " Phía nhận: id=" << +g2.id << ", T=" << g2.nhietDoX10 / 10.0 << "°C, H=" << g2.doAm
<< "%, P=" << g2.apSuat << " hPa\n";
std::cout << "\n== Lấy bit pattern của float ==\n";
float f = 1.0f;
uint32_t bits;
std::memcpy(&bits, &f, sizeof(bits)); // cách đúng chuẩn (tránh vi phạm strict aliasing)
std::printf(" 1.0f = 0x%08X (dấu | mũ | phần định trị theo IEEE-754)\n", bits);
std::cout << "\n== Ép địa chỉ thành con trỏ tới thanh ghi (cách viết driver) ==\n";
// Trên STM32: #define GPIOA ((GPIO_TypeDef*)0x40020000)
GPIO_TypeDef* GPIOA = reinterpret_cast<GPIO_TypeDef*>(vungNhoGiaLap);
GPIOA->MODER |= (1u << (5 * 2)); // chân 5 = output
GPIOA->ODR |= (1u << 5); // xuất mức 1 ra chân 5
std::printf(" MODER = 0x%08X, ODR = 0x%08X\n", (unsigned)GPIOA->MODER, (unsigned)GPIOA->ODR);
std::cout << "\n== Các toán tử ép kiểu của C++ ==\n";
std::cout << " static_cast : chuyển đổi hợp lệ (số <-> số, void* -> T*)\n";
std::cout << " reinterpret_cast : diễn giải lại bit/địa chỉ (thanh ghi, byte) – nguy hiểm\n";
std::cout << " const_cast : bỏ const – hầu như không nên dùng\n";
std::cout << " dynamic_cast : ép xuống lớp con có kiểm tra (cần RTTI)\n";
}
g++ -std=c++17 08a-ep-kieu-con-tro.cpp -o app && ./app== int <-> char == 'B' = 66, '0'+7 = '7', '9'-'0' = 9 (char)300 = 44 (mất dữ liệu!) == Xem số nguyên 32 bit như 4 byte (ép uint32_t* -> uint8_t*) == pb[0..3] = 78 56 34 12 -> LITTLE endian (x86, ARM, ESP32: byte thấp nằm trước) == Đóng gói dữ liệu cảm biến thành mảng byte để gửi UART/LoRa == GoiTin : 01 1D 01 41 00 00 50 7D 44 sizeof(GoiTin) = 9 byte (không pack sẽ là 12) Phía nhận: id=1, T=28.5°C, H=65%, P=1013.25 hPa == Lấy bit pattern của float == 1.0f = 0x3F800000 (dấu | mũ | phần định trị theo IEEE-754) == Ép địa chỉ thành con trỏ tới thanh ghi (cách viết driver) == MODER = 0x00000400, ODR = 0x00000020 == Các toán tử ép kiểu của C++ == static_cast : chuyển đổi hợp lệ (số <-> số, void* -> T*) reinterpret_cast : diễn giải lại bit/địa chỉ (thanh ghi, byte) – nguy hiểm const_cast : bỏ const – hầu như không nên dùng dynamic_cast : ép xuống lớp con có kiểm tra (cần RTTI)
Giải thích
'0' + 7ra ký tự'7','9' - '0'ra số 9 — mẹo chuyển chữ số ↔ ký tự.#pragma pack(1)bỏ byte đệm để struct có đúng 9 byte như giao thức quy định.- Đọc/ghi qua
memcpyan toàn hơn ép con trỏ trực tiếp (tránh lỗi căn lề và “strict aliasing”). reinterpret_cast<GPIO_TypeDef*>(địa_chỉ)là đúng cách thư viện STM32 định nghĩaGPIOA.
2Con trỏ hàm
Hàm cũng có địa chỉ. Con trỏ hàm lưu địa chỉ đó để gọi hàm “gián tiếp”: int (*f)(int, int) = cong; f(3, 4);. Dùng để:
- Callback: đăng ký hàm sẽ được gọi khi có sự kiện (ngắt, hết giờ, nhận dữ liệu).
- Bảng xử lý: mảng các cặp (tên lệnh, hàm) thay cho chuỗi
if/elsedài. - Thuật toán tổng quát:
qsortnhận hàm so sánh.
// Con trỏ hàm: callback, bảng xử lý lệnh, so sánh với std::function và lambda
#include <cstdlib>
#include <cstring>
#include <functional>
#include <iostream>
int cong(int a, int b) { return a + b; }
int nhan(int a, int b) { return a * b; }
// Hàm nhận một hàm khác làm tham số (callback)
int apDung(int (*phepToan)(int, int), int a, int b) { return phepToan(a, b); }
// Kiểu con trỏ hàm có tên cho dễ đọc
using HamXuLy = void (*)(int);
void batLed(int v) { std::cout << " -> LED " << (v ? "bật" : "tắt") << "\n"; }
void quayServo(int v) { std::cout << " -> Servo quay " << v << " độ\n"; }
void datToc(int v) { std::cout << " -> Động cơ tốc độ " << v << "\n"; }
// Bảng tra lệnh: tên -> hàm xử lý. Thêm lệnh mới chỉ cần thêm một dòng.
struct MucLenh { const char* ten; HamXuLy ham; };
const MucLenh BANG_LENH[] = {
{"LED", batLed},
{"SERVO", quayServo},
{"SPEED", datToc},
};
void thucThi(const char* ten, int giaTri) {
for (const MucLenh& m : BANG_LENH) {
if (std::strcmp(m.ten, ten) == 0) { m.ham(giaTri); return; }
}
std::cout << " -> Không có lệnh " << ten << "\n";
}
// Giả lập attachInterrupt(pin, callback): đăng ký hàm sẽ được gọi khi có sự kiện
void (*callbackNut)() = nullptr;
void attachInterrupt(void (*cb)()) { callbackNut = cb; }
void khiNhanNut() { std::cout << " [ISR] Nút được nhấn!\n"; }
int soSanhTang(const void* a, const void* b) {
return *static_cast<const int*>(a) - *static_cast<const int*>(b);
}
int main() {
std::cout << "== Con trỏ hàm cơ bản ==\n";
int (*f)(int, int) = cong;
std::cout << " f = cong: f(3, 4) = " << f(3, 4) << "\n";
f = nhan;
std::cout << " f = nhan: f(3, 4) = " << f(3, 4) << "\n";
std::cout << " apDung(cong, 10, 5) = " << apDung(cong, 10, 5) << "\n";
std::cout << "\n== Bảng xử lý lệnh ==\n";
thucThi("LED", 1);
thucThi("SERVO", 90);
thucThi("BUZZER", 1);
std::cout << "\n== Callback kiểu attachInterrupt ==\n";
attachInterrupt(khiNhanNut);
if (callbackNut) callbackNut(); // phần cứng "gọi lại" khi có ngắt
std::cout << "\n== qsort của C nhận con trỏ hàm so sánh ==\n ";
int a[] = {42, 7, 19, 3, 25};
std::qsort(a, 5, sizeof(int), soSanhTang);
for (int v : a) std::cout << v << " ";
std::cout << "\n";
std::cout << "\n== std::function + lambda: linh hoạt hơn (có thể mang theo biến) ==\n";
int nguong = 30;
std::function<bool(int)> kiemTra = [nguong](int t) { return t > nguong; }; // lambda "bắt" biến nguong
for (int t : {25, 35}) std::cout << " " << t << "°C vượt ngưỡng? " << std::boolalpha << kiemTra(t) << "\n";
std::cout << " sizeof(con trỏ hàm) = " << sizeof(f) << ", sizeof(std::function) = " << sizeof(kiemTra)
<< " -> trên MCU ưu tiên con trỏ hàm\n";
}
g++ -std=c++17 08b-con-tro-ham.cpp -o app && ./app== Con trỏ hàm cơ bản == f = cong: f(3, 4) = 7 f = nhan: f(3, 4) = 12 apDung(cong, 10, 5) = 15 == Bảng xử lý lệnh == -> LED bật -> Servo quay 90 độ -> Không có lệnh BUZZER == Callback kiểu attachInterrupt == [ISR] Nút được nhấn! == qsort của C nhận con trỏ hàm so sánh == 3 7 19 25 42 == std::function + lambda: linh hoạt hơn (có thể mang theo biến) == 25°C vượt ngưỡng? false 35°C vượt ngưỡng? true sizeof(con trỏ hàm) = 8, sizeof(std::function) = 32 -> trên MCU ưu tiên con trỏ hàm
Giải thích
using HamXuLy = void (*)(int);đặt tên cho kiểu con trỏ hàm — cú pháp gốc rất khó đọc.- Bảng
BANG_LENHlàconstnên được đặt trong Flash; thêm lệnh mới chỉ cần thêm một dòng. std::functionlinh hoạt hơn (chứa được lambda có “bắt” biến) nhưng tốn bộ nhớ hơn con trỏ hàm.
🔧Áp dụng trong lập trình nhúng
attachInterrupt(digitalPinToInterrupt(2), khiNhanNut, FALLING)nhận con trỏ hàm.- Bảng vector ngắt của vi điều khiển thực chất là mảng con trỏ hàm ở đầu bộ nhớ Flash.
- Gửi struct cảm biến qua LoRa/ESP-NOW: đóng gói thành mảng byte, kèm CRC, phía nhận
memcpyngược lại — hai bên phải cùng endian và cùng cách pack.
⚠Lỗi thường gặp
- Ép
float*thànhint*rồi đọc — vi phạm strict aliasing, kết quả sai khi bật tối ưu. - Hai thiết bị khác endian gửi số nhiều byte mà không chuẩn hoá thứ tự.
- Gọi con trỏ hàm
nullptr.
✎Bài tập về nhà
🧱 Tham chiếu, cấu trúc (struct), vào/ra và ngày giờ
Dùng tham chiếu thay con trỏ khi phù hợp, gom dữ liệu bằng struct, nhập/xuất có định dạng và làm việc với thời gian.
- Phân biệt tham chiếu và con trỏ; dùng
const& - Định nghĩa và dùng struct, hiểu padding
- Nhập bằng
cin, in có định dạng bằng<iomanip> - Dùng
<ctime>và<chrono>
1Tham chiếu (reference)
Tham chiếu là tên khác (bí danh) của một biến đã có: int& r = x;. Mọi thao tác trên r chính là trên x.
Con trỏ T* | Tham chiếu T& | |
|---|---|---|
| Có thể rỗng? | Có (nullptr) | Không — phải gắn với biến ngay khi tạo |
| Đổi đối tượng trỏ tới? | Có | Không |
| Cú pháp dùng | *p, p->x | như biến thường |
// Tham chiếu (reference): bí danh của một biến – so sánh với con trỏ
#include <iostream>
#include <string>
#include <vector>
struct CamBien { std::string ten; float giaTri; };
void hieuChinh(CamBien& cb, float lech) { cb.giaTri += lech; } // sửa trực tiếp
// const & : đọc nhanh (không sao chép) nhưng không sửa được – cách truyền object lớn chuẩn nhất
void inCamBien(const CamBien& cb) {
std::cout << " " << cb.ten << " = " << cb.giaTri << "\n";
// cb.giaTri = 0; // LỖI: cb là const
}
// Trả về tham chiếu -> có thể gán vào kết quả của hàm
int& phanTu(std::vector<int>& v, size_t i) { return v[i]; }
int main() {
std::cout << "== Tham chiếu là tên khác của cùng một biến ==\n";
int tocDo = 100;
int& rTocDo = tocDo; // PHẢI khởi tạo ngay, và không thể đổi sang biến khác
rTocDo = 150;
std::cout << " tocDo = " << tocDo << ", &rTocDo == &tocDo ? " << std::boolalpha << (&rTocDo == &tocDo) << "\n";
std::cout << "\n== Truyền tham chiếu vào hàm ==\n";
CamBien dht{"DHT22", 27.0f};
hieuChinh(dht, -0.5f);
inCamBien(dht);
std::cout << "\n== Trả về tham chiếu ==\n";
std::vector<int> ds = {1, 2, 3};
phanTu(ds, 1) = 99;
std::cout << " ds[1] = " << ds[1] << "\n";
std::cout << "\n== range-for: có & thì sửa được, không có & thì chỉ sửa bản sao ==\n";
std::vector<CamBien> cacCamBien = {{"A0", 1.0f}, {"A1", 2.0f}};
for (CamBien c : cacCamBien) c.giaTri *= 10; // bản sao -> không đổi
std::cout << " không &: A0 = " << cacCamBien[0].giaTri << "\n";
for (CamBien& c : cacCamBien) c.giaTri *= 10; // tham chiếu -> đổi thật
std::cout << " có & : A0 = " << cacCamBien[0].giaTri << "\n";
for (const auto& c : cacCamBien) inCamBien(c); // chỉ đọc: const auto&
std::cout << "\n== Con trỏ vs tham chiếu ==\n";
std::cout << " Con trỏ: có thể nullptr, đổi được nơi trỏ tới, dùng *p / p->\n";
std::cout << " Tham chiếu: luôn gắn với một biến, không đổi được, dùng như biến thường\n";
}
g++ -std=c++17 09a-tham-chieu.cpp -o app && ./app== Tham chiếu là tên khác của cùng một biến == tocDo = 150, &rTocDo == &tocDo ? true == Truyền tham chiếu vào hàm == DHT22 = 26.5 == Trả về tham chiếu == ds[1] = 99 == range-for: có & thì sửa được, không có & thì chỉ sửa bản sao == không &: A0 = 1 có & : A0 = 10 A0 = 10 A1 = 20 == Con trỏ vs tham chiếu == Con trỏ: có thể nullptr, đổi được nơi trỏ tới, dùng *p / p-> Tham chiếu: luôn gắn với một biến, không đổi được, dùng như biến thường
Giải thích
const CamBien&là cách truyền đối tượng chuẩn nhất: không sao chép, không sửa.- Vòng
for (CamBien c : ds)làm việc trên bản sao; muốn sửa phải viếtfor (CamBien& c : ds).
2Struct và Input/Output
struct gom nhiều dữ liệu liên quan thành một kiểu mới: struct DiemDo { std::string viTri; float nhietDo; int doAm; };. Truy cập bằng dấu chấm d.nhietDo (hoặc p->nhietDo qua con trỏ).
I/O: std::cin >> đọc từ bàn phím (tách theo khoảng trắng), std::getline đọc cả dòng; <iomanip> có setw, setprecision, fixed, hex, setfill. std::cerr là luồng báo lỗi.
// Struct (cấu trúc dữ liệu) và Input/Output cơ bản: cin, cout, định dạng
// Dữ liệu nhập được đưa vào từ bàn phím (ở đây đã chuẩn bị sẵn – xem ô "Dữ liệu nhập")
#include <iomanip>
#include <iostream>
#include <string>
struct DiemDo { // gom các dữ liệu liên quan thành một kiểu mới
std::string viTri;
float nhietDo;
int doAm;
};
struct KhongToiUu { char a; int b; char c; }; // trình biên dịch chèn byte đệm (padding)
struct ToiUu { int b; char a; char c; }; // sắp trường lớn trước -> nhỏ hơn
void inDiemDo(const DiemDo& d) {
std::cout << " | " << std::left << std::setw(10) << d.viTri
<< " | " << std::right << std::setw(6) << std::fixed << std::setprecision(1) << d.nhietDo
<< " | " << std::setw(4) << d.doAm << " |\n";
}
int main() {
std::cout << "== Nhập dữ liệu bằng cin ==\n";
int soDiem;
std::cout << "Nhập số điểm đo: ";
std::cin >> soDiem;
std::cout << soDiem << "\n";
DiemDo ds[10];
for (int i = 0; i < soDiem && i < 10; i++) {
std::cout << "Điểm " << i + 1 << " (vị trí nhiệt-độ độ-ẩm): ";
std::cin >> ds[i].viTri >> ds[i].nhietDo >> ds[i].doAm;
std::cout << ds[i].viTri << " " << ds[i].nhietDo << " " << ds[i].doAm << "\n";
}
std::cout << "\n== In bảng có định dạng (iomanip) ==\n";
std::cout << " +------------+--------+------+\n";
std::cout << " | Vị trí | T°C | H% |\n";
std::cout << " +------------+--------+------+\n";
float tong = 0;
for (int i = 0; i < soDiem; i++) { inDiemDo(ds[i]); tong += ds[i].nhietDo; }
std::cout << " +------------+--------+------+\n";
std::cout << " Nhiệt độ trung bình: " << tong / soDiem << "°C\n";
std::cout << "\n== Struct lồng nhau, khởi tạo nhanh, gán cả struct ==\n";
struct ThietBi { int id; DiemDo docGanNhat; };
ThietBi tb = {7, {"Vuon", 30.5f, 70}};
ThietBi banSao = tb; // sao chép toàn bộ các trường
banSao.docGanNhat.nhietDo = 0;
std::cout << " tb.docGanNhat.nhietDo = " << tb.docGanNhat.nhietDo << " (bản sao độc lập)\n";
std::cout << "\n== Kích thước struct và padding ==\n";
std::cout << " sizeof(KhongToiUu) = " << sizeof(KhongToiUu) << ", sizeof(ToiUu) = " << sizeof(ToiUu) << "\n";
std::cout << "\n== Các cách in số ==\n";
int v = 255;
std::cout << " dec " << std::dec << v << " | hex 0x" << std::hex << std::uppercase << v
<< " | oct " << std::oct << v << std::dec << "\n";
std::cout << " setfill: [" << std::setfill('0') << std::setw(5) << 42 << "]" << std::setfill(' ') << "\n";
std::cerr << "(cerr: luồng báo lỗi riêng, không bị đệm)\n";
}
g++ -std=c++17 09b-struct-io.cpp -o app && ./app3 PhongHoc 29.5 62 Vuon 31.2 70 Kho 27.8 55
== Nhập dữ liệu bằng cin == Nhập số điểm đo: 3 Điểm 1 (vị trí nhiệt-độ độ-ẩm): PhongHoc 29.5 62 Điểm 2 (vị trí nhiệt-độ độ-ẩm): Vuon 31.2 70 Điểm 3 (vị trí nhiệt-độ độ-ẩm): Kho 27.8 55 == In bảng có định dạng (iomanip) == +------------+--------+------+ | Vị trí | T°C | H% | +------------+--------+------+ | PhongHoc | 29.5 | 62 | | Vuon | 31.2 | 70 | | Kho | 27.8 | 55 | +------------+--------+------+ Nhiệt độ trung bình: 29.5°C == Struct lồng nhau, khởi tạo nhanh, gán cả struct == tb.docGanNhat.nhietDo = 30.5 (bản sao độc lập) == Kích thước struct và padding == sizeof(KhongToiUu) = 12, sizeof(ToiUu) = 8 == Các cách in số == dec 255 | hex 0xFF | oct 377 setfill: [00042] (cerr: luồng báo lỗi riêng, không bị đệm)
Giải thích
- Ô “Dữ liệu nhập” là những gì gõ vào bàn phím; chương trình in lại giá trị để kết quả dễ theo dõi.
- Trình biên dịch chèn byte đệm (padding) để căn lề:
{char, int, char}chiếm 12 byte, sắp lại{int, char, char}chỉ 8 byte. setwchỉ áp dụng cho lần in kế tiếp;fixed,setprecision,hexcó hiệu lực tới khi đổi.
3Ngày và giờ
// Ngày & giờ: <ctime> kiểu C và <chrono> kiểu C++
#include <chrono>
#include <ctime>
#include <iostream>
#include <thread>
int main() {
std::cout << "== time_t: số giây kể từ 1/1/1970 (Unix time) ==\n";
// Dùng một mốc cố định để ví dụ lần nào chạy cũng ra cùng kết quả.
// Thực tế: std::time(nullptr) lấy giờ hiện tại; ESP32 đồng bộ giờ qua NTP rồi dùng y hệt.
std::time_t t = 1760000000;
std::tm utc = *std::gmtime(&t);
char buf[64];
std::strftime(buf, sizeof(buf), "%d/%m/%Y %H:%M:%S", &utc);
std::cout << " t = " << t << " -> " << buf << " (UTC)\n";
std::cout << " Thứ trong tuần (0 = CN): " << utc.tm_wday << ", ngày thứ " << utc.tm_yday + 1 << " trong năm\n";
std::time_t gioVN = t + 7 * 3600; // múi giờ Việt Nam UTC+7
std::tm vn = *std::gmtime(&gioVN);
std::strftime(buf, sizeof(buf), "%H:%M ngày %d/%m", &vn);
std::cout << " Giờ Việt Nam: " << buf << "\n";
std::cout << "\n== Tính khoảng cách giữa hai ngày ==\n";
std::tm ngayThi{};
ngayThi.tm_year = 2026 - 1900; ngayThi.tm_mon = 11 - 1; ngayThi.tm_mday = 15; // 15/11/2026
std::tm homNay{};
homNay.tm_year = 2026 - 1900; homNay.tm_mon = 10 - 1; homNay.tm_mday = 9; // 09/10/2026
double giay = std::difftime(std::mktime(&ngayThi), std::mktime(&homNay));
std::cout << " Từ 09/10 tới ngày thi 15/11 còn " << giay / 86400 << " ngày\n";
std::cout << "\n== <chrono>: đơn vị thời gian an toàn kiểu ==\n";
using namespace std::chrono;
auto chuKy = milliseconds(1500);
std::cout << " 1500 ms = " << duration_cast<seconds>(chuKy).count() << " s (cắt) = "
<< duration<double>(chuKy).count() << " s\n";
auto tong = minutes(2) + seconds(30);
std::cout << " 2 phút + 30 giây = " << tong.count() << " giây\n";
std::cout << "\n== Đo thời gian thực thi (giống micros() trên Arduino) ==\n";
auto batDau = steady_clock::now();
std::this_thread::sleep_for(milliseconds(50));
auto daQua = duration_cast<milliseconds>(steady_clock::now() - batDau).count();
std::cout << " sleep_for(50ms) mất khoảng " << (daQua >= 50 ? ">= 50" : "< 50") << " ms\n";
}
g++ -std=c++17 09c-thoi-gian.cpp -o app && ./app== time_t: số giây kể từ 1/1/1970 (Unix time) == t = 1760000000 -> 09/10/2025 08:53:20 (UTC) Thứ trong tuần (0 = CN): 4, ngày thứ 282 trong năm Giờ Việt Nam: 15:53 ngày 09/10 == Tính khoảng cách giữa hai ngày == Từ 09/10 tới ngày thi 15/11 còn 37 ngày == <chrono>: đơn vị thời gian an toàn kiểu == 1500 ms = 1 s (cắt) = 1.5 s 2 phút + 30 giây = 150 giây == Đo thời gian thực thi (giống micros() trên Arduino) == sleep_for(50ms) mất khoảng >= 50 ms
Giải thích
time_tlà số giây từ 1/1/1970 (Unix time).gmtimeđổi sang ngày giờ UTC; Việt Nam là UTC+7.<chrono>gắn đơn vị vào số (milliseconds,seconds) — không còn nhầm ms với giây.steady_clockdùng để đo khoảng thời gian (không bị ảnh hưởng khi chỉnh đồng hồ hệ thống) — tương tựmicros().
🔧Áp dụng trong lập trình nhúng
- ESP32 đồng bộ giờ qua NTP:
configTime(7*3600, 0, "pool.ntp.org"); time(&now); localtime_r(&now, &tm);— dùng y hệt bài này. - Module RTC DS3231 lưu giờ khi mất điện; struct là cách tự nhiên để giữ một bản ghi đo đạc (thời gian, nhiệt độ, độ ẩm).
- Struct gửi qua mạng nên dùng kiểu cố định độ rộng và
#pragma pack(xem bài 8).
⚠Lỗi thường gặp
- Trả về tham chiếu tới biến cục bộ.
- Trộn
cin >>vàgetlinemà quên bỏ ký tự xuống dòng còn sót. - Dùng
int16 bit lưu Unix time trên AVR.
✎Bài tập về nhà
🏗️ Hướng đối tượng: lớp, đối tượng và tính bao đóng
Mô hình hoá thiết bị phần cứng thành lớp có dữ liệu riêng tư và phương thức công khai; hiểu vòng đời đối tượng và RAII.
- Định nghĩa lớp với
private/public, constructor, destructor - Hiểu tính bao đóng và vì sao cần setter có kiểm tra
- Dùng thành viên
static, hàmconst - Áp dụng RAII; viết/cấm copy constructor
Lập trình hướng đối tượng (OOP) có 4 trụ cột: bao đóng (encapsulation), kế thừa (inheritance), đa hình (polymorphism) và trừu tượng (abstraction). Bài này bắt đầu với lớp, đối tượng và bao đóng.
1Lớp và đối tượng
- Lớp (class) = bản thiết kế gồm dữ liệu (thuộc tính) và hành vi (phương thức).
- Đối tượng (object) = một thể hiện cụ thể của lớp.
- Bao đóng: giấu dữ liệu ở
private, chỉ cho truy cập qua phương thứcpubliccó kiểm tra → đối tượng luôn ở trạng thái hợp lệ. - Constructor chạy khi tạo đối tượng (khởi tạo), destructor
~TenLop()chạy khi huỷ (dọn dẹp).
struct và class trong C++ gần như giống nhau; khác biệt duy nhất là mặc định public (struct) hay private (class). Quy ước: struct cho dữ liệu đơn giản, class cho đối tượng có hành vi.// Lớp & đối tượng, tính bao đóng (encapsulation): private/public, constructor, destructor, getter/setter
#include <iostream>
#include <string>
class Led {
public:
// Constructor: chạy khi đối tượng được tạo. Dùng danh sách khởi tạo ": pin_(pin), ..."
Led(int pin, std::string ten = "LED") : pin_(pin), ten_(std::move(ten)) {
soLuong++;
std::cout << " [tạo] " << ten_ << " ở chân " << pin_ << "\n";
}
// Destructor: chạy khi đối tượng bị huỷ (ra khỏi phạm vi)
~Led() {
soLuong--;
std::cout << " [huỷ] " << ten_ << "\n";
}
void bat() { trangThai_ = true; doSang_ = 255; }
void tat() { trangThai_ = false; doSang_ = 0; }
// Setter có KIỂM TRA dữ liệu – lý do chính của bao đóng
bool datDoSang(int giaTri) {
if (giaTri < 0 || giaTri > 255) return false; // từ chối giá trị sai
doSang_ = giaTri;
trangThai_ = giaTri > 0;
return true;
}
// Getter là hàm const: hứa không sửa đối tượng
int doSang() const { return doSang_; }
bool dangBat() const { return trangThai_; }
void inTrangThai() const {
std::cout << " " << ten_ << "(chân " << pin_ << "): " << (trangThai_ ? "BẬT" : "TẮT")
<< ", độ sáng " << doSang_ << "\n";
}
static int soLuong; // thành viên static: dùng chung cho MỌI đối tượng của lớp
private: // bên ngoài không truy cập trực tiếp được
const int pin_; // const: chân cố định suốt đời đối tượng
std::string ten_;
bool trangThai_ = false; // giá trị mặc định
int doSang_ = 0;
};
int Led::soLuong = 0; // định nghĩa thành viên static
int main() {
std::cout << "== Tạo đối tượng ==\n";
Led ledDo(13, "LED đỏ");
Led ledXanh(12, "LED xanh");
std::cout << " Số LED đang tồn tại: " << Led::soLuong << "\n";
std::cout << "\n== Gọi phương thức ==\n";
ledDo.bat();
ledXanh.datDoSang(80);
ledDo.inTrangThai();
ledXanh.inTrangThai();
std::cout << "\n== Bao đóng bảo vệ dữ liệu ==\n";
// ledXanh.doSang_ = 999; // LỖI biên dịch: doSang_ là private
bool ok = ledXanh.datDoSang(999);
std::cout << " datDoSang(999) -> " << (ok ? "chấp nhận" : "từ chối") << ", độ sáng vẫn = " << ledXanh.doSang() << "\n";
std::cout << "\n== Vòng đời: đối tượng trong khối {} bị huỷ khi ra khỏi khối ==\n";
{
Led tam(2, "LED tạm");
std::cout << " Số LED: " << Led::soLuong << "\n";
}
std::cout << " Số LED sau khối: " << Led::soLuong << "\n";
std::cout << "\n== Kết thúc main: các đối tượng còn lại bị huỷ theo thứ tự NGƯỢC lúc tạo ==\n";
}
g++ -std=c++17 10a-lop-doi-tuong.cpp -o app && ./app== Tạo đối tượng ==
[tạo] LED đỏ ở chân 13
[tạo] LED xanh ở chân 12
Số LED đang tồn tại: 2
== Gọi phương thức ==
LED đỏ(chân 13): BẬT, độ sáng 255
LED xanh(chân 12): BẬT, độ sáng 80
== Bao đóng bảo vệ dữ liệu ==
datDoSang(999) -> từ chối, độ sáng vẫn = 80
== Vòng đời: đối tượng trong khối {} bị huỷ khi ra khỏi khối ==
[tạo] LED tạm ở chân 2
Số LED: 3
[huỷ] LED tạm
Số LED sau khối: 2
== Kết thúc main: các đối tượng còn lại bị huỷ theo thứ tự NGƯỢC lúc tạo ==
[huỷ] LED xanh
[huỷ] LED đỏGiải thích
- Danh sách khởi tạo
: pin_(pin), ten_(...)là cách duy nhất khởi tạo thành viênconst. datDoSang(999)bị từ chối — bên ngoài không thể đưa đối tượng vào trạng thái sai.static int soLuongdùng chung cho mọi đối tượng — đếm số LED đang tồn tại.- Đối tượng huỷ theo thứ tự ngược với lúc tạo.
2RAII và sao chép đối tượng
RAII (Resource Acquisition Is Initialization): chiếm tài nguyên trong constructor, trả trong destructor. Vì destructor luôn được gọi khi đối tượng ra khỏi phạm vi, tài nguyên không bao giờ bị “quên trả” — kể cả khi return sớm hay có ngoại lệ.
Quy tắc 3: lớp tự quản lý tài nguyên (con trỏ cấp phát, file, khoá) cần tự viết destructor, copy constructor và operator=; hoặc cấm sao chép bằng = delete.
// RAII (Resource Acquisition Is Initialization) và quy tắc sao chép (copy constructor, operator=)
#include <cstring>
#include <iostream>
// Ví dụ RAII: "khoá bus I2C" – lấy khi tạo, TỰ trả khi huỷ, kể cả khi return sớm
class I2CLock {
public:
I2CLock() { std::cout << " [lock] chiếm bus I2C\n"; }
~I2CLock() { std::cout << " [unlock] trả bus I2C\n"; }
};
bool docCamBien(int diaChi) {
I2CLock khoa; // chiếm bus
std::cout << " đọc thiết bị 0x" << std::hex << diaChi << std::dec << "\n";
if (diaChi == 0x77) {
std::cout << " thiết bị không phản hồi -> return sớm\n";
return false; // bus VẪN được trả nhờ destructor
}
return true;
}
// Lớp tự quản lý bộ nhớ -> cần viết copy constructor & operator= (quy tắc 3)
class BoDem {
public:
explicit BoDem(size_t n) : n_(n), data_(new int[n]{}) {}
~BoDem() { delete[] data_; }
BoDem(const BoDem& khac) : n_(khac.n_), data_(new int[khac.n_]) { // sao chép SÂU
std::memcpy(data_, khac.data_, n_ * sizeof(int));
std::cout << " [copy constructor]\n";
}
BoDem& operator=(const BoDem& khac) {
if (this != &khac) { // tự gán cho chính mình
int* moi = new int[khac.n_];
std::memcpy(moi, khac.data_, khac.n_ * sizeof(int));
delete[] data_;
data_ = moi;
n_ = khac.n_;
}
std::cout << " [operator=]\n";
return *this;
}
int& operator[](size_t i) { return data_[i]; }
private:
size_t n_;
int* data_;
};
// Lớp quản lý phần cứng duy nhất thì nên CẤM sao chép
class UartPort {
public:
UartPort() = default;
UartPort(const UartPort&) = delete;
UartPort& operator=(const UartPort&) = delete;
};
int main() {
std::cout << "== RAII ==\n";
bool kq1 = docCamBien(0x76);
std::cout << " -> kết quả " << std::boolalpha << kq1 << "\n";
bool kq2 = docCamBien(0x77);
std::cout << " -> kết quả " << kq2 << "\n";
std::cout << "\n== Sao chép sâu ==\n";
BoDem a(3);
a[0] = 10;
BoDem b = a; // copy constructor
b[0] = 99;
std::cout << " a[0] = " << a[0] << ", b[0] = " << b[0] << " -> hai bộ đệm độc lập\n";
BoDem c(1);
c = a; // operator=
std::cout << " c[0] = " << c[0] << "\n";
std::cout << "\n== Cấm sao chép ==\n";
[[maybe_unused]] UartPort uart0;
// UartPort uart1 = uart0; // LỖI biên dịch: copy constructor đã bị delete
std::cout << " UartPort không thể sao chép -> tránh 2 đối tượng cùng điều khiển một cổng\n";
}
g++ -std=c++17 10b-raii-sao-chep.cpp -o app && ./app== RAII ==
[lock] chiếm bus I2C
đọc thiết bị 0x76
[unlock] trả bus I2C
-> kết quả true
[lock] chiếm bus I2C
đọc thiết bị 0x77
thiết bị không phản hồi -> return sớm
[unlock] trả bus I2C
-> kết quả false
== Sao chép sâu ==
[copy constructor]
a[0] = 10, b[0] = 99 -> hai bộ đệm độc lập
[operator=]
c[0] = 10
== Cấm sao chép ==
UartPort không thể sao chép -> tránh 2 đối tượng cùng điều khiển một cổngGiải thích
- Dòng
[unlock]vẫn xuất hiện khi hàmreturn falsesớm — đó là sức mạnh của RAII. - Copy constructor sao chép sâu: cấp phát mảng mới, nên sửa
bkhông ảnh hưởnga. UartPort(const UartPort&) = delete;biến việc sao chép thành lỗi biên dịch.
🔧Áp dụng trong lập trình nhúng
- Thư viện Arduino đều là lớp:
Servo myservo; myservo.attach(9); myservo.write(90);. - Đóng gói mỗi thiết bị (LED, động cơ, cảm biến) thành một lớp giúp code robot gọn và tái sử dụng được.
- RAII hợp với khoá mutex (
std::lock_guard), tắt/bật ngắt tạm thời, chọn chip SPI (CS xuống khi tạo, lên khi huỷ).
⚠Lỗi thường gặp
- Để thuộc tính
publicrồi sửa tuỳ tiện ở khắp nơi. - Quên định nghĩa thành viên
staticngoài lớp → lỗi liên kết. - Lớp giữ con trỏ cấp phát nhưng dùng copy mặc định → hai đối tượng cùng
deletemột vùng nhớ.
✎Bài tập về nhà
🧬 Tính kế thừa và nạp chồng (overloading)
Tái sử dụng code bằng kế thừa; dùng nạp chồng hàm và toán tử để viết code tự nhiên hơn.
- Tạo lớp con từ lớp cha, dùng
protected - Hiểu thứ tự gọi constructor/destructor
- Gọi hàm lớp cha từ lớp con; biết đa kế thừa
- Nạp chồng hàm, constructor, toán tử
+ - == << [] ()
1Kế thừa
Lớp con class CamBienNhiet : public CamBien nhận toàn bộ thuộc tính và phương thức của lớp cha, rồi thêm/thay đổi phần riêng.
| Thành viên lớp cha | Lớp con truy cập? | Bên ngoài truy cập? |
|---|---|---|
public | ✓ | ✓ |
protected | ✓ | ✗ |
private | ✗ | ✗ |
Constructor lớp cha chạy trước, destructor lớp con chạy trước.
// Tính kế thừa: lớp cha / lớp con, protected, thứ tự constructor-destructor, gọi hàm lớp cha
#include <iostream>
#include <string>
class CamBien { // lớp CHA (base class)
public:
CamBien(std::string ten, int chan, int giaLap) : ten_(std::move(ten)), chan_(chan), giaLap_(giaLap) {
std::cout << " CamBien() " << ten_ << "\n";
}
~CamBien() { std::cout << " ~CamBien() " << ten_ << "\n"; }
void moTa() const { std::cout << " " << ten_ << " nối chân " << chan_ << "\n"; }
int docThoTho() const { return giaLap_; } // giả lập analogRead (ADC 10 bit, 0..1023)
protected: // lớp con dùng được, bên ngoài thì không
std::string ten_;
int chan_;
private:
int giaLap_;
};
class CamBienNhiet : public CamBien { // lớp CON kế thừa public
public:
CamBienNhiet(int chan) : CamBien("LM35", chan, 88) { // gọi constructor lớp cha trước
std::cout << " CamBienNhiet()\n";
}
~CamBienNhiet() { std::cout << " ~CamBienNhiet()\n"; }
float docC() const { // thêm chức năng mới
return docThoTho() * 3.3f / 1023 * 100; // dùng lại hàm của lớp cha
}
void moTa() const { // che (hide) hàm cùng tên của lớp cha
CamBien::moTa(); // vẫn gọi được bản của lớp cha
std::cout << " -> loại cảm biến nhiệt, 10 mV/°C, ten_ (protected) = " << ten_ << "\n";
}
};
class CamBienAnhSang : public CamBien {
public:
CamBienAnhSang(int chan) : CamBien("LDR", chan, 512) {}
bool troiToi() const { return docThoTho() < 600; }
};
// Đa kế thừa: một lớp có nhiều lớp cha (dùng thận trọng)
class CoTheGuiWifi { public: void gui(const std::string& s) const { std::cout << " WiFi gửi: " << s << "\n"; } };
class TramThoiTiet : public CamBienNhiet, public CoTheGuiWifi {
public:
TramThoiTiet() : CamBienNhiet(34) {}
void baoCao() const { gui("T=" + std::to_string(int(docC())) + "C"); }
};
int main() {
std::cout << "== Tạo đối tượng lớp con: constructor CHA chạy trước ==\n";
{
CamBienNhiet lm35(36);
lm35.moTa();
std::cout << " Nhiệt độ: " << lm35.docC() << "°C\n";
// lm35.ten_ = "x"; // LỖI: protected không truy cập được từ bên ngoài
std::cout << "== Ra khỏi khối: destructor CON chạy trước ==\n";
}
std::cout << "\n== Lớp con khác cùng lớp cha ==\n";
CamBienAnhSang ldr(39);
ldr.moTa();
std::cout << " Trời tối? " << std::boolalpha << ldr.troiToi() << "\n";
std::cout << "\n== Đa kế thừa ==\n";
TramThoiTiet tram;
tram.baoCao();
std::cout << "\n== Hết main ==\n";
}
g++ -std=c++17 11a-ke-thua.cpp -o app && ./app== Tạo đối tượng lớp con: constructor CHA chạy trước == CamBien() LM35 CamBienNhiet() LM35 nối chân 36 -> loại cảm biến nhiệt, 10 mV/°C, ten_ (protected) = LM35 Nhiệt độ: 28.3871°C == Ra khỏi khối: destructor CON chạy trước == ~CamBienNhiet() ~CamBien() LM35 == Lớp con khác cùng lớp cha == CamBien() LDR LDR nối chân 39 Trời tối? true == Đa kế thừa == CamBien() LM35 CamBienNhiet() WiFi gửi: T=28C == Hết main == ~CamBienNhiet() ~CamBien() LM35 ~CamBien() LDR
Giải thích
- Lớp con gọi constructor lớp cha trong danh sách khởi tạo:
CamBienNhiet(int chan) : CamBien("LM35", chan, 88). CamBien::moTa()gọi bản của lớp cha từ bên trong lớp con.- Đa kế thừa (
TramThoiTiet) hữu ích khi ghép các “khả năng” độc lập, nhưng dễ rối — ưu tiên kế thừa interface (bài 12). - Lớp con định nghĩa lại
moTa()không cóvirtualchỉ là “che” hàm — xem sự khác biệt với đa hình ở bài 12.
2Nạp chồng
Nạp chồng hàm: nhiều hàm cùng tên nhưng khác kiểu hoặc số tham số; trình biên dịch chọn bản phù hợp lúc biên dịch. Nạp chồng toán tử: định nghĩa ý nghĩa của +, ==, <<… cho kiểu do bạn tạo, ví dụ cộng hai vector vị trí.
// Nạp chồng (overloading): nạp chồng hàm, constructor và toán tử
#include <cmath>
#include <iostream>
#include <string>
// ===== Nạp chồng hàm: cùng tên, khác tham số =====
void guiSerial(int v) { std::cout << " gửi số nguyên: " << v << "\n"; }
void guiSerial(float v) { std::cout << " gửi số thực: " << v << "\n"; }
void guiSerial(const std::string& s) { std::cout << " gửi chuỗi: \"" << s << "\"\n"; }
void guiSerial(int v, int coSo) { std::cout << " gửi " << v << " theo cơ số " << coSo << "\n"; }
// ===== Nạp chồng toán tử: lớp Vector2 cho vị trí robot =====
class Vector2 {
public:
Vector2(float x = 0, float y = 0) : x(x), y(y) {} // nạp chồng constructor qua tham số mặc định
Vector2 operator+(const Vector2& o) const { return {x + o.x, y + o.y}; }
Vector2 operator-(const Vector2& o) const { return {x - o.x, y - o.y}; }
Vector2 operator*(float k) const { return {x * k, y * k}; }
Vector2& operator+=(const Vector2& o) { x += o.x; y += o.y; return *this; }
bool operator==(const Vector2& o) const { return x == o.x && y == o.y; }
float doDai() const { return std::sqrt(x * x + y * y); }
// operator<< để in trực tiếp bằng cout – là hàm friend (không phải thành viên)
friend std::ostream& operator<<(std::ostream& os, const Vector2& v) {
return os << "(" << v.x << ", " << v.y << ")";
}
float x, y;
};
// ===== operator[] và operator() =====
class BangLed {
public:
bool& operator[](int i) { return led_[i]; } // truy cập như mảng
void operator()() const { // gọi đối tượng như một hàm
std::cout << " ";
for (bool b : led_) std::cout << (b ? "●" : "○");
std::cout << "\n";
}
private:
bool led_[8] = {};
};
int main() {
std::cout << "== Nạp chồng hàm ==\n";
guiSerial(42);
guiSerial(3.14f);
guiSerial(std::string("OK"));
guiSerial(255, 16);
std::cout << "\n== Nạp chồng toán tử ==\n";
Vector2 viTri(1, 2);
Vector2 vanToc(0.5f, 0);
for (int buoc = 0; buoc < 3; buoc++) viTri += vanToc;
std::cout << " vị trí sau 3 bước: " << viTri << "\n";
Vector2 dich(4, 6);
Vector2 conLai = dich - viTri;
std::cout << " còn cách đích: " << conLai << ", khoảng cách = " << conLai.doDai() << "\n";
std::cout << " vanToc * 4 = " << vanToc * 4 << ", viTri == (2.5, 2)? " << std::boolalpha
<< (viTri == Vector2(2.5f, 2)) << "\n";
std::cout << "\n== operator[] và operator() ==\n";
BangLed bang;
bang[0] = bang[3] = bang[7] = true;
bang();
}
g++ -std=c++17 11b-nap-chong.cpp -o app && ./app== Nạp chồng hàm == gửi số nguyên: 42 gửi số thực: 3.14 gửi chuỗi: "OK" gửi 255 theo cơ số 16 == Nạp chồng toán tử == vị trí sau 3 bước: (2.5, 2) còn cách đích: (1.5, 4), khoảng cách = 4.272 vanToc * 4 = (2, 0), viTri == (2.5, 2)? true == operator[] và operator() == ●○○●○○○●
Giải thích
operator+trả về đối tượng mới;operator+=sửa chính đối tượng và trả về*this.operator<<là hàmfriendnhậnstd::ostream&để in được bằngcout.operator[]trả về tham chiếu nên gán được:bang[0] = true;.
🔧Áp dụng trong lập trình nhúng
- Lớp
Printcủa Arduino là lớp cha củaSerial,LiquidCrystal,WiFiClient… — tất cả đều cóprint()/println()nhờ kế thừa. Serial.print()có hàng chục bản nạp chồng cho int, float, chuỗi, cơ số (Serial.print(255, HEX)).
⚠Lỗi thường gặp
- Kế thừa chỉ để dùng lại vài hàm dù không có quan hệ “là một” — dùng composition (chứa đối tượng) thay thế.
- Nạp chồng toán tử với ý nghĩa bất ngờ (ví dụ
+mà lại trừ). - Gọi hàm nạp chồng với đối số mơ hồ (
f(0)khi cóf(int)vàf(char*)).
✎Bài tập về nhà
🎭 Tính đa hình, tính trừu tượng và interface
Viết code làm việc với “bất kỳ loại thiết bị nào” qua lớp cha/interface — nền tảng của mọi thiết kế mềm dẻo.
- Dùng
virtual/override, phân biệt liên kết tĩnh và động - Hiểu vtable và chi phí của hàm ảo
- Luôn có destructor ảo cho lớp cha đa hình
- Thiết kế lớp trừu tượng và interface (hàm thuần ảo
= 0)
1Đa hình
Đa hình: cùng một lời gọi dc->chay(128) nhưng hành vi khác nhau tuỳ kiểu thật của đối tượng. Điều kiện: hàm ở lớp cha khai báo virtual, gọi qua con trỏ hoặc tham chiếu lớp cha.
override: yêu cầu trình biên dịch kiểm tra bạn thật sự ghi đè (gõ sai tên/kiểu sẽ báo lỗi).- Hàm không
virtualđược chọn theo kiểu con trỏ lúc biên dịch (liên kết tĩnh). - Lớp cha có hàm ảo bắt buộc có
virtual ~LopCha(), nếu không xoá qua con trỏ cha sẽ bỏ sót destructor lớp con.
// Tính đa hình (polymorphism): hàm ảo (virtual), override, liên kết động, destructor ảo
#include <iostream>
#include <memory>
class DongCo {
public:
virtual ~DongCo() { std::cout << " ~DongCo()\n"; } // destructor ẢO – bắt buộc với lớp cha đa hình
virtual void chay(int tocDo) { std::cout << " DongCo chung chạy " << tocDo << "\n"; }
void tenLop() { std::cout << " (hàm KHÔNG ảo của DongCo)\n"; }
};
class DongCoDC : public DongCo {
public:
~DongCoDC() override { std::cout << " ~DongCoDC() – tắt PWM\n"; }
void chay(int tocDo) override { std::cout << " Động cơ DC: PWM duty = " << tocDo * 100 / 255 << "%\n"; }
void tenLop() { std::cout << " (hàm KHÔNG ảo của DongCoDC)\n"; }
};
class DongCoBuoc : public DongCo {
public:
~DongCoBuoc() override { std::cout << " ~DongCoBuoc() – nhả cuộn dây\n"; }
void chay(int tocDo) override { std::cout << " Động cơ bước: " << tocDo * 10 << " bước/giây\n"; }
};
class Servo : public DongCo {
public:
void chay(int goc) override { std::cout << " Servo quay tới " << goc * 180 / 255 << " độ\n"; }
};
// Hàm này viết MỘT LẦN, chạy đúng với MỌI loại động cơ – đó là đa hình
void khoiDong(DongCo& dc, int tocDo) { dc.chay(tocDo); }
int main() {
std::cout << "== Liên kết động: hàm nào được gọi do KIỂU THẬT của đối tượng quyết định ==\n";
DongCoDC dc;
DongCoBuoc buoc;
Servo servo;
DongCo* ds[] = {&dc, &buoc, &servo};
for (DongCo* p : ds) p->chay(128);
std::cout << "\n== Qua tham chiếu cũng vậy ==\n";
khoiDong(buoc, 200);
std::cout << "\n== Hàm KHÔNG ảo: gọi theo KIỂU CON TRỎ (liên kết tĩnh) ==\n";
DongCo* p = &dc;
p->tenLop(); // gọi bản của DongCo dù đối tượng là DongCoDC
dc.tenLop();
std::cout << "\n== Destructor ảo: xoá qua con trỏ lớp cha vẫn gọi đủ destructor lớp con ==\n";
{
std::unique_ptr<DongCo> dongCo = std::make_unique<DongCoDC>();
dongCo->chay(255);
}
std::cout << "\n== Chi phí của đa hình ==\n";
struct KhongAo { int x; };
struct CoAo { virtual ~CoAo() = default; int x; };
std::cout << " sizeof(lớp không ảo) = " << sizeof(KhongAo) << ", sizeof(lớp có hàm ảo) = " << sizeof(CoAo)
<< " (thêm con trỏ vtable)\n";
std::cout << "\n== Hết main ==\n";
}
g++ -std=c++17 12a-da-hinh.cpp -o app && ./app== Liên kết động: hàm nào được gọi do KIỂU THẬT của đối tượng quyết định ==
Động cơ DC: PWM duty = 50%
Động cơ bước: 1280 bước/giây
Servo quay tới 90 độ
== Qua tham chiếu cũng vậy ==
Động cơ bước: 2000 bước/giây
== Hàm KHÔNG ảo: gọi theo KIỂU CON TRỎ (liên kết tĩnh) ==
(hàm KHÔNG ảo của DongCo)
(hàm KHÔNG ảo của DongCoDC)
== Destructor ảo: xoá qua con trỏ lớp cha vẫn gọi đủ destructor lớp con ==
Động cơ DC: PWM duty = 100%
~DongCoDC() – tắt PWM
~DongCo()
== Chi phí của đa hình ==
sizeof(lớp không ảo) = 4, sizeof(lớp có hàm ảo) = 16 (thêm con trỏ vtable)
== Hết main ==
~DongCo()
~DongCoBuoc() – nhả cuộn dây
~DongCo()
~DongCoDC() – tắt PWM
~DongCo()Giải thích
- Mảng
DongCo* ds[]chứa ba loại động cơ khác nhau, một vòng lặp điều khiển tất cả. p->tenLop()gọi bản củaDongCovì hàm không ảo — dù đối tượng thật làDongCoDC.- Mỗi đối tượng có hàm ảo tốn thêm một con trỏ (8 byte trên PC, 4 byte trên ESP32/STM32) — chấp nhận được trong hầu hết trường hợp.
2Trừu tượng và interface
Hàm thuần ảo virtual float doc() = 0; không có thân; lớp chứa nó là lớp trừu tượng — không tạo đối tượng được, chỉ dùng làm lớp cha. Lớp chỉ toàn hàm thuần ảo gọi là interface: mô tả “làm được gì” mà không nói “làm thế nào”.
IHienThi, CamBien; đổi LCD sang OLED hay đổi DHT22 sang SHT31 không phải sửa logic — chỉ cần lớp mới cài đặt interface.// Tính trừu tượng: lớp trừu tượng (hàm thuần ảo = 0) và interface
#include <cstdio>
#include <iostream>
#include <string>
// INTERFACE: chỉ có hàm thuần ảo – mô tả "làm được gì", không nói "làm thế nào"
class IHienThi {
public:
virtual ~IHienThi() = default;
virtual void xoa() = 0;
virtual void inDong(int dong, const std::string& s) = 0;
};
class IGhiLog {
public:
virtual ~IGhiLog() = default;
virtual void ghi(const std::string& s) = 0;
};
// LỚP TRỪU TƯỢNG: có cả hàm thuần ảo lẫn code dùng chung
class CamBien {
public:
virtual ~CamBien() = default;
virtual float doc() = 0; // lớp con BẮT BUỘC cài đặt
virtual std::string donVi() const = 0;
std::string docChuoi() { // code chung, dùng hàm trừu tượng ở trên
char buf[16];
std::snprintf(buf, sizeof(buf), "%.1f", doc());
return buf + donVi();
}
};
// ===== Các lớp cụ thể =====
class Lcd1602 : public IHienThi {
public:
void xoa() override { std::cout << " [LCD] xoá màn hình\n"; }
void inDong(int d, const std::string& s) override { std::cout << " [LCD dòng " << d << "] " << s << "\n"; }
};
class SerialMonitor : public IHienThi, public IGhiLog { // một lớp cài nhiều interface
public:
void xoa() override {}
void inDong(int d, const std::string& s) override { std::cout << " [Serial] " << d << ": " << s << "\n"; }
void ghi(const std::string& s) override { std::cout << " [Serial LOG] " << s << "\n"; }
};
class Dht22 : public CamBien {
public:
float doc() override { return 28.46f; }
std::string donVi() const override { return "C"; }
};
class Bh1750 : public CamBien {
public:
float doc() override { return 356.7f; }
std::string donVi() const override { return " lux"; }
};
// Logic ứng dụng chỉ phụ thuộc vào TRỪU TƯỢNG -> đổi phần cứng không phải sửa hàm này
void capNhatManHinh(IHienThi& man, CamBien& nhiet, CamBien& anhSang) {
man.xoa();
man.inDong(0, "Nhiet do: " + nhiet.docChuoi());
man.inDong(1, "Anh sang: " + anhSang.docChuoi());
}
int main() {
Dht22 dht;
Bh1750 bh;
Lcd1602 lcd;
SerialMonitor serial;
std::cout << "== Cùng một hàm, hiển thị lên LCD ==\n";
capNhatManHinh(lcd, dht, bh);
std::cout << "\n== ... hoặc lên Serial Monitor, không sửa code ==\n";
capNhatManHinh(serial, dht, bh);
std::cout << "\n== Dùng qua interface khác ==\n";
IGhiLog& log = serial;
log.ghi("Khởi động xong");
// CamBien cb; // LỖI: không tạo được đối tượng từ lớp trừu tượng
std::cout << "\n Không thể tạo đối tượng CamBien trực tiếp vì nó có hàm thuần ảo.\n";
}
g++ -std=c++17 12b-truu-tuong-interface.cpp -o app && ./app== Cùng một hàm, hiển thị lên LCD == [LCD] xoá màn hình [LCD dòng 0] Nhiet do: 28.5C [LCD dòng 1] Anh sang: 356.7 lux == ... hoặc lên Serial Monitor, không sửa code == [Serial] 0: Nhiet do: 28.5C [Serial] 1: Anh sang: 356.7 lux == Dùng qua interface khác == [Serial LOG] Khởi động xong Không thể tạo đối tượng CamBien trực tiếp vì nó có hàm thuần ảo.
Giải thích
capNhatManHinh()chạy được với cảLcd1602lẫnSerialMonitormà không cóifnào.SerialMonitorcài đặt hai interface cùng lúc (IHienThivàIGhiLog).- Lớp trừu tượng
CamBienvẫn có code dùng chung (docChuoi()) gọi tới hàm trừu tượng — đây là mẫu Template Method.
🔧Áp dụng trong lập trình nhúng
- Tầng HAL (Hardware Abstraction Layer) là interface: cùng một firmware chạy trên nhiều board chỉ cần thay lớp cài đặt.
- Viết lớp giả (mock) cài đặt interface cảm biến để kiểm thử logic trên máy tính trước khi nạp vào chip.
- Khi cần tốc độ tối đa và loại thiết bị cố định lúc biên dịch, có thể thay đa hình bằng template (xem khóa Design Pattern — Strategy).
⚠Lỗi thường gặp
- Quên
virtualở lớp cha → gọi nhầm hàm lớp cha. - Thiếu destructor ảo.
- Truyền đối tượng lớp con vào hàm nhận lớp cha theo giá trị → bị “cắt lát” (object slicing), mất phần lớp con.
✎Bài tập về nhà
🧭 Con trỏ lớp: Singleton, đổi đối tượng, Adapter, Abstract
Dùng con trỏ tới đối tượng để xây các kỹ thuật thiết kế thường gặp trong firmware.
- Dùng
->,this, mảng đối tượng - Viết Singleton bằng con trỏ static
- Đổi hành vi lúc chạy bằng cách đổi đối tượng mà con trỏ trỏ tới
- Bọc lớp có sẵn bằng Adapter; làm việc qua con trỏ lớp trừu tượng
1Con trỏ tới đối tượng, Singleton, đổi đối tượng
p->ham()là cách viết gọn của(*p).ham().thislà con trỏ tới chính đối tượng đang gọi; trả vềthischo phép gọi nối tiếp.- Singleton (một thể hiện duy nhất): constructor
private+ con trỏstatic+ hàmlayThucThe(). - Đổi đối tượng qua con trỏ: giữ
CheDo* hienTai; đổi chế độ chỉ là gán con trỏ sang đối tượng khác — không cầnif/else.
// Con trỏ lớp: con trỏ tới đối tượng, ->, this, Singleton bằng con trỏ static,
// thay đổi đối tượng đang dùng qua con trỏ (đổi chế độ lúc chạy)
#include <iostream>
#include <string>
class Robot {
public:
explicit Robot(std::string ten) : ten_(std::move(ten)) {}
Robot* tien(int cm) { x_ += cm; return this; } // trả về this để gọi nối tiếp
Robot* re(int doc) { huong_ = (huong_ + doc) % 360; return this; }
void baoCao() const { std::cout << " " << ten_ << ": x = " << x_ << " cm, hướng " << huong_ << "°\n"; }
private:
std::string ten_;
int x_ = 0, huong_ = 0;
};
// ===== Singleton bằng con trỏ static (kiểu cổ điển) =====
class CauHinh {
public:
static CauHinh* layThucThe() {
if (thucThe_ == nullptr) {
thucThe_ = new CauHinh(); // tạo lần đầu
std::cout << " (tạo CauHinh)\n";
}
return thucThe_;
}
int nguongNhiet = 30;
private:
CauHinh() = default; // không cho new từ bên ngoài
static CauHinh* thucThe_;
};
CauHinh* CauHinh::thucThe_ = nullptr;
// ===== Thay đổi đối tượng qua con trỏ: đổi "chế độ" lúc chạy =====
class CheDo {
public:
virtual ~CheDo() = default;
virtual void xuLy(float t) = 0;
};
class CheDoTietKiem : public CheDo {
public:
void xuLy(float t) override { std::cout << " [Tiết kiệm] " << t << "°C -> quạt " << (t > 32 ? "chậm" : "tắt") << "\n"; }
};
class CheDoManh : public CheDo {
public:
void xuLy(float t) override { std::cout << " [Mạnh] " << t << "°C -> quạt " << (t > 26 ? "MAX" : "vừa") << "\n"; }
};
int main() {
std::cout << "== Con trỏ tới đối tượng và -> ==\n";
Robot r("BK-01");
Robot* p = &r;
p->tien(20); // p->tien() tương đương (*p).tien()
(*p).re(90);
p->tien(10)->re(90)->tien(5); // nối tiếp nhờ trả về this
p->baoCao();
std::cout << "\n== Mảng đối tượng & con trỏ duyệt mảng ==\n";
Robot doi[3] = {Robot("R1"), Robot("R2"), Robot("R3")};
for (Robot* it = doi; it != doi + 3; ++it) it->tien(10 * (it - doi + 1));
for (const Robot& rb : doi) rb.baoCao();
std::cout << "\n== Singleton: mọi nơi lấy về cùng một con trỏ ==\n";
CauHinh* c1 = CauHinh::layThucThe();
CauHinh* c2 = CauHinh::layThucThe();
c1->nguongNhiet = 35;
std::cout << " c1 == c2 ? " << std::boolalpha << (c1 == c2) << ", c2->nguongNhiet = " << c2->nguongNhiet << "\n";
std::cout << "\n== Đổi đối tượng mà con trỏ trỏ tới -> đổi hành vi lúc chạy ==\n";
CheDoTietKiem tietKiem;
CheDoManh manh;
CheDo* cheDoHienTai = &tietKiem;
for (float t : {28.0f, 33.0f}) cheDoHienTai->xuLy(t);
std::cout << " -- người dùng bấm nút đổi chế độ --\n";
cheDoHienTai = &manh; // chỉ đổi con trỏ, không if/else
for (float t : {28.0f, 33.0f}) cheDoHienTai->xuLy(t);
}
g++ -std=c++17 13a-con-tro-lop.cpp -o app && ./app== Con trỏ tới đối tượng và -> == BK-01: x = 35 cm, hướng 180° == Mảng đối tượng & con trỏ duyệt mảng == R1: x = 10 cm, hướng 0° R2: x = 20 cm, hướng 0° R3: x = 30 cm, hướng 0° == Singleton: mọi nơi lấy về cùng một con trỏ == (tạo CauHinh) c1 == c2 ? true, c2->nguongNhiet = 35 == Đổi đối tượng mà con trỏ trỏ tới -> đổi hành vi lúc chạy == [Tiết kiệm] 28°C -> quạt tắt [Tiết kiệm] 33°C -> quạt chậm -- người dùng bấm nút đổi chế độ -- [Mạnh] 28°C -> quạt MAX [Mạnh] 33°C -> quạt MAX
Giải thích
p->tien(10)->re(90)->tien(5)chạy được vì mỗi hàm trả vềthis.it - doi(hiệu hai con trỏ) cho ra chỉ số của phần tử trong mảng.- Singleton kiểu con trỏ static đơn giản nhưng không an toàn đa luồng; cách tốt hơn là biến static cục bộ (xem khóa Design Pattern).
- Đổi
cheDoHienTaichính là ý tưởng của mẫu Strategy/State.
2Adapter và Abstract qua con trỏ
// Con trỏ lớp trong thiết kế: Adapter (bọc lớp có sẵn) và Abstract (làm việc qua con trỏ lớp trừu tượng)
#include <iostream>
#include <memory>
#include <vector>
// ===== Interface mà chương trình của ta dùng =====
class IKhoangCach {
public:
virtual ~IKhoangCach() = default;
virtual float docCm() = 0;
};
class SieuAmHcSr04 : public IKhoangCach { // đã tương thích sẵn
public:
float docCm() override { return 42.0f; }
};
// ===== Thư viện bên thứ ba: trả về MILIMET với tên hàm khác, không sửa được =====
class VL53L0X {
public:
int readRangeMillimeters() { return 875; }
};
// ===== Adapter: giữ CON TRỎ tới đối tượng cũ, chuyển lời gọi + đổi đơn vị =====
class VL53Adapter : public IKhoangCach {
public:
explicit VL53Adapter(VL53L0X* cb) : cb_(cb) {}
float docCm() override { return cb_->readRangeMillimeters() / 10.0f; }
private:
VL53L0X* cb_;
};
// ===== Abstract: robot chỉ cầm con trỏ tới IKhoangCach =====
class RobotTranhVatCan {
public:
void them(std::unique_ptr<IKhoangCach> cb) { camBien_.push_back(std::move(cb)); }
void buoc() {
float gan = 1e9;
for (auto& cb : camBien_) { // gọi qua con trỏ lớp trừu tượng
float d = cb->docCm();
std::cout << " cảm biến báo " << d << " cm\n";
if (d < gan) gan = d;
}
std::cout << " -> vật cản gần nhất " << gan << " cm: " << (gan < 50 ? "RẼ" : "đi thẳng") << "\n";
}
private:
std::vector<std::unique_ptr<IKhoangCach>> camBien_;
};
int main() {
static VL53L0X laser; // đối tượng thư viện
RobotTranhVatCan robot;
robot.them(std::make_unique<SieuAmHcSr04>());
robot.them(std::make_unique<VL53Adapter>(&laser)); // bọc bằng adapter rồi dùng như mọi cảm biến khác
robot.buoc();
std::cout << "\n Xem thêm khóa Design Pattern: Adapter, Singleton, Strategy, State.\n";
}
g++ -std=c++17 13b-adapter-abstract.cpp -o app && ./appcảm biến báo 42 cm cảm biến báo 87.5 cm -> vật cản gần nhất 42 cm: RẼ Xem thêm khóa Design Pattern: Adapter, Singleton, Strategy, State.
Giải thích
VL53Adaptergiữ con trỏ tới đối tượng thư viện, cài đặt interface của ta và đổi mm → cm.- Robot giữ
std::vector<std::unique_ptr<IKhoangCach>>: danh sách cảm biến bất kỳ, tự giải phóng khi robot bị huỷ. - Đối tượng
laserkhai báostaticđể sống suốt chương trình — adapter chỉ “mượn”, không sở hữu.
🔧Áp dụng trong lập trình nhúng
- Firmware thường có một con trỏ
CheDo* cheDoHienTaitrongloop(); nút MODE chỉ đổi con trỏ. - Adapter là cách chuẩn để dùng nhiều thư viện cảm biến của các hãng khác nhau dưới một interface chung.
- Muốn hiểu sâu hơn: xem khóa Design Pattern — bài Singleton, Adapter, Strategy, State.
⚠Lỗi thường gặp
- Giữ con trỏ tới đối tượng đã bị huỷ (đối tượng cục bộ trong hàm khác).
- Singleton tạo bằng
newnhưng không bao giờ giải phóng (chấp nhận được trên MCU, nhưng cần biết). - Dùng
dynamic_castkhi firmware tắt RTTI.
✎Bài tập về nhà
💾 File I/O, xử lý ngoại lệ và bộ nhớ động
Ghi/đọc dữ liệu ra file, xử lý lỗi bằng ngoại lệ hoặc std::optional, và quản lý bộ nhớ động an toàn.
- Dùng
ofstream/ifstream, chế độ ghi thêm, file nhị phân - Phân tích file CSV
- Dùng
try/catch/throw, tự định nghĩa ngoại lệ - Hiểu stack/heap,
new/delete, rò rỉ bộ nhớ, con trỏ thông minh, placement new
1File I/O và Stream
| Lớp | Dùng để |
|---|---|
std::ofstream | ghi file (std::ios::app để ghi thêm, std::ios::binary cho file nhị phân) |
std::ifstream | đọc file; std::getline đọc từng dòng |
std::stringstream | coi một chuỗi như luồng để tách dữ liệu |
File tự đóng khi đối tượng stream bị huỷ (RAII). Luôn kiểm tra mở file thành công: if (!f) ....
// File I/O và Stream: ghi/đọc file văn bản (CSV log), ghi thêm, file nhị phân
// (Trên ESP32 dùng y hệt với thẻ SD hoặc LittleFS qua thư viện File/FS)
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
struct BanGhi { int giay; float nhietDo; int doAm; };
int main() {
std::cout << "== Ghi file CSV ==\n";
{
std::ofstream f("log.csv"); // mở để GHI (xoá nội dung cũ)
if (!f) { std::cerr << "Không mở được file\n"; return 1; }
f << "giay,nhiet_do,do_am\n";
BanGhi duLieu[] = {{0, 28.5f, 60}, {10, 28.9f, 61}, {20, 29.4f, 59}};
for (const auto& b : duLieu) f << b.giay << "," << b.nhietDo << "," << b.doAm << "\n";
} // ra khỏi khối -> file tự đóng (RAII)
std::cout << " đã ghi log.csv\n";
std::cout << "\n== Ghi THÊM vào cuối file (append) ==\n";
{
std::ofstream f("log.csv", std::ios::app);
f << 30 << "," << 30.1 << "," << 58 << "\n";
}
std::cout << "\n== Đọc lại từng dòng và phân tích CSV ==\n";
std::ifstream in("log.csv");
std::string dong;
std::getline(in, dong); // bỏ dòng tiêu đề
float tong = 0;
int n = 0;
while (std::getline(in, dong)) {
std::stringstream ss(dong);
std::string o;
BanGhi b{};
std::getline(ss, o, ','); b.giay = std::stoi(o);
std::getline(ss, o, ','); b.nhietDo = std::stof(o);
std::getline(ss, o, ','); b.doAm = std::stoi(o);
std::cout << " t=" << b.giay << "s T=" << b.nhietDo << " H=" << b.doAm << "\n";
tong += b.nhietDo;
n++;
}
std::cout << " Nhiệt độ trung bình: " << tong / n << "\n";
std::cout << "\n== File nhị phân: lưu cấu hình (giống EEPROM.put/get) ==\n";
struct CauHinh { int nguong; float heSo; char wifi[16]; };
CauHinh luu{32, 1.05f, "BKSTAR-Lab"};
std::ofstream fb("config.bin", std::ios::binary);
fb.write(reinterpret_cast<const char*>(&luu), sizeof(luu));
fb.close();
CauHinh doc{};
std::ifstream fr("config.bin", std::ios::binary);
fr.read(reinterpret_cast<char*>(&doc), sizeof(doc));
std::cout << " đọc lại: nguong=" << doc.nguong << ", heSo=" << doc.heSo << ", wifi=" << doc.wifi
<< " (" << fr.gcount() << " byte)\n";
std::cout << "\n== Mở file không tồn tại ==\n";
std::ifstream khong("khong_co.txt");
std::cout << " is_open() = " << std::boolalpha << khong.is_open() << " -> luôn kiểm tra!\n";
}
g++ -std=c++17 14a-file-io.cpp -o app && ./app== Ghi file CSV == đã ghi log.csv == Ghi THÊM vào cuối file (append) == == Đọc lại từng dòng và phân tích CSV == t=0s T=28.5 H=60 t=10s T=28.9 H=61 t=20s T=29.4 H=59 t=30s T=30.1 H=58 Nhiệt độ trung bình: 29.225 == File nhị phân: lưu cấu hình (giống EEPROM.put/get) == đọc lại: nguong=32, heSo=1.05, wifi=BKSTAR-Lab (24 byte) == Mở file không tồn tại == is_open() = false -> luôn kiểm tra!
Giải thích
- Định dạng CSV (giá trị cách nhau bởi dấu phẩy) mở được bằng Excel — cách ghi log cảm biến phổ biến nhất.
getline(ss, o, ',')tách chuỗi theo dấu phẩy.- File nhị phân lưu nguyên bộ nhớ của struct — nhỏ gọn, nhưng chỉ đọc lại đúng trên máy cùng kiến trúc.
2Xử lý ngoại lệ
Ngoại lệ tách đường đi “bình thường” khỏi đường xử lý lỗi: hàm phát hiện lỗi throw; nơi gọi bọc trong try và bắt bằng catch. Ngoại lệ “bay” ngược qua các hàm cho tới khi gặp catch phù hợp; các đối tượng trên đường đi được huỷ đúng cách (RAII).
-fno-exceptions để tiết kiệm Flash và đảm bảo thời gian thực. Khi đó trả lỗi bằng giá trị: bool, mã lỗi, hoặc std::optional.// Xử lý ngoại lệ: try / catch / throw, lớp ngoại lệ riêng – và cách thay thế khi firmware tắt exception
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
class LoiCamBien : public std::runtime_error { // ngoại lệ tự định nghĩa
public:
LoiCamBien(const std::string& ten, int maLoi)
: std::runtime_error("Cảm biến " + ten + " lỗi mã " + std::to_string(maLoi)), maLoi(maLoi) {}
int maLoi;
};
float docNhietDo(int lanThu) {
if (lanThu == 2) throw LoiCamBien("DHT22", 4); // NÉM ngoại lệ
if (lanThu == 3) throw std::out_of_range("giá trị ngoài dải -40..80");
return 27.5f + lanThu;
}
int chia(int a, int b) {
if (b == 0) throw std::invalid_argument("chia cho 0");
return a / b;
}
// Cách KHÔNG dùng exception (phổ biến trong firmware biên dịch với -fno-exceptions)
std::optional<float> docNhietDoAnToan(int lanThu) {
if (lanThu == 2) return std::nullopt; // "không có giá trị" = lỗi
return 27.5f + lanThu;
}
int main() {
std::cout << "== try / catch nhiều loại ngoại lệ ==\n";
for (int i = 1; i <= 4; i++) {
try {
float t = docNhietDo(i);
std::cout << " lần " << i << ": " << t << "°C\n";
} catch (const LoiCamBien& e) { // bắt loại cụ thể trước
std::cout << " lần " << i << ": " << e.what() << " -> khởi động lại cảm biến\n";
} catch (const std::exception& e) { // rồi tới loại tổng quát
std::cout << " lần " << i << ": lỗi chung: " << e.what() << "\n";
}
}
std::cout << "\n== Ngoại lệ của thư viện chuẩn ==\n";
try {
std::vector<int> v = {1, 2, 3};
int x = v.at(10); // ném ngoại lệ ở đây, dòng dưới không chạy
std::cout << " v.at(10) = " << x << "\n";
} catch (const std::out_of_range&) {
std::cout << " v.at(10) ném std::out_of_range\n";
}
try {
std::stoi("abc");
} catch (const std::invalid_argument&) {
std::cout << " stoi(\"abc\") ném std::invalid_argument\n";
}
try {
std::cout << " chia(10, 2) = " << chia(10, 2) << "\n";
int kq = chia(1, 0);
std::cout << " chia(1, 0) = " << kq << "\n";
} catch (const std::invalid_argument& e) {
std::cout << " bắt được: " << e.what() << "\n";
}
std::cout << "\n== Không dùng exception: std::optional ==\n";
for (int i = 1; i <= 2; i++) {
if (auto t = docNhietDoAnToan(i)) std::cout << " lần " << i << ": " << *t << "°C\n";
else std::cout << " lần " << i << ": đọc lỗi, dùng giá trị cũ\n";
}
}
g++ -std=c++17 14b-ngoai-le.cpp -o app && ./app== try / catch nhiều loại ngoại lệ ==
lần 1: 28.5°C
lần 2: Cảm biến DHT22 lỗi mã 4 -> khởi động lại cảm biến
lần 3: lỗi chung: giá trị ngoài dải -40..80
lần 4: 31.5°C
== Ngoại lệ của thư viện chuẩn ==
v.at(10) ném std::out_of_range
stoi("abc") ném std::invalid_argument
chia(10, 2) = 5
bắt được: chia cho 0
== Không dùng exception: std::optional ==
lần 1: 28.5°C
lần 2: đọc lỗi, dùng giá trị cũGiải thích
- Bắt loại cụ thể (
LoiCamBien) trước, loại tổng quát (std::exception) sau. - Bắt bằng
const&để không sao chép và không bị cắt lát. std::optional<float>: có giá trị hoặcstd::nullopt— kiểm tra bằngif (auto t = ...).
3Bộ nhớ động
| Vùng | Chứa gì | Ai giải phóng |
|---|---|---|
| Stack | biến cục bộ, tham số | tự động khi ra khỏi hàm/khối |
| Heap | new, malloc | bạn phải delete/free — hoặc giao cho con trỏ thông minh |
| Static | biến toàn cục, static | sống suốt chương trình |
Con trỏ thông minh: std::unique_ptr (một chủ, tự delete, không sao chép được, chuyển bằng std::move), std::shared_ptr (nhiều chủ, đếm tham chiếu).
// Bộ nhớ động: stack vs heap, new/delete, rò rỉ bộ nhớ, con trỏ thông minh, placement new vào bộ đệm tĩnh
#include <iostream>
#include <memory>
#include <new>
class GoiTin {
public:
explicit GoiTin(int id) : id_(id) { soDangSong++; std::cout << " + GoiTin " << id_ << "\n"; }
~GoiTin() { soDangSong--; std::cout << " - GoiTin " << id_ << "\n"; }
int id() const { return id_; }
static int soDangSong;
private:
int id_;
char payload_[60] = {};
};
int GoiTin::soDangSong = 0;
int main() {
std::cout << "== Stack: biến cục bộ tự giải phóng ==\n";
{
GoiTin g(1);
}
std::cout << "\n== Heap với new / delete ==\n";
GoiTin* p = new GoiTin(2); // cấp phát trên heap, sống tới khi delete
std::cout << " dùng p->id() = " << p->id() << "\n";
delete p; // QUÊN dòng này = rò rỉ bộ nhớ
p = nullptr; // tránh con trỏ treo (dangling)
std::cout << "\n== Mảng động new[] / delete[] ==\n";
int n = 5; // kích thước chỉ biết lúc chạy
int* mau = new int[n];
for (int i = 0; i < n; i++) mau[i] = i * i;
std::cout << " mau[4] = " << mau[4] << "\n";
delete[] mau; // mảng phải dùng delete[]
std::cout << "\n== Rò rỉ bộ nhớ (memory leak) ==\n";
for (int i = 0; i < 3; i++) {
GoiTin* ro = new GoiTin(10 + i); // cố ý không delete
(void)ro;
}
std::cout << " Còn " << GoiTin::soDangSong << " gói tin không ai giải phóng! (trên MCU sẽ cạn RAM dần rồi treo)\n";
std::cout << "\n== unique_ptr: một chủ sở hữu, tự delete ==\n";
{
std::unique_ptr<GoiTin> u = std::make_unique<GoiTin>(20);
std::unique_ptr<GoiTin> u2 = std::move(u); // chuyển quyền sở hữu, không sao chép được
std::cout << " u rỗng? " << std::boolalpha << (u == nullptr) << ", u2->id() = " << u2->id() << "\n";
}
std::cout << "\n== shared_ptr: nhiều chủ sở hữu, đếm tham chiếu ==\n";
{
auto s1 = std::make_shared<GoiTin>(30);
{
auto s2 = s1;
std::cout << " use_count = " << s1.use_count() << "\n";
}
std::cout << " use_count = " << s1.use_count() << "\n";
}
std::cout << "\n== Placement new: dựng đối tượng trong bộ đệm TĨNH (không đụng heap) ==\n";
alignas(GoiTin) static unsigned char vungNho[sizeof(GoiTin)];
GoiTin* g = new (vungNho) GoiTin(40);
std::cout << " đối tượng nằm trong vungNho? " << (static_cast<void*>(g) == vungNho) << "\n";
g->~GoiTin(); // tự gọi destructor, KHÔNG delete
std::cout << "\n sizeof(GoiTin) = " << sizeof(GoiTin) << " byte. Arduino Uno chỉ có 2048 byte RAM!\n";
std::cout << " Còn sống ở cuối chương trình: " << GoiTin::soDangSong << " (chính là 3 gói bị rò rỉ)\n";
}
g++ -std=c++17 14c-bo-nho-dong.cpp -o app && ./app== Stack: biến cục bộ tự giải phóng ==
+ GoiTin 1
- GoiTin 1
== Heap với new / delete ==
+ GoiTin 2
dùng p->id() = 2
- GoiTin 2
== Mảng động new[] / delete[] ==
mau[4] = 16
== Rò rỉ bộ nhớ (memory leak) ==
+ GoiTin 10
+ GoiTin 11
+ GoiTin 12
Còn 3 gói tin không ai giải phóng! (trên MCU sẽ cạn RAM dần rồi treo)
== unique_ptr: một chủ sở hữu, tự delete ==
+ GoiTin 20
u rỗng? true, u2->id() = 20
- GoiTin 20
== shared_ptr: nhiều chủ sở hữu, đếm tham chiếu ==
+ GoiTin 30
use_count = 2
use_count = 1
- GoiTin 30
== Placement new: dựng đối tượng trong bộ đệm TĨNH (không đụng heap) ==
+ GoiTin 40
đối tượng nằm trong vungNho? true
- GoiTin 40
sizeof(GoiTin) = 64 byte. Arduino Uno chỉ có 2048 byte RAM!
Còn sống ở cuối chương trình: 3 (chính là 3 gói bị rò rỉ)Giải thích
- Ba
GoiTintạo bằngnewmà khôngdeletevẫn còn sống ở cuối chương trình — đó là rò rỉ bộ nhớ. new[]phải đi vớidelete[].- Placement new dựng đối tượng vào một vùng nhớ tĩnh có sẵn — kỹ thuật của Object Pool trong firmware, không đụng tới heap.
🔧Áp dụng trong lập trình nhúng
- ESP32 ghi log ra thẻ SD hoặc LittleFS bằng API rất giống
fstream:File f = SD.open("/log.csv", FILE_APPEND); f.println(...);. - Arduino Uno có 2 KB RAM: tránh
newvàStringtrongloop()— heap bị phân mảnh và chip treo sau vài giờ. - Quy tắc an toàn (MISRA, AUTOSAR): chỉ cấp phát động lúc khởi động, không cấp phát trong vòng lặp chính.
⚠Lỗi thường gặp
- Quên kiểm tra file mở thành công.
deletehai lần cùng một con trỏ; dùng con trỏ sau khi đãdelete.- Ném ngoại lệ trong destructor; bắt ngoại lệ bằng giá trị.
✎Bài tập về nhà
🧰 Namespace, template, bộ tiền xử lý và xử lý tín hiệu
Tổ chức code lớn bằng namespace, viết code tổng quát bằng template, cấu hình biên dịch bằng tiền xử lý và hiểu tín hiệu.
- Tạo và dùng namespace, bí danh, namespace ẩn danh
- Viết hàm mẫu và lớp mẫu (RingBuffer)
- Dùng
#define,#if/#ifdef, macro có sẵn, include guard - Đăng ký signal handler và hiểu sự tương đồng với ngắt
1Namespace
Namespace gom tên vào một “họ” để tránh trùng: dht::doc() và bmp::doc() cùng tồn tại. Mọi thứ của thư viện chuẩn nằm trong std::.
// Namespace: tránh trùng tên khi ghép nhiều thư viện
#include <iostream>
namespace dht {
float doc() { return 28.4f; }
const char* ten = "DHT22";
}
namespace bmp {
float doc() { return 1012.7f; } // cùng tên doc() nhưng không xung đột
const char* ten = "BMP280";
namespace hieuChinh { // namespace lồng nhau
float heSo = 1.002f;
}
}
namespace robot::dongCo { // cú pháp lồng gọn (C++17)
void chay() { std::cout << " robot::dongCo::chay()\n"; }
}
namespace { // namespace ẩn danh: chỉ thấy trong file này (như static)
int biMat = 42;
}
namespace dongCoRatDaiTen { void dung() { std::cout << " dừng động cơ\n"; } }
int main() {
std::cout << "== Gọi bằng tên đầy đủ ==\n";
std::cout << " " << dht::ten << ": " << dht::doc() << "\n";
std::cout << " " << bmp::ten << ": " << bmp::doc() * bmp::hieuChinh::heSo << "\n";
robot::dongCo::chay();
std::cout << "\n== using: đưa MỘT tên vào phạm vi hiện tại ==\n";
using std::cout;
cout << " dùng cout không cần std::\n";
std::cout << "\n== Bí danh namespace ==\n";
namespace dc = dongCoRatDaiTen;
dc::dung();
std::cout << "\n== Namespace ẩn danh ==\n";
std::cout << " biMat = " << biMat << "\n";
std::cout << "\n Lưu ý: tránh viết 'using namespace std;' trong file .h – dễ gây trùng tên ở nơi include.\n";
}
g++ -std=c++17 15a-namespace.cpp -o app && ./app== Gọi bằng tên đầy đủ == DHT22: 28.4 BMP280: 1014.73 robot::dongCo::chay() == using: đưa MỘT tên vào phạm vi hiện tại == dùng cout không cần std:: == Bí danh namespace == dừng động cơ == Namespace ẩn danh == biMat = 42 Lưu ý: tránh viết 'using namespace std;' trong file .h – dễ gây trùng tên ở nơi include.
Giải thích
namespace robot::dongColà cú pháp lồng gọn của C++17.- Namespace ẩn danh thay cho
staticở phạm vi file. - Tránh
using namespace std;trong file header.
2Template
Template cho phép viết code một lần cho nhiều kiểu; trình biên dịch sinh ra bản cụ thể cho từng kiểu được dùng. Tham số template có thể là kiểu (typename T) hoặc giá trị hằng (size_t N) — rất hợp để tạo cấu trúc dữ liệu kích thước cố định cho vi điều khiển.
// Template: hàm mẫu, lớp mẫu, tham số không phải kiểu (kích thước), chuyên biệt hoá
#include <cstdint>
#include <iostream>
#include <string>
// ===== Hàm mẫu: viết một lần, dùng cho nhiều kiểu =====
template <typename T>
T lonNhat(T a, T b) { return a > b ? a : b; }
template <typename T>
T gioiHan(T x, T thap, T cao) { return x < thap ? thap : (x > cao ? cao : x); }
// ===== Lớp mẫu: bộ đệm vòng kích thước cố định – KHÔNG dùng heap, rất hợp vi điều khiển =====
template <typename T, size_t N>
class RingBuffer {
public:
bool push(const T& v) {
if (dem_ == N) return false; // đầy
buf_[duoi_] = v;
duoi_ = (duoi_ + 1) % N;
dem_++;
return true;
}
bool pop(T& ra) {
if (dem_ == 0) return false; // rỗng
ra = buf_[dau_];
dau_ = (dau_ + 1) % N;
dem_--;
return true;
}
size_t size() const { return dem_; }
static constexpr size_t capacity() { return N; }
private:
T buf_[N];
size_t dau_ = 0, duoi_ = 0, dem_ = 0;
};
// ===== Chuyên biệt hoá: xử lý riêng cho một kiểu =====
template <typename T>
std::string moTa(const T& v) { return "giá trị " + std::to_string(v); }
template <>
std::string moTa<bool>(const bool& v) { return v ? "BẬT" : "TẮT"; }
int main() {
std::cout << "== Hàm mẫu ==\n";
std::cout << " lonNhat(3, 7) = " << lonNhat(3, 7) << "\n";
std::cout << " lonNhat(2.5, 1.5) = " << lonNhat(2.5, 1.5) << "\n";
std::cout << " lonNhat<std::string>(\"abc\", \"abd\") = " << lonNhat<std::string>("abc", "abd") << "\n";
std::cout << " gioiHan(300, 0, 255) = " << gioiHan(300, 0, 255) << "\n";
std::cout << "\n== Lớp mẫu RingBuffer<uint16_t, 4> ==\n";
RingBuffer<uint16_t, 4> hangDoiAdc;
for (uint16_t v : {100, 200, 300, 400, 500}) {
bool ok = hangDoiAdc.push(v);
std::cout << " push " << v << (ok ? " ok" : " -> ĐẦY, bỏ qua") << "\n";
}
uint16_t v;
while (hangDoiAdc.pop(v)) std::cout << " pop " << v << "\n";
std::cout << " sizeof(RingBuffer<uint16_t,4>) = " << sizeof(hangDoiAdc) << " byte, cố định lúc biên dịch\n";
RingBuffer<std::string, 2> lenh;
lenh.push("LED:1");
std::string s;
lenh.pop(s);
std::cout << " RingBuffer<std::string, 2> dùng cho lệnh: " << s << "\n";
std::cout << "\n== Chuyên biệt hoá ==\n";
std::cout << " moTa(42) = " << moTa(42) << ", moTa(true) = " << moTa(true) << "\n";
}
g++ -std=c++17 15b-template.cpp -o app && ./app== Hàm mẫu ==
lonNhat(3, 7) = 7
lonNhat(2.5, 1.5) = 2.5
lonNhat<std::string>("abc", "abd") = abd
gioiHan(300, 0, 255) = 255
== Lớp mẫu RingBuffer<uint16_t, 4> ==
push 100 ok
push 200 ok
push 300 ok
push 400 ok
push 500 -> ĐẦY, bỏ qua
pop 100
pop 200
pop 300
pop 400
sizeof(RingBuffer<uint16_t,4>) = 32 byte, cố định lúc biên dịch
RingBuffer<std::string, 2> dùng cho lệnh: LED:1
== Chuyên biệt hoá ==
moTa(42) = giá trị 42, moTa(true) = BẬTGiải thích
RingBuffer<uint16_t, 4>có kích thước cố định lúc biên dịch, không cấp phát động.- Template thường được viết toàn bộ trong file
.hvì trình biên dịch cần thấy định nghĩa để sinh code. - Chuyên biệt hoá
moTa<bool>cho phép xử lý riêng một kiểu.
3Bộ tiền xử lý
Bộ tiền xử lý chạy trước trình biên dịch, chỉ thay thế văn bản: #include, #define, #if/#ifdef/#elif/#else/#endif, #error, #pragma. Macro có sẵn: __FILE__, __LINE__, __func__, __cplusplus.
// Bộ tiền xử lý (preprocessor): #define, macro, biên dịch có điều kiện, macro có sẵn
// Chạy TRƯỚC khi biên dịch: chỉ thay thế văn bản, không hiểu kiểu dữ liệu.
#include <iostream>
#define LED_PIN 13 // hằng kiểu C (C++ nên dùng constexpr)
#define BINH_PHUONG_SAI(x) x * x // macro thiếu ngoặc -> bẫy!
#define BINH_PHUONG(x) ((x) * (x))
#define DEBUG 1 // bật/tắt log khi biên dịch
#define BOARD_ESP32 // chọn phần cứng
#if DEBUG
#define LOG(msg) std::cout << " [DEBUG " << __FILE__ << ":" << __LINE__ << "] " << msg << "\n"
#else
#define LOG(msg) // bản phát hành: log biến mất hoàn toàn, không tốn byte nào
#endif
#ifdef BOARD_ESP32
constexpr int ADC_MAX = 4095; // 12 bit
constexpr const char* TEN_BOARD = "ESP32";
#elif defined(BOARD_UNO)
constexpr int ADC_MAX = 1023; // 10 bit
constexpr const char* TEN_BOARD = "Arduino Uno";
#else
#error "Chưa chọn board!"
#endif
#define CHUOI_HOA(x) #x // toán tử # biến tham số thành chuỗi
#define NOI(a, b) a##b // toán tử ## nối hai token
int main() {
std::cout << "== #define hằng ==\n";
std::cout << " LED_PIN = " << LED_PIN << "\n";
std::cout << "\n== Macro hàm và bẫy thiếu ngoặc ==\n";
std::cout << " BINH_PHUONG_SAI(2 + 3) = " << BINH_PHUONG_SAI(2 + 3) << " (thành 2 + 3 * 2 + 3!)\n";
std::cout << " BINH_PHUONG(2 + 3) = " << BINH_PHUONG(2 + 3) << "\n";
std::cout << "\n== Biên dịch có điều kiện ==\n";
std::cout << " Board: " << TEN_BOARD << ", ADC_MAX = " << ADC_MAX << "\n";
LOG("khởi động xong");
std::cout << "\n== # và ## ==\n";
int NOI(dem, 1) = 5; // tạo biến tên dem1
std::cout << " " << CHUOI_HOA(dem1) << " = " << dem1 << "\n";
std::cout << "\n== Macro có sẵn ==\n";
std::cout << " __cplusplus = " << __cplusplus << " (201703 = C++17)\n";
std::cout << " __func__ = " << __func__ << "\n";
#ifdef __GNUC__
std::cout << " Trình biên dịch: GCC " << __GNUC__ << "\n";
#endif
std::cout << "\n Include guard trong file .h:\n"
" #ifndef CAM_BIEN_H\n #define CAM_BIEN_H\n ...\n #endif (hoặc #pragma once)\n";
}
g++ -std=c++17 15c-tien-xu-ly.cpp -o app && ./app== #define hằng ==
LED_PIN = 13
== Macro hàm và bẫy thiếu ngoặc ==
BINH_PHUONG_SAI(2 + 3) = 11 (thành 2 + 3 * 2 + 3!)
BINH_PHUONG(2 + 3) = 25
== Biên dịch có điều kiện ==
Board: ESP32, ADC_MAX = 4095
[DEBUG D:\BKSTAR\Portfolio\hocj2me.github.io\_src\cpp-co-ban\code\15c-tien-xu-ly.cpp:40] khởi động xong
== # và ## ==
dem1 = 5
== Macro có sẵn ==
__cplusplus = 201703 (201703 = C++17)
__func__ = main
Trình biên dịch: GCC 16
Include guard trong file .h:
#ifndef CAM_BIEN_H
#define CAM_BIEN_H
...
#endif (hoặc #pragma once)Giải thích
- Macro thiếu ngoặc:
BINH_PHUONG_SAI(2 + 3)thành2 + 3 * 2 + 3 = 11. - Biên dịch có điều kiện cho phép một mã nguồn hỗ trợ nhiều board; khi
DEBUG 0, macroLOGbiến mất hoàn toàn. - Trong C++ hiện đại, ưu tiên
constexpr,inline, template thay cho macro khi có thể.
4Xử lý tín hiệu (signal)
Tín hiệu là cách hệ điều hành báo sự kiện bất đồng bộ cho chương trình (Ctrl+C, lỗi bộ nhớ…). Hàm xử lý (handler) chạy “chen ngang” chương trình chính — giống hệt hàm phục vụ ngắt (ISR) trên vi điều khiển: phải ngắn, chỉ đặt cờ, việc nặng để vòng lặp chính làm.
// Xử lý tín hiệu (signal): hệ điều hành báo sự kiện bất đồng bộ cho chương trình
// Ý tưởng rất giống NGẮT (interrupt) trên vi điều khiển: handler phải ngắn, chỉ đặt cờ.
#include <csignal>
#include <iostream>
volatile std::sig_atomic_t coDung = 0; // kiểu an toàn để ghi trong signal handler
volatile std::sig_atomic_t soLanNhan = 0;
void xuLyTinHieu(int maTinHieu) { // signal handler: KHÔNG in, KHÔNG cấp phát ở đây
soLanNhan++;
if (maTinHieu == SIGINT) coDung = 1;
}
int main() {
std::signal(SIGINT, xuLyTinHieu); // đăng ký: khi nhấn Ctrl+C -> gọi xuLyTinHieu
std::signal(SIGTERM, xuLyTinHieu);
std::cout << "== Vòng lặp chính chạy tới khi nhận SIGINT ==\n";
for (int vong = 1; !coDung; vong++) {
std::cout << " vòng " << vong << ": đọc cảm biến, cập nhật màn hình\n";
if (vong == 3) {
std::cout << " (giả lập người dùng nhấn Ctrl+C)\n";
std::raise(SIGINT); // tự gửi tín hiệu cho chính chương trình
}
}
std::cout << " Nhận tín hiệu " << soLanNhan << " lần -> lưu dữ liệu, đóng file, thoát an toàn\n";
std::cout << "\n== Một số tín hiệu chuẩn ==\n";
std::cout << " SIGINT = " << SIGINT << " ngắt từ bàn phím (Ctrl+C)\n";
std::cout << " SIGTERM = " << SIGTERM << " yêu cầu kết thúc\n";
std::cout << " SIGSEGV = " << SIGSEGV << " truy cập bộ nhớ sai (con trỏ hỏng)\n";
std::cout << " SIGFPE = " << SIGFPE << " lỗi số học (chia nguyên cho 0)\n";
std::cout << " SIGABRT = " << SIGABRT << " abort()\n";
std::signal(SIGINT, SIG_DFL); // trả về xử lý mặc định
return 0;
}
g++ -std=c++17 15d-tin-hieu.cpp -o app && ./app== Vòng lặp chính chạy tới khi nhận SIGINT == vòng 1: đọc cảm biến, cập nhật màn hình vòng 2: đọc cảm biến, cập nhật màn hình vòng 3: đọc cảm biến, cập nhật màn hình (giả lập người dùng nhấn Ctrl+C) Nhận tín hiệu 1 lần -> lưu dữ liệu, đóng file, thoát an toàn == Một số tín hiệu chuẩn == SIGINT = 2 ngắt từ bàn phím (Ctrl+C) SIGTERM = 15 yêu cầu kết thúc SIGSEGV = 11 truy cập bộ nhớ sai (con trỏ hỏng) SIGFPE = 8 lỗi số học (chia nguyên cho 0) SIGABRT = 22 abort()
Giải thích
- Biến dùng chung với handler có kiểu
volatile std::sig_atomic_t. std::raise(SIGINT)tự gửi tín hiệu — dùng ở đây để ví dụ chạy tự động.- Giá trị số của tín hiệu phụ thuộc hệ điều hành (ví dụ SIGABRT trên Windows là 22, Linux là 6).
🔧Áp dụng trong lập trình nhúng
- Arduino định nghĩa sẵn macro theo board:
#if defined(ESP32),#ifdef ARDUINO_AVR_UNO. - PlatformIO cho phép đặt macro trong
build_flags = -DDEBUG=1mà không sửa code. - Template +
constexprtạo ra code nhỏ và nhanh như C viết tay — nhiều thư viện nhúng hiện đại (ETL) dựa vào đây.
⚠Lỗi thường gặp
- Macro có tác dụng phụ:
BINH_PHUONG(i++)tăng i hai lần. - Quên include guard → lỗi định nghĩa trùng khi include file .h hai lần.
- Làm việc nặng (in, cấp phát) trong signal handler / ISR.
✎Bài tập về nhà
🧵 Đa luồng, thư viện STL, thư viện chuẩn và lập trình web
Chạy nhiều việc song song an toàn, dùng thành thạo container và thuật toán STL, biết C++ làm web thế nào.
- Tạo luồng, nhận biết data race, dùng mutex / atomic
- Mẫu producer–consumer với condition_variable
- Dùng vector, map, set, queue, stack, priority_queue và
<algorithm> - Biết
optional,variant,tuple,string_view,bitset
1Đa luồng
Luồng (thread) là một dòng thực thi chạy song song với các luồng khác trong cùng chương trình và dùng chung bộ nhớ. Dùng chung tiện nhưng nguy hiểm: hai luồng cùng ghi một biến gây data race.
| Công cụ | Dùng để |
|---|---|
std::thread, join() | tạo luồng, chờ luồng kết thúc |
std::mutex + std::lock_guard | đảm bảo mỗi lúc chỉ một luồng vào vùng găng |
std::atomic<T> | thao tác đơn giản (tăng, gán) không thể bị chen ngang |
std::condition_variable | cho luồng ngủ tới khi có dữ liệu — không tốn CPU chờ |
// Đa luồng (multithread): std::thread, data race, mutex, atomic, producer–consumer với condition_variable
// Cùng các khái niệm này sẽ gặp lại trong khóa FreeRTOS (task, mutex, queue).
#include <atomic>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
int demKhongAnToan = 0;
int demCoMutex = 0;
std::atomic<int> demAtomic{0};
std::mutex khoa;
void tangKhongAnToan() { for (int i = 0; i < 100000; i++) demKhongAnToan++; } // data race!
void tangCoMutex() { for (int i = 0; i < 100000; i++) { std::lock_guard<std::mutex> lk(khoa); demCoMutex++; } }
void tangAtomic() { for (int i = 0; i < 100000; i++) demAtomic++; }
int main() {
std::cout << "== Tạo và join luồng ==\n";
std::thread t([] { std::cout << " xin chào từ luồng phụ\n"; });
t.join(); // chờ luồng phụ kết thúc
std::cout << " luồng chính tiếp tục\n";
std::cout << "\n== 4 luồng cùng tăng một biến 100 000 lần (kỳ vọng 400 000) ==\n";
auto chay4 = [](void (*f)()) {
std::vector<std::thread> ds;
for (int i = 0; i < 4; i++) ds.emplace_back(f);
for (auto& th : ds) th.join();
};
chay4(tangKhongAnToan);
chay4(tangCoMutex);
chay4(tangAtomic);
std::cout << " không bảo vệ : " << (demKhongAnToan == 400000 ? "400000 (may mắn)" : "SAI – bị mất lượt cộng") << "\n";
std::cout << " có mutex : " << demCoMutex << "\n";
std::cout << " std::atomic : " << demAtomic << "\n";
std::cout << "\n== Producer–consumer: luồng đọc cảm biến -> hàng đợi -> luồng xử lý ==\n";
std::queue<int> hangDoi;
std::mutex m;
std::condition_variable cv;
bool xong = false;
std::thread sanXuat([&] {
for (int i = 1; i <= 5; i++) {
{
std::lock_guard<std::mutex> lk(m);
hangDoi.push(500 + i * 10);
}
cv.notify_one(); // đánh thức consumer
}
{ std::lock_guard<std::mutex> lk(m); xong = true; }
cv.notify_one();
});
std::thread tieuThu([&] {
while (true) {
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [&] { return !hangDoi.empty() || xong; }); // ngủ tới khi có dữ liệu
while (!hangDoi.empty()) {
std::cout << " xử lý mẫu " << hangDoi.front() << "\n";
hangDoi.pop();
}
if (xong) break;
}
});
sanXuat.join();
tieuThu.join();
std::cout << " Phần cứng có " << std::thread::hardware_concurrency() << " luồng; ESP32 có 2 nhân\n";
}
g++ -std=c++17 16a-da-luong.cpp -o app && ./app== Tạo và join luồng == xin chào từ luồng phụ luồng chính tiếp tục == 4 luồng cùng tăng một biến 100 000 lần (kỳ vọng 400 000) == không bảo vệ : SAI – bị mất lượt cộng có mutex : 400000 std::atomic : 400000 == Producer–consumer: luồng đọc cảm biến -> hàng đợi -> luồng xử lý == xử lý mẫu 510 xử lý mẫu 520 xử lý mẫu 530 xử lý mẫu 540 xử lý mẫu 550 Phần cứng có 8 luồng; ESP32 có 2 nhân
Giải thích
- Kết quả của bản “không bảo vệ” thay đổi theo từng lần chạy — đặc trưng của lỗi đa luồng, rất khó tái hiện.
lock_guardkhoá trong constructor, mở trong destructor (RAII) — không thể quên mở khoá.- Producer–consumer: một luồng đọc cảm biến đẩy vào hàng đợi, một luồng xử lý lấy ra — đúng mô hình task + queue của FreeRTOS.
2Thư viện STL
STL (Standard Template Library) gồm container (chứa dữ liệu), iterator (duyệt) và thuật toán (sort, find, count_if, accumulate…). Thuật toán làm việc qua iterator nên dùng được với mọi container.
// Thư viện STL: container (vector, map, set, queue, stack), iterator, thuật toán, lambda
#include <algorithm>
#include <iostream>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <unordered_map>
#include <vector>
int main() {
std::cout << "== vector: mảng động ==\n";
std::vector<int> mau = {512, 498, 530, 1023, 505, 487};
mau.push_back(520);
std::cout << " size = " << mau.size() << ", phần tử cuối = " << mau.back() << "\n";
mau.erase(std::remove(mau.begin(), mau.end(), 1023), mau.end()); // xoá giá trị nhiễu
std::cout << " sau khi bỏ 1023: ";
for (int v : mau) std::cout << v << " ";
std::cout << "\n";
std::cout << "\n== <algorithm> & <numeric> ==\n";
std::vector<int> sapXep = mau;
std::sort(sapXep.begin(), sapXep.end());
std::cout << " sort: ";
for (int v : sapXep) std::cout << v << " ";
std::cout << "\n min = " << *std::min_element(mau.begin(), mau.end())
<< ", max = " << *std::max_element(mau.begin(), mau.end())
<< ", tổng = " << std::accumulate(mau.begin(), mau.end(), 0)
<< ", trung vị = " << sapXep[sapXep.size() / 2] << "\n";
int tren500 = std::count_if(mau.begin(), mau.end(), [](int v) { return v > 500; });
std::cout << " số mẫu > 500: " << tren500 << "\n";
auto it = std::find(mau.begin(), mau.end(), 530);
std::cout << " tìm 530 ở vị trí " << (it - mau.begin()) << "\n";
std::sort(sapXep.begin(), sapXep.end(), [](int a, int b) { return a > b; });
std::cout << " sắp giảm dần bằng lambda: " << sapXep.front() << " ... " << sapXep.back() << "\n";
std::cout << "\n== map: từ điển có thứ tự (khoá -> giá trị) ==\n";
std::map<std::string, int> chanThietBi = {{"led", 2}, {"coi", 4}, {"servo", 18}};
chanThietBi["quat"] = 23;
for (const auto& [ten, chan] : chanThietBi) std::cout << " " << ten << " -> chân " << chan << "\n";
std::cout << " có 'relay'? " << std::boolalpha << (chanThietBi.count("relay") > 0) << "\n";
std::cout << "\n== unordered_map: đếm tần suất lệnh ==\n";
std::unordered_map<char, int> dem;
for (char c : std::string("FFLFRBFFS")) dem[c]++;
std::cout << " lệnh 'F' xuất hiện " << dem['F'] << " lần\n";
std::cout << "\n== set: tập hợp không trùng, tự sắp xếp ==\n";
std::set<int> diaChiI2C = {0x76, 0x3C, 0x68, 0x3C};
std::cout << " thiết bị I2C:";
for (int a : diaChiI2C) std::cout << " 0x" << std::hex << a << std::dec;
std::cout << " (" << diaChiI2C.size() << " thiết bị)\n";
std::cout << "\n== queue (FIFO) và stack (LIFO) ==\n";
std::queue<std::string> lenh;
lenh.push("tiến"); lenh.push("rẽ trái"); lenh.push("dừng");
std::cout << " queue lấy ra: ";
while (!lenh.empty()) { std::cout << lenh.front() << " | "; lenh.pop(); }
std::stack<std::string> lichSu;
lichSu.push("mở menu"); lichSu.push("chọn WiFi"); lichSu.push("nhập mật khẩu");
std::cout << "\n stack (nút Back): ";
while (!lichSu.empty()) { std::cout << lichSu.top() << " | "; lichSu.pop(); }
std::cout << "\n";
std::cout << "\n== priority_queue: lấy việc ưu tiên cao nhất trước ==\n";
std::priority_queue<std::pair<int, std::string>> viec;
viec.push({1, "cập nhật màn hình"});
viec.push({9, "CẢNH BÁO CHÁY"});
viec.push({5, "gửi dữ liệu MQTT"});
while (!viec.empty()) { std::cout << " [" << viec.top().first << "] " << viec.top().second << "\n"; viec.pop(); }
}
g++ -std=c++17 16b-stl.cpp -o app && ./app== vector: mảng động == size = 7, phần tử cuối = 520 sau khi bỏ 1023: 512 498 530 505 487 520 == <algorithm> & <numeric> == sort: 487 498 505 512 520 530 min = 487, max = 530, tổng = 3052, trung vị = 512 số mẫu > 500: 4 tìm 530 ở vị trí 2 sắp giảm dần bằng lambda: 530 ... 487 == map: từ điển có thứ tự (khoá -> giá trị) == coi -> chân 4 led -> chân 2 quat -> chân 23 servo -> chân 18 có 'relay'? false == unordered_map: đếm tần suất lệnh == lệnh 'F' xuất hiện 5 lần == set: tập hợp không trùng, tự sắp xếp == thiết bị I2C: 0x3c 0x68 0x76 (3 thiết bị) == queue (FIFO) và stack (LIFO) == queue lấy ra: tiến | rẽ trái | dừng | stack (nút Back): nhập mật khẩu | chọn WiFi | mở menu | == priority_queue: lấy việc ưu tiên cao nhất trước == [9] CẢNH BÁO CHÁY [5] gửi dữ liệu MQTT [1] cập nhật màn hình
Giải thích
- Thành ngữ “erase–remove”:
v.erase(std::remove(...), v.end())xoá mọi phần tử bằng một giá trị. for (const auto& [ten, chan] : map)— structured binding duyệt cặp khoá/giá trị.priority_queueluôn lấy ra phần tử lớn nhất — dùng cho hàng việc theo mức ưu tiên.
3Thư viện chuẩn C++ và lập trình web
Ngoài STL, thư viện chuẩn còn có <optional>, <variant>, <tuple>, <string_view>, <bitset>, <chrono>, <filesystem>, <regex>… và toàn bộ thư viện C (<cstring>, <cstdlib>, <cmath>).
Lập trình web với C++: C++ thường làm phần máy chủ hiệu năng cao hoặc web server nhúng:
- ESP32: thư viện
WebServer/ESPAsyncWebServer— chip tự phát trang web điều khiển LED, xem cảm biến. - Máy tính:
cpp-httplib(một file header), Crow, Drogon. Ví dụ:svr.Get("/temp", [](auto&, auto& res){ res.set_content("28.5", "text/plain"); }); - WebAssembly: biên dịch C++ chạy trong trình duyệt (Emscripten).
// Một vòng thư viện chuẩn C++ hay dùng: <optional>, <variant>, <tuple>, <string_view>, <bitset>,
// <array>, <functional>, <cstring>/<cstdlib> (thư viện C) – và ghép lại thành bộ phân tích gói tin
#include <array>
#include <bitset>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <variant>
// string_view: "nhìn" vào chuỗi có sẵn, không sao chép – tiết kiệm RAM
std::string_view layTen(std::string_view lenh) { return lenh.substr(0, lenh.find(':')); }
// tuple / structured binding: trả về nhiều giá trị
std::tuple<float, float, bool> docDht() { return {28.6f, 64.0f, true}; }
// variant: một biến chứa MỘT trong nhiều kiểu (an toàn hơn union)
using GiaTri = std::variant<int, float, std::string>;
void inGiaTri(const GiaTri& g) {
if (auto p = std::get_if<int>(&g)) std::cout << " int: " << *p << "\n";
else if (auto p = std::get_if<float>(&g)) std::cout << " float: " << *p << "\n";
else std::cout << " string: " << std::get<std::string>(g) << "\n";
}
// optional: có thể "không có giá trị"
std::optional<int> timChan(std::string_view ten) {
if (ten == "led") return 2;
if (ten == "coi") return 4;
return std::nullopt;
}
int main() {
std::cout << "== string_view ==\n";
std::cout << " layTen(\"SERVO:90\") = " << layTen("SERVO:90") << "\n";
std::cout << "\n== tuple + structured binding ==\n";
auto [t, h, ok] = docDht();
std::cout << " T = " << t << ", H = " << h << ", hợp lệ = " << std::boolalpha << ok << "\n";
std::cout << "\n== variant ==\n";
for (GiaTri g : {GiaTri{42}, GiaTri{3.5f}, GiaTri{std::string("OK")}}) inGiaTri(g);
std::cout << "\n== optional ==\n";
for (auto ten : {"led", "relay"}) {
auto chan = timChan(ten);
std::cout << " " << ten << " -> " << (chan ? "chân " + std::to_string(*chan) : "không có") << "\n";
}
std::cout << " value_or: " << timChan("relay").value_or(-1) << "\n";
std::cout << "\n== bitset: thao tác cờ trạng thái ==\n";
std::bitset<8> co; // 8 cờ: bit0 WiFi, bit1 MQTT, bit2 SD...
co.set(0); co.set(2);
std::cout << " co = " << co << ", số cờ bật = " << co.count() << ", MQTT? " << co.test(1) << "\n";
std::cout << "\n== Thư viện C trong C++: <cstring>, <cstdlib> ==\n";
char goi[] = "T=28;H=64;P=1012";
for (char* tok = std::strtok(goi, ";"); tok; tok = std::strtok(nullptr, ";")) {
std::cout << " " << tok[0] << " = " << std::atoi(tok + 2) << "\n";
}
std::array<uint8_t, 4> a{}, b{};
a = {1, 2, 3, 4};
std::memcpy(b.data(), a.data(), a.size());
std::cout << " memcpy xong, memcmp = " << std::memcmp(a.data(), b.data(), 4) << " (0 = giống nhau)\n";
}
g++ -std=c++17 16c-thu-vien-chuan.cpp -o app && ./app== string_view ==
layTen("SERVO:90") = SERVO
== tuple + structured binding ==
T = 28.6, H = 64, hợp lệ = true
== variant ==
int: 42
float: 3.5
string: OK
== optional ==
led -> chân 2
relay -> không có
value_or: -1
== bitset: thao tác cờ trạng thái ==
co = 00000101, số cờ bật = 2, MQTT? false
== Thư viện C trong C++: <cstring>, <cstdlib> ==
T = 28
H = 64
P = 1012
memcpy xong, memcmp = 0 (0 = giống nhau)Giải thích
std::string_viewkhông sao chép chuỗi — tốt cho hàm phân tích lệnh.auto [t, h, ok] = docDht();nhận nhiều giá trị trả về cùng lúc.std::variantthay chounion: biết đang chứa kiểu nào, truy cập sai kiểu sẽ báo lỗi.
🔧Áp dụng trong lập trình nhúng
- ESP32 chạy FreeRTOS: mỗi “luồng” là một task,
std::mutextương ứng mutex của FreeRTOS, hàng đợi tương ứngxQueue— xem khóa FreeRTOS. - Trên ESP32,
std::thread,std::vector,std::mapđều dùng được (ESP-IDF hỗ trợ C++17); trên Arduino Uno thì không có STL. - Biến dùng chung giữa task và ngắt: dùng
std::atomichoặc tắt ngắt trong vùng găng.
⚠Lỗi thường gặp
- Quên
join()hoặcdetach()→ chương trình bị huỷ (std::terminate). - Khoá hai mutex theo thứ tự khác nhau ở hai luồng → deadlock.
- Giữ iterator sau khi
push_backlàm vector cấp phát lại.
✎Bài tập về nhà
🏆 12 bài tập C/C++ phổ biến có lời giải
Rèn tư duy thuật toán qua 12 bài kinh điển (thường gặp trong kiểm tra và phỏng vấn) — tự làm trước, rồi mở lời giải để đối chiếu.
- Tự giải được các bài toán cơ bản về số, chuỗi, mảng, bit
- Biết đánh giá thuật toán nhanh/chậm
- Tổng hợp kiến thức của cả khóa trong một chương trình
Mỗi bài có đề, gợi ý và lời giải ẩn (bấm “Xem lời giải”). Hãy tự viết trước ít nhất 15 phút cho mỗi bài.
1Tổng chữ số và đảo ngược số
Gợi ý: Dùng % 10 lấy chữ số cuối và / 10 bỏ chữ số cuối.
Xem lời giải
// Bài 1: Tính tổng các chữ số và đảo ngược một số nguyên
#include <iostream>
int tongChuSo(long n) {
if (n < 0) n = -n;
int tong = 0;
while (n > 0) {
tong += n % 10; // lấy chữ số cuối
n /= 10; // bỏ chữ số cuối
}
return tong;
}
long daoNguoc(long n) {
long kq = 0;
while (n != 0) {
kq = kq * 10 + n % 10;
n /= 10;
}
return kq;
}
int main() {
for (long n : {12345L, 9081L, 7L, 1000L}) {
std::cout << n << ": tổng chữ số = " << tongChuSo(n) << ", đảo ngược = " << daoNguoc(n) << "\n";
}
}
g++ -std=c++17 17-01-tong-chu-so.cpp -o app && ./app12345: tổng chữ số = 15, đảo ngược = 54321 9081: tổng chữ số = 18, đảo ngược = 1809 7: tổng chữ số = 7, đảo ngược = 7 1000: tổng chữ số = 1, đảo ngược = 1
2Số nguyên tố và sàng Eratosthenes
Gợi ý: Chỉ cần thử chia tới √n; sàng gạch bỏ bội số để liệt kê nhanh.
Xem lời giải
// Bài 2: Kiểm tra số nguyên tố và liệt kê số nguyên tố bằng sàng Eratosthenes
#include <iostream>
#include <vector>
bool laNguyenTo(int n) {
if (n < 2) return false;
for (int i = 2; i * i <= n; i++) // chỉ cần thử tới căn bậc hai của n
if (n % i == 0) return false;
return true;
}
std::vector<int> sang(int n) {
std::vector<bool> laHop(n + 1, false);
std::vector<int> kq;
for (int i = 2; i <= n; i++) {
if (laHop[i]) continue;
kq.push_back(i);
for (int j = i * i; j <= n; j += i) laHop[j] = true; // gạch bỏ bội số
}
return kq;
}
int main() {
for (int n : {1, 2, 17, 21, 97}) std::cout << n << (laNguyenTo(n) ? " là" : " không là") << " số nguyên tố\n";
std::cout << "Các số nguyên tố <= 50: ";
for (int p : sang(50)) std::cout << p << " ";
std::cout << "\n";
}
g++ -std=c++17 17-02-so-nguyen-to.cpp -o app && ./app1 không là số nguyên tố 2 là số nguyên tố 17 là số nguyên tố 21 không là số nguyên tố 97 là số nguyên tố Các số nguyên tố <= 50: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
3Dãy Fibonacci: đệ quy vs vòng lặp
Gợi ý: So sánh độ phức tạp: đệ quy O(2ⁿ), vòng lặp O(n).
Xem lời giải
// Bài 3: Dãy Fibonacci – đệ quy (chậm) và vòng lặp (nhanh)
#include <cstdint>
#include <iostream>
uint64_t fibDeQuy(int n) { return n < 2 ? n : fibDeQuy(n - 1) + fibDeQuy(n - 2); }
uint64_t fibLap(int n) {
uint64_t a = 0, b = 1;
for (int i = 0; i < n; i++) {
uint64_t c = a + b;
a = b;
b = c;
}
return a;
}
int main() {
std::cout << "15 số Fibonacci đầu: ";
for (int i = 0; i < 15; i++) std::cout << fibLap(i) << " ";
std::cout << "\nfibDeQuy(25) = " << fibDeQuy(25) << " (gọi hàm hơn 240 000 lần)\n";
std::cout << "fibLap(90) = " << fibLap(90) << " (chỉ 90 vòng lặp)\n";
}
g++ -std=c++17 17-03-fibonacci.cpp -o app && ./app15 số Fibonacci đầu: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 fibDeQuy(25) = 75025 (gọi hàm hơn 240 000 lần) fibLap(90) = 2880067194370816120 (chỉ 90 vòng lặp)
4Đảo chuỗi, chuỗi đối xứng, đếm nguyên âm
Gợi ý: Kỹ thuật hai con trỏ chạy từ hai đầu vào giữa.
Xem lời giải
// Bài 4: Đảo ngược chuỗi, kiểm tra chuỗi đối xứng (palindrome), đếm nguyên âm – dùng chuỗi kiểu C
#include <cctype>
#include <cstring>
#include <iostream>
void daoChuoi(char* s) {
int i = 0, j = std::strlen(s) - 1;
while (i < j) {
char t = s[i]; s[i] = s[j]; s[j] = t; // hoán đổi hai đầu, tiến vào giữa
i++; j--;
}
}
bool doiXung(const char* s) {
int i = 0, j = std::strlen(s) - 1;
while (i < j) {
while (i < j && !std::isalnum((unsigned char)s[i])) i++; // bỏ qua dấu cách, dấu câu
while (i < j && !std::isalnum((unsigned char)s[j])) j--;
if (std::tolower((unsigned char)s[i]) != std::tolower((unsigned char)s[j])) return false;
i++; j--;
}
return true;
}
int demNguyenAm(const char* s) {
int dem = 0;
for (; *s; s++) if (std::strchr("aeiouAEIOU", *s)) dem++;
return dem;
}
int main() {
char s[] = "Arduino";
daoChuoi(s);
std::cout << "Đảo \"Arduino\" -> \"" << s << "\"\n";
for (const char* t : {"radar", "Never odd or even", "ESP32"})
std::cout << "\"" << t << "\" đối xứng? " << (doiXung(t) ? "có" : "không") << "\n";
std::cout << "Số nguyên âm trong \"Embedded Programming\": " << demNguyenAm("Embedded Programming") << "\n";
}
g++ -std=c++17 17-04-chuoi.cpp -o app && ./appĐảo "Arduino" -> "oniudrA" "radar" đối xứng? có "Never odd or even" đối xứng? có "ESP32" đối xứng? không Số nguyên âm trong "Embedded Programming": 6
5Sắp xếp nổi bọt và chèn
Gợi ý: Đếm số phép so sánh để thấy thuật toán nào hiệu quả hơn.
Xem lời giải
// Bài 5: Sắp xếp mảng – nổi bọt (bubble sort) và chèn (insertion sort), đếm số phép so sánh
#include <iostream>
int bubbleSort(int a[], int n) {
int soSanh = 0;
for (int i = 0; i < n - 1; i++) {
bool coDoi = false;
for (int j = 0; j < n - 1 - i; j++) {
soSanh++;
if (a[j] > a[j + 1]) { int t = a[j]; a[j] = a[j + 1]; a[j + 1] = t; coDoi = true; }
}
if (!coDoi) break; // đã có thứ tự -> dừng sớm
}
return soSanh;
}
int insertionSort(int a[], int n) {
int soSanh = 0;
for (int i = 1; i < n; i++) {
int x = a[i], j = i - 1;
while (j >= 0 && (++soSanh, a[j] > x)) { a[j + 1] = a[j]; j--; }
a[j + 1] = x;
}
return soSanh;
}
void in(const char* ten, const int a[], int n) {
std::cout << ten;
for (int i = 0; i < n; i++) std::cout << a[i] << " ";
std::cout << "\n";
}
int main() {
int a[] = {64, 25, 12, 22, 11, 90, 3};
int b[] = {64, 25, 12, 22, 11, 90, 3};
in("Ban đầu : ", a, 7);
int s1 = bubbleSort(a, 7);
int s2 = insertionSort(b, 7);
in("Bubble sort : ", a, 7);
in("Insertion : ", b, 7);
std::cout << "Số phép so sánh: bubble = " << s1 << ", insertion = " << s2 << "\n";
}
g++ -std=c++17 17-05-sap-xep.cpp -o app && ./appBan đầu : 64 25 12 22 11 90 3 Bubble sort : 3 11 12 22 25 64 90 Insertion : 3 11 12 22 25 64 90 Số phép so sánh: bubble = 21, insertion = 17
6Tìm kiếm tuần tự và nhị phân
Gợi ý: Nhị phân chỉ cần ~log₂(1000) ≈ 10 bước trên mảng đã sắp xếp.
Xem lời giải
// Bài 6: Tìm kiếm tuần tự và tìm kiếm nhị phân (mảng đã sắp xếp)
#include <iostream>
int timTuanTu(const int a[], int n, int x, int& buoc) {
for (int i = 0; i < n; i++) { buoc++; if (a[i] == x) return i; }
return -1;
}
int timNhiPhan(const int a[], int n, int x, int& buoc) {
int trai = 0, phai = n - 1;
while (trai <= phai) {
buoc++;
int giua = trai + (phai - trai) / 2; // tránh tràn số so với (trai + phai) / 2
if (a[giua] == x) return giua;
if (a[giua] < x) trai = giua + 1;
else phai = giua - 1;
}
return -1;
}
int main() {
const int N = 1000;
static int a[N];
for (int i = 0; i < N; i++) a[i] = i * 3; // 0, 3, 6, ... 2997
for (int x : {2991, 1500, 1501}) {
int b1 = 0, b2 = 0;
int i1 = timTuanTu(a, N, x, b1);
int i2 = timNhiPhan(a, N, x, b2);
std::cout << "Tìm " << x << ": vị trí " << i1 << "/" << i2
<< " | tuần tự " << b1 << " bước, nhị phân " << b2 << " bước\n";
}
}
g++ -std=c++17 17-06-tim-kiem.cpp -o app && ./appTìm 2991: vị trí 997/997 | tuần tự 998 bước, nhị phân 10 bước Tìm 1500: vị trí 500/500 | tuần tự 501 bước, nhị phân 9 bước Tìm 1501: vị trí -1/-1 | tuần tự 1000 bước, nhị phân 10 bước
7UCLN, BCNN và rút gọn phân số
Gợi ý: Thuật toán Euclid: UCLN(a, b) = UCLN(b, a % b).
Xem lời giải
// Bài 7: Ước chung lớn nhất (thuật toán Euclid) và bội chung nhỏ nhất – rút gọn phân số
#include <iostream>
int ucln(int a, int b) {
while (b != 0) { int r = a % b; a = b; b = r; }
return a;
}
long bcnn(int a, int b) { return (long)a / ucln(a, b) * b; // chia trước để tránh tràn số
}
int main() {
std::cout << "UCLN(48, 18) = " << ucln(48, 18) << ", BCNN(48, 18) = " << bcnn(48, 18) << "\n";
std::cout << "UCLN(17, 5) = " << ucln(17, 5) << " (nguyên tố cùng nhau)\n";
int tu = 84, mau = 126;
int g = ucln(tu, mau);
std::cout << "Rút gọn " << tu << "/" << mau << " = " << tu / g << "/" << mau / g << "\n";
// Ứng dụng: hai đèn nháy chu kỳ 400ms và 600ms sẽ cùng sáng lại sau BCNN ms
std::cout << "Đèn 400ms và đèn 600ms cùng nháy lại sau " << bcnn(400, 600) << " ms\n";
}
g++ -std=c++17 17-07-ucln-bcnn.cpp -o app && ./appUCLN(48, 18) = 6, BCNN(48, 18) = 144 UCLN(17, 5) = 1 (nguyên tố cùng nhau) Rút gọn 84/126 = 2/3 Đèn 400ms và đèn 600ms cùng nháy lại sau 1200 ms
8Đếm bit 1, lũy thừa của 2, đảo bit
Gợi ý: Mẹo x & (x - 1) xoá bit 1 thấp nhất.
Xem lời giải
// Bài 8: Đếm số bit 1, kiểm tra lũy thừa của 2, đảo thứ tự bit – bài kinh điển của lập trình nhúng
#include <cstdint>
#include <iostream>
int demBit1(uint32_t x) {
int dem = 0;
while (x) { x &= x - 1; dem++; } // mẹo Kernighan: mỗi vòng xoá 1 bit 1 thấp nhất
return dem;
}
bool laLuyThua2(uint32_t x) { return x && !(x & (x - 1)); }
uint8_t daoBit(uint8_t b) {
uint8_t kq = 0;
for (int i = 0; i < 8; i++) {
kq = (kq << 1) | (b & 1);
b >>= 1;
}
return kq;
}
int main() {
for (uint32_t x : {0u, 7u, 255u, 0xF0F0u, 1024u}) {
std::cout << x << ": " << demBit1(x) << " bit 1, lũy thừa của 2? " << (laLuyThua2(x) ? "có" : "không") << "\n";
}
std::cout << "đảo bit 0b00000001 -> " << +daoBit(0b00000001) << " (0b10000000)\n";
std::cout << "đảo bit 0b11010000 -> " << +daoBit(0b11010000) << " (0b00001011)\n";
std::cout << "GCC có sẵn: __builtin_popcount(0xF0F0) = " << __builtin_popcount(0xF0F0) << "\n";
}
g++ -std=c++17 17-08-dem-bit.cpp -o app && ./app0: 0 bit 1, lũy thừa của 2? không 7: 3 bit 1, lũy thừa của 2? không 255: 8 bit 1, lũy thừa của 2? không 61680: 8 bit 1, lũy thừa của 2? không 1024: 1 bit 1, lũy thừa của 2? có đảo bit 0b00000001 -> 128 (0b10000000) đảo bit 0b11010000 -> 11 (0b00001011) GCC có sẵn: __builtin_popcount(0xF0F0) = 8
9Đổi cơ số 10 ↔ 2/8/16
Gợi ý: Chia liên tiếp cho cơ số, lấy phần dư theo thứ tự ngược.
Xem lời giải
// Bài 9: Đổi số thập phân sang nhị phân / thập lục phân và ngược lại (không dùng thư viện)
#include <iostream>
#include <string>
std::string sangCoSo(unsigned n, unsigned coSo) {
const char* kyTu = "0123456789ABCDEF";
if (n == 0) return "0";
std::string kq;
while (n > 0) {
kq = kyTu[n % coSo] + kq; // thêm chữ số vào đầu chuỗi
n /= coSo;
}
return kq;
}
unsigned tuChuoi(const std::string& s, unsigned coSo) {
unsigned kq = 0;
for (char c : s) {
unsigned d = (c >= '0' && c <= '9') ? c - '0' : (c & ~0x20) - 'A' + 10; // & ~0x20: chữ thường -> hoa
kq = kq * coSo + d;
}
return kq;
}
int main() {
for (unsigned n : {10u, 255u, 2026u}) {
std::cout << n << " = 0b" << sangCoSo(n, 2) << " = 0x" << sangCoSo(n, 16) << " = 0o" << sangCoSo(n, 8) << "\n";
}
std::cout << "\"1011\" (cơ số 2) = " << tuChuoi("1011", 2) << "\n";
std::cout << "\"7e9\" (cơ số 16) = " << tuChuoi("7e9", 16) << "\n";
}
g++ -std=c++17 17-09-doi-co-so.cpp -o app && ./app10 = 0b1010 = 0xA = 0o12 255 = 0b11111111 = 0xFF = 0o377 2026 = 0b11111101010 = 0x7EA = 0o3752 "1011" (cơ số 2) = 11 "7e9" (cơ số 16) = 2025
10Ma trận: nhân, chuyển vị, vết
Gợi ý: Phần tử C[i][j] = tổng A[i][k] × B[k][j].
Xem lời giải
// Bài 10: Ma trận – cộng, nhân, chuyển vị (ứng dụng: xoay toạ độ, xử lý ảnh nhỏ)
#include <iomanip>
#include <iostream>
const int N = 3;
void in(const char* ten, const int m[N][N]) {
std::cout << ten << ":\n";
for (int i = 0; i < N; i++) {
std::cout << " ";
for (int j = 0; j < N; j++) std::cout << std::setw(5) << m[i][j];
std::cout << "\n";
}
}
void nhan(const int a[N][N], const int b[N][N], int kq[N][N]) {
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++) {
kq[i][j] = 0;
for (int k = 0; k < N; k++) kq[i][j] += a[i][k] * b[k][j]; // hàng i của A nhân cột j của B
}
}
void chuyenVi(const int a[N][N], int kq[N][N]) {
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++) kq[j][i] = a[i][j];
}
int main() {
int A[N][N] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int B[N][N] = {{9, 8, 7}, {6, 5, 4}, {3, 2, 1}};
int C[N][N], T[N][N];
nhan(A, B, C);
chuyenVi(A, T);
in("A", A);
in("A x B", C);
in("Chuyển vị của A", T);
int vet = 0;
for (int i = 0; i < N; i++) vet += A[i][i];
std::cout << "Vết (tổng đường chéo) của A = " << vet << "\n";
}
g++ -std=c++17 17-10-ma-tran.cpp -o app && ./appA:
1 2 3
4 5 6
7 8 9
A x B:
30 24 18
84 69 54
138 114 90
Chuyển vị của A:
1 4 7
2 5 8
3 6 9
Vết (tổng đường chéo) của A = 1511Checksum XOR và CRC-8
Gợi ý: Kiểm tra lỗi gói tin — kết quả khớp đúng ví dụ trong datasheet cảm biến SHT3x.
Xem lời giải
// Bài 11: Checksum và CRC-8 – kiểm tra lỗi khi truyền dữ liệu qua UART/I2C/LoRa
#include <cstdint>
#include <cstdio>
uint8_t checksumXor(const uint8_t* d, size_t n) {
uint8_t c = 0;
for (size_t i = 0; i < n; i++) c ^= d[i];
return c;
}
// CRC-8 đa thức 0x31 (dùng trong cảm biến Sensirion SHT3x, khởi tạo 0xFF)
uint8_t crc8(const uint8_t* d, size_t n) {
uint8_t crc = 0xFF;
for (size_t i = 0; i < n; i++) {
crc ^= d[i];
for (int b = 0; b < 8; b++) crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : (crc << 1);
}
return crc;
}
int main() {
uint8_t goi[] = {0xBE, 0xEF};
std::printf("Dữ liệu BE EF: XOR = 0x%02X, CRC-8 = 0x%02X (datasheet SHT3x ghi 0x92)\n",
checksumXor(goi, 2), crc8(goi, 2));
uint8_t gui[] = {0x01, 0x1D, 0x01, 0x41};
uint8_t crcGui = crc8(gui, 4);
uint8_t nhan[] = {0x01, 0x1D, 0x03, 0x41}; // 1 bit bị nhiễu trên đường truyền
std::printf("Gói gửi CRC = 0x%02X, gói nhận CRC = 0x%02X -> %s\n", crcGui, crc8(nhan, 4),
crcGui == crc8(nhan, 4) ? "OK" : "PHÁT HIỆN LỖI, yêu cầu gửi lại");
}
g++ -std=c++17 17-11-crc-checksum.cpp -o app && ./appDữ liệu BE EF: XOR = 0x51, CRC-8 = 0x92 (datasheet SHT3x ghi 0x92) Gói gửi CRC = 0x2D, gói nhận CRC = 0xF4 -> PHÁT HIỆN LỖI, yêu cầu gửi lại
12Quản lý danh sách học sinh
Gợi ý: Bài tổng hợp: struct, vector, sort với lambda, find_if, thống kê.
Xem lời giải
// Bài 12: Chương trình quản lý danh sách học sinh – tổng hợp struct, mảng, hàm, sắp xếp, tìm kiếm, thống kê
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
struct HocSinh {
std::string ten;
std::string lop;
float diem;
};
std::string xepLoai(float d) {
if (d >= 8) return "Giỏi";
if (d >= 6.5) return "Khá";
if (d >= 5) return "Trung bình";
return "Yếu";
}
void inDanhSach(const std::vector<HocSinh>& ds) {
for (size_t i = 0; i < ds.size(); i++)
std::cout << " " << i + 1 << ". " << std::left << std::setw(10) << ds[i].ten << std::setw(6) << ds[i].lop
<< std::right << std::fixed << std::setprecision(1) << std::setw(5) << ds[i].diem
<< " " << xepLoai(ds[i].diem) << "\n";
}
int main() {
std::vector<HocSinh> ds = {
{"An", "10A1", 8.5f}, {"Binh", "10A2", 6.0f}, {"Chi", "10A1", 9.2f},
{"Dung", "10A2", 4.5f}, {"Giang", "10A1", 7.1f},
};
std::cout << "Danh sách ban đầu:\n";
inDanhSach(ds);
std::sort(ds.begin(), ds.end(), [](const HocSinh& a, const HocSinh& b) { return a.diem > b.diem; });
std::cout << "\nSắp xếp theo điểm giảm dần:\n";
inDanhSach(ds);
float tong = 0;
int soGioi = 0;
for (const auto& h : ds) { tong += h.diem; if (h.diem >= 8) soGioi++; }
std::cout << "\nĐiểm trung bình: " << tong / ds.size() << ", số học sinh giỏi: " << soGioi << "\n";
auto it = std::find_if(ds.begin(), ds.end(), [](const HocSinh& h) { return h.ten == "Giang"; });
if (it != ds.end()) std::cout << "Tìm thấy Giang: lớp " << it->lop << ", điểm " << it->diem << "\n";
std::cout << "Học sinh lớp 10A1:";
for (const auto& h : ds) if (h.lop == "10A1") std::cout << " " << h.ten;
std::cout << "\n";
}
g++ -std=c++17 17-12-quan-ly-sinh-vien.cpp -o app && ./appDanh sách ban đầu: 1. An 10A1 8.5 Giỏi 2. Binh 10A2 6.0 Trung bình 3. Chi 10A1 9.2 Giỏi 4. Dung 10A2 4.5 Yếu 5. Giang 10A1 7.1 Khá Sắp xếp theo điểm giảm dần: 1. Chi 10A1 9.2 Giỏi 2. An 10A1 8.5 Giỏi 3. Giang 10A1 7.1 Khá 4. Binh 10A2 6.0 Trung bình 5. Dung 10A2 4.5 Yếu Điểm trung bình: 7.1, số học sinh giỏi: 2 Tìm thấy Giang: lớp 10A1, điểm 7.1 Học sinh lớp 10A1: Chi An Giang
✓Tóm tắt
- Biến – kiểu – toán tử – rẽ nhánh – vòng lặp – hàm là nền tảng của mọi bài.
- Con trỏ, mảng, chuỗi là chìa khoá khi làm việc với phần cứng và giao thức.
- OOP giúp tổ chức firmware lớn; STL và template giúp viết ít mà đúng.
- Bước tiếp theo: khóa FreeRTOS (đa nhiệm thời gian thực) và Design Pattern (thiết kế phần mềm).