Bài 28.3: Kết hợp chuỗi ký tự với mảng và vector trong C++

Chào mừng trở lại với loạt bài viết về C++! Sau khi đã làm quen với chuỗi ký tự cơ bản và các cấu trúc dữ liệu như mảng và vector, đã đến lúc chúng ta khám phá một chủ đề vô cùng quan trọng và thực tế: làm thế nào để lưu trữ và làm việc với nhiều chuỗi ký tự cùng một lúc. Đây là tình huống thường gặp khi bạn cần quản lý danh sách tên, danh sách sản phẩm, các dòng văn bản từ một tệp, và vô vàn các ứng dụng khác.

Trong C++, chúng ta có thể tận dụng sức mạnh của mảng (arrays) và vector (vector) để làm điều này. Mỗi cấu trúc này đều có những ưu điểm riêng và phù hợp với các trường hợp sử dụng khác nhau. Hãy cùng đi sâu vào chi tiết nhé!

Lưu trữ Chuỗi ký tự trong Mảng (Arrays of Strings)

Mảng trong C++ là một tập hợp các phần tử cùng kiểu dữ liệu được lưu trữ liên tục trong bộ nhớ. Khi làm việc với chuỗi ký tự, chúng ta có hai cách tiếp cận chính với mảng:

1. Mảng các Chuỗi ký tự kiểu C (C-style String Arrays)

Theo truyền thống, chuỗi ký tự trong C/C++ được biểu diễn bằng mảng các ký tự (char) kết thúc bằng ký tự null (\0). Do đó, một "mảng chuỗi" có thể được hiểu là một mảng hai chiều của các ký tự, nơi mỗi hàng là một chuỗi ký tự riêng biệt.

Ví dụ:

#include <iostream>
#include <cstring> // Thu vien de lam viec voi chuoi C-style nhu strcpy

int main() {
    // Khai bao mot mang chua 3 chuoi, moi chuoi toi da 19 ky tu + 1 ky tu '\0'
    char danhSachTen[3][20];

    // Gan gia tri cho cac chuoi
    // LU Y: Can su dung cac ham an toan nhu strncpy hoac luon kiem tra kich thuoc
    // strcpy co the gay tran bo dem (buffer overflow) neu chuoi nguon lon hon kich thuoc mang dich
    strcpy(danhSachTen[0], "Alice");
    strcpy(danhSachTen[1], "Bob");
    strcpy(danhSachTen[2], "Charlie");

    // In ra cac chuoi trong mang
    cout << "Danh sach ten (C-style array):\n";
    for (int i = 0; i < 3; ++i) {
        cout << danhSachTen[i] << endl;
    }

    // Vi du ket hop chuoi (can su dung cac ham cua <cstring>)
    char tenDayDu[50]; // Can du bo dem
    strcpy(tenDayDu, danhSachTen[0]);
    strcat(tenDayDu, " va ");
    strcat(tenDayDu, danhSachTen[1]);
    cout << "Ket hop (C-style): " << tenDayDu << endl;


    return 0;
}

Giải thích code:

  • Chúng ta khai báo char danhSachTen[3][20] để tạo ra một mảng có 3 hàng và 20 cột. Mỗi hàng danhSachTen[i] có thể chứa một chuỗi ký tự C-style có độ dài tối đa 19 ký tự, còn chỗ cho ký tự \0.
  • Để gán giá trị cho các chuỗi, chúng ta không thể dùng toán tử =. Thay vào đó, chúng ta phải dùng hàm strcpy (hoặc strncpy an toàn hơn) từ thư viện <cstring> để sao chép nội dung chuỗi.
  • Truy cập từng chuỗi con rất đơn giản, chỉ cần dùng danhSachTen[i].
  • Kết hợp các chuỗi con cũng yêu cầu sử dụng các hàm của <cstring> như strcat.

Hạn chế: Cách tiếp cận này khá cồng kềnh và tiềm ẩn rủi ro tràn bộ đệm (buffer overflow) nếu bạn không cẩn thận với kích thước của mảng và độ dài chuỗi.

2. Mảng các Đối tượng string

Trong C++ hiện đại, cách tốt hơn và an toàn hơn là sử dụng lớp string. Khi đó, một "mảng chuỗi" đơn giản là một mảng mà mỗi phần tử của nó là một đối tượng string.

Ví dụ:

#include <iostream>
#include <string> // Thu vien cho string

