C++ std::string

C++ 标准库中的 std::string 是处理字符串的核心类,封装了字符串的存储、管理和操作,相比 C 风格的 char* 更安全、易用。

1、基本概念

1.1 基本特性

std::string 定义在 <string> 头文件中(属于 std 命名空间),本质是对动态字符数组的封装

  • 动态大小:自动扩容,无需手动管理内存(避免 C 风格字符串的缓冲区溢出问题)。
  • 值语义:赋值、传参时默认进行深拷贝(C++11 后支持移动语义,提升性能)。
  • 丰富接口:提供拼接、查找、替换等数十种字符串操作函数。
  • 与 C 兼容 :可通过 c_str() 转换为 C 风格字符串(const char*)。

1.2 与 C 风格字符串的区别

cpp 复制代码
// C 风格字符串
const char* cstr = "Hello";
char cstr_array[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

// C++ std::string
std::string cppstr = "Hello";

主要区别

  • 内存管理std::string 自动管理内存,无需手动分配/释放
  • 安全性std::string 避免缓冲区溢出等安全问题
  • 功能性std::string 提供丰富的成员函数
  • 便利性 :支持运算符重载(+, ==, < 等)

2、基本操作

2.1 构造与初始化

std::string 提供多种构造函数,支持从字符串字面量、字符数组、其他 string 等初始化

cpp 复制代码
#include <string>
#include <iostream>

int main() {
    // 默认构造函数
    std::string s1;
    
    // 从C风格字符串构造
    std::string s2 = "Hello";
    std::string s3("World");
    
    // 从部分C风格字符串构造
    const char* text = "Hello World";
    std::string s4(text, 5); // "Hello"
    std::string s5(text + 6, 5); // "World"
    
    // 重复字符构造
    std::string s6(5, 'A'); // "AAAAA"
    
    // 拷贝构造
    std::string s7 = s2; // "Hello"
    
    // 移动构造 (C++11)
    std::string s8 = std::move(s2); // s2变为有效但未指定状态
    
    // 初始化列表构造 (C++11)
    std::string s9 = {'H', 'e', 'l', 'l', 'o'};
    
    return 0;
}

2.2 赋值操作

cpp 复制代码
std::string s1, s2;

// 赋值操作
s1 = "Hello";           // 从C风格字符串赋值
s2 = s1;                // 从另一个string赋值
s1 = 'A';               // 从单个字符赋值

// assign() 方法
s1.assign("Hello");     // 等同于 s1 = "Hello"
s1.assign("Hello", 3);  // "Hel"
s1.assign(5, 'A');      // "AAAAA"
s1.assign(s2, 1, 3);    // 从s2的下标1开始,取3个字符

// 移动赋值 (C++11)
s1 = std::move(s2);

2.3 访问元素

cpp 复制代码
std::string str = "Hello";

// 使用 [] 运算符(不检查边界)
char c1 = str[0]; // 'H'
str[0] = 'h';     // 修改第一个字符

// 使用 at() 方法(检查边界,越界抛出std::out_of_range)
char c2 = str.at(1); // 'e'
try {
    char c3 = str.at(10); // 抛出异常
} catch (const std::out_of_range& e) {
    std::cerr << "Out of range: " << e.what() << std::endl;
}

// 访问第一个和最后一个字符 (C++11)
char first = str.front(); // 'H'
char last = str.back();   // 'o'

// 获取C风格字符串
const char* cstr = str.c_str();
const char* data = str.data(); // C++17起,与c_str()相同

2.4 容量操作

cpp 复制代码
std::string str = "Hello";

// 大小和容量
std::cout << "Size: " << str.size() << std::endl;     // 5
std::cout << "Length: " << str.length() << std::endl; // 5 (与size()相同)
std::cout << "Capacity: " << str.capacity() << std::endl; // 当前分配的存储空间
std::cout << "Empty: " << str.empty() << std::endl;   // 0 (false)

// 调整容量
str.reserve(100); // 预分配至少100字符的空间
str.shrink_to_fit(); // 请求减少容量以适应大小 (C++11)

// 调整大小
str.resize(3);    // "Hel" (截断)
str.resize(8, '!'); // "Hel!!!!!" (扩展并用'!'填充)

2.5 修改操作

cpp 复制代码
std::string str = "Hello";

// 追加
str.append(" World");     // "Hello World"
str += "!";               // "Hello World!"
str.push_back('!');       // "Hello World!!"

// 插入
str.insert(5, " C++");    // "Hello C++ World!!"

// 删除
str.erase(5, 4);          // "Hello World!!" (删除从位置5开始的4个字符)
str.erase(str.begin() + 5); // 删除单个字符
str.clear();              // 清空字符串

// 替换
str = "Hello World";
str.replace(6, 5, "C++"); // "Hello C++" (从位置6开始,替换5个字符)

// 交换
std::string other = "Other";
str.swap(other);          // str = "Other", other = "Hello C++"

2.6 字符串操作

cpp 复制代码
std::string str = "Hello World";

// 子字符串
std::string sub = str.substr(6, 5); // "World"

// 查找
size_t pos = str.find("World");     // 6
pos = str.find('o');                // 4
pos = str.find('x');                // std::string::npos

// 反向查找
pos = str.rfind('o');               // 7

// 查找首次出现/未出现的字符
pos = str.find_first_of("aeiou");   // 1 ('e')
pos = str.find_first_not_of("Helo "); // 6 ('W')

// 比较
int result = str.compare("Hello");  // > 0 (str > "Hello")
result = str.compare(6, 5, "World"); // 比较子字符串

2.7 输入输出

cpp 复制代码
#include <string>
#include <iostream>
#include <sstream>

int main() {
    // 从标准输入读取
    std::string input;
    std::cout << "Enter a string: ";
    std::getline(std::cin, input);
    
    // 输出到标准输出
    std::cout << "You entered: " << input << std::endl;
    
    // 使用字符串流
    std::stringstream ss;
    ss << "Number: " << 42 << " Pi: " << 3.14;
    std::string result = ss.str();
    std::cout << result << std::endl;
    
    return 0;
}

3、高级特性

3.1 迭代器支持

cpp 复制代码
std::string str = "Hello";

// 使用迭代器遍历
for (auto it = str.begin(); it != str.end(); ++it) {
    std::cout << *it;
}
std::cout << std::endl;

// 反向迭代器
for (auto rit = str.rbegin(); rit != str.rend(); ++rit) {
    std::cout << *rit;
}
std::cout << std::endl;

// 基于范围的for循环 (C++11)
for (char c : str) {
    std::cout << c;
}
std::cout << std::endl;

// 使用算法
#include <algorithm>
std::reverse(str.begin(), str.end()); // "olleH"
std::sort(str.begin(), str.end());    // "hlloe" (注意大小写)

3.2 数值转换

cpp 复制代码
// 字符串到数值
std::string num_str = "123.45";
int i = std::stoi(num_str);           // 123
long l = std::stol(num_str);          // 123
double d = std::stod(num_str);        // 123.45

// 数值到字符串
int value = 42;
double pi = 3.14159;
std::string s1 = std::to_string(value); // "42"
std::string s2 = std::to_string(pi);    // "3.141590"

3.3 字符串视图 (C++17)

当函数只需要只读访问 字符串而不需要拥有其所有权时,使用 std::string_view可以避免构造 std::string带来的拷贝开销,尤其适用于处理子串。

cpp 复制代码
#include <string_view>
void process_string(std::string_view sv) { // 接受 string, string_view, char[] 等
    std::cout << sv.substr(0, 5) << std::endl; // 操作轻量,无拷贝
}

4、底层原理

4.1 内存布局

std::string 通常包含三个核心成员(不同编译器实现可能不同,但逻辑一致):

  • 指向字符数组的指针char*):存储字符串数据(以 \0 结尾,兼容 C 风格)。
  • 大小(size) :当前字符串的字符数(不含 \0)。
  • 容量(capacity) :当前内存可容纳的最大字符数(不含 \0)。

