Giao diện
📦 Vector First: Container Mặc Định Của Bạn Module 1
Quên raw array đi. Trong C++ hiện đại,
std::vector là container bạn nên nghĩ đến đầu tiên. Bài này giải thích tại sao — và cách sử dụng nó đúng cách để tránh những lỗi tinh vi nhất. 🎯 Mục tiêu
Sau bài này, bạn sẽ:
- Hiểu tại sao
std::vectorlà lựa chọn mặc định thay vì raw array - Nắm vững các thao tác cơ bản: khởi tạo, truy cập, thêm/xóa phần tử
- Phân biệt capacity vs size — và tại sao điều này quan trọng
- Biết khi nào dùng
push_backvsemplace_back - Nhận diện iterator invalidation — nguồn gốc của nhiều bug production
🚫 Tại Sao Không Dùng Raw Array?
Trước khi nói về vector, hãy nhìn lại "người bạn cũ" — C-style array — và hiểu tại sao nó là nguồn gốc của vô số bug.
Vấn đề #1: Không biết kích thước của chính mình
cpp
#include <iostream>
void printArray(int arr[], int size) {
// ❌ arr ở đây chỉ là pointer — KHÔNG biết size!
// sizeof(arr) = 8 bytes (kích thước pointer), KHÔNG phải kích thước mảng
for (int i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}
}
int main() {
int scores[5] = {90, 85, 78, 92, 88};
// sizeof(scores) = 20 (5 × 4 bytes) — đúng
std::cout << "Trong main: " << sizeof(scores) << " bytes\n";
// Nhưng khi truyền vào function → mất size info!
printArray(scores, 5); // Phải truyền size riêng — dễ sai!
return 0;
}🔥 Array Decay
Khi truyền C-array vào function, nó decay thành pointer. Mọi thông tin về kích thước bị mất hoàn toàn. Đây là nguyên nhân hàng đầu của buffer overflow.
Vấn đề #2: Không bounds checking
cpp
#include <iostream>
int main() {
int arr[3] = {10, 20, 30};
// ❌ Truy cập ngoài phạm vi — KHÔNG có cảnh báo!
arr[5] = 999; // Ghi đè vùng nhớ khác → silent corruption
arr[-1] = 666; // Ghi lùi vào stack → có thể crash
// Chương trình có thể chạy "bình thường"...
// ...cho đến khi nó crash lúc 3 giờ sáng trong production 💀
return 0;
}Vấn đề #3: Kích thước cố định
cpp
int main() {
int arr[100]; // Cấp phát 100 — nếu cần 101 thì sao?
// Phải tạo mảng mới, copy, delete cũ...
// Hoặc cấp phát dư thừa 10,000 "cho chắc" → lãng phí bộ nhớ
return 0;
}So sánh cùng bài toán: Đọc N số và in ngược
cpp
// ❌ C-style: Phức tạp, không an toàn
#include <iostream>
int main() {
int n;
std::cin >> n;
int* arr = new int[n]; // Manual allocation
for (int i = 0; i < n; ++i)
std::cin >> arr[i];
for (int i = n - 1; i >= 0; --i)
std::cout << arr[i] << " ";
delete[] arr; // Phải nhớ delete!
return 0;
}cpp
// ✅ Modern C++: Sạch, an toàn, tự động
#include <iostream>
#include <vector>
#include <ranges>
int main() {
int n;
std::cin >> n;
std::vector<int> nums(n);
for (auto& x : nums) std::cin >> x;
for (auto x : nums | std::views::reverse)
std::cout << x << " ";
// Không cần delete — vector tự dọn dẹp (RAII)!
return 0;
}📌 Quy Tắc Vàng
"Default to std::vector." — Bjarne Stroustrup (cha đẻ C++)
Đừng bao giờ bắt đầu bằng raw array. Bắt đầu bằng std::vector. Chỉ đổi sang container khác khi profiling chứng minh bạn cần.
📘 std::vector — Kiến Thức Nền Tảng
Khai báo & Khởi tạo
cpp
#include <vector>
#include <string>
int main() {
// === Khai báo rỗng ===
std::vector<int> empty; // size = 0, capacity = 0
// === Initializer list (C++11) ===
std::vector<int> primes = {2, 3, 5, 7, 11};
// === N phần tử với giá trị mặc định ===
std::vector<int> zeros(10); // 10 số 0
std::vector<int> fives(10, 5); // 10 số 5
// === Copy từ vector khác ===
std::vector<int> copy = primes; // Deep copy
// === Từ iterator range ===
std::vector<int> partial(primes.begin(), primes.begin() + 3);
// partial = {2, 3, 5}
// === Vector of strings ===
std::vector<std::string> names = {"An", "Bình", "Cường"};
// === C++17: Class Template Argument Deduction (CTAD) ===
std::vector nums = {1, 2, 3, 4, 5}; // Tự suy ra vector<int>
return 0;
}Truy cập phần tử
cpp
#include <iostream>
#include <vector>
int main() {
std::vector<int> scores = {90, 85, 78, 92, 88};
// === operator[] — KHÔNG bounds check ===
std::cout << scores[0] << "\n"; // 90
// scores[99] → Undefined Behavior! Không crash, không warning.
// === .at() — CÓ bounds check ===
std::cout << scores.at(0) << "\n"; // 90
// scores.at(99) → throw std::out_of_range! An toàn.
// === .front() và .back() ===
std::cout << scores.front() << "\n"; // 90 (phần tử đầu)
std::cout << scores.back() << "\n"; // 88 (phần tử cuối)
// === .data() — Raw pointer đến mảng nội bộ ===
int* raw = scores.data();
std::cout << raw[2] << "\n"; // 78
return 0;
}🔥 Gotcha: [] vs .at()
operator[] KHÔNG kiểm tra giới hạn. Nếu index sai, bạn sẽ đọc/ghi vào vùng nhớ lạ mà không có bất kỳ lỗi nào — silent corruption. Trong debug/development, hãy dùng .at(). Trong production hot path, dùng [] sau khi đã validate index.
Thêm & Xóa phần tử
cpp
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums;
// === Thêm vào cuối ===
nums.push_back(10); // nums = {10}
nums.push_back(20); // nums = {10, 20}
nums.push_back(30); // nums = {10, 20, 30}
// === Xóa phần tử cuối ===
nums.pop_back(); // nums = {10, 20}
// === Chèn vào vị trí bất kỳ ===
nums.insert(nums.begin() + 1, 15); // nums = {10, 15, 20}
// === Xóa tại vị trí ===
nums.erase(nums.begin()); // nums = {15, 20}
// === Xóa theo giá trị (C++20 std::erase) ===
std::vector<int> vals = {1, 2, 3, 2, 4, 2};
std::erase(vals, 2); // vals = {1, 3, 4} — xóa TẤT CẢ số 2
// === Xóa toàn bộ ===
nums.clear(); // nums = {}, size = 0
return 0;
}Duyệt vector — Modern Style
cpp
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> scores = {90, 85, 78, 92, 88};
// ✅ Range-based for (C++11) — Cách phổ biến nhất
for (int score : scores) {
std::cout << score << " ";
}
std::cout << "\n";
// ✅ Range-based for với reference (nếu cần modify)
for (int& score : scores) {
score += 5; // Cộng thêm 5 điểm cho mỗi học sinh
}
// ✅ Range-based for với auto (tiện lợi)
for (const auto& score : scores) {
std::cout << score << " ";
}
std::cout << "\n";
// ✅ std::for_each với lambda
std::for_each(scores.begin(), scores.end(), [](int s) {
std::cout << s << " ";
});
std::cout << "\n";
// === Kiểm tra rỗng ===
if (scores.empty()) {
std::cout << "Không có điểm nào!\n";
}
std::cout << "Số lượng: " << scores.size() << "\n";
return 0;
}📐 Capacity vs Size — Bí Mật Bên Trong Vector
Đây là phần quan trọng nhất để hiểu hiệu suất của vector. Nhiều lập trình viên không phân biệt được hai khái niệm này.
Visualization: Mô hình bộ nhớ
std::vector<int> v = {1, 2, 3};
Bộ nhớ heap (do vector quản lý):
┌───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ │ ← capacity = 4 (chỗ trống)
└───┴───┴───┴───┘
▲ ▲
│ │
size = 3 capacity = 4
● size = số phần tử THỰC SỰ có trong vector
● capacity = số phần tử vector CÓ THỂ chứa trước khi cần cấp phát lạiĐiều gì xảy ra khi push_back?
Bước 1: v.push_back(4) → size=4, capacity=4 (vừa đủ, OK!)
┌───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │
└───┴───┴───┴───┘
Bước 2: v.push_back(5) → REALLOCATION! 🔄
❌ Không đủ chỗ → vector phải:
1. Cấp phát vùng nhớ MỚI (capacity × 2 = 8)
2. Copy/move TẤT CẢ phần tử sang vùng mới
3. Giải phóng vùng nhớ cũ
Trước:
┌───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ ← vùng cũ (sẽ bị giải phóng)
└───┴───┴───┴───┘
Sau:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ │ │ │ ← vùng MỚI, capacity = 8
└───┴───┴───┴───┴───┴───┴───┴───┘
▲ ▲
size=5 capacity=8Theo dõi capacity thực tế
cpp
#include <iostream>
#include <vector>
int main() {
std::vector<int> v;
std::cout << "push_back | size | capacity | reallocated?\n";
std::cout << "----------+------+----------+-------------\n";
size_t prevCap = 0;
for (int i = 1; i <= 20; ++i) {
v.push_back(i);
bool reallocated = (v.capacity() != prevCap);
std::cout << " " << i
<< " | " << v.size()
<< " | " << v.capacity()
<< " | " << (reallocated ? "✅ YES" : " no") << "\n";
prevCap = v.capacity();
}
return 0;
}Output (GCC/libstdc++ — chiến lược nhân đôi):
push_back | size | capacity | reallocated?
----------+------+----------+-------------
1 | 1 | 1 | ✅ YES
2 | 2 | 2 | ✅ YES
3 | 3 | 4 | ✅ YES
4 | 4 | 4 | no
5 | 5 | 8 | ✅ YES
6 | 6 | 8 | no
...
9 | 9 | 16 | ✅ YES
...
17 | 17 | 32 | ✅ YES.reserve() — Pre-allocation để tránh reallocation
cpp
#include <vector>
#include <iostream>
#include <chrono>
int main() {
const int N = 1'000'000;
// ❌ Không reserve — nhiều lần reallocation
auto start1 = std::chrono::high_resolution_clock::now();
std::vector<int> slow;
for (int i = 0; i < N; ++i) slow.push_back(i);
auto end1 = std::chrono::high_resolution_clock::now();
// ✅ Có reserve — KHÔNG reallocation
auto start2 = std::chrono::high_resolution_clock::now();
std::vector<int> fast;
fast.reserve(N); // Cấp phát 1 lần duy nhất!
for (int i = 0; i < N; ++i) fast.push_back(i);
auto end2 = std::chrono::high_resolution_clock::now();
auto ms1 = std::chrono::duration_cast<std::chrono::microseconds>(end1 - start1);
auto ms2 = std::chrono::duration_cast<std::chrono::microseconds>(end2 - start2);
std::cout << "Không reserve: " << ms1.count() << " μs\n";
std::cout << "Có reserve: " << ms2.count() << " μs\n";
return 0;
}📌 Performance Note
.reserve() trước khi bulk insertion có thể tránh O(n) lần reallocation, mỗi lần copy O(n) phần tử. Trong hot loop, đây là sự khác biệt giữa 1ms và 100ms. Nếu bạn biết trước (hoặc ước lượng được) số phần tử, luôn reserve.
.shrink_to_fit() — Giải phóng bộ nhớ thừa
cpp
std::vector<int> v(1000); // size=1000, capacity=1000
v.clear(); // size=0, capacity vẫn = 1000!
v.shrink_to_fit(); // Gợi ý compiler giảm capacity về 0
// Lưu ý: đây là "hint", không phải guarantee⚡ push_back vs emplace_back
Hai hàm này đều thêm phần tử vào cuối vector, nhưng cơ chế khác nhau.
Với kiểu đơn giản — Không khác biệt
cpp
std::vector<int> nums;
nums.push_back(42); // Copy giá trị 42 vào vector
nums.emplace_back(42); // Construct trực tiếp tại chỗ → cùng kết quảVới kiểu phức tạp — emplace_back hiệu quả hơn
cpp
#include <vector>
#include <string>
#include <iostream>
struct Student {
std::string name;
int age;
double gpa;
Student(std::string n, int a, double g)
: name(std::move(n)), age(a), gpa(g) {
std::cout << " Constructor: " << name << "\n";
}
Student(const Student& other)
: name(other.name), age(other.age), gpa(other.gpa) {
std::cout << " Copy: " << name << "\n";
}
Student(Student&& other) noexcept
: name(std::move(other.name)), age(other.age), gpa(other.gpa) {
std::cout << " Move: " << name << "\n";
}
};
int main() {
std::vector<Student> students;
students.reserve(3); // Tránh reallocation để thấy rõ sự khác biệt
std::cout << "=== push_back (tạo rồi move vào) ===\n";
students.push_back(Student("An", 20, 3.5));
// → Constructor: An → Move: An (2 bước)
std::cout << "\n=== emplace_back (construct trực tiếp) ===\n";
students.emplace_back("Bình", 21, 3.8);
// → Constructor: Bình (1 bước — hiệu quả hơn!)
// C++17: emplace_back trả về reference
auto& newStudent = students.emplace_back("Cường", 19, 3.2);
std::cout << "\nVừa thêm: " << newStudent.name << "\n";
return 0;
}=== push_back (tạo rồi move vào) ===
Constructor: An
Move: An
=== emplace_back (construct trực tiếp) ===
Constructor: Bình
Constructor: Cường
Vừa thêm: Cường🎓 Khi nào dùng cái nào?
push_back: Khi bạn đã có object sẵn và muốn đẩy vào vectoremplace_back: Khi bạn muốn construct trực tiếp bên trong vector — truyền arguments thay vì object- Quy tắc đơn giản: Với
int,double,std::string— không khác biệt đáng kể. Với custom class có constructor phức tạp — ưu tiênemplace_back.
⚠️ Iterator Invalidation — Kẻ Giết Người Thầm Lặng
Đây là nguồn gốc của bug tinh vi nhất khi làm việc với vector.
Khi nào iterator bị vô hiệu?
Thao tác | Iterator bị invalidated?
─────────────────────────────────────────────────────
push_back (có realloc)| ❌ TẤT CẢ iterators, pointers, references
push_back (đủ chỗ) | ❌ Chỉ end() iterator
insert/emplace | ❌ Từ điểm chèn trở về sau
erase | ❌ Từ điểm xóa trở về sau
clear | ❌ TẤT CẢ
reserve | ❌ TẤT CẢ (nếu capacity tăng)
[], at, front, back | ✅ Không ảnh hưởngBug kinh điển: Lưu reference rồi push_back
cpp
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {10, 20, 30};
int& ref = v[0]; // ref trỏ đến phần tử đầu tiên
int* ptr = &v[1]; // ptr trỏ đến phần tử thứ hai
std::cout << "Trước: ref=" << ref << ", *ptr=" << *ptr << "\n";
// 💥 push_back CÓ THỂ trigger reallocation!
v.push_back(40);
v.push_back(50);
v.push_back(60);
// ☠️ ref và ptr giờ trỏ đến vùng nhớ ĐÃ BỊ GIẢI PHÓNG!
// Dòng dưới đây là UNDEFINED BEHAVIOR:
// std::cout << "Sau: ref=" << ref << ", *ptr=" << *ptr << "\n";
// ✅ Cách an toàn: truy cập lại qua vector
std::cout << "An toàn: v[0]=" << v[0] << ", v[1]=" << v[1] << "\n";
return 0;
}☠️ Production Anti-Pattern
Giữ reference/pointer đến phần tử vector trong khi vector có thể grow là một trong những bug nghiêm trọng nhất. Nó không crash ngay — nó gây silent data corruption có thể mất hàng ngày để debug. Iterator invalidation đã gây ra nhiều production outage thực tế.
Bug kinh điển: Xóa phần tử trong khi duyệt
cpp
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8};
// ❌ SAI: Xóa phần tử chẵn trong khi duyệt
for (auto it = nums.begin(); it != nums.end(); ++it) {
if (*it % 2 == 0) {
nums.erase(it); // 💥 it bị invalidated sau erase!
// ++it ở trên sẽ gây UB
}
}
// ✅ ĐÚNG: erase trả về iterator hợp lệ đến phần tử tiếp theo
std::vector<int> nums2 = {1, 2, 3, 4, 5, 6, 7, 8};
for (auto it = nums2.begin(); it != nums2.end(); ) {
if (*it % 2 == 0) {
it = nums2.erase(it); // erase trả về iterator tiếp theo
} else {
++it;
}
}
// nums2 = {1, 3, 5, 7}
// ✅ C++20: Cách đẹp nhất
std::vector<int> nums3 = {1, 2, 3, 4, 5, 6, 7, 8};
std::erase_if(nums3, [](int x) { return x % 2 == 0; });
// nums3 = {1, 3, 5, 7}
return 0;
}📊 Bảng So Sánh: Raw Array vs std::vector
| Tính năng | Raw Array int arr[N] | std::vector<int> |
|---|---|---|
| Bounds checking | ❌ Không — silent corruption | ✅ .at() throw exception |
| Dynamic size | ❌ Cố định lúc compile | ✅ Grow/shrink runtime |
| Biết size của mình | ❌ Decay thành pointer | ✅ .size() luôn chính xác |
| An toàn bộ nhớ | ❌ Manual delete, leak risk | ✅ RAII — tự giải phóng |
| Tương thích STL | ⚠️ Một phần (begin/end) | ✅ Hoàn toàn |
| Copy semantics | ❌ Phải copy thủ công | ✅ Deep copy tự động |
| Pass to function | ❌ Decay → mất size | ✅ Pass by ref, giữ size |
| Stack allocation | ✅ Nhanh cho mảng nhỏ | ⚠️ Metadata trên stack, data trên heap |
| Debugging | ❌ Khó — no info | ✅ IDE hỗ trợ tốt |
| Move semantics | ❌ Không | ✅ O(1) move |
🏢 Ví Dụ Thực Tế: Xử Lý Đơn Hàng E-Commerce
Một scenario production thực tế — quản lý danh sách sản phẩm trong đơn hàng:
cpp
#include <vector>
#include <string>
#include <algorithm>
#include <iostream>
#include <numeric>
#include <iomanip>
struct OrderItem {
std::string name;
double price;
int quantity;
double subtotal() const { return price * quantity; }
};
class Order {
std::vector<OrderItem> items_;
public:
// Thêm sản phẩm — dùng emplace_back (construct tại chỗ)
void addItem(std::string name, double price, int qty) {
items_.emplace_back(OrderItem{std::move(name), price, qty});
}
// Xóa sản phẩm hết hàng (quantity = 0)
void removeOutOfStock() {
std::erase_if(items_, [](const OrderItem& item) {
return item.quantity <= 0;
});
}
// Sắp xếp theo giá giảm dần
void sortByPriceDesc() {
std::sort(items_.begin(), items_.end(),
[](const OrderItem& a, const OrderItem& b) {
return a.price > b.price;
});
}
// Tính tổng đơn hàng
double total() const {
return std::accumulate(items_.begin(), items_.end(), 0.0,
[](double sum, const OrderItem& item) {
return sum + item.subtotal();
});
}
// Lọc sản phẩm theo khoảng giá
std::vector<OrderItem> filterByPrice(double minPrice, double maxPrice) const {
std::vector<OrderItem> result;
std::copy_if(items_.begin(), items_.end(), std::back_inserter(result),
[minPrice, maxPrice](const OrderItem& item) {
return item.price >= minPrice && item.price <= maxPrice;
});
return result; // Move semantics — hiệu quả!
}
void print() const {
std::cout << std::fixed << std::setprecision(0);
std::cout << "┌─────────────────────────────────────────┐\n";
std::cout << "│ ĐƠN HÀNG │\n";
std::cout << "├──────────────┬──────────┬────┬──────────┤\n";
std::cout << "│ Tên SP │ Đơn giá │ SL │ Thành tiền│\n";
std::cout << "├──────────────┼──────────┼────┼──────────┤\n";
for (const auto& item : items_) {
std::cout << "│ " << std::setw(12) << std::left << item.name
<< " │ " << std::setw(8) << std::right << item.price
<< " │ " << std::setw(2) << item.quantity
<< " │ " << std::setw(8) << item.subtotal() << " │\n";
}
std::cout << "├──────────────┴──────────┴────┼──────────┤\n";
std::cout << "│ TỔNG │ " << std::setw(8) << total() << " │\n";
std::cout << "└───────────────────────────────┴──────────┘\n";
}
};
int main() {
Order order;
order.addItem("Laptop", 25000000, 1);
order.addItem("Chuột", 500000, 2);
order.addItem("Bàn phím", 1200000, 1);
order.addItem("Tai nghe", 800000, 3);
order.addItem("Webcam", 350000, 0); // Hết hàng
order.removeOutOfStock(); // Xóa webcam
order.sortByPriceDesc(); // Sắp xếp giá giảm dần
order.print();
// Lọc sản phẩm 500k-2M
auto midRange = order.filterByPrice(500000, 2000000);
std::cout << "\nSản phẩm tầm trung (500k-2M):\n";
for (const auto& item : midRange) {
std::cout << " - " << item.name << ": " << item.price << "đ\n";
}
return 0;
}🎓 Nhận xét Production Code
Ví dụ trên minh họa cách vector kết hợp với STL algorithms (sort, accumulate, copy_if, erase_if) tạo ra code vừa sạch vừa hiệu quả. So sánh với cùng bài toán dùng raw array — bạn sẽ cần gấp 3 lần code và dễ bug gấp 10 lần.
🔄 Khi Nào KHÔNG Dùng Vector?
Vector là mặc định, nhưng không phải lúc nào cũng tốt nhất.
Tình huống │ Container thay thế
──────────────────────────────────┼──────────────────────────
Kích thước biết lúc compile │ std::array<T, N>
→ Stack allocation, zero overhead│ (nhanh hơn vector)
│
Chèn/xóa đầu mảng thường xuyên │ std::deque<T>
→ vector phải dịch toàn bộ O(n) │ (O(1) ở cả hai đầu)
│
Cần iterator ổn định sau insert │ std::list<T>
→ vector invalidate iterator │ (hiếm khi cần — benchmark trước!)
│
Tra cứu theo key │ std::unordered_map<K,V>
→ vector phải duyệt O(n) │ (O(1) average lookup)
│
Tập hợp unique, sắp xếp │ std::set<T>
→ vector cần sort + unique │ (tự động sắp xếp)cpp
#include <array>
#include <deque>
// ✅ std::array — khi biết trước size lúc compile
std::array<int, 12> months = {31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31};
// Ưu điểm: stack allocation, zero overhead, biết .size()
// ✅ std::deque — khi cần push_front hiệu quả
std::deque<int> dq;
dq.push_front(1); // O(1) — vector phải shift tất cả!
dq.push_back(2); // O(1) — giống vector📌 Quy Tắc Lựa Chọn Container
"Start with std::vector. Switch only when profiling proves you need something else."
90% thời gian, vector là đúng. Cache locality (các phần tử liên tiếp trong bộ nhớ) của vector thường đánh bại lợi thế lý thuyết O(1) của list/deque trong thực tế.
🧠 Bài Tập Nhanh: Dự Đoán Size & Capacity
🧠 Fast Exercise
Đoạn code sau — sau mỗi dòng, size và capacity là bao nhiêu?
Giả sử compiler dùng chiến lược nhân đôi capacity (GCC/libstdc++).
cpp
std::vector<int> v; // (1) size=?, cap=?
v.push_back(10); // (2) size=?, cap=?
v.push_back(20); // (3) size=?, cap=?
v.push_back(30); // (4) size=?, cap=?
v.reserve(10); // (5) size=?, cap=?
v.push_back(40); // (6) size=?, cap=?
v.push_back(50); // (7) size=?, cap=?
v.clear(); // (8) size=?, cap=?
v.shrink_to_fit(); // (9) size=?, cap=?💡 Đáp án
(1) size=0, cap=0 — Vừa khởi tạo, chưa cấp phát
(2) size=1, cap=1 — Lần đầu push → cấp phát 1
(3) size=2, cap=2 — Đầy → nhân đôi: 1 → 2
(4) size=3, cap=4 — Đầy → nhân đôi: 2 → 4
(5) size=3, cap=10 — reserve(10): cấp phát ít nhất 10
(6) size=4, cap=10 — Còn chỗ, không realloc
(7) size=5, cap=10 — Còn chỗ, không realloc
(8) size=0, cap=10 — clear() chỉ xóa phần tử, KHÔNG giảm capacity!
(9) size=0, cap=0 — shrink_to_fit() giải phóng bộ nhớ thừaĐiểm mấu chốt: clear() không giảm capacity. Nếu bạn cần giải phóng bộ nhớ thật sự, gọi shrink_to_fit() sau clear().
🐛 Spot the Bug: Vector Edition
🐛 Bug Hunt Challenge #1
Chương trình sau có bug gì?
cpp
#include <vector>
#include <iostream>
int main() {
std::vector<int> data = {1, 2, 3, 4, 5};
for (size_t i = 0; i <= data.size(); ++i) {
std::cout << data[i] << " ";
}
return 0;
}💡 Gợi ý
Để ý điều kiện vòng lặp: i <= data.size() hay i < data.size()?
🔍 Giải thích & Fix
Bug: Off-by-one! i <= data.size() khiến vòng lặp truy cập data[5] — ngoài phạm vi. Vì dùng operator[] nên không có bounds check → Undefined Behavior (silent corruption).
Fix:
cpp
// Cách 1: Sửa điều kiện
for (size_t i = 0; i < data.size(); ++i) { // < thay vì <=
std::cout << data[i] << " ";
}
// Cách 2 (recommended): Range-based for
for (int x : data) {
std::cout << x << " ";
}Bài học: Range-based for không bao giờ off-by-one. Hãy ưu tiên dùng nó.
🐛 Bug Hunt Challenge #2
Tại sao chương trình này crash ngẫu nhiên?
cpp
#include <vector>
#include <iostream>
void processLargeDataset() {
std::vector<int> data = {10, 20, 30, 40, 50};
// Lưu pointer đến phần tử đầu
int* first = &data[0];
// Thêm nhiều dữ liệu mới...
for (int i = 0; i < 1000; ++i) {
data.push_back(i);
}
// Sử dụng pointer đã lưu
std::cout << "First element: " << *first << "\n"; // 💥
}💡 Gợi ý
Sau 1000 lần push_back, vector đã realloc bao nhiêu lần? first còn hợp lệ không?
🔍 Giải thích & Fix
Bug: Iterator invalidation! Sau nhiều lần push_back, vector đã realloc nhiều lần — dữ liệu được di chuyển sang vùng nhớ mới. first vẫn trỏ đến vùng nhớ cũ đã bị giải phóng → dangling pointer → UB.
Fix 1 — Lưu index thay vì pointer:
cpp
size_t firstIdx = 0; // Index không bao giờ invalidate!
for (int i = 0; i < 1000; ++i) {
data.push_back(i);
}
std::cout << "First: " << data[firstIdx] << "\n"; // ✅ An toànFix 2 — Reserve trước:
cpp
int* first = &data[0];
data.reserve(1005); // Đảm bảo đủ capacity
for (int i = 0; i < 1000; ++i) {
data.push_back(i); // Không realloc → first vẫn hợp lệ
}
std::cout << "First: " << *first << "\n"; // ✅ An toànBài học: Không bao giờ giữ pointer/reference đến vector element nếu vector có thể grow. Dùng index thay thế.
🎯 Scenario: Hệ Thống Log Realtime
🎯 Production Scenario
Bạn đang xây hệ thống thu thập log cho microservice. Yêu cầu:
- Buffer tối đa 10,000 log entries
- Khi đầy, xóa 20% cũ nhất
- Tính thống kê: đếm ERROR, WARNING, INFO
Bạn sẽ thiết kế thế nào với std::vector?
💡 Giải pháp mẫu
cpp
#include <vector>
#include <string>
#include <algorithm>
#include <iostream>
enum class LogLevel { INFO, WARNING, ERROR };
struct LogEntry {
LogLevel level;
std::string message;
long timestamp;
};
class LogBuffer {
static constexpr size_t MAX_SIZE = 10'000;
static constexpr size_t PURGE_COUNT = MAX_SIZE / 5; // 20%
std::vector<LogEntry> logs_;
public:
LogBuffer() {
logs_.reserve(MAX_SIZE); // ✅ Pre-allocate — tránh realloc
}
void add(LogLevel level, std::string msg, long ts) {
if (logs_.size() >= MAX_SIZE) {
// Xóa 20% cũ nhất (đầu vector)
logs_.erase(logs_.begin(), logs_.begin() + PURGE_COUNT);
}
logs_.emplace_back(LogEntry{level, std::move(msg), ts});
}
size_t countByLevel(LogLevel level) const {
return std::count_if(logs_.begin(), logs_.end(),
[level](const LogEntry& e) { return e.level == level; });
}
void printStats() const {
std::cout << "📊 Log Stats:\n";
std::cout << " Total: " << logs_.size() << "\n";
std::cout << " INFO: " << countByLevel(LogLevel::INFO) << "\n";
std::cout << " WARNING: " << countByLevel(LogLevel::WARNING) << "\n";
std::cout << " ERROR: " << countByLevel(LogLevel::ERROR) << "\n";
}
};Lưu ý: Trong production thực tế, std::deque có thể tốt hơn cho trường hợp này vì xóa đầu mảng là O(1) thay vì O(n). Nhưng với 10k entries, vector vẫn đủ nhanh nhờ cache locality.
🎉 Module 1 Hoàn Tất — Checkpoint!
Chúc mừng! 🎉 Bạn đã hoàn thành Module 1: Nền Tảng C++:
| Bài | Chủ đề | Key Takeaway |
|---|---|---|
| 01 | Memory Model | Stack vs Heap, RAII |
| 02 | Pointers & References | Địa chỉ, dereference, dangling pointer |
| 03 | Vector First | Container mặc định, capacity vs size, iterator invalidation |
Bạn đã sẵn sàng cho Module 2: OOP & RAII — nơi bạn học cách thiết kế class, encapsulation, và quản lý resource tự động.
📚 Tổng kết
| Concept | Key Takeaway |
|---|---|
| Raw array vs vector | Vector an toàn hơn, linh hoạt hơn — luôn là lựa chọn đầu tiên |
size vs capacity | size = số phần tử thực, capacity = bộ nhớ đã cấp phát |
.reserve(n) | Pre-allocate để tránh reallocation tốn kém |
push_back vs emplace_back | emplace_back construct tại chỗ — hiệu quả hơn cho custom types |
| Iterator invalidation | Sau realloc, TẤT CẢ pointer/ref/iterator bị vô hiệu |
[] vs .at() | [] nhanh nhưng không check — .at() an toàn |
.erase() trong vòng lặp | Dùng it = v.erase(it) hoặc C++20 std::erase_if |
➡️ Tiếp theo
Bạn đã nắm vững std::vector! Tiếp theo — Module 2: OOP & Encapsulation — Thiết kế class, access modifiers, và tư duy hướng đối tượng.
🧠 Quiz
Câu 1: Lợi thế LỚN NHẤT của std::vector so với raw array là gì?
- [ ] A) Vector nhanh hơn raw array
- [ ] B) Vector dùng ít bộ nhớ hơn
- [x] C) Vector tự quản lý bộ nhớ (RAII) và biết size của mình
- [ ] D) Vector chỉ hoạt động trên heap
💡 Giải thích: Vector tự giải phóng bộ nhớ khi ra khỏi scope (RAII), biết
.size()chính xác, và hỗ trợ dynamic resizing. Raw array không có bất kỳ tính năng nào trong số này. Về tốc độ truy cập, cả hai đều O(1) — vector không chậm hơn.
Câu 2: Sau đoạn code sau, v.capacity() là bao nhiêu?
cpp
std::vector<int> v;
v.reserve(100);
v.push_back(1);
v.push_back(2);
v.push_back(3);- [ ] A) 3
- [ ] B) 4
- [x] C) 100
- [ ] D) 200
💡 Giải thích:
reserve(100)cấp phát bộ nhớ cho 100 phần tử. Sau 3 lầnpush_back,size = 3nhưngcapacityvẫn là 100 vì chưa vượt quá. Reserve chỉ tăng capacity, không giảm.
Câu 3: Khi nào emplace_back hiệu quả hơn push_back?
- [ ] A) Luôn luôn — emplace_back tốt hơn 100% trường hợp
- [ ] B) Khi vector chứa kiểu int hoặc double
- [x] C) Khi construct object trực tiếp bên trong vector thay vì tạo rồi copy/move
- [ ] D) Khi vector đã đầy capacity
💡 Giải thích:
emplace_backtruyền arguments trực tiếp đến constructor, tránh tạo object tạm rồi move/copy vào vector. Vớiint/double, khác biệt không đáng kể. Với custom class có constructor phức tạp,emplace_backtiết kiệm một bước move.
Câu 4: Đoạn code sau có vấn đề gì?
cpp
std::vector<int> v = {1, 2, 3};
int& ref = v[0];
v.push_back(4);
v.push_back(5);
std::cout << ref;- [ ] A) Không có vấn đề — output là 1
- [ ] B) Lỗi biên dịch
- [x] C) Undefined Behavior — ref có thể bị invalidated sau push_back
- [ ] D) Output luôn là 0
💡 Giải thích:
push_backcó thể trigger reallocation khi capacity không đủ (ở đây capacity ban đầu có thể là 3, push thêm 2 phần tử → realloc). Sau reallocation, tất cả references, pointers, và iterators đều bị invalidated. Truy cậprefsau đó là Undefined Behavior.
Câu 5: Cách nào tốt nhất để xóa tất cả phần tử có giá trị 0 khỏi vector trong C++20?
- [ ] A) Dùng vòng for và
v.erase(it) - [ ] B)
v.remove(0) - [x] C)
std::erase(v, 0) - [ ] D)
v.clear()rồi thêm lại phần tử khác 0
💡 Giải thích: C++20 cung cấp
std::erase(container, value)— sạch, an toàn, không cần lo iterator invalidation. Cách A dễ bug (phải dùngit = v.erase(it)đúng pattern).v.remove()không tồn tại (chỉ cóstd::removealgorithm, và nó cũng cầneraseđi kèm — erase-remove idiom).