int main() {
    // Khai bao mot mang chua 3 doi tuong string
    string danhSachTen[3];

    // Gan gia tri cho cac chuoi (don gian hon rat nhieu!)
    danhSachTen[0] = "Alice Wonderland"; // string tu dong quan ly bo nho
    danhSachTen[1] = "Bob";
    danhSachTen[2] = "Charlie Day La Mot Cai Ten Rat Dai Ma Khong Can Lo Lo Tran Bo Dem";

    // In ra cac chuoi trong mang
    cout << "Danh sach ten (string array):\n";
    for (int i = 0; i < 3; ++i) {
        cout << danhSachTen[i] << endl;
    }

    // Vi du ket hop chuoi (dung toan tu + nhu thong thuong)
    string tenDayDu = danhSachTen[0] + " va " + danhSachTen[1];
    cout << "Ket hop (string): " << tenDayDu << endl;

    return 0;
}

Giải thích code:

  • Chúng ta khai báo string danhSachTen[3]; để tạo ra một mảng chứa 3 đối tượng string.
  • Việc gán giá trị trở nên cực kỳ đơn giản bằng cách sử dụng toán tử gán =. string tự động quản lý bộ nhớ cần thiết cho chuỗi, bất kể độ dài của nó (trong giới hạn bộ nhớ hệ thống).
  • Truy cập và in ra cũng tương tự như mảng thông thường: danhSachTen[i].
  • Việc kết hợp (nối) các chuỗi cũng rất trực quan, chỉ cần sử dụng toán tử + giữa các đối tượng string.

Ưu điểm: An toàn hơn, dễ sử dụng hơn, tự động quản lý bộ nhớ.

Hạn chế của cả hai loại mảng: Kích thước của mảng phải được xác định tại thời điểm biên dịch. Bạn không thể thêm hoặc bớt các chuỗi một cách linh hoạt sau khi mảng đã được tạo. Đây là lúc vector phát huy tác dụng!

Lưu trữ Chuỗi ký tự trong Vector (vector<string>)

vector là một container (vùng chứa) động trong Thư viện Chuẩn C++ (STL - Standard Template Library). Nó hoạt động giống như một mảng, nhưng kích thước của nó có thể thay đổi trong quá trình thực thi chương trình. Khi kết hợp với string, chúng ta có một giải pháp mạnh mẽ và linh hoạt để quản lý bộ sưu tập các chuỗi ký tự.

Khai báo một vector chứa các chuỗi string có dạng vector<string>.

Ví dụ:

#include <iostream>
#include <string>
#include <vector> // Thu vien cho vector

int main() {
    // Khai bao mot vector rong chua cac doi tuong string
    vector<string> danhSachMonHoc;

    // Them cac phan tu (chuoi) vao cuoi vector
    danhSachMonHoc.push_back("Toan roi rac");
    danhSachMonHoc.push_back("Lap trinh C++ nang cao"); // The hien tinh linh hoat
    danhSachMonHoc.push_back("Cau truc du lieu va Giai thuat");

    // Kich thuoc cua vector thay doi tu dong
    cout << "So luong mon hoc ban dau: " << danhSachMonHoc.size() << endl; // size() cho biet so phan tu

    // Truy cap va in ra cac phan tu (giong mang, hoac dung at() an toan hon)
    cout << "Danh sach mon hoc:\n";
    for (size_t i = 0; i < danhSachMonHoc.size(); ++i) { // size() tra ve size_t, nen dung size_t cho i
        cout << "- " << danhSachMonHoc[i] << endl;
        // Hoac an toan hon: cout << "- " << danhSachMonHoc.at(i) << endl;
    }

    // Them mot mon nua
    danhSachMonHoc.push_back("He dieu hanh");
    cout << "\nSo luong mon hoc sau khi them: " << danhSachMonHoc.size() << endl;

    // In lai danh sach su dung range-based for loop (cach hien dai va ngan gon)
    cout << "Danh sach mon hoc moi:\n";
    for (const string& monHoc : danhSachMonHoc) { // Dung const& de tranh sao chep
        cout << "- " << monHoc << endl;
    }

    // Vi du ket hop (noi) chuoi tu vector
    if (danhSachMonHoc.size() > 1) {
        string monHocChinh = danhSachMonHoc[1] + " va " + danhSachMonHoc[2];
         cout << "\nMon hoc chinh trong danh sach: " << monHocChinh << endl;
    }

    // Mot vi du lam viec voi chuoi trong vector: Tim chuoi co chua "C++"
    cout << "\nMon hoc co chua 'C++':\n";
    for (const string& monHoc : danhSachMonHoc) {
        if (monHoc.find("C++") != string::npos) { // Su dung phuong thuc find cua string
            cout << "- " << monHoc << endl;
        }
    }


    return 0;
}

