Bài 28.1: Khái niệm và sử dụng chuỗi ký tự trong C++

Chào mừng trở lại với loạt bài blog về C++! Hôm nay, chúng ta sẽ đào sâu vào một khái niệm cực kỳ quan trọng và được sử dụng rất thường xuyên trong lập trình: chuỗi ký tự (strings). Trong C++, làm việc với chuỗi ký tự trở nên mạnh mẽlinh hoạt hơn rất nhiều nhờ vào lớp string được cung cấp trong thư viện chuẩn.

Bài này sẽ giúp bạn hiểu rõ:

  • Chuỗi ký tự là gì trong ngữ cảnh C++.
  • Tại sao nên sử dụng string thay vì mảng ký tự kiểu C.
  • Cách khai báo và khởi tạo string.
  • Cách nhập và xuất chuỗi sử dụng cin, coutgetline.
  • Các thao tác cơ bản và phổ biến với string.

Hãy cùng bắt đầu nhé!

Chuỗi ký tự là gì?

Về cơ bản, chuỗi ký tự là một dãy các ký tự được sắp xếp theo một thứ tự nhất định. Ví dụ: "Xin chào", "C++ thật tuyệt", "12345".

Trong C++ truyền thống (tức là C-style strings), chuỗi được biểu diễn bằng một mảng các ký tự kết thúc bằng ký tự null (\0). Ví dụ: char greeting[] = "Hello"; thực chất là một mảng chứa các ký tự 'H', 'e', 'l', 'l', 'o', '\0'.

Tuy nhiên, cách tiếp cận này có một số nhược điểm:

  • Quản lý bộ nhớ thủ công, dễ gây ra lỗi tràn bộ đệm (buffer overflow).
  • Các thao tác như nối chuỗi, sao chép, tìm kiếm... yêu cầu sử dụng các hàm riêng biệt (strcpy, strcat, strlen, etc.) và thường phức tạp.
  • Không có tính năng tự động thay đổi kích thước.

Để giải quyết những vấn đề này, C++ hiện đại cung cấp lớp **string** như một phần của Thư viện Chuẩn (Standard Library). string là một đối tượng linh hoạt hơn rất nhiều, nó tự động quản lý bộ nhớ, cung cấp nhiều phương thức tiện lợi để làm việc với chuỗi.

Tại sao sử dụng string?

Như đã nói ở trên, string mang lại nhiều lợi ích so với C-style strings:

  • Quản lý bộ nhớ tự động: Bạn không cần lo lắng về việc cấp phát hay giải phóng bộ nhớ. string sẽ tự động thay đổi kích thước khi cần thiết.
  • An toàn hơn: Giảm thiểu rủi ro tràn bộ đệm vì đối tượng biết kích thước của nó.
  • Dễ sử dụng: Cung cấp các toán tử và phương thức trực quan cho các thao tác phổ biến (nối chuỗi dùng +, gán dùng =, lấy độ dài dùng .length() hoặc .size()).
  • Giàu tính năng: Có sẵn nhiều phương thức để tìm kiếm, trích xuất, chèn, xóa ký tự/chuỗi con...

Hầu hết thời gian làm việc với chuỗi trong C++, bạn sẽ muốn sử dụng string. Để sử dụng nó, bạn cần bao gồm header <string>.

#include <iostream>
#include <string> // Quan trọng: cần include header này!

int main() {
    // Bây giờ bạn có thể sử dụng string
    string myString = "Chào thế giới!";
    cout << myString << endl;
    return 0;
}

Giải thích:

  • Dòng **#include <string>** là bắt buộc để có thể sử dụng lớp string.
  • Chúng ta khai báo một biến kiểu string tên là myString và gán cho nó giá trị "Chào thế giới!".
  • Sử dụng cout để in chuỗi ra màn hình, giống như cách in các kiểu dữ liệu cơ bản khác.

Khai báo và Khởi tạo string

