Khi lập trình trong C++, một trong những cấu trúc dữ liệu phổ biến nhất bạn sẽ gặp là vector. Nó hoạt động giống như một mảng nhưng với sự linh hoạt tại thời gian chạy — có thể tăng hoặc giảm kích thước một cách động, khiến nó trở thành một trong những công cụ mạnh mẽ nhất trong Thư viện Mẫu Chuẩn (STL).
Trong hướng dẫn này, chúng ta sẽ đề cập đến:
- Các khái niệm cơ bản về Vector và STL
- Các phương pháp khởi tạo
- Các hàm và thao tác phổ biến
- Phân bổ bộ nhớ tĩnh và động
- Sự khác biệt giữa mảng và vector
- Cách vector quản lý bộ nhớ
- Iterators
- Vector 2D
- Các yếu tố hiệu suất
- So sánh với các container khác
- Tự động dọn dẹp bộ nhớ
1. STL và Vectors
Thư viện Mẫu Chuẩn (STL) cung cấp các container và thuật toán sẵn có. Một số container thường được sử dụng:
vector→ Mảng độngqueue→ FIFOstack→ LIFOset→ Các phần tử duy nhất, có thứ tự
👉 Lưu ý biên dịch: luôn sử dụng ít nhất C++11:
cpp
g++ -std=c++11 code.cpp -o runfile && ./runfile
Nếu bạn quên -std=c++11, bạn có thể gặp lỗi biên dịch hoặc các vấn đề không mong muốn trong thời gian chạy.
2. Khởi tạo Vector
Có nhiều cách để khởi tạo std::vector:
cpp
#include <iostream>
#include <vector>
using namespace std;
int main() {
// Khởi tạo trực tiếp (C++11)
vector<int> v1 = {1, 2, 3};
// Khởi tạo bằng constructor (kích thước, giá trị khởi tạo)
vector<int> v2(3, 0); // [0, 0, 0]
// Khởi tạo trống rồi thêm phần tử
vector<int> v3;
v3.push_back(10);
v3.push_back(20);
// In ra sử dụng vòng lặp range-for
for (int x : v1) cout << x << " ";
cout << endl;
}
Kết quả:
plaintext
1 2 3
3. Các hàm và thao tác của Vector
Các thao tác phổ biến với vector:
-
Thêm / Xóa
push_back(x)→ thêm vào cuốipop_back()→ xóa phần tử cuối
-
Truy cập
v[i]→ phần tử theo chỉ số (không kiểm tra giới hạn)v.at(i)→ truy cập có kiểm tra giới hạn (némout_of_range)front()/back()
-
Thông tin
size()→ số lượng phần tửcapacity()→ kích thước bộ nhớ đã cấp phátempty()→ kiểm tra xem có rỗng hay không
Ví dụ:
cpp
vector<int> v = {10, 20, 30};
v.push_back(40); // [10,20,30,40]
v.pop_back(); // [10,20,30]
cout << v.front() << endl; // 10
cout << v.back() << endl; // 30
cout << v.at(1) << endl; // 20
cout << v.size() << endl; // 3
cout << v.capacity() << endl; // capacity >= 3
4. Phân bổ bộ nhớ tĩnh vs động
Mảng tĩnh
cpp
int arr[5]; // kích thước cố định tại thời gian biên dịch
- Thường được lưu trữ trên stack
- Kích thước cứng nhắc, không thể thay đổi
Mảng động (thủ công)
cpp
int* arr = new int[5];
delete[] arr;
- Được lưu trữ trên heap
- Cần quản lý bộ nhớ thủ công
Vectors sử dụng bộ nhớ động nhưng quản lý tự động, làm cho chúng an toàn và linh hoạt hơn so với các mảng động thô.
5. Sự khác biệt giữa Mảng và Vector
| Tính năng | Mảng (Tĩnh) | Vector (Động) |
|---|---|---|
| Kích thước | Cố định tại thời gian biên dịch | Có thể tăng/giảm tại thời gian chạy |
| Vị trí bộ nhớ | Stack (thường) | Heap (quản lý nội bộ) |
| Quản lý bộ nhớ | Thủ công (nếu động) | Tự động |
| Các hàm có sẵn | Không (chỉ truy cập thô) | API phong phú: push_back, at, size... |
| Khởi tạo | Hình thức hạn chế | Constructor linh hoạt |
| An toàn | Không kiểm tra giới hạn | at() cung cấp truy cập có kiểm tra giới hạn |
6. Cách Vectors hoạt động trong bộ nhớ
vector duy trì hai khái niệm cốt lõi:
- size → số phần tử đã lưu
- capacity → không gian lưu trữ đã cấp phát (có thể lớn hơn hoặc bằng kích thước)
Khi capacity bị vượt quá, các triển khai thường tăng capacity (thường bằng cách gấp đôi), điều này giúp giảm chi phí thêm.
Ví dụ:
cpp
vector<int> v;
for (int i = 0; i < 10; i++) {
v.push_back(i);
cout << "Kích thước: " << v.size() << "
" " Capacity: " << v.capacity() << endl;
}
Kết quả điển hình (tùy thuộc vào triển khai):
plaintext
Kích thước: 1 Capacity: 1
Kích thước: 2 Capacity: 2
Kích thước: 3 Capacity: 4
Kích thước: 5 Capacity: 8
Kích thước: 9 Capacity: 16
Sử dụng reserve(n) để cấp phát bộ nhớ trước nếu bạn biết kích thước dự kiến — điều này giúp tránh việc cấp phát lại nhiều lần.
7. Iterators
Iterators hoạt động giống như con trỏ và được sử dụng để duyệt qua các container:
cpp
vector<int> v = {10,20,30,40};
// Duyệt tới trước
for (auto it = v.begin(); it != v.end(); ++it) cout << *it << " ";
// Duyệt ngược
for (auto it = v.rbegin(); it != v.rend(); ++it) cout << *it << " ";
Kết quả:
plaintext
10 20 30 40
40 30 20 10
Iterators là cách phổ biến để sử dụng các thuật toán STL như std::sort, std::find, v.v.
8. Vector 2D
Các vector lồng nhau rất hữu ích cho ma trận, đồ thị và lưu trữ 2D động:
cpp
vector<vector<int>> matrix(3, vector<int>(3, 0));
matrix[0][1] = 5;
matrix[2][2] = 7;
for (auto& row : matrix) {
for (auto val : row) cout << val << " ";
cout << endl;
}
Kết quả:
plaintext
0 5 0
0 0 0
0 0 7
9. Mẹo hiệu suất
size()là O(1). Sử dụng nó một cách thoải mái.capacity()cho thấy bộ nhớ đã được dự trữ; sử dụngreserve(n)khi bạn biết kích thước dự kiến.- Tránh sao chép quá mức — ưu tiên
emplace_back()để xây dựng tại chỗ. - Truy cập ngẫu nhiên là O(1); việc thêm/xóa ở giữa là O(n).
- Đối với việc thêm vào đầu thường xuyên, hãy xem xét
deque; đối với việc thêm/xóa giữa thường xuyên, hãy xem xétlist(nhưng danh sách có độ địa phương bộ nhớ kém).
10. So sánh Vector với các Container khác
- Vector → tốt nhất cho truy cập ngẫu nhiên, việc thêm vào cuối nhanh chóng.
- Deque → nhanh chóng thêm vào cả hai đầu.
- List → nhanh chóng thêm/xóa ở giữa (không có truy cập ngẫu nhiên).
- Set → các phần tử duy nhất, có thứ tự (hoạt động logarithmic).
👉 Lựa chọn mặc định: vector trừ khi bạn có lý do cụ thể để chọn container khác.
11. Tự động dọn dẹp bộ nhớ
Vectors tự động giải phóng bộ nhớ đã quản lý khi chúng ra khỏi phạm vi:
cpp
void func() {
vector<int> v(1000, 1);
} // bộ nhớ được giải phóng khi hàm trả về
✅ Kết luận
std::vector giống như mảng nhưng linh hoạt, an toàn (với at()) và hiệu quả cho hầu hết các nhu cầu sử dụng chung. Ưu tiên sử dụng vector thay vì các mảng thô trừ khi bạn cần một bộ đệm có kích thước cố định, được cấp phát trên stack hoặc có các yêu cầu hiệu suất rất cụ thể.
Câu hỏi thường gặp
1. Vector có thể chứa các kiểu dữ liệu khác nhau không?
Không, vector trong C++ chỉ có thể chứa các phần tử cùng kiểu dữ liệu.
2. Làm thế nào để xóa một phần tử khỏi vector?
Sử dụng phương thức erase() để xóa một phần tử tại chỉ số cụ thể.
3. Vector có thể chứa các vector khác không?
Có, bạn có thể tạo vector 2D bằng cách sử dụng vector<vector<T>>.
4. Làm thế nào để kiểm tra xem một vector có rỗng không?
Sử dụng phương thức empty() để kiểm tra.