4.2 扩容机制

当字符串需要增长(如拼接、插入)且 size 超过 capacity 时,std::string 会自动扩容:

  1. 分配一块更大的内存(通常是当前 capacity 的 1.5 倍或 2 倍,取决于编译器)。
  2. 将原数据拷贝到新内存。
  3. 释放旧内存,更新指针。

注意:扩容会导致迭代器、指针、引用失效(因为内存地址改变)。

4.3. 小字符串优化(SSO,Small String Optimization)

为提升短字符串性能,现代编译器(如 GCC、Clang)对 std::string 实现了 SSO:

  • 当字符串较短(如长度 ≤ 15 字符)时,数据直接存储在 std::string 对象内部(无需动态分配内存)。
  • 当字符串较长时,才使用动态内存分配(通过指针指向堆内存)。

优势:避免短字符串的动态内存分配开销(malloc/free 成本),提升性能。

5、常见问题

  1. std::stringsize()capacity() 有什么区别?

    • size():返回字符串当前包含的字符数(实际存储的有效字符,不含结尾 \0)。

    • capacity():返回当前已分配内存可容纳的最大字符数(不包含 \0),即无需扩容可存储的最大字符数。
      举例 :若 string s = "abc",则 s.size() = 3s.capacity() 可能为 15(GCC 下 SSO 对短字符串的默认容量)。

  2. reserve(n)resize(n) 的区别?

    • reserve(n):仅调整 capacity(至少为 n),不改变 size(字符串内容不变)。用于预分配内存,避免后续操作频繁扩容。
    • resize(n):调整size为n(改变字符串长度):
      • n > 当前 size:用指定字符(默认 \0)填充新增位置。
      • n < 当前 size:截断字符串至前 n 个字符。
        使用场景reserve 用于性能优化(预分配),resize 用于实际修改字符串长度。
  3. std::string 的拷贝构造函数是深拷贝还是浅拷贝?为什么?

    是深拷贝。std::string 封装了动态内存,拷贝构造时会复制底层字符数组的内容(而非仅复制指针),确保两个字符串相互独立(修改一个不会影响另一个)。

    cpp 复制代码
    string a = "hello";
    string b = a; // 深拷贝:b 拥有独立的 "hello" 副本
    b[0] = 'H'; 
    cout << a << endl; // 仍为 "hello"(不受 b 影响)

    C++11 优化 :若源字符串是临时对象(右值),会触发移动构造(string(string&&)),直接接管源字符串的内存(避免拷贝,提升性能)。

  4. 什么是小字符串优化(SSO)?其目的是什么?

    SSO 是 std::string 的一种优化策略:对于短字符串(如长度 ≤ 15),数据直接存储在 std::string 对象内部(栈内存),无需分配堆内存;长字符串才使用堆内存。

    目的 :减少短字符串的内存分配开销(堆内存的 malloc/free 成本较高),提升访问速度(栈内存访问比堆内存快)。

    实现std::string 对象内部预留一块固定大小的缓冲区(如 16 字节),短字符串直接存在这里;长字符串则用指针指向堆内存。

  5. 如何将 std::string 转换为 C 风格字符串(const char*)?为什么需要这样做?

    通过 c_str()data() 方法(C++11 后二者等价),返回指向字符串的 const char* 指针(以 \0 结尾)。

    原因 :很多 legacy 函数(如 C 库函数 printf、系统调用 open 等)要求传入 C 风格字符串(const char*),c_str() 提供了兼容性接口。

    注意 :返回的指针有效期与 std::string 一致,若字符串被修改(如扩容、赋值),指针可能失效。

  6. std::string 是线程安全的吗?

    不是。C++ 标准规定,多个线程同时读写同一个 std::string 对象会导致未定义行为(如数据竞争)。

    线程安全场景

    • 多个线程同时读取同一个 const std::string 对象(安全)。
    • 多个线程操作不同的 std::string 对象(安全)。
  7. std::stringoperator[]at() 有什么区别?

    • 越界检查:operator[] 不做越界检查(越界访问导致未定义行为,可能崩溃);at() 会做越界检查,越界时抛出 out_of_range 异常。
    • 性能:operator[] 略快(无检查开销);at() 因检查略慢,但更安全。
      使用场景 :确定索引有效时用 [](追求性能);索引可能无效时用 at()(需捕获异常)。
  8. 为什么 std::stringlength()size() 是等价的?

    历史原因。std::string 起源于早期 C++ 的 basic_string 模板,size() 是容器类的通用接口(如 vector::size()),而 length() 是为了与 C 风格字符串的 strlen() 保持命名习惯。为兼容两种习惯,标准规定二者等价,返回值相同。

  9. 什么情况下使用 std::string::reserve()

    当你知道字符串将增长到特定大小时,使用 reserve() 预分配足够的内存,可以避免多次重新分配和拷贝,提高性能。

  10. 为什么 std::string 的拼接操作可能效率低下?

    多次使用 +=+ 进行拼接可能导致多次内存重新分配。更高效的方式是使用 reserve() 预分配足够空间,或者使用 std::ostringstream

    cpp 复制代码
    // 低效的方式
    std::string result;
    for (int i = 0; i < 1000; ++i) {
        result += "string"; // 可能导致多次重新分配
    }
    
    // 高效的方式
    std::string result;
    result.reserve(6000); // 预分配足够空间
    for (int i = 0; i < 1000; ++i) {
        result += "string";
    }
    
    // 或者使用 ostringstream
    std::ostringstream oss;
    for (int i = 0; i < 1000; ++i) {
        oss << "string";
    }
    std::string result = oss.str();
  11. std::string_view 是什么?有什么优势?

    • std::string_view (C++17) 是一个非拥有式的字符串视图
    • 它提供对字符串数据的只读访问,不管理内存
    • 优势:避免不必要的字符串拷贝,提高性能
    • 注意:必须确保底层字符串的生命周期比 string_view
  12. 移动语义如何影响 std::string

    • C++11 引入移动语义,允许高效地转移字符串所有权
    • 移动操作通常只是复制指针和大小信息,时间复杂度为 O(1)
    • 这大大提高了返回字符串和传递字符串的性能
相关推荐
感哥18 小时前
C++ 面向对象
c++
沐怡旸20 小时前
【底层机制】std::shared_ptr解决的痛点?是什么?如何实现?如何正确用?
c++·面试
感哥1 天前
C++ STL 常用算法
c++
saltymilk2 天前
C++ 模板参数推导问题小记(模板类的模板构造函数)
c++·模板元编程
感哥2 天前
C++ lambda 匿名函数
c++
沐怡旸2 天前
【底层机制】std::unique_ptr 解决的痛点?是什么?如何实现?怎么正确使用?
c++·面试
感哥2 天前
C++ 内存管理
c++
博笙困了2 天前
AcWing学习——双指针算法
c++·算法
感哥2 天前
C++ 指针和引用
c++