String và vector trong C++: chuỗi và mảng động kiểu C++

String và vector trong C++: chuỗi và mảng động kiểu C++ - Khuê Nguyễn Creator

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ọi strcmp, 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;
}

std::string nối chuỗi bằng dấu cộng, lấy độ dài, ký tự đầu cuối và so sánh chuỗi

  • 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;
}

Tách dòng dữ liệu sách bằng find và substr, đổi chuỗi sang số bằng stoi

  • find(x) trả về vị trí xuất hiện đầu tiên của x. find(x, batDau) tìm từ vị trí batDau trở đi.
  • Không tìm thấy thì find trả về hằng đặc biệt std::string::npos. Luôn so sánh với npos trướ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;
}

Lỗi trôi dòng: nhập năm bằng cin xong getline đọc ngay chuỗi rỗng

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;
}

Dùng cin.ignore trước getline, nhập được cả năm xuất bản và tên sách

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;
}

Các thao tác std::vector: push_back, pop_back, erase, insert, size, empty

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ũ:

Sơ đồ vector tự co giãn: hết chỗ thì cấp vùng mới gấp đôi, size và capacity

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;
}

g++ cảnh báo so sánh int với size_t khác dấu khi duyệt vector bằng chỉ số

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;
}

Dùng string và vector trong C++ lưu danh sách sách, tìm theo từ khóa và xóa khỏi kho

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

  1. 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.
  2. 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”).
  3. 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.
  4. 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::getline từ bàn phím rồi push_back và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é.

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *