深入浅出 C++ string 类:从原理到实战

1. 为什么学习 string 类?

在C语言中,字符串是以 \0 结尾的字符数组。虽然C标准库提供了 strlenstrcpystrcat 等一系列强大的库函数,但在实际开发中,它们存在明显的局限性:

  1. 不符合面向对象思想:数据和操作数据的方法是分离的(函数操作字符串)。

  2. 安全性低 :底层空间需要程序员手动管理(malloc/free),稍有不慎就会导致内存泄漏或缓冲区溢出(越界访问)。

  3. 操作繁琐:进行字符串拼接、查找、赋值等操作时,代码逻辑往往比较繁琐。

为了解决这些问题,C++ 标准库提供了 string。它封装了字符串的底层存储,自动管理内存,并提供了丰富的成员函数。

2. 标准库中的 string 类

2.1 string 类简介

string 是C++标准模板库(STL)中的一个类模板 basic_string 的实例化。

  • 本质typedef basic_string<char, char_traits, allocator> string;

  • 头文件#include <string>

  • 命名空间using namespace std;

注意string 类独立于编码处理字节。如果处理多字节字符(如UTF-8)size()length() 返回的是字节数,而不是实际字符数(这一点在多语言环境下需格外留意)。

2.2 string 类的常用接口详解

2.2.1 常见构造
构造函数 功能说明
string() 构造空的string对象(空字符串)
string(const char* s) 用C风格字符串构造
string(size_t n, char c) 构造包含n个字符c的对象
string(const string& s) 拷贝构造函数

代码演示:

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

void TestStringConstruct() 
{
    string s1;                // 空字符串
    string s2("Hello C++");   // 使用C字符串初始化
    string s3(5, 'A');        // "AAAAA"
    string s4(s2);            // 拷贝构造,s4也是"Hello C++"

    cout << "s2: " << s2 << endl;
    cout << "s3: " << s3 << endl;
    cout << "s4: " << s4 << endl;
}
2.2.2 容量操作
函数名称 功能说明
size() / length() 返回字符串有效字符长度(重点:不含 \0
capacity() 返回当前分配的空间总大小(即无需重新分配内存就能容纳的字符数)
empty() 判断是否为空串
clear() 清空有效字符,但不释放底层内存
reserve() 预留空间(改变 capacity,不改变 size
resize() 改变有效字符个数(改变 size)。若增加,多出的空间用指定字符填充;若减少,截断字符串。

代码演示(容量与预留空间):

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

void TestCapacity() 
{
    string s("Hello");
    cout << "size: " << s.size() << " capacity: " << s.capacity() << endl; // 输出: size:5 capacity:15(可能因编译器而异)

    // 1. reserve: 预留空间,避免频繁扩容
    s.reserve(100);
    cout << "After reserve(100), capacity: " << s.capacity() << endl;

    // 2. resize: 改变长度
    s.resize(10, 'X'); // 将字符串长度扩展到10,多出的位置用'X'填充
    cout << "After resize(10, 'X'): " << s << endl; // HelloXXXXX

    s.resize(3);// 截断为3个字符
    cout << "After resize(3): " << s << endl;      // Hel

    // 3. clear: 清空内容
    s.clear();
    cout << "After clear, size: " << s.size() << ", empty: " << s.empty() << endl; // size:0, empty:1
}
2.2.3 访问与遍历
函数名称 功能说明
operator[] 返回 pos 位置的字符(推荐,支持随机访问)
begin() + end() 正向迭代器
rbegin() + rend() 反向迭代器
范围for C++11语法糖,简洁高效

代码演示(三种遍历方式):

cpp 复制代码
void TestTraverse() 
{
    string s("Hello World");

    // 1. 下标 + operator[]
    for (size_t i = 0; i < s.size(); ++i) 
    {
        cout << s[i] << " ";
    }
    cout << endl;

    // 2. 迭代器 (类似指针)
    //auto it = s.begin(); 可以使用auto
    for (string::iterator it = s.begin(); it != s.end(); ++it) 
    {
        cout << *it << " ";
    }
    cout << endl;

    // 3. 范围for (C++11)
    for (char ch : s) 
    {
        cout << ch << " ";
    }
    cout << endl;
}
2.2.4 修改操作
函数名称 功能说明
push_back(char c) 尾插一个字符
append(const string& str) 追加字符串
operator+= 最常用,追加字符串或字符
c_str() 返回C风格的字符串(const char*
find() / rfind() 查找字符或子串,返回索引,未找到返回 npos
substr() 截取子串

代码演示(追加、查找与截取):

cpp 复制代码
void TestModify() 
{
    string s = "Hello";

    // 1. 追加
    s.push_back(' ');
    s.append("C++");
    s += " 2026";
    cout << s << endl; // Hello C++ 2026

    // 2. 查找
    string url = "https://www.example.com";
    size_t pos = url.find("www");
    if (pos != string::npos) 
    {
        cout << "'www' found at index: " << pos << endl; // 8
    }

    // 3. 截取子串 (从pos开始,截取n个字符)
    string domain = url.substr(pos, 3);
    cout << "Domain: " << domain << endl; // www

    // 4. C风格输出
    const char* cstr = s.c_str();
    printf("C style: %s\n", cstr);
}
2.2.5 非成员函数
函数 功能说明
operator+ 字符串拼接(效率较低,可能深拷贝,建议少用)
operator>> 流提取(输入),以空格为分隔符
operator<< 流插入(输出)
getline() 获取一行字符串(包含空格)
relational operators 支持 ><== 等比较操作

注意 :当需要读取包含空格的字符串时,必须使用 getline 而不是 cin >>

2.3 底层结构探秘(vs 与 g++)

了解底层结构有助于我们理解性能开销:

  • VS (Visual Studio) 下的 string

    采用 小字符串优化 (SSO)string 对象占用 28 字节(32位)。

    • 如果字符串长度小于 16,存储在对象内部的固定数组中,不进行堆分配。

    • 如果长度大于等于 16,则在堆上开辟空间。

      这种设计极大提升了短字符串的处理效率。

  • G++ 下的 string

    采用 写时拷贝 (COW) 策略(早期版本,现代版本已变化)。string 对象只占用 4 字节(32位),内部包含一个指向堆空间的指针。堆空间除了存储字符串外,还存储了长度、容量和引用计数,用于管理内存共享。

3. 经典题目实战(牛刀小试)

理论结合实践,下面我们通过几道经典OJ题目,来感受 string 的威力。

3.1 题目一:仅仅反转字母

题目链接LeetCode 917. 仅仅反转字母

题目描述

给定一个字符串 S,返回 "反转后的" 字符串,其中不是字母的字符都保留在原地,而所有字母的位置发生反转。

完整代码

cpp 复制代码
class Solution 
{
public:
    // 判断是否为字母
    bool isLetter(char ch) 
    {
        return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
    }

    string reverseOnlyLetters(string S) 
    {
        if (S.empty()) return S;

        int begin = 0;
        int end = S.size() - 1;

        while (begin < end) 
        {
            // 跳过非字母
            while (!isLetter(S[begin]) && begin < end) begin++;
            while (!isLetter(S[end])   && begin < end) end--;

            // 交换字母
            swap(S[begin++], S[end--]);
        }
        return S;
    }
};

3.2 题目二:找字符串中第一个只出现一次的字符

题目链接LeetCode 387. 字符串中的第一个唯一字符

题目描述

给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。

思路解析

  1. 利用哈希表(数组模拟)统计每个字符出现的次数。由于ASCII字符范围是0-255,可以开辟一个大小为256的数组。

  2. 第一次遍历:统计 count[s[i]]++

  3. 第二次遍历:从前往后扫描字符串,如果某个字符的计数为1,则直接返回其下标。

  4. 遍历结束未找到,返回 -1。

完整代码

cpp 复制代码
class Solution 
{
public:
    int firstUniqChar(string s) 
    {
        // 1. 统计频率
        int count[256] = { 0 }; // 初始化为0
        for (char ch : s) 
        {
            count[ch]++;
        }

        // 2. 查找第一个频率为1的字符
        for (int i = 0; i < s.size(); ++i) 
        {
            if (count[s[i]] == 1) 
            {
                return i;
            }
        }
        return -1;
    }
};

3.3 题目三:验证回文串

题目链接LeetCode 125. 验证回文串

题目描述

如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个回文串。

思路解析

  1. 预处理:遍历字符串,将大写字母转为小写(或统一转为大写)。这一步也可以不提前做,在比较时动态转换。

  2. 双指针 :定义 beginend

  3. 跳过非字母数字 :如果 begin 指向的字符不是字母或数字,begin++;如果 end 指向的不是,end--

  4. 比较 :比较 s[begin]s[end](忽略大小写),如果不相等返回 false。相等则移动指针继续。

完整代码

cpp 复制代码
class Solution 
{
public:
    bool isSmallLetter(char ch) 
    {
        return (ch >= 'a' && ch <= 'z');
    }

    bool isPalindrome(string s) 
    {
        // 1. 统一转成小写,方便比较
        for (char& ch : s) 
        {
            if (ch >= 'A' && ch <= 'Z') 
            {
                ch += 32; // 转小写
            }
        }

        int begin = 0;
        int end = s.size() - 1;

        while (begin < end) 
        {
            // 跳过小写字母
            while (begin < end && !isSmallLetter(s[begin])) begin++;
            while (begin < end && !isSmallLetter(s[end])) end--;

            // 比较
            if (s[begin++] != s[end--]) 
            {
                return false;
            }
        }
        return true;
    }
};

3.4 题目四:字符串相加

题目链接LeetCode 415. 字符串相加

题目描述

给定两个字符串形式的非负整数 num1num2,计算它们的和并同样以字符串形式返回。不能使用任何内建的用于处理大整数的库(比如 BigInteger),也不能直接将输入的字符串转换为整数形式。

思路解析

模拟竖式加法。

  1. 定义两个指针 end1end2 分别指向两个字符串的末尾。

  2. 定义进位 next 初始为0。

  3. 循环条件:end1 >= 0end2 >= 0next != 0

    • 取出当前位的数字(如果指针越界,则取0)。

    • 计算和:val = value1 + value2 + next

    • 更新进位:next = val / 10;当前位:cur = val % 10

    • cur 转换为字符插入到结果字符串中。

  4. 因为我们是按从低位到高位计算的,最后需要将结果字符串反转(或者使用头插法,但头插效率低,推荐尾插后反转)。

完整代码

cpp 复制代码
class Solution 
{
public:
    string addStrings(string num1, string num2) 
{
        int end1 = num1.size() - 1;
        int end2 = num2.size() - 1;
        int next = 0; // 进位
        string result;
        
        while (end1 >= 0 || end2 >= 0 || next != 0) 
        {
            // 获取当前位的数字
            int val1 = (end1 >= 0) ? num1[end1--] - '0' : 0;
            int val2 = (end2 >= 0) ? num2[end2--] - '0' : 0;
            
            int sum = val1 + val2 + next;
            next = sum / 10;
            int cur = sum % 10;
            
            // 尾插当前位
            result += (cur + '0');
        }
        
        // 因为我们是从个位开始算的,结果顺序是反的,需要反转
        reverse(result.begin(), result.end());
        return result;
    }
};

4. 课后作业(挑战升级)

下面几道题留给大家作为练习,进一步巩固 string 的使用:

  1. 翻转字符串 II :给定一个字符串 s 和一个整数 k,从开头开始,每隔 2k 个字符,反转前 k 个字符。如果剩余字符少于 k 个,则将剩余字符全部反转;如果剩余字符小于 2k 但大于等于 k 个,则反转前 k 个字符。 (LeetCode 541)

  2. 翻转字符串 III:给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。 (LeetCode 557)

  3. 字符串相乘 :给定两个以字符串形式表示的非负整数 num1num2,返回 num1num2 的乘积,它们的乘积也表示为字符串形式。不能使用内置库处理大整数。 (LeetCode 43)

  4. 找出字符串中第一个只出现一次的字符(进阶版):如果字符串很长,且字符集很大(不仅仅是字母),如何优化?

5. 总结

string 类是C++中最常用、最重要的类之一。

  • 核心要点

    • 使用 size() 获取长度,capacity() 获取容量。

    • 利用 reserve() 预分配空间,减少扩容开销。

    • += 操作符是追加字符串最优雅的方式。

    • 通过 c_str() 获取底层C字符串以便与旧代码交互。

    • findsubstr 是处理子串问题的利器。

相关推荐
誰能久伴不乏2 小时前
给开发板装上嘴巴与耳朵:i.MX6ULL 裸机串口 (UART) 驱动终极指南
arm开发·c++·单片机·嵌入式硬件·arm
okiseethenwhat2 小时前
反射在 JVM 层面的实现原理
开发语言·jvm·python
星梦清河2 小时前
Java并发编程
java·开发语言
XiYang-DING2 小时前
【Java SE】sealed关键字
java·开发语言·python
Lhan.zzZ2 小时前
Qt多线程数据库操作:安全分离连接,彻底解决段错误
数据库·c++·qt·安全
祈澈菇凉3 小时前
Next.js + OpenAI API 跑通一个带流式输出的聊天机器人
开发语言·javascript·机器人
lsx2024063 小时前
MySQL 删除数据表
开发语言
前端程序猿i3 小时前
纯JS 导出 Excel 工具
开发语言·javascript·excel
沐知全栈开发3 小时前
XML Schema 复合类型 - 仅含元素
开发语言