【C++ string 类实战指南】:从接口用法到 OJ 解题的全方位解析


🎬 博主名称月夜的风吹雨
🔥 个人专栏 : 《C语言》《基础数据结构》《C++入门到进阶》

⛺️任何一个伟大的思想,都有一个微不足道的开始!


一篇吃透 string 常用接口、C++11 简化技巧与编译器差异的深度教程 ✨

💬 前言

用 C 语言处理字符串时,你是否曾为strcpy的越界风险、strlen的重复计算、手动管理字符数组内存而头疼?在 OJ 题中,是否因频繁处理字符串细节而耽误解题思路?

其实 C++ 的string类早已封装了这些复杂操作,它不仅能自动管理内存,还提供了丰富的接口简化字符串处理。但很多开发者只停留在 "用 string 存字符串" 的层面,没吃透其核心接口的设计逻辑,遇到稍复杂的场景就频繁踩坑(如容量浪费、遍历效率低、接口误用)。

本篇文章将从 "实战需求" 出发,结合 C++11 语法与 OJ 例题,带你彻底掌握string类 ------ 不仅教你 "怎么用接口",更帮你理解 "为什么这么用",让字符串处理从 "麻烦事" 变成 "顺手活"。
✨ 阅读后,你将掌握:

  • string 核心接口的使用场景与避坑点(构造、容量、访问、修改);
  • auto 与范围 for 如何简化 string 的遍历与操作;
  • VS 与 G++ 下 string 的底层差异(小字符串优化、写时拷贝);
  • 用 string 接口高效解决 OJ 字符串题的思路与技巧。