Có nhiều cách để khai báo và khởi tạo một đối tượng string:

  1. Khai báo rỗng:

    #include <iostream>
    #include <string>
    
    int main() {
        string emptyString; // Một chuỗi rỗng
        cout << "Chuoi rong: '" << emptyString << "'" << endl;
        return 0;
    }
    

    Giải thích: Biến emptyString được tạo ra nhưng chưa chứa ký tự nào.

  2. Khởi tạo từ một chuỗi ký tự cố định (string literal):

    #include <iostream>
    #include <string>
    
    int main() {
        string greeting = "Xin chào C++"; // Khởi tạo trực tiếp
        string another = string("Thật thú vị!"); // Có thể dùng constructor rõ ràng
        cout << greeting << endl;
        cout << another << endl;
        return 0;
    }
    

    Giải thích: Cách phổ biến nhất, gán trực tiếp một chuỗi literal cho biến string.

  3. Khởi tạo từ một chuỗi string khác:

    #include <iostream>
    #include <string>
    
    int main() {
        string original = "Sao chep toi!";
        string copied = original; // Sao chép nội dung
        string alsoCopied(original); // Cũng là sao chép
        cout << "Ban dau: " << original << endl;
        cout << "Sao chep: " << copied << endl;
        cout << "Cung sao chep: " << alsoCopied << endl;
        return 0;
    }
    

    Giải thích: Tạo một chuỗi mới là bản sao của một chuỗi string đã tồn tại.

  4. Khởi tạo với N ký tự lặp lại:

    #include <iostream>
    #include <string>
    
    int main() {
        string stars(10, '*'); // Chuỗi gồm 10 ký tự '*'
        string dashes(5, '-');
        cout << stars << endl;
        cout << dashes << endl;
        return 0;
    }
    

    Giải thích: Tạo một chuỗi có độ dài xác định, chứa lặp đi lặp lại một ký tự cụ thể.

Nhập xuất chuỗi (cin, cout, getline)

Tương tác với người dùng là một phần cốt lõi của hầu hết các chương trình. Với string, việc nhập xuất cũng rất thuận tiện.

  • cout: Dùng để in chuỗi ra màn hình. Như bạn đã thấy ở các ví dụ trên.
  • cin: Dùng để đọc chuỗi từ bàn phím. Tuy nhiên, cin dừng đọc khi gặp ký tự khoảng trắng (space, tab, newline). Điều này có nghĩa nó chỉ đọc được một từ duy nhất.
  • getline: Dùng để đọc toàn bộ một dòng từ luồng nhập, bao gồm cả khoảng trắng, cho đến khi gặp ký tự xuống dòng (\n). Đây là cách phổ biến để đọc các câu hoặc đoạn văn bản có chứa khoảng trắng.

Hãy xem ví dụ:

#include <iostream>
#include <string>

int main() {
    string word;
    cout << "Nhap mot tu: ";
    cin >> word; // Chi doc mot tu
    cout << "Tu ban vua nhap: " << word << endl;

    // Problem: cin de lai ky tu newline trong bo dem
    // Neu dung cin >> tiep theo se bi loi.
    // Neu dung getline() ngay sau cin >> se bi loi.
    // Can xoa bo dem nhap
    cin.ignore(1000, '\n'); // Xoa toi da 1000 ky tu hoac den khi gap newline

    string line;
    cout << "Nhap mot dong van ban: ";
    getline(cin, line); // Doc ca dong
    cout << "Dong ban vua nhap: " << line << endl;

    return 0;
}

Giải thích:

  • Ví dụ đầu tiên với **cin >> word** chỉ đọc từ đầu tiên bạn gõ. Nếu bạn nhập "lap trinh C++", word sẽ chỉ là "lap".
  • Sau khi dùng cin >>, ký tự xuống dòng (Enter) bạn nhấn vẫn còn lại trong bộ đệm nhập. Nếu bạn gọi getline ngay lập tức, getline sẽ đọc ký tự xuống dòng đó và coi như đã đọc xong một dòng rỗng.
  • **cin.ignore(1000, '\n')** là một cách để "xóa" hoặc bỏ qua các ký tự còn lại trong bộ đệm nhập, lên đến 1000 ký tự hoặc cho đến khi gặp ký tự \n. Điều này giúp getline tiếp theo hoạt động đúng.
  • **getline(cin, line)** đọc toàn bộ dòng văn bản bạn nhập cho đến khi bạn nhấn Enter và lưu vào biến line.

