Khi làm việc với vector trong C++, chúng ta thường gặp tình huống cần chèn một phần tử vào trước một phần tử hiện có và sau đó thực hiện các thao tác sửa đổi trên phần tử vừa chèn. Dưới đây là các tình huống phổ biến dẫn đến việc mất hiệu lực của bộ lặp (iterator):
Tình huống 1: Mất hiệu lực bộ lặp sau khi chèn phần tử
Đoạn mã sau biên dịch thành công nhưng sẽ gây ra sự sụp đổ chương trình khi chạy:
int chinh()
{
vector<int> danhSach;
danhSach.push_back(10);
danhSach.push_back(20);
danhSach.push_back(30);
danhSach.push_back(40);
danhSach.reserve(60);
vector<int>::iterator boLap = timKiem(danhSach.begin(), danhSach.end(), 20);
if (boLap != danhSach.end())
{
danhSach.insert(boLap, 50);
}
*boLap *= 10;
for (auto phanTu : danhSach)
{
cout << phanTu << " ";
}
return 0;
}
Nguyên nhân của sự cố này nằm ở cơ chế hoạt động của hàm insert. Khi chèn phần tử, tất cả các phần tử từ vị trí chèn trở đi được dịch chuyển sang một vị trí, tạo chỗ trống cho phần tử mới. Khi hàm insert kết thúc, bộ lặp boLap vẫn trỏ đến vị trí cũ, nhưng dữ liệu tại vị trí đó đã thay đổi. Trình biên dịch trong môi trường Visual Studio phát hiện sự thay đổi này và coi bộ lặp đã mất hiệu lực, dẫn đến sự sụp đổ chương trình.
Tình huống 2: Mất hiệu lực bộ lặp khi vector cần mở rộng dung lượng
Tình huống này tương tự tình huống đầu tiên nhưng xảy ra khi container cần mở rộng dung lượng:
int chinh()
{
vector<double> mang;
mang.push_back(1.1);
mang.push_back(2.2);
mang.push_back(3.3);
mang.push_back(4.4);
vector<double>::iterator boLap = timKiem(mang.begin(), mang.end(), 2.2);
if (boLap != mang.end())
{
mang.insert(boLap, 5.5);
}
*boLap *= 10;
for (auto giaTri : mang)
{
cout << giaTri << " ";
}
return 0;
}
Khi dung lượng của vector không đủ, bộ nhớ sẽ được cấp phát lại ở một vùng mới lớn hơn. Dữ liệu từ vùng cũ được sao chép sang vùng mới, và vùng cũ được giải phóng. Vấn đề xảy ra khi boLap vẫn trỏ đến vị trí trong vùng nhớ cũ đã được giải phóng. Để giải quyết vấn đề này trong hàm insert, chúng ta có thể sử dụng khoảng cách tương đối giữa bộ lặp it và bộ lặp begin của đối tượng.
Tuy nhiên, ngay cả khi giải quyết được vấn đề trên trong hàm insert, bộ lặp trong hàm main vẫn trỏ đến vùng nhớ cũ đã được giải phóng, trở thành một con trỏ hoang dã (wild pointer).
Tình huống 3: Mất hiệu lực bộ lặp khi xóa phần tử
Tình huống thứ ba xảy ra khi sử dụng bộ lặp để xóa một phần tử và sau đó cố gắng sửa đổi vị trí đó:
int chinh()
{
vector<string> chuoi;
chuoi.push_back("mot");
chuoi.push_back("hai");
chuoi.push_back("ba");
chuoi.push_back("bon");
vector<string>::iterator boLap = timKiem(chuoi.begin(), chuoi.end(), "hai");
if (boLap != chuoi.end())
{
chuoi.erase(boLap);
}
*boLap = "thaydoi";
for (auto text : chuoi)
{
cout << text << " ";
}
return 0;
}
Nguyên nhân gây ra sự cố tương tự như tình huống đầu tiên. Khi xóa một phần tử, tất cả các phần tử sau vị trí xóa được dịch chuyển lên một vị trí, làm cho phần tử cần xóa bị che phủ. Dù bộ lặp boLap vẫn trỏ đến vị trí đó, ý nghĩa của vị trí đã thay đổi. Trình biên dịch Visual Studio kiểm tra phát hiện sự thay đổi này và coi bộ lặp đã mất hiệu lực. Đáng chú ý, không chỉ boLap mà tất cả các bộ lặp trỏ đến các vị trí sau vị trí xóa cũng sẽ mất hiệu lực.
Giải pháp: Cập nhật lại bộ lặp sau khi chèn hoặc xóa
Sau khi thực hiện thao tác chèn hoặc xóa trên container, tốt nhất không nên sử dụng lại bộ lặp cũ. Nếu cần tiếp tục sử dụng, nên gán lại giá trị mới cho bộ lặp:
// Với thao tác chèn
vector<int>::iterator boLap = timKiem(danhSach.begin(), danhSach.end(), 20);
if (boLap != danhSach.end())
{
boLap = danhSach.insert(boLap, 50);
}
// Với thao tác xóa
vector<string>::iterator boLap = timKiem(chuoi.begin(), chuoi.end(), "hai");
if (boLap != chuoi.end())
{
boLap = chuoi.erase(boLap);
}
Lưu ý rằng giá trị trả về của hàm insert là bộ lặp trỏ đến phần tử vừa được chèn, trong khi giá trị trả về của hàm erase là bộ lặp trỏ đến phần tử đứng ngay sau phần tử đã xóa (tức là phần tử mới chiếm vị trí của phần tử đã xóa).