Hướng dẫn chi tiết về std::string trong C++

Tổng quan về std::string

Khi phát triển phần mềm bằng C++, việc quản lý chuỗi ký tự là một tác vụ phổ biến. Trong ngôn ngữ C truyền thống, chuỗi được biểu diễn dưới dạng mảng ký tự kết thúc bằng ký tự NULL (\0). Việc xử lý trực tiếp trên mảng này thường yêu cầu lập trình viên tự quản lý dung lượng bộ nhớ, dễ dẫn đến lỗi tràn biên hoặc quên giải phóng tài nguyên.

Thư viện tiêu chuẩn của C++ cung cấp lớp std::string nhằm giải quyết các vấn đề này. Về bản chất, đây là một kiểu dữ liệu động chứa dãy các ký tự (char), kế thừa đặc tính của các cấu trúc dữ liệu danh sách tuần tự. Điều này cho phép thực hiện các thao tác phức tạp như nối chuỗi, xóa ký tự, tìm kiếm mà không cần lo lắng về giới hạn chiều dài ban đầu.

(Biểu đồ minh họa cấu trúc bộ nhớ nội tại của string)

Một lưu ý quan trọng liên quan đến bảng mã (encoding): lớp string hoạt động dựa trên đơn vị byte. Khi làm việc với các chuẩn đa字节 như UTF-8, các hàm đo kích thước (như size()) sẽ trả về số lượng byte chiếm dụng chứ không phải số lượng ký tự hiển thị trên màn hình.

Các phương thức khởi tạo

Lớp string hỗ trợ nhiều cách khởi tạo khác nhau thông qua các hàm xây dựng (constructors). Dưới đây là các trường hợp sử dụng thường gặp:

Khai báo mặc định và từ chuỗi

#include <iostream>
#include <string>
using namespace std;

int main() {
    string defaultStr;           // Mảng rỗng, độ dài 0
    
    const char* cStr = "Hello World";
    string strFromChar(cStr);    // Khởi tạo từ chuỗi C

    cout << "Length: " << strFromChar.length() << endl;
    return 0;
}

Khởi tạo từ đoạn văn bản hoặc ký tự

Bạn cũng có thể lấy một phân đoạn cụ thể từ một chuỗi nguồn để tạo đối tượng mới, hoặc tạo chuỗi gồm n ký tự giống nhau:

string source = "ABCDEFGHIJ";

// Lấy chuỗi con bắt đầu từ index 3, dài 4 ký tự ('DEFG')
string subPart(source, 3, 4); 

// Tạo chuỗi có 5 ký tự '*'
string repeatStr(5, '*');

Thao tác gán và truy cập dữ liệu

Việc gán giá trị cho biến string sử dụng toán tử =. C++ cung cấp quá tải toán tử để chấp nhận cả đối tượng string và chuỗi ký tự.

string dest1, dest2;
dest1 = dest2;         // Gán string
dest1 = "New Data";    // Gán chuỗi ký tự

// Lưu ý: Phân biệt giữa khởi tạo (Constructor) và gán (Assignment)
// String s = "Init"; gọi Constructor
// String s; s = "Init"; gọi operator=

Để truy cập từng ký tự, bạn có hai lựa chọn chính:

  • Toán tử []: Truy cập nhanh, tương tự mảng. Tuy nhiên, nếu chỉ số vượt quá phạm vi cho phép, chương trình sẽ crash.
  • Hàm at(): Cung cấp khả năng kiểm tra an toàn. Nếu chỉ số ngoài tầm, nó sẽ ném ngoại lệ (exception).

Quản lý kích thước và sức chứa

Có hai khái niệm cần phân biệt: kích thước hiện tại (size()/length()) và sức chứa tối đa chưa dùng (capacity()).

  • length()size(): Đếm số ký tự thực tế đang lưu trữ. Hai hàm này hoàn toàn tương đương trong std::string.
  • capacity(): Số lượng byte bộ nhớ đã được phân bổ sẵn để chứa ký tự, chưa chắc đã đầy.