Mẹo: Khi bạn trộn lẫn giữa cin >> (đọc số, ký tự, từ) và getline (đọc dòng), luôn luôn cân nhắc việc sử dụng cin.ignore() sau cin >> để tránh lỗi đọc dòng rỗng với getline. Nếu bạn chỉ cần đọc dòng, hãy dùng getline một cách nhất quán.

Các Thao tác Cơ bản với string

string cung cấp rất nhiều phương thức và toán tử để làm việc với chuỗi. Dưới đây là một số thao tác phổ biến nhất:

Lấy độ dài chuỗi

Bạn có thể dùng .length() hoặc .size(). Chúng thường trả về cùng một giá trị.

#include <iostream>
#include <string>

int main() {
    string text = "Hello C++";
    cout << "Do dai cua chuoi: " << text.length() << endl;
    cout << "Kich thuoc cua chuoi: " << text.size() << endl; // size() cung tuong tu
    return 0;
}

Giải thích: Cả length()size() đều trả về số lượng ký tự trong chuỗi.

Truy cập ký tự theo chỉ số

Bạn có thể truy cập từng ký tự trong chuỗi bằng toán tử [] hoặc phương thức .at(). Chỉ số bắt đầu từ 0.

#include <iostream>
#include <string>

int main() {
    string word = "Lap trinh";
    cout << "Ky tu dau tien: " << word[0] << endl; // 'L'
    cout << "Ky tu thu ba: " << word.at(2) << endl; // 'p'

    // Can than: truy cap ngoai chi so bang [] co the gay loi chuong trinh
    // cout << word[100] << endl; // Danger!
    // truy cap ngoai chi so bang .at() se throw exception
    try {
        cout << word.at(100) << endl; // An toan hon, se bao loi
    } catch (const out_of_range& oor) {
        cerr << "Loi: Truy cap ngoai pham vi: " << oor.what() << endl;
    }

    return 0;
}

Giải thích:

  • word[0] truy cập ký tự ở vị trí đầu tiên (chỉ số 0).
  • word.at(2) truy cập ký tự ở vị trí thứ ba (chỉ số 2).
  • Sử dụng [] nhanh hơn nhưng không kiểm tra xem chỉ số có hợp lệ không. Nếu chỉ số nằm ngoài phạm vi, chương trình có thể bị lỗi không mong muốn.
  • Sử dụng .at() an toàn hơn vì nó kiểm tra chỉ số. Nếu chỉ số không hợp lệ, nó sẽ ném ra một ngoại lệ (out_of_range), giúp bạn bắt và xử lý lỗi một cách rõ ràng.
Nối chuỗi (Concatenation)

Sử dụng toán tử + hoặc phương thức .append().

#include <iostream>
#include <string>

int main() {
    string part1 = "Hello";
    string part2 = " World";

    string combined = part1 + part2; // Dung toan tu +
    cout << "Chuoi sau khi noi (+): " << combined << endl;

    string sentence = "C++ ";
    sentence.append("rat "); // Noi them chuoi
    sentence.append("tuyet!");
    cout << "Chuoi sau khi noi (.append()): " << sentence << endl;

    return 0;
}

Giải thích:

  • Toán tử + tạo ra một chuỗi mới là kết quả nối của hai chuỗi.
  • Phương thức .append() nối thêm nội dung vào cuối chuỗi hiện có, thay đổi trực tiếp chuỗi đó.
So sánh chuỗi