Giải thích code:

  • vector<string> danhSachMonHoc; khai báo một vector rỗng có thể chứa các đối tượng string.
  • Chúng ta sử dụng phương thức push_back() để thêm các chuỗi mới vào cuối vector. Vector sẽ tự động điều chỉnh kích thước.
  • size() trả về số lượng phần tử hiện có trong vector.
  • Truy cập các phần tử tương tự như mảng, sử dụng toán tử [] hoặc phương thức at() (phương thức at() kiểm tra lỗi ngoài phạm vi và ném ngoại lệ, an toàn hơn).
  • Chúng ta có thể lặp qua vector bằng vòng lặp for truyền thống với chỉ số hoặc sử dụng range-based for loop rất tiện lợi cho việc duyệt qua tất cả các phần tử.
  • Việc kết hợp chuỗi từ các phần tử vector cũng đơn giản như với mảng string, sử dụng toán tử +.
  • Chúng ta cũng có thể dễ dàng áp dụng các phương thức của string lên từng phần tử của vector, ví dụ như sử dụng find() để tìm kiếm chuỗi con.

Ưu điểm của vector<string>:

  • Linh hoạt: Kích thước có thể thay đổi dynamically (thêm/bớt phần tử).
  • An toàn: Tự động quản lý bộ nhớ.
  • Tiện lợi: Cung cấp nhiều phương thức hữu ích như push_back, pop_back, insert, erase, size, empty, v.v.
  • Tương thích: Kết hợp tốt với các thuật toán và iterator trong STL.

Khi nào sử dụng Mảng, khi nào sử dụng Vector?

  • Sử dụng mảng string khi bạn biết chính xác số lượng chuỗi cần lưu trữ tại thời điểm biên dịch và số lượng này sẽ không thay đổi. Nó có thể hơi hiệu quả hơn về mặt bộ nhớ và tốc độ truy cập nếu kích thước rất lớn và cố định, nhưng sự khác biệt thường không đáng kể với kích thước vừa phải.
  • Sử dụng vector<string> trong hầu hết các trường hợp khác. Khi bạn không biết trước số lượng chuỗi, số lượng có thể thay đổi, hoặc bạn cần thêm/bớt các chuỗi một cách linh hoạt trong quá trình chạy chương trình. vector là lựa chọn linh hoạt và hiện đại hơn.

Nên tránh sử dụng mảng C-style (char[][]) cho các bộ sưu tập chuỗi trừ khi bạn có lý do đặc biệt (ví dụ: làm việc với mã cũ) do những rủi ro và sự bất tiện mà nó mang lại.

Kết hợp và Thao tác nâng cao hơn

Với cả mảng stringvector<string>, bạn có thể áp dụng tất cả các kiến thức đã học về string lên từng phần tử.

Ví dụ:

  • Truy cập và sửa đổi: danhSachTen[0] = "Alice moi"; hoặc danhSachMonHoc[1] += " va hon the nua";
  • Truy cập ký tự cụ thể trong một chuỗi: char kyTuDau = danhSachTen[0][0]; hoặc char kyTuCuoi = danhSachMonHoc[2].back();
  • Tìm kiếm: size_t viTri = danhSachMonHoc[1].find("C++");
  • Trích xuất chuỗi con: string phanDau = danhSachMonHoc[2].substr(0, 10);

Sức mạnh nằm ở việc bạn coi mỗi phần tử của mảng hoặc vector như một đối tượng string độc lập và áp dụng các phương thức của nó.

#include <iostream>
#include <string>
#include <vector>

int main() {
    vector<string> danhSachThanhPho = {"Ha Noi", "TP Ho Chi Minh", "Da Nang", "Hue"};

    // In ra chuoi thu 2 va do dai cua no
    if (danhSachThanhPho.size() > 1) {
        cout << "Thanh pho thu 2: " << danhSachThanhPho[1]
                  << " (Do dai: " << danhSachThanhPho[1].length() << ")" << endl;
    }

    // Thay doi chuoi cuoi cung
    if (!danhSachThanhPho.empty()) {
        danhSachThanhPho.back() = "Can Tho"; // back() tra ve tham chieu toi phan tu cuoi
    }

    // In lai danh sach sau khi thay doi
    cout << "\nDanh sach thanh pho sau khi thay doi:\n";
    for (const string& tp : danhSachThanhPho) {
        cout << "- " << tp << endl;
    }

    // Tim mot chuoi cu the trong vector
    string thanhPhoCanTim = "Da Nang";
    bool found = false;
    for (const string& tp : danhSachThanhPho) {
        if (tp == thanhPhoCanTim) { // So sanh hai chuoi string
            found = true;
            break;
        }
    }
    if (found) {
        cout << "\nDa tim thay thanh pho: " << thanhPhoCanTim << endl;
    } else {
        cout << "\nKhong tim thay thanh pho: " << thanhPhoCanTim << endl;
    }


    return 0;
}