文章目录

  • [一、为什么要学 string 类?告别 C 语言字符串的 "手动时代"](#一、为什么要学 string 类?告别 C 语言字符串的 “手动时代”)
    • [1. C 语言字符串的 3 大痛点](#1. C 语言字符串的 3 大痛点)
    • [2. string 类的核心优势](#2. string 类的核心优势)
  • [二、C++11 简化技巧:auto 与范围 for 让 string 操作更简洁](#二、C++11 简化技巧:auto 与范围 for 让 string 操作更简洁)
    • [1. auto:自动推导类型,告别复杂声明](#1. auto:自动推导类型,告别复杂声明)
    • [2. 范围 for:自动遍历,无需关心下标或迭代器](#2. 范围 for:自动遍历,无需关心下标或迭代器)
  • [三、string 类常用接口实战:从基础到进阶](#三、string 类常用接口实战:从基础到进阶)
    • [1. 字符串构造:3 种核心方式](#1. 字符串构造:3 种核心方式)
    • [2. 容量操作:避免空间浪费与频繁扩容](#2. 容量操作:避免空间浪费与频繁扩容)
    • [3. 访问与遍历:3 种常用方式](#3. 访问与遍历:3 种常用方式)
    • [4. 修改操作:拼接、查找、截取的高频接口](#4. 修改操作:拼接、查找、截取的高频接口)
    • [5. 非成员函数:输入输出与比较](#5. 非成员函数:输入输出与比较)
  • [四、编译器差异:VS 与 G++ 下的 string 底层结构](#四、编译器差异:VS 与 G++ 下的 string 底层结构)
    • [1. VS 下的 string:小字符串优化(SSO)](#1. VS 下的 string:小字符串优化(SSO))
    • [2. G++ 下的 string:写时拷贝(Copy-On-Write)](#2. G++ 下的 string:写时拷贝(Copy-On-Write))
  • [五、OJ 实战:用 string 接口解决 4 道经典字符串题](#五、OJ 实战:用 string 接口解决 4 道经典字符串题)
    • [1. 仅反转字母(LeetCode 917)](#1. 仅反转字母(LeetCode 917))
    • [2. 找第一个只出现一次的字符(LeetCode 387)](#2. 找第一个只出现一次的字符(LeetCode 387))
    • [3. 验证回文串(LeetCode 125)](#3. 验证回文串(LeetCode 125))
    • [4. 字符串相加(LeetCode 415)](#4. 字符串相加(LeetCode 415))
  • [六、思考与总结 ✨](#六、思考与总结 ✨)
  • [七、自测题与答案解析 🧩](#七、自测题与答案解析 🧩)
  • 八、延伸阅读推荐
  • [九、下篇预告:C++ string 类模拟实现 ------ 揭开底层内存管理的面纱](#九、下篇预告:C++ string 类模拟实现 —— 揭开底层内存管理的面纱)

一、为什么要学 string 类?告别 C 语言字符串的 "手动时代"

C 语言中,字符串是 "以\0结尾的字符数组",搭配str系列函数(strcpystrlenstrcmp)使用,但这种方式存在明显缺陷:

1. C 语言字符串的 3 大痛点

  • 数据与操作分离 :字符串(字符数组)和操作函数(strcpy)是分开的,不符合面向对象思想,且容易遗漏操作(如忘记strlen计算长度);
  • 内存需手动管理 :动态申请的字符数组(char* p = (char*)malloc(...))需手动free,稍不注意就会内存泄漏;
  • 越界风险高strcpy不检查目标数组大小,若源字符串过长,直接导致内存越界,引发程序崩溃。

2. string 类的核心优势

  • 自动化内存管理 :无需手动malloc / freestring会自动处理空间申请与释放;
  • 丰富的接口:内置构造、容量控制、查找、修改等接口,无需重复实现基础功能;
  • OJ 与工作刚需 :OJ 中 90% 以上的字符串题以string为输入输出,工作中用string能大幅提升开发效率,极少有人再用 C 语言字符串函数。

二、C++11 简化技巧:auto 与范围 for 让 string 操作更简洁

在学习string接口前,先掌握两个 C++11 语法 ------auto范围 for ,它们能大幅简化string的遍历与迭代器操作,避免冗长代码。

1. auto:自动推导类型,告别复杂声明

auto会让编译器在编译时自动推导变量类型,尤其适合string迭代器这类 "长类型名" 的场景:

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

int main() {
    // 场景1:简化string迭代器声明
    string str = "hello world";
    // 传统写法:string::iterator it = str.begin();
    auto it = str.begin(); // auto自动推导为string::iterator
    while (it != str.end()) {
        cout << *it << " "; // 输出:h e l l o   w o r l d
        ++it;
    }
    cout << endl;

    // 场景2:简化复杂容器的迭代器(结合string使用)
    map<string, string> dict = {{"apple", "苹果"}, {"orange", "橙子"}};
    // 传统写法:map<string, string>::iterator dictIt = dict.begin();
    auto dictIt = dict.begin();
    while (dictIt != dict.end()) {
        cout << dictIt->first << ":" << dictIt->second << endl;
        ++dictIt;
    }
    return 0;
}

auto 使用注意事项

  • 声明引用需加&auto& ref = strrefstr的引用,修改ref会改变str);
  • 同一行声明的变量类型需一致:auto a = 1, b = 2(正确),auto c = 3, d = 4.0(错误,int 与 double 冲突);
  • 不能直接声明数组:auto arr[] = {1,2,3}(编译报错,数组类型不能用 auto 推导)。

2. 范围 for:自动遍历,无需关心下标或迭代器

范围 for 是专门为 "有范围的集合"(如string、数组、容器)设计的遍历方式,自动迭代、自动取数据、自动判断结束,语法简洁且不易出错:

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

int main() {
    // 场景1:遍历string并修改字符(需加&,否则是值拷贝)
    string str = "hello";
    for (auto& ch : str) { // &表示引用,修改ch即修改str的字符
        ch -= 32; // 小写转大写
    }
    cout << str << endl; // 输出:HELLO

    // 场景2:只读遍历string(无需加&)
    for (auto ch : str) {
        cout << ch << " "; // 输出:H E L L O
    }
    cout << endl;

    // 对比C++98的遍历方式(繁琐且易出错)
    string oldStr = "world";
    for (int i = 0; i < oldStr.size(); ++i) {
        cout << oldStr[i] << " "; // 输出:w o r l d
    }
    return 0;
}

范围 for 的底层逻辑

范围 for 遍历string时,底层会自动转换为 "迭代器遍历"(从begin()end()),汇编层面可验证这一点 ------ 它本质是迭代器的 "语法糖",但代码简洁度大幅提升。


三、string 类常用接口实战:从基础到进阶

string的接口众多,我们聚焦 "最常用、最高频" 的接口,按 "构造→容量→访问→修改" 的逻辑拆解,每个接口搭配代码示例与使用场景。

1. 字符串构造:3 种核心方式

string的构造函数能满足不同初始化需求,重点掌握以下 3 种:


构造函数使用示例

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

void TestStringConstructor() {
    string s1; // 空字符串,size=0,capacity根据编译器默认值(如VS下为0)
    cout << "s1 size: " << s1.size() << ", empty: " << s1.empty() << endl;

    string s2("hello bit"); // 用C风格字符串构造
    cout << "s2: " << s2 << ", size: " << s2.size() << endl;

    string s3(s2); // 拷贝构造
    cout << "s3: " << s3 << ", address diff: " << &s2 << " vs " << &s3 << endl;
    // 注:s2和s3是不同对象,地址不同,底层为深拷贝
}

int main() {
    TestStringConstructor();
    return 0;
}

2. 容量操作:避免空间浪费与频繁扩容

容量相关接口是string效率优化的关键,核心是 "合理控制空间,减少扩容开销":

容量接口使用示例(含效率优化)

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

void TestStringCapacity() {
    string s;
    // 场景1:reserve预留空间,避免频繁扩容
    s.reserve(100); // 提前预留100个字符空间
    for (int i = 0; i < 50; ++i) {
        s += 'a'; // 无需扩容,效率高
    }
    cout << "s size: " << s.size() << ", capacity: " << s.capacity() << endl; // size=50, capacity=100

    // 场景2:resize修改有效字符数
    s.resize(80, 'b'); // 有效字符数从50扩到80,新增的30个字符为'b'
    cout << "s after resize(80, 'b'): " << s << endl;
    cout << "s size: " << s.size() << ", capacity: " << s.capacity() << endl; // size=80, capacity=100

    s.resize(30); // 有效字符数从80缩到30,截断后面50个字符
    cout << "s after resize(30): " << s << endl;
    cout << "s size: " << s.size() << ", capacity: " << s.capacity() << endl; // size=30, capacity=100

    // 场景3:clear清空内容
    s.clear();
    cout << "s after clear: size=" << s.size() << ", capacity=" << s.capacity() << endl; // size=0, capacity=100
}

int main() {
    TestStringCapacity();
    return 0;
}

💡 效率优化建议:

如果能预估string的最终长度(如读取固定格式的日志、拼接已知长度的字符串),先用reserve(n)预留空间,可避免string自动扩容时的 "申请新空间→拷贝旧内容→释放旧空间" 操作,大幅提升效率。

3. 访问与遍历:3 种常用方式

string提供了多种访问字符的方式,根据场景选择最合适的:


访问与遍历示例对比

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

void TestStringAccess() {
    string str = "hello string";

    // 方式1:operator[](随机访问+修改)
    str[0] = 'H'; // 修改第一个字符为大写
    cout << "方式1(operator[]): ";
    for (size_t i = 0; i < str.size(); ++i) {
        cout << str[i] << " ";
    }
    cout << endl;

    // 方式2:迭代器(通用遍历,支持反向遍历)
    cout << "方式2(迭代器): ";
    auto it = str.begin();
    while (it != str.end()) {
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    // 方式3:范围for(最简洁)
    cout << "方式3(范围for): ";
    for (auto ch : str) {
        cout << ch << " ";
    }
    cout << endl;
}

int main() {
    TestStringAccess();
    return 0;
}

输出结果:

plaintext 复制代码
方式1(operator[]): H e l l o   s t r i n g 
方式2(迭代器): H e l l o   s t r i n g 
方式3(范围for): H e l l o   s t r i n g 

4. 修改操作:拼接、查找、截取的高频接口

string的修改接口是 OJ 题和工作中的核心,重点掌握以下 5 个:


修改接口实战示例(含 OJ 常用逻辑)

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

void TestStringModify() {
    string str = "hello";

    // 1. 追加操作:+=最灵活
    str += " world"; // 追加字符串
    str += '!';     // 追加单个字符
    cout << "追加后:" << str << endl; // 输出:hello world!

    // 2. 查找操作:find找字符或子串
    size_t pos1 = str.find('w'); // 找字符'w'的位置
    if (pos1 != string::npos) {
        cout << "'w'的位置:" << pos1 << endl; // 输出:6
    }

    size_t pos2 = str.find("world"); // 找子串"world"的位置
    if (pos2 != string::npos) {
        cout << "\"world\"的位置:" << pos2 << endl; // 输出:6
    }

    // 3. 截取操作:substr截取子串
    string sub = str.substr(pos2, 5); // 从pos2开始,截取5个字符
    cout << "截取的子串:" << sub << endl; // 输出:world

    // 4. 兼容C语言:c_str()返回const char*
    printf("C风格输出:%s\n", str.c_str()); // 输出:hello world!
}

int main() {
    TestStringModify();
    return 0;
}

💡 OJ 高频技巧:

findsubstr结合可实现 "分割字符串"(如按逗号分割),示例逻辑:

cpp 复制代码
string s = "a,b,c,d";
size_t start = 0;
size_t pos = s.find(',');
while (pos != string::npos) {
    string part = s.substr(start, pos - start); // 截取从start到pos的子串
    cout << part << endl;
    start = pos + 1;
    pos = s.find(',', start); // 从start后继续找逗号
}
string lastPart = s.substr(start); // 截取最后一个子串
cout << lastPart << endl;

5. 非成员函数:输入输出与比较

string的非成员函数主要用于输入输出和字符串比较,用法直观:

非成员函数使用示例(重点:getline 的正确用法)

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

void TestStringIO() {
    // 注意:cin >> str后会留下换行符,需用cin.ignore()清除
    string str1;
    cout << "输入str1(不含空格):";
    cin >> str1; // 输入:hello
    cin.ignore(); // 清除cin留下的换行符,否则getline会读取空字符串

    string str2;
    cout << "输入str2(可含空格):";
    getline(cin, str2); // 输入:hello world

    cout << "str1: " << str1 << ", size: " << str1.size() << endl;
    cout << "str2: " << str2 << ", size: " << str2.size() << endl;

    // 字符串比较
    if (str1 < str2) {
        cout << "str1 < str2" << endl;
    } else if (str1 == str2) {
        cout << "str1 == str2" << endl;
    } else {
        cout << "str1 > str2" << endl;
    }
}

int main() {
    TestStringIO();
    return 0;
}

四、编译器差异:VS 与 G++ 下的 string 底层结构

不同编译器对string的实现不同,核心差异在于 "空间存储策略",了解这一点可避免跨平台开发时的效率问题。

1. VS 下的 string:小字符串优化(SSO)

VS 的string对象占 28 字节,内部用联合体存储字符串:

  • 当字符串长度≤15 时:使用内部固定的 16 字节字符数组(_Buf[16])存储,无需申请堆空间,效率极高;
  • 当字符串长度≥16 时:从堆上申请空间,用指针(_Ptr)指向堆空间。

这种设计的优势是 ------ 大多数场景下字符串长度较短(如变量名、日志信息),无需堆申请,减少内存开销与访问延迟。

2. G++ 下的 string:写时拷贝(Copy-On-Write)

G++ 的string对象仅占 4 字节(32 位平台),内部只有一个指针,指向堆上的一块空间,该空间包含 3 个核心字段:

  • _M_length:有效字符长度;
    - _M_capacity:总空间大小;
  • _M_refcount:引用计数(记录使用该堆空间的string对象个数)。

"写时拷贝" 的逻辑是:

  • 读取时:多个string对象共享同一块堆空间(引用计数递增);
  • 修改时:若引用计数 > 1,先拷贝一块新空间,再修改新空间(避免影响其他对象),引用计数调整。

💡 注意:写时拷贝在多线程环境下可能存在线程安全问题,C++11 后部分 G++ 版本已弃用,改用类似 VS 的小字符串优化。


五、OJ 实战:用 string 接口解决 4 道经典字符串题

掌握接口后,通过 OJ 题巩固用法,以下 4 道题是面试高频题,均用string接口高效实现。

1. 仅反转字母(LeetCode 917)

题目: 给定一个字符串,反转其中所有字母,非字母字符位置不变。

思路: 双指针(左指针找字母,右指针找字母,交换后移动指针)。

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

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;
        size_t left = 0, right = S.size() - 1;
        while (left < right) {
            // 左指针找字母
            while (left < right && !isLetter(S[left])) ++left;
            // 右指针找字母
            while (left < right && !isLetter(S[right])) --right;
            // 交换字母
            swap(S[left], S[right]);
            ++left;
            --right;
        }
        return S;
    }
};

int main() {
    Solution sol;
    string s = "a-bC-dEf-ghIj";
    cout << sol.reverseOnlyLetters(s) << endl; // 输出:j-Ih-gfE-dCba
    return 0;
}

2. 找第一个只出现一次的字符(LeetCode 387)

题目:给定一个字符串,找到第一个只出现一次的字符,返回其下标;若无,返回 - 1。

思路:用数组统计字符出现次数(256 个 ASCII 码),再遍历字符串找第一个次数为 1 的字符。

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

class Solution {
public:
    int firstUniqChar(string s) {
        int count[256] = {0}; // 统计每个字符出现次数
        // 第一步:统计次数
        for (auto ch : s) {
            count[ch]++;
        }
        // 第二步:找第一个次数为1的字符
        for (int i = 0; i < s.size(); ++i) {
            if (count[s[i]] == 1) {
                return i;
            }
        }
        return -1;
    }
};

int main() {
    Solution sol;
    string s = "loveleetcode";
    cout << sol.firstUniqChar(s) << endl; // 输出:2(字符'v'的下标)
    return 0;
}

3. 验证回文串(LeetCode 125)

题目:给定一个字符串,验证它是否是回文串(只考虑字母和数字字符,忽略大小写)。

思路:双指针(左指针找字母 / 数字,右指针找字母 / 数字,转大写后比较)。

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

class Solution {
public:
    bool isLetterOrNum(char ch) {
        return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
    }

    bool isPalindrome(string s) {
        // 统一转为大写
        for (auto& ch : s) {
            if (ch >= 'a' && ch <= 'z') {
                ch -= 32;
            }
        }

        int left = 0, right = s.size() - 1;
        while (left < right) {
            // 左指针找字母/数字
            while (left < right && !isLetterOrNum(s[left])) ++left;
            // 右指针找字母/数字
            while (left < right && !isLetterOrNum(s[right])) --right;
            // 比较
            if (s[left] != s[right]) {
                return false;
            }
            ++left;
            --right;
        }
        return true;
    }
};

int main() {
    Solution sol;
    string s = "A man, a plan, a canal: Panama";
    cout << (sol.isPalindrome(s) ? "是回文串" : "不是回文串") << endl; // 输出:是回文串
    return 0;
}

4. 字符串相加(LeetCode 415)

题目:给定两个非负整数的字符串表示(如 "123"+"456"),返回它们的和的字符串表示。

思路:双指针从后往前加,记录进位,结果尾插后反转(避免头插效率低)。

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

class Solution {
public:
    string addStrings(string num1, string num2) {
        int i = num1.size() - 1, j = num2.size() - 1;
        int carry = 0; // 进位
        string res;

        while (i >= 0 || j >= 0 || carry > 0) {
            // 取当前位的数字(越界则为0)
            int val1 = (i >= 0) ? (num1[i] - '0') : 0;
            int val2 = (j >= 0) ? (num2[j] - '0') : 0;
            // 计算当前位和
            int sum = val1 + val2 + carry;
            carry = sum / 10; // 更新进位
            res += (sum % 10) + '0'; // 尾插当前位字符

            // 移动指针
            if (i >= 0) --i;
            if (j >= 0) --j;
        }

        reverse(res.begin(), res.end()); // 反转结果
        return res;
    }
};

int main() {
    Solution sol;
    string num1 = "123", num2 = "456";
    cout << sol.addStrings(num1, num2) << endl; // 输出:579
    return 0;
}

六、思考与总结 ✨

💡 一句话总结:

string类的核心价值是 "自动化内存管理 + 丰富接口",掌握reserve的效率优化、find + substr的子串操作、双指针的遍历思路,就能轻松应对 90% 以上的字符串场景。


七、自测题与答案解析 🧩

  1. 判断题stringclear()接口会释放底层空间吗?

    ❌ 不会。clear()仅清空有效字符(将size设为 0),capacity保持不变,底层空间仍存在。

  2. 选择题 :下列关于reserveresize的说法错误的是( )

    A. reserve(n)会预留 n 个字符的空间,不改变size

    B. resize(n)会将size改为 n,可能改变capacity

    C. reserve(n)若 n 小于当前capacity,会缩小空间

    D. resize(n, 'a')会将新增字符填充为 'a'

    答案:✅ C。reserve(n)仅在 n 大于当前capacity时扩容,n 小于时不做任何操作。

  3. 简答题 :为什么string+=operator+效率高?

    答案:operator+是传值返回,会创建新的string对象(深拷贝);+=是在原对象上直接追加,无需创建新对象,效率更高。


八、延伸阅读推荐

📗 建议阅读顺序

  1. 《C++ 内存管理、模板初阶与 STL 简介》
  2. 《C++ string 类实战指南:从接口用法到 OJ 解题》(本文)
  3. 《C++ string 类模拟实现:从浅拷贝到深拷贝》(下篇)

九、下篇预告:C++ string 类模拟实现 ------ 揭开底层内存管理的面纱

学会string的接口用法后,你是否好奇:

  • string的深拷贝是如何实现的?为什么浅拷贝会导致程序崩溃?
  • string的扩容机制是怎样的(VS 下 1.5 倍扩容,还是 G++ 下 2 倍扩容)?
  • 如何自己实现一个支持构造、拷贝构造、赋值重载的简易string类?

下一篇《C++ string 类模拟实现》将带你深入string的底层,从 "浅拷贝陷阱" 讲到 "深拷贝的两种实现(传统版 + 现代版)",再到 "扩容逻辑与内存释放",让你不仅 "会用string",更 "懂string的底层实现"。

✨ 敬请期待,我们将从 "接口用法" 走向 "底层原理",彻底掌握string的设计逻辑与内存管理技巧。


🖋 作者寄语

string看似简单,却是 C++ 中 "封装思想" 的典型体现 ------ 它把复杂的内存管理、字符操作隐藏在接口背后,让开发者专注业务逻辑。学习string不仅是掌握一个工具,更是理解 "如何用面向对象思想解决实际问题" 的过程。

相关推荐
OKkankan4 小时前
模板的进阶
开发语言·数据结构·c++·算法
拾光Ծ4 小时前
【高阶数据结构】哈希表
数据结构·c++·哈希算法·散列表
终焉代码4 小时前
【C++】C++11特性学习(1)——列表初始化 | 右值引用与移动语义
c语言·c++·学习·1024程序员节
会灭火的程序员5 小时前
银河麒麟V10 SP3 升级GCC环境
linux·c++·supermap
shaominjin1235 小时前
OpenCV 4.1.2 SDK 静态库作用与功能详解
android·c++·人工智能·opencv·计算机视觉·中间件
qq_310658515 小时前
webrtc代码走读(七)-QOS-FEC-ulpfec rfc5109
网络·c++·webrtc
草莓熊Lotso5 小时前
模板进阶:从非类型参数到分离编译,吃透 C++ 泛型编程的核心逻辑
linux·服务器·开发语言·c++·人工智能·笔记·后端
草莓熊Lotso5 小时前
《算法闯关指南:优选算法--前缀和》--25.【模板】前缀和,26.【模板】二维前缀和
开发语言·c++·算法
hetao17338375 小时前
[CSP-S 2024] 超速检测
c++·算法