Sử dụng các toán tử so sánh (==, !=, <, >, <=, >=) hoặc phương thức .compare(). Phép so sánh dựa trên thứ tự từ điển (lexicographical order).

#include <iostream>
#include <string>

int main() {
    string s1 = "apple";
    string s2 = "banana";
    string s3 = "apple";

    if (s1 == s3) { // So sanh bang nhau
        cout << s1 << " bang " << s3 << endl;
    }

    if (s1 != s2) { // So sanh khac nhau
        cout << s1 << " khac " << s2 << endl;
    }

    if (s1 < s2) { // So sanh nho hon (theo tu dien)
        cout << s1 << " nho hon " << s2 << endl;
    }

    // Su dung .compare()
    // Tra ve 0 neu bang nhau
    // Tra ve < 0 neu chuoi goi nho hon chuoi doi so
    // Tra ve > 0 neu chuoi goi lon hon chuoi doi so
    if (s1.compare(s3) == 0) {
        cout << s1 << ".compare(" << s3 << ") tra ve 0" << endl;
    }

    if (s1.compare(s2) < 0) {
         cout << s1 << ".compare(" << s2 << ") tra ve < 0" << endl;
    }

    return 0;
}

Giải thích:

  • Các toán tử so sánh rất trực quan và dễ dùng cho các phép so sánh bằng, khác, lớn hơn, nhỏ hơn.
  • Phương thức .compare() cung cấp kiểm soát chi tiết hơn và có thể so sánh các phần của chuỗi, nhưng thường ít được dùng cho các phép so sánh đơn giản. Kết quả trả về của nó cho biết mối quan hệ thứ tự.
Các Phương thức hữu ích khác

string có rất nhiều phương thức mạnh mẽ. Dưới đây là một vài cái tên đáng chú ý:

  • empty(): Kiểm tra xem chuỗi có rỗng không (không chứa ký tự nào).
  • clear(): Xóa tất cả ký tự, làm cho chuỗi trở nên rỗng.
  • find(substring): Tìm vị trí xuất hiện đầu tiên của một chuỗi con. Trả về string::npos nếu không tìm thấy.
  • substr(pos, len): Trích xuất một chuỗi con từ vị trí pos với độ dài len.
  • push_back(char): Thêm một ký tự vào cuối chuỗi.
  • pop_back(): Xóa ký tự cuối cùng của chuỗi (C++11 trở lên).
#include <iostream>
#include <string>

int main() {
    string s = "Hoc C++ that vui!";

    // empty() va clear()
    if (!s.empty()) { // Kiem tra khong rong
        cout << "'" << s << "' khong rong." << endl;
        s.clear(); // Xoa chuoi
        cout << "Sau khi clear, chuoi rong? " << (s.empty() ? "Co" : "Khong") << endl;
    }

    s = "Day la mot chuoi de tim kiem.";

    // find()
    string keyword = "tim";
    size_t pos = s.find(keyword); // Tim vi tri cua "tim"

    if (pos != string::npos) { // string::npos la gia tri khi khong tim thay
        cout << "Tim thay '" << keyword << "' tai vi tri: " << pos << endl;
    } else {
        cout << "Khong tim thay '" << keyword << "'" << endl;
    }

    // substr()
    string original = "Lap trinh C++ rat hay";
    // trich xuat tu vi tri 10 (chu 'C') voi do dai 3 ky tu
    string sub = original.substr(10, 3);
    cout << "Chuoi con trich xuat: " << sub << endl; // Ket qua: C++

    // push_back() va pop_back()
    string dynamic_str = "abc";
    dynamic_str.push_back('d'); // Them 'd' vao cuoi
    cout << "Sau khi push_back('d'): " << dynamic_str << endl; // abcd
    dynamic_str.pop_back(); // Xoa ky tu cuoi 'd'
    cout << "Sau khi pop_back(): " << dynamic_str << endl; // abc

    return 0;
}