Giải thích code:

  • Ví dụ này minh họa cách truy cập một phần tử cụ thể trong vector (danhSachThanhPho[1]), sử dụng phương thức .length() của string để lấy độ dài của chuỗi đó.
  • Sử dụng .back() để truy cập (và ở đây là sửa đổi) phần tử cuối cùng của vector.
  • Thực hiện một vòng lặp đơn giản để tìm kiếm một chuỗi cụ thể trong vector bằng cách so sánh trực tiếp các đối tượng string sử dụng toán tử ==.

Bài tập ví dụ: C++ Bài 16.A3: Lời chào Giáng sinh

Lời chào Giáng sinh

FullHouse Dev đang chuẩn bị cho mùa Giáng sinh. Anh ấy biết rằng Giáng sinh được tổ chức vào ngày 25 tháng 12 hàng năm.

Mô tả

Cho một ngày X trong tháng 12. Hãy xác định xem đó có phải là ngày Giáng sinh hay không.

In ra "CHRISTMAS" nếu đó là ngày Giáng sinh. Ngược lại, in ra "ORDINARY".

Input

  • Dòng đầu tiên chứa số nguyên T - số lượng test case.
  • T dòng tiếp theo, mỗi dòng chứa một số nguyên X, biểu thị một ngày trong tháng 12.

Output

Với mỗi test case, in ra trên một dòng mới:

  • "CHRISTMAS" nếu đó là ngày Giáng sinh
  • "ORDINARY" nếu không phải ngày Giáng sinh

Ràng buộc

  • 1 ≤ T ≤ 100
  • 1 ≤ X ≤ 31

Ví dụ

Input:

2 25 12

Output:

CHRISTMAS ORDINARY

Giải thích:
  • Test case 1: Ngày 25 là Giáng sinh, nên in ra "CHRISTMAS".
  • Test case 2: Ngày 12 không phải Giáng sinh, nên in ra "ORDINARY".

Hướng dẫn

  1. Đọc số lượng test case T.
  2. Với mỗi test case:
    • Đọc ngày X
    • Kiểm tra xem X có bằng 25 không
    • Nếu X = 25, in ra "CHRISTMAS"
    • Ngược lại, in ra "ORDINARY"

Hãy giúp FullHouse Dev xác định ngày Giáng sinh! Chào bạn, đây là hướng dẫn giải bài "Lời chào Giáng sinh" bằng C++ một cách ngắn gọn và sử dụng các thành phần chuẩn (std):

Bài toán rất đơn giản: với mỗi ngày X trong tháng 12, kiểm tra xem nó có phải ngày 25 hay không.

Các bước thực hiện:

  1. Bao gồm thư viện cần thiết: Bạn cần thư viện cho việc nhập xuất dữ liệu chuẩn (standard input/output).
  2. Hàm main: Mọi chương trình C++ bắt đầu từ hàm main.
  3. Đọc số lượng test case: Khai báo một biến kiểu số nguyên (ví dụ int) để lưu số lượng test case T. Đọc giá trị của T từ đầu vào chuẩn (sử dụng cin).
  4. Vòng lặp xử lý test case: Sử dụng một cấu trúc lặp (ví dụ: while hoặc for) để thực hiện các bước xử lý cho mỗi test case. Vòng lặp này sẽ chạy đúng T lần. Một cách ngắn gọn để làm vòng lặp T lần là dùng while (T--).
  5. Đọc ngày trong tháng: Bên trong vòng lặp, khai báo một biến kiểu số nguyên (ví dụ int) để lưu ngày X. Đọc giá trị của X từ đầu vào chuẩn (sử dụng cin).
  6. Kiểm tra điều kiện: Sử dụng cấu trúc điều kiện if để kiểm tra xem giá trị của X có bằng 25 hay không.
  7. In kết quả:
    • Nếu điều kiện X == 25 là đúng, sử dụng cout để in ra chuỗi "CHRISTMAS".
    • Ngược lại (sử dụng else), sử dụng cout để in ra chuỗi "ORDINARY".
    • Sau khi in chuỗi, đảm bảo in thêm ký tự xuống dòng (\n hoặc endl) để mỗi kết quả của test case nằm trên một dòng riêng biệt.
  8. Kết thúc hàm main: Trả về 0 để báo hiệu chương trình kết thúc thành công.

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.