Trong bài này chúng ta sẽ học string và vector trong C++ (std::string và std::vector): hai kiểu dữ liệu dùng nhiều nhất trong code C++ hiện đại. std::string thay cho mảng ký tự char[] của C, còn std::vector thay cho mảng thô có kích thước cố định. Từ bài này trở đi, chương trình thư viện sẽ lưu danh sách sách bằng vector thay vì mảng.
Bài 9 trong Serie Lập trình C++ từ A tới Z
Vì sao không dùng char[] và mảng thô?
Trong C, chuỗi là mảng char kết thúc bằng ký tự '\0', và mảng có kích thước cố định từ lúc khai báo. Hệ quả là:
- Khai báo
char ten[20]mà tên sách dài 30 ký tự là tràn mảng. - Nối chuỗi phải gọi
strcat, so sánh phải gọistrcmp, quên chừa chỗ cho'\0'là lỗi. - Mảng
int soLuong[100]thì không thêm được phần tử thứ 101, mà thư viện có bao nhiêu sách thì không biết trước.
Các bạn đã học bài các hàm xử lý chuỗi trong C sẽ thấy phải cẩn thận thế nào. std::string và std::vector tự quản lý bộ nhớ, tự co giãn theo dữ liệu, nên những lỗi trên không còn nữa.
std::string: chuỗi ký tự kiểu C++
Tạo, nối và so sánh chuỗi
#include <iostream>
#include <string>
int main()
{
std::string ten = "Lap trinh";
std::string phu = "C++";
std::string tieuDe = ten + " " + phu; // noi chuoi
tieuDe += " co ban";
std::cout << "Tieu de: " << tieuDe << '\n';
std::cout << "Do dai: " << tieuDe.size() << " ky tu\n";
std::cout << "Ky tu dau: " << tieuDe[0] << ", ky tu cuoi: " << tieuDe.back() << '\n';
std::cout << "So sanh: " << (phu == "C++") << '\n';
std::cout << "\"Clean\" < \"Lap\"? " << (std::string("Clean") < std::string("Lap")) << '\n';
return 0;
}
- Nối chuỗi bằng
+và+=, không cần lo chuỗi kết quả dài bao nhiêu. .size()(hoặc.length(), giống nhau) trả về số ký tự.- Truy cập ký tự bằng
[i]như mảng,.back()lấy ký tự cuối. - So sánh bằng
==,!=,<,>như số.<so theo thứ tự từ điển, nên “Clean” đứng trước “Lap”. Đây là cách dùng để sắp xếp danh sách tên sách.
Tìm và cắt chuỗi
Dữ liệu sách lưu trong file thường có dạng nhiều trường nối bằng ký tự phân cách. Tách ra bằng find và substr:
#include <iostream>
#include <string>
int main()
{
std::string dong = "1024|Lap trinh C++|Bjarne Stroustrup";
std::size_t viTri1 = dong.find('|');
std::size_t viTri2 = dong.find('|', viTri1 + 1);
std::string ma = dong.substr(0, viTri1);
std::string ten = dong.substr(viTri1 + 1, viTri2 - viTri1 - 1);
std::string tacGia = dong.substr(viTri2 + 1);
std::cout << "Ma: " << ma << '\n';
std::cout << "Ten: " << ten << '\n';
std::cout << "Tac gia: " << tacGia << '\n';
if (dong.find("Python") == std::string::npos)
std::cout << "Khong co chu Python trong dong\n";
int maSo = std::stoi(ma);
std::cout << "Ma + 1 = " << maSo + 1 << ", doi lai thanh chuoi: " << std::to_string(maSo + 1) << '\n';
return 0;
}
find(x)trả về vị trí xuất hiện đầu tiên củax.find(x, batDau)tìm từ vị tríbatDautrở đi.- Không tìm thấy thì
findtrả về hằng đặc biệtstd::string::npos. Luôn so sánh vớinpostrước khi dùng kết quả. substr(viTri, soKyTu)cắt chuỗi con. Bỏ tham số thứ hai thì cắt tới hết chuỗi.std::stoiđổi chuỗi thành số,std::to_stringđổi số thành chuỗi.
Kết quả của find có kiểu std::size_t, là số nguyên không dấu. Mình dùng đúng kiểu này thay vì int, lý do có ở cuối bài.
Lỗi cin rồi getline bị trôi dòng
Đây là lỗi mà gần như ai học C++ cũng gặp. Nhập năm bằng std::cin >> rồi nhập tên sách bằng std::getline:
#include <iostream>
#include <string>
int main()
{
int namXuatBan;
std::string tenSach;
std::cout << "Nam xuat ban: ";
std::cin >> namXuatBan;
std::cout << "Ten sach: ";
std::getline(std::cin, tenSach);
std::cout << "Da luu: '" << tenSach << "' (" << namXuatBan << ")\n";
return 0;
}
Chương trình không cho nhập tên sách mà lưu luôn chuỗi rỗng. Khi gõ 2020 rồi Enter, bộ đệm bàn phím chứa 2020\n. std::cin >> đọc lấy số 2020 và để lại ký tự xuống dòng \n. std::getline gặp ngay \n đó, hiểu là một dòng rỗng và kết thúc.
Cách sửa là bỏ phần còn lại của dòng bằng std::cin.ignore trước khi gọi getline:
#include <iostream>
#include <limits>
#include <string>
int main()
{
int namXuatBan;
std::string tenSach;
std::cout << "Nam xuat ban: ";
std::cin >> namXuatBan;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // bo phan con lai cua dong
std::cout << "Ten sach: ";
std::getline(std::cin, tenSach);
std::cout << "Da luu: '" << tenSach << "' (" << namXuatBan << ")\n";
return 0;
}
Lưu ý: Cứ sau std::cin >> mà tiếp theo là std::getline thì phải có std::cin.ignore(...) ở giữa. Một cách khác gọn hơn: đọc mọi thứ bằng std::getline, rồi đổi sang số bằng std::stoi, như vậy không bao giờ bị trôi dòng.
std::vector: mảng động tự co giãn
std::vector trong thư viện <vector> là mảng có thể thêm, bớt phần tử lúc chạy. Kiểu phần tử viết trong ngoặc nhọn: std::vector<int>, std::vector<std::string>.
Các thao tác cơ bản
#include <iostream>
#include <vector>
int main()
{
std::vector<int> soLuong = {3, 10, 7};
soLuong.push_back(2); // them vao cuoi
soLuong.push_back(5);
std::cout << "So phan tu: " << soLuong.size() << '\n';
soLuong[1] = 12; // sua phan tu
soLuong.pop_back(); // bo phan tu cuoi
std::cout << "Danh sach:";
for (int x : soLuong)
std::cout << ' ' << x;
std::cout << '\n';
soLuong.erase(soLuong.begin() + 1); // xoa phan tu o vi tri 1
soLuong.insert(soLuong.begin(), 99); // chen 99 vao dau
std::cout << "Sau khi xoa va chen:";
for (int x : soLuong)
std::cout << ' ' << x;
std::cout << "\nRong? " << soLuong.empty() << '\n';
return 0;
}
| Thao tác | Cú pháp |
|---|---|
| Thêm vào cuối | v.push_back(x) |
| Bỏ phần tử cuối | v.pop_back() |
| Số phần tử | v.size() |
| Truy cập, không kiểm tra | v[i] |
| Truy cập, có kiểm tra | v.at(i) |
| Xóa phần tử thứ i | v.erase(v.begin() + i) |
| Chèn x vào vị trí i | v.insert(v.begin() + i, x) |
| Rỗng hay không | v.empty() |
| Xóa hết | v.clear() |
v.begin() là vị trí của phần tử đầu tiên, v.begin() + 1 là phần tử thứ hai. Đây là iterator, một khái niệm giống con trỏ mà chúng ta sẽ học kỹ ở bài STL.
Vector co giãn thế nào?
Vector lưu các phần tử liền nhau trong bộ nhớ giống mảng, nên v[i] nhanh như mảng thường. Khi hết chỗ, vector xin một vùng nhớ lớn hơn, chép toàn bộ phần tử sang rồi trả vùng cũ:
size() là số phần tử đang có, còn capacity() là số chỗ đã cấp sẵn. Vì mỗi lần mở rộng thì cấp dư ra, nên đa số lần push_back không phải chép lại gì cả.
Lưu ý: Khi vector mở rộng, toàn bộ phần tử chuyển sang vùng nhớ mới. Mọi con trỏ, tham chiếu, iterator đang giữ tới phần tử cũ đều trở thành vô hiệu. Đừng giữ int& x = v[0]; rồi push_back xong mới dùng x. Nếu biết trước số phần tử, gọi v.reserve(n) để cấp đủ chỗ một lần.
Duyệt vector bằng chỉ số
Range-based for như ở bài vòng lặp trong C++ là cách duyệt gọn nhất. Khi cần chỉ số thì dùng vòng lặp for thường, nhưng để ý kiểu của biến đếm:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> soLuong = {3, 10, 7};
for (int i = 0; i < soLuong.size(); i++)
std::cout << soLuong[i] << ' ';
std::cout << '\n';
return 0;
}
Chương trình chạy đúng, nhưng g++ cảnh báo comparison of integer expressions of different signedness. size() trả về std::size_t không dấu, còn i là int có dấu. Với vector nhỏ thì không sao, nhưng so sánh có dấu với không dấu là nguồn gốc của lỗi số âm thành số rất lớn như bài các kiểu dữ liệu trong C++. Cách sửa: khai báo std::size_t i = 0, hoặc dùng range-based for khi không cần chỉ số.
Áp dụng string và vector trong C++ vào chương trình thư viện
Giờ danh sách sách là một std::vector<std::string>, thêm bao nhiêu sách cũng được. Kết hợp với hàm ở bài trước để tìm và xóa sách:
#include <iostream>
#include <string>
#include <vector>
void inDanhSach(const std::vector<std::string>& ds)
{
for (std::size_t i = 0; i < ds.size(); i++)
std::cout << " " << i + 1 << ". " << ds[i] << '\n';
}
int timSach(const std::vector<std::string>& ds, const std::string& tuKhoa)
{
for (std::size_t i = 0; i < ds.size(); i++)
if (ds[i].find(tuKhoa) != std::string::npos)
return static_cast<int>(i);
return -1;
}
int main()
{
std::vector<std::string> danhSach;
danhSach.push_back("Lap trinh C++ co ban");
danhSach.push_back("Clean Code");
danhSach.push_back("Cau truc du lieu va giai thuat");
std::cout << "Kho sach:\n";
inDanhSach(danhSach);
int viTri = timSach(danhSach, "Clean");
if (viTri >= 0)
{
std::cout << "Tim thay 'Clean' o vi tri " << viTri + 1 << ", xoa khoi kho\n";
danhSach.erase(danhSach.begin() + viTri);
}
std::cout << "Kho sach con lai:\n";
inDanhSach(danhSach);
return 0;
}
Vector được truyền vào hàm bằng const std::vector<std::string>&: không sao chép cả danh sách, và hàm không sửa được. Hàm timSach tìm theo từ khóa trong tên bằng find, trả về -1 khi không thấy.
Bài tập
- Nhập một câu, đếm số từ trong câu (các từ cách nhau bởi dấu cách) và in ra từ dài nhất.
- Nhập một chuỗi, in ra chuỗi đảo ngược và kiểm tra nó có phải chuỗi đối xứng không (ví dụ “abcba”).
- Nhập điểm của n sinh viên vào
std::vector<double>(n do người dùng nhập), in ra điểm trung bình và các điểm cao hơn trung bình. - Thêm chức năng “thêm sách” vào chương trình thư viện: nhập tên sách bằng
std::getlinetừ bàn phím rồipush_backvào danh sách. Nhớ xử lý lỗi trôi dòng nếu có nhập số trước đó.
Lời giải sẽ có trong bài tổng hợp bài tập C++ cơ bản nhé.
Kết
String và vector trong C++ là 2 kiểu các bạn sẽ dùng trong gần như mọi chương trình. std::string thay cho char[] với đầy đủ thao tác nối, so sánh, tìm, cắt, còn std::vector là mảng tự co giãn, an toàn hơn mảng thô. Hai cạm bẫy cần nhớ: cin >> rồi getline bị trôi dòng, và tham chiếu tới phần tử vector bị vô hiệu sau khi vector mở rộng.
Danh sách đầy đủ các hàm của 2 kiểu này có trên cppreference: std::string và std::vector. Bài tiếp theo là tổng hợp bài tập C++ cơ bản có lời giải cho cả chương 1 và 2. Nếu thấy bài viết có ích, hãy chia sẻ cho bạn bè cùng học nhé.