Giải thích:

  • empty() trả về true nếu chuỗi không có ký tự nào, false nếu ngược lại.
  • clear() làm cho chuỗi trở thành rỗng.
  • find() tìm kiếm chuỗi con và trả về chỉ số bắt đầu của nó. Nếu không tìm thấy, nó trả về một giá trị đặc biệt là string::npos.
  • substr(pos, len) trả về một chuỗi mới là một phần của chuỗi gốc, bắt đầu từ chỉ số pos và có độ dài len.
  • push_back() thêm một ký tự đơn vào cuối chuỗi.
  • pop_back() xóa ký tự cuối cùng.

C-style Strings vs string: Tóm tắt

Mặc dù C-style strings vẫn tồn tại trong C++ và đôi khi cần thiết khi làm việc với các thư viện cũ hoặc API cấp thấp, stringlựa chọn ưu tiên cho hầu hết các tác vụ lập trình hiện đại. Nó cung cấp sự an toàn, tiện lợi và linh hoạt mà C-style strings không có.

Khi cần chuyển đổi một string sang C-style string (ví dụ để dùng với hàm C như printf hoặc các hàm cần const char*), bạn có thể sử dụng phương thức .c_str().

#include <iostream>
#include <string>
#include <cstdio> // Can cho printf

int main() {
    string cppString = "Day la string";

    // Chuyen doi sang C-style string de su dung voi printf
    const char* cString = cppString.c_str();

    printf("Su dung C-style string voi printf: %s\n", cString);

    return 0;
}

Giải thích: Phương thức .c_str() trả về một con trỏ const char* trỏ đến dữ liệu ký tự bên trong string, kết thúc bằng ký tự null \0. Giá trị trả về này chỉ hợp lệ cho đến khi chuỗi string bị thay đổi hoặc bị hủy.

Bài tập ví dụ: C++ Bài 16.A1: Wordle phiên bản FullHouse Dev

Wordle phiên bản FullHouse Dev

FullHouse Dev đã phát minh ra một phiên bản Wordle được sửa đổi.

Mô tả bài toán

Có một từ ẩn S và một từ đoán T, cả hai đều có độ dài 5.

FullHouse Dev định nghĩa một chuỗi M để xác định độ chính xác của từ đoán. Đối với chỉ số thứ i:

  • Nếu ký tự đoán ở vị trí thứ i đúng, ký tự thứ i của M là G.
  • Nếu ký tự đoán ở vị trí thứ i sai, ký tự thứ i của M là B.

Cho từ ẩn S và từ đoán T, hãy xác định chuỗi M.

Input

  • Dòng đầu tiên chứa T, số lượng test case. Sau đó là các test case.
  • Mỗi test case gồm hai dòng:
    • Dòng đầu chứa chuỗi S - từ ẩn.
    • Dòng thứ hai chứa chuỗi T - từ đoán.

Output

Với mỗi test case, in ra giá trị của chuỗi M.

Bạn có thể in mỗi ký tự của chuỗi bằng chữ hoa hoặc chữ thường.

Ràng buộc

  • 1 ≤ T ≤ 1000
  • |S| = |T| = 5
  • S, T chỉ chứa các chữ cái tiếng Anh in hoa.

Ví dụ

Input:
3
ABCDE
EDCBA
ROUND
RINGS
START
STUNT
Output:
BBGBB
GBBBB
GGBBG

Giải thích

Test Case 1:

S = ABCDE và T = EDCBA. Chuỗi M là:

  • A ≠ E, nên M[1] = B
  • B ≠ D, nên M[2] = B
  • C = C, nên M[3] = G
  • D ≠ B, nên M[4] = B
  • E ≠ A, nên M[5] = B Vậy M = BBGBB. Tuyệt vời! Đây là hướng dẫn giải bài Wordle phiên bản FullHouse Dev bằng C++, tập trung vào tư duy và sử dụng các công cụ chuẩn của C++ (std), không đưa ra code hoàn chỉnh.