Để tối ưu hiệu năng khi biết trước kích thước dự kiến, hãy sử dụng reserve(). Hàm này mở rộng vùng nhớ nhưng không thay đổi size. Ngược lại, resize(n) sẽ điều chỉnh kích thước dữ liệu lên/n xuống n, phần dư được điền vào bằng ký tự rỗng hoặc giá trị mặc định.

string buffer;
buffer.reserve(1024); // Cấp phát sẵn 1KB, tăng tốc độ thêm dữ liệu sau này
buffer.push_back('A'); // Không gây tái cấp phát ngay lập tức
buffer.push_back('B');

Lặp lại với Iterator

Iterator trong C++ đóng vai trò như con trỏ giúp duyệt qua các phần tử trong container. Với std::string, iterator thực chất là con trỏ địa chỉ tới ký tự.

Cú pháp cơ bản bao gồm begin() (đầu chuỗi) và end() (vị trí sau cùng).

string message = "Test Code";
for (auto it = message.begin(); it != message.end(); ++it) {
    cout << *it << " ";
}

Đối với các đối tượng không cho phép sửa đổi (const objects), cần sử dụng cbegin()cend(). Ngoài ra, từ C++11 trở đi, cú pháp vòng lặp phạm vi (range-based for) được khuyến khích vì ngắn gọn hơn:

string text = "Example";
for (char ch : text) {
    if (ch == 'x') continue; 
    cout << ch;
}

Thao tác Nối và Chèn/Xóa

Có ba cách chính để thêm dữ liệu vào cuối chuỗi:

  1. Nạp thêm ký tự đơn: push_back()
  2. Gọi hàm append() với nhiều dạng tham số
  3. Sử dụng toán tử += (Đơn giản nhất)
string path;
path += "/usr";
path += "/bin";
// Hoặc
path.append("/home");

Khi cần chèn dữ liệu vào giữa hoặc xóa bớt đoạn văn bản, insert()erase() là hai công cụ mạnh mẽ. Tuy nhiên, lưu ý rằng hai hàm này có thể tốn kém hiệu năng do buộc phải dịch chuyển các byte ký tự còn lại trong bộ nhớ.

string line = "abcdef";
line.insert(3, "123");    // Kết quả: abc123def
line.erase(4, 3);         // Xóa từ index 4, 3 ký tự

Tìm kiếm và Thay thế

Hệ thống tìm kiếm của string khá linh hoạt, hỗ trợ tìm kiếm theo chuỗi con, ký tự đơn hoặc vị trí cụ thể.

  • find(str, pos): Tìm vị trí đầu tiên xuất hiện của str từ vị trí pos. Trả về string::npos nếu không tìm thấy.
  • rfind(...): Duyệt ngược từ đuôi về đầu.
  • find_first_of: Tìm ký tự bất kỳ thuộc tập hợp chỉ định xuất hiện sớm nhất.
// Thay thế chuỗi cũ bằng chuỗi mới
string config = "mode=debug";
config.replace(6, 5, "release"); // Thay "debug" bằng "release"

Chuyển đổi và Nhập dữ liệu

Trong quá trình giao diện người dùng hoặc đọc file, đôi khi cần chuyển đổi qua lại giữa các kiểu dữ liệu.

  • Convert sang C-Style String: Dùng c_str() để lấy dữ liệu dạng const char* phục vụ cho các thư viện C.
  • Số sang Chuỗi: Dùng hàm tĩnh std::to_string().
  • Đọc dòng nhập liệu: Thay vì dùng cin >> s (dừng ở khoảng trắng), hãy dùng std::getline(cin, s) để đọc trọn vẹn dòng bao gồm cả dấu cách.
#include <algorithm> // Cần cho stoi, to_string
#include <sstream>

int number = 123;
string numStr = to_string(number);

string inputLine;
cout << "Nhập dữ liệu: ";
getline(cin, inputLine); // Đọc cả câu văn có dấu cách

Thẻ: c++11 std-string stl-containers dynamic-memory

Đăng vào ngày 8 tháng 6 lúc 21:13