Vì sao dùng vector thay cho mảng thô
Mảng thô có kích thước cố định từ lúc biên dịch và quên mất độ dài của nó ngay khi bạn truyền nó vào một hàm. std::vector giải quyết cả hai vấn đề: đó là một mảng có thể thay đổi kích thước, tự theo dõi độ dài, mở rộng khi cần và tự dọn dẹp bộ nhớ. Trong C++ hiện đại, vector là container mặc định; chỉ dùng mảng thô khi bạn có lý do cụ thể.
Include <vector>, rồi khai báo với kiểu phần tử trong dấu ngoặc nhọn:
scores.size() luôn cho bạn biết độ dài hiện tại: không cần một biến int n riêng phải giữ đồng bộ, cũng không cần mẹo sizeof. {90, 75, 100, 60} là một brace initializer; vector tự biết nó cần bốn ô.
Tạo và khởi tạo vector
Có nhiều cách để tạo vector, tùy vào những gì bạn biết từ trước:
Cẩn thận với cái bẫy ngoặc tròn và ngoặc nhọn: vector<int> tens(5, 10) tạo năm bản sao của 10, còn vector<int> tens{5, 10} tạo một vector hai phần tử chứa 5 và 10. Ngoặc tròn nghĩa là "kích thước và giá trị điền vào"; ngoặc nhọn nghĩa là "chính các phần tử này".
Thêm và xóa phần tử
Điểm mấu chốt của vector là nó có thể lớn lên. push_back thêm vào cuối, còn pop_back xóa ở cuối:
back() trả về phần tử cuối cùng và front() trả về phần tử đầu tiên, gọn hơn v[v.size() - 1] và v[0]. Từ C++11, bạn cũng có thể dùng emplace_back(args...) để tạo phần tử ngay tại chỗ, tránh một bản sao tạm thời với các kiểu nặng.
Một lỗi hay gặp của người mới là gọi front() hoặc back() trên một vector rỗng. Đó là hành vi không xác định chứ không phải lỗi được báo, nên hãy luôn kiểm tra if (!v.empty()) trước.
Đọc phần tử: [] và at()
Bạn truy cập vector bằng chỉ số với [] giống hệt mảng. Nhưng [] không kiểm tra phạm vi: một chỉ số ngoài phạm vi là hành vi không xác định, có thể âm thầm đọc giá trị rác hoặc gây sập sau đó ở một chỗ khó hiểu:
vector<int> v = {1, 2, 3};
cout << v[10]; // UNDEFINED BEHAVIOR - no check, no error
Khi bạn muốn an toàn, hãy dùng at(). Nó kiểm tra chỉ số và ném std::out_of_range khi truy cập sai, một trong các ngoại lệ của thư viện chuẩn mà bạn có thể bắt, nhờ vậy bạn nhận được lỗi rõ ràng thay vì dữ liệu bị hỏng:
Quy tắc chung: dùng [] trong các vòng lặp chặt khi bạn đã chứng minh chỉ số hợp lệ, và at() ở những ranh giới nơi dữ liệu đầu vào xấu có thể lọt vào.
Duyệt qua vector
Cách gọn nhất để duyệt vector là vòng lặp for dạng range-based. Lấy phần tử bằng const auto& để đọc mà không sao chép, hoặc auto& để sửa trực tiếp:
Nếu bạn thật sự cần chỉ số (chẳng hạn để so sánh các phần tử kề nhau), hãy dùng vòng lặp đếm truyền thống, nhưng lưu ý size() trả về kiểu không dấu (size_t). So sánh một int i có dấu với nó có thể gây cảnh báo của trình biên dịch và hiện tượng quay vòng bất ngờ, vì vậy hãy ưu tiên size_t i hoặc vòng lặp range-based khi có thể:
for (size_t i = 0; i < v.size(); i++) { // size_t, not int
cout << v[i];
}
size, capacity và reserve
Vector giữ hai con số: size() (đang chứa bao nhiêu phần tử) và capacity() (chứa được bao nhiêu trước khi phải mở rộng). Khi một lần push_back vượt quá capacity, vector cấp phát một khối lớn hơn, sao chép mọi phần tử sang, rồi giải phóng khối cũ. Đó là lý do việc gọi push_back liên tục có chi phí trung bình rẻ, nhưng mỗi lần cấp phát lại riêng lẻ thì không hề miễn phí:
Nếu bạn biết sơ bộ sẽ thêm bao nhiêu phần tử, hãy gọi reserve() trước để bỏ qua các lần cấp phát lại lặp đi lặp lại. Lưu ý reserve() thay đổi capacity chứ không thay đổi size: vector vẫn có 0 phần tử cho đến khi bạn thêm vào.
Việc cấp phát lại này cũng là nguồn gốc của bug khó chịu nhất với vector. Vì khi mở rộng, vùng lưu trữ bị di chuyển, nên mọi con trỏ, tham chiếu hoặc iterator bạn đã lưu vào vector đều trở thành con trỏ treo sau một lần push_back có cấp phát lại:
vector<int> v = {1, 2, 3};
int& first = v[0]; // reference into the vector
v.push_back(4); // may reallocate...
cout << first; // DANGLING - may point at freed memory
Điều tương tự áp dụng cho iterator: đừng push_back hay erase trong lúc đang duyệt bằng một iterator đã lưu. Nếu bắt buộc phải xóa phần tử trong lúc lặp, hãy dùng giá trị trả về của erase, hoặc thành ngữ erase-remove với std::remove.
Tiếp theo: map
vector hoàn hảo khi bạn tra cứu theo vị trí: phần tử 0, phần tử 1, v.v. Nhưng thường thì bạn muốn tra cứu theo khóa: tên người dùng, mã sản phẩm, một từ. Đó là việc của std::map. Tiếp theo, chúng ta sẽ tìm hiểu map, container khóa và giá trị của C++, gồm cách thêm, tra cứu và duyệt các phần tử, cùng cái bẫy [] tự tạo giá trị mặc định khiến gần như ai cũng vấp.
Câu hỏi thường gặp
Vector trong C++ là gì?
std::vector là một mảng động (có thể thay đổi kích thước) trong thư viện chuẩn C++. Khác với mảng thô, nó biết kích thước của chính nó, tự động mở rộng khi bạn thêm phần tử bằng push_back, và tự giải phóng bộ nhớ cho bạn. Include <vector> và viết vector<int> v; để tạo một vector.
[] và at() trên vector C++ khác nhau thế nào?
v[i] không kiểm tra phạm vi: một chỉ số vượt phạm vi là hành vi không xác định (sập hoặc âm thầm hỏng dữ liệu). v.at(i) kiểm tra chỉ số và ném std::out_of_range nếu chỉ số không hợp lệ. Dùng [] trong các vòng lặp cần hiệu năng khi bạn đã kiểm tra chỉ số, và at() khi bạn muốn lỗi xảy ra an toàn, dễ gỡ lỗi.
push_back có làm vô hiệu con trỏ và tham chiếu vào vector C++ không?
Có thể có. Khi vector hết dung lượng, push_back cấp phát lại vùng lưu trữ sang một khối mới, làm vô hiệu mọi con trỏ, tham chiếu và iterator trỏ vào các phần tử cũ. Đừng giữ tham chiếu đến một phần tử qua một lần push_back, và hãy gọi reserve() từ trước nếu có thể để tránh những lần cấp phát lại bất ngờ.