Phân tích bài toán:

  1. Chúng ta cần xử lý nhiều test case (T).
  2. Mỗi test case có hai chuỗi input: S (ẩn) và T (đoán), cả hai đều có độ dài chính xác là 5.
  3. Cần tạo một chuỗi kết quả M có độ dài 5.
  4. Quy tắc tạo M: So sánh ký tự tại cùng một vị trí i trong ST.
    • Nếu S[i] == T[i], ký tự thứ i của M là 'G'.
    • Nếu S[i] != T[i], ký tự thứ i của M là 'B'.
  5. In chuỗi M cho mỗi test case.

Hướng dẫn giải bằng C++:

  1. Bao gồm các thư viện cần thiết:

    • Bạn sẽ cần thư viện để xử lý nhập/xuất (<iostream>).
    • Bạn sẽ cần thư viện để làm việc với chuỗi ký tự (<string>).
  2. Hàm main: Đây là điểm bắt đầu của chương trình C++.

  3. Đọc số lượng test case:

    • Khai báo một biến nguyên để lưu số lượng test case (ví dụ: int T;).
    • Sử dụng cin >> T; để đọc giá trị này.
  4. Vòng lặp xử lý test case:

    • Sử dụng một vòng lặp while hoặc for để chạy đúng T lần. Một cách phổ biến và ngắn gọn là while (T--). Mỗi lần lặp, giá trị của T sẽ giảm đi 1 và vòng lặp tiếp tục miễn là T lớn hơn 0.
  5. Bên trong vòng lặp xử lý test case:

    • Đọc chuỗi S và T: Khai báo hai đối tượng string để lưu chuỗi ẩn và chuỗi đoán (ví dụ: string S, T;). Sử dụng cin >> S;cin >> T; để đọc chúng.
    • Tạo chuỗi kết quả M: Khai báo một đối tượng string để xây dựng chuỗi kết quả M (ví dụ: string M = "";). Bạn có thể bắt đầu với một chuỗi rỗng.
    • Vòng lặp so sánh ký tự: Vì độ dài của S và T luôn là 5, sử dụng một vòng lặp for chạy từ chỉ số 0 đến 4 (tổng cộng 5 lần). Ví dụ: for (int i = 0; i < 5; ++i).
    • So sánh và xây dựng M: Bên trong vòng lặp for:
      • Sử dụng câu lệnh if để so sánh ký tự tại vị trí i của ST. S[i] sẽ truy cập ký tự thứ i+1 (do chỉ số bắt đầu từ 0) của chuỗi S.
      • Nếu S[i] == T[i], thêm ký tự 'G' vào cuối chuỗi M. Sử dụng toán tử +=.
      • Nếu S[i] != T[i], thêm ký tự 'B' vào cuối chuỗi M. Sử dụng toán tử +=.
    • In chuỗi kết quả M: Sau khi vòng lặp for kết thúc (đã xây dựng xong chuỗi M), sử dụng cout << M << endl; để in chuỗi M theo sau là một ký tự xuống dòng. endl đảm bảo mỗi kết quả test case nằm trên một dòng riêng biệt.
  6. Kết thúc chương trình: Hàm main thường trả về 0 để báo hiệu chương trình chạy thành công.

Tóm tắt luồng xử lý:

Đọc T
Trong khi T > 0:
  Đọc chuỗi S
  Đọc chuỗi T
  Khởi tạo chuỗi kết quả M rỗng
  Lặp từ i = 0 đến 4:
    Nếu S[i] == T[i]:
      Thêm 'G' vào M
    Ngược lại:
      Thêm 'B' vào M
  In M
  Giảm T đi 1

Lưu ý:

  • Sử dụng cincout để nhập xuất.
  • Sử dụng string để làm việc với các chuỗi.
  • Tận dụng việc độ dài chuỗi là cố định (= 5) để dùng vòng lặp for với giới hạn rõ ràng (0 đến 4).
  • Đảm bảo in ký tự xuống dòng (endl) sau mỗi chuỗi kết quả M.

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

Comments

There are no comments at the moment.