news 2026/5/10 2:36:30

C++第八讲:string 类

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++第八讲:string 类

C++第八讲:string 类

string 是STL 中最常用的容器,也是所有 C++ 开发者每天都会用到的工具。它彻底解决了 C 语言字符串操作繁琐、容易越界、需要手动管理内存的痛点。


一、为什么必须学 string 类?

1. C 语言字符串的致命缺陷

C 语言中字符串是以\0结尾的字符数组,操作依赖<string.h>库函数,存在以下问题:

  1. 内存手动管理:需要自己申请 / 释放空间,容易内存泄漏

  2. 容易越界访问:strcpy、strcat 等函数不检查边界,导致缓冲区溢出

  3. 不符合面向对象思想:数据和操作分离,使用麻烦

  4. 功能单一:很多常用操作(如查找、替换、截取)需要自己实现

2. C++ string 类的优势

  • 自动管理内存,无需手动申请释放

  • 重载了常用运算符(++=[]==等),操作直观

  • 提供了丰富的成员函数,满足绝大多数字符串操作需求

  • 类型安全,不容易出现越界错误

一句话:工作中 99% 的字符串场景都用 string,几乎没人用 C 语言的字符数组


二、前置知识:C++11 两个核心语法

在学习 string 之前,先掌握两个 C++11 的重要语法,后面会频繁用到。

1. auto 关键字:自动类型推导

作用

让编译器自动推导变量的类型,不用手动写复杂的类型名。

语法

auto 变量名 = 初始值;

示例

int a = 10; auto b = a; // b自动推导为int auto c = 'a'; // c自动推导为char auto d = 3.14; // d自动推导为double ​ // 最常用场景:简化复杂类型 #include <map> map<string, string> dict = {{"apple", "苹果"}, {"orange", "橙子"}}; // 不用写map<string, string>::iterator auto it = dict.begin();

易错点

  1. 必须初始化auto e;编译报错,没有初始值无法推导类型

  2. 同一行变量类型必须一致auto aa=1, bb=2.0;报错,int 和 double 类型不同

  3. 声明引用必须加 &auto& m = a;是引用,auto m = a;是拷贝

  4. 不能作为函数参数void func(auto a);编译报错

2. 范围 for 循环:简化遍历

作用

自动遍历数组、容器等有范围的集合,不用手动控制下标或迭代器。

语法

for (元素类型 变量名 : 遍历范围) { // 操作变量 }

示例

// 遍历数组 int arr[] = {1,2,3,4,5}; for (auto e : arr) { cout << e << " "; } ​ // 遍历string string s = "hello"; for (auto ch : s) { cout << ch << " "; } ​ // 修改元素:加引用 for (auto& ch : s) { ch -= 32; // 转大写 }

原理

范围 for 的底层就是迭代器,编译器会自动替换为迭代器遍历。


三、string 类常用接口(重点)

使用 string 必须包含头文件:#include <string>,且所有接口都在std命名空间中。

1. 构造函数(4 个最常用)

构造函数功能说明示例
string()构造空字符串string s1;
string(const char* s)用 C 风格字符串构造string s2("hello");
string(size_t n, char c)构造 n 个字符 c 的字符串string s3(5, 'a'); // "aaaaa"
string(const string& s)拷贝构造string s4(s2);

代码示例

#include <iostream> #include <string> using namespace std; ​ int main() { string s1; // 空字符串 string s2("hello world"); // 用C字符串构造 string s3(5, 'x'); // "xxxxx" string s4(s2); // 拷贝s2 ​ cout << s1 << endl; // 空 cout << s2 << endl; // hello world cout << s3 << endl; // xxxxx cout << s4 << endl; // hello world return 0; }

2. 容量操作

函数功能说明注意事项
size_t size() const返回有效字符长度✅ 推荐使用,和其他容器接口统一
size_t length() const返回有效字符长度和 size () 完全一样,历史遗留
size_t capacity() const返回总容量(能存多少字符)容量≥size,预留空间避免频繁扩容
bool empty() const判断是否为空空返回 true,否则 false
void clear()清空有效字符不改变底层容量
void reserve(size_t n)预留 n 个字符的空间只改容量,不改 size;n < 当前容量时无作用
void resize(size_t n, char c='\0')把有效字符改为 n 个n>size:用 c 填充;n<size:截断;可能改变容量

核心区别:reserve vs resize

  • reserve:只预留空间,不改变有效字符个数,用于提前预估大小,避免频繁扩容

  • resize:改变有效字符个数,会初始化新增的字符

代码示例

int main() { string s = "hello"; cout << s.size() << endl; // 5 cout << s.capacity() << endl; // 15(VS下短字符串优化) ​ s.reserve(100); // 预留100个字符空间 cout << s.size() << endl; // 5(不变) cout << s.capacity() << endl; // 100 ​ s.resize(10, 'a'); // 有效字符改为10个,新增的用'a'填充 cout << s << endl; // helloaaaaa cout << s.size() << endl; // 10 ​ s.resize(3); // 截断为3个字符 cout << s << endl; // hel cout << s.size() << endl; // 3 cout << s.capacity() << endl; // 100(不变) ​ s.clear(); // 清空 cout << s.size() << endl; // 0 cout << s.capacity() << endl; // 100(不变) return 0; }

3. 访问与遍历(3 种方式)

方式语法特点
operator[]s[pos]✅ 最常用,像数组一样访问,支持读写
迭代器begin()/end()通用所有容器,支持反向迭代器rbegin()/rend()
范围 forfor (auto ch : s)✅ C++11 推荐,最简洁

代码示例

int main() { string s = "hello"; ​ // 1. []访问(推荐) for (int i=0; i<s.size(); ++i) { cout << s[i] << " "; s[i] += 1; // 可以修改 } cout << endl; ​ // 2. 迭代器 string::iterator it = s.begin(); while (it != s.end()) { cout << *it << " "; ++it; } cout << endl; ​ // 反向迭代器:从后往前遍历 string::reverse_iterator rit = s.rbegin(); while (rit != s.rend()) { cout << *rit << " "; ++rit; } cout << endl; ​ // 3. 范围for(最简洁) for (auto ch : s) { cout << ch << " "; } cout << endl; ​ return 0; }

4. 修改操作(最常用)

函数功能说明推荐度
void push_back(char c)尾插一个字符⭐⭐
string& operator+=(const string& str)追加字符串 / 字符⭐⭐⭐ 最常用
string& append(const char* s)追加 C 风格字符串
const char* c_str() const返回 C 风格字符串(const char*)⭐⭐⭐ 用于和 C 语言接口交互
size_t find(char c, size_t pos=0) const从 pos 开始找 c,返回下标,找不到返回string::npos⭐⭐⭐
size_t rfind(char c, size_t pos=npos) const从 pos 开始往前找 c⭐⭐
string substr(size_t pos=0, size_t len=npos) const从 pos 开始截取 len 个字符返回⭐⭐⭐

代码示例

int main() { string s = "hello"; ​ // 追加 s += ' '; // 追加字符 s += "world"; // 追加字符串 cout << s << endl; // hello world ​ // 查找 size_t pos = s.find('o'); if (pos != string::npos) { cout << "找到'o'在位置:" << pos << endl; // 4 } ​ pos = s.find("world"); if (pos != string::npos) { cout << "找到'world'在位置:" << pos << endl; // 6 } ​ // 截取子串 string sub = s.substr(6, 5); // 从位置6开始截取5个字符 cout << sub << endl; // world ​ // 转C风格字符串 const char* cstr = s.c_str(); printf("%s\n", cstr); // hello world ​ return 0; }

5. 非成员函数

函数功能说明注意事项
operator+字符串拼接❌ 尽量少用,传值返回会产生深拷贝,效率低
operator>>输入字符串遇到空格、换行结束
operator<<输出字符串正常输出
getline(istream& in, string& s)读取一行字符串✅ 读取带空格的字符串,遇到换行结束
relational operators大小比较(==!=<>等)按字典序比较

易错点:cin vs getline

  • cin >> s:遇到空格、制表符、换行就停止,无法读取带空格的字符串

  • getline(cin, s):读取整行,直到遇到换行符,会丢弃换行符

int main() { string s; // 错误:输入"hello world"只会读取"hello" // cin >> s; ​ // 正确:读取整行 getline(cin, s); cout << s << endl; return 0; }

四、string 的底层结构(面试高频)

不同编译器的 string 实现不同,主要有两种:VS 的短字符串优化G++ 的写时拷贝

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

结构(32 位平台占 28 字节)

  • 一个联合体:

    • 长度 < 16:用内部 16 字节的字符数组存储(栈上)

    • 长度≥16:用堆空间存储,联合体存指向堆的指针

  • size_t _Mysize:有效字符长度

  • size_t _Myres:总容量

  • 一个指针:用于其他管理

优势

大多数字符串长度都小于 16,直接用栈空间,不需要申请堆内存,效率更高。

2. G++ 下的 string:写时拷贝(COW)

结构(32 位平台占 4 字节)

  • 只有一个指针,指向堆上的控制块:

    • size_t _M_length:有效长度

    • size_t _M_capacity:总容量

    • _Atomic_word _M_refcount:引用计数

    • 后面跟着实际的字符串数据

原理

多个 string 对象共享同一块堆内存,只有当某个对象修改字符串时,才会重新分配空间并拷贝数据(写时才拷贝)。

优势

拷贝构造和赋值效率极高,只需要拷贝指针和增加引用计数。


五、面试必考题:string 类的模拟实现

面试官几乎 100% 会让你手写 string 类的核心函数(构造、拷贝构造、赋值重载、析构),考察你对深拷贝和浅拷贝的理解。

1. 浅拷贝的问题

如果不自己实现拷贝构造和赋值重载,编译器会生成默认的浅拷贝,导致多个对象共享同一块内存,析构时重复释放,程序崩溃。

// 错误的string实现(浅拷贝) class String { public: String(const char* str = "") { _str = new char[strlen(str)+1]; strcpy(_str, str); } ​ ~String() { delete[] _str; _str = nullptr; } ​ private: char* _str; }; ​ int main() { String s1("hello"); String s2(s1); // 浅拷贝,s1和s2的_str指向同一块内存 // 程序结束时,s2先析构释放内存,s1再析构时释放同一块内存,崩溃 return 0; }

2. 深拷贝的两种实现方式

方式 1:传统版(容易理解)

自己申请独立的空间,拷贝数据,每个对象有自己的资源。

class String { public: // 构造函数 String(const char* str = "") { if (str == nullptr) { assert(false); return; } _str = new char[strlen(str)+1]; strcpy(_str, str); } ​ // 拷贝构造:深拷贝 String(const String& s) { _str = new char[strlen(s._str)+1]; strcpy(_str, s._str); } ​ // 赋值运算符重载:深拷贝 String& operator=(const String& s) { if (this != &s) { // 防止自己给自己赋值 // 先申请新空间 char* tmp = new char[strlen(s._str)+1]; strcpy(tmp, s._str); // 释放旧空间 delete[] _str; // 指向新空间 _str = tmp; } return *this; } ​ // 析构函数 ~String() { if (_str) { delete[] _str; _str = nullptr; } } ​ private: char* _str; };

方式 2:现代版(更简洁高效)

利用局部对象的析构函数自动释放资源,通过 swap 交换指针。

class String { public: String(const char* str = "") { if (str == nullptr) { assert(false); return; } _str = new char[strlen(str)+1]; strcpy(_str, str); } ​ // 拷贝构造 String(const String& s) : _str(nullptr) { String tmp(s._str); // 构造临时对象 swap(_str, tmp._str); // 交换指针,tmp析构时释放旧空间 } ​ // 赋值运算符重载(现代版) String& operator=(String s) { // 传值参数,自动拷贝构造 swap(_str, s._str); // 交换指针,s析构时释放旧空间 return *this; } ​ ~String() { if (_str) { delete[] _str; _str = nullptr; } } ​ private: char* _str; };

六、经典 OJ 实战(笔试必练)

1. 反转字符串中的字母

class Solution { public: bool isLetter(char ch) { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); } ​ string reverseOnlyLetters(string s) { int 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; } };

2. 字符串中第一个只出现一次的字符

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

七、本章核心总结

  1. string 是 STL 最常用的容器,自动管理内存,操作简单安全

  2. 常用接口:构造、size、operator []、+=、find、substr、c_str、getline

  3. 底层实现:VS 用短字符串优化,G++ 用写时拷贝

  4. 面试必考点:深拷贝的实现,浅拷贝的问题

  5. 易错点:cin 和 getline 的区别,operator + 的效率问题,clear 不改变容量

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/10 2:35:51

魔兽争霸3终极优化指南:WarcraftHelper让你的经典游戏重获新生

魔兽争霸3终极优化指南&#xff1a;WarcraftHelper让你的经典游戏重获新生 【免费下载链接】WarcraftHelper Warcraft III Helper , support 1.20e, 1.24e, 1.26a, 1.27a, 1.27b 项目地址: https://gitcode.com/gh_mirrors/wa/WarcraftHelper 还在为魔兽争霸3的闪退、卡…

作者头像 李华
网站建设 2026/5/10 2:33:51

CANN/ops-cv一维线性上采样

UpsampleLinear1d 【免费下载链接】ops-cv 本项目是CANN提供的图像处理、目标检测相关的算子库&#xff0c;实现网络在NPU上加速计算。 项目地址: https://gitcode.com/cann/ops-cv 产品支持情况 产品是否支持Ascend 950PR/Ascend 950DTAtlas A3 训练系列产品/Atlas A3…

作者头像 李华
网站建设 2026/5/10 2:30:48

Rust构建AI API网关:将Cursor CLI封装为OpenAI/Anthropic兼容接口

1. 项目概述&#xff1a;用Rust为Cursor AI模型打造一个通用API网关 如果你和我一样&#xff0c;既是Cursor的深度用户&#xff0c;又经常需要把AI能力集成到自己的应用里&#xff0c;那你肯定遇到过这个痛点&#xff1a;Cursor的 agent 命令行工具功能强大&#xff0c;但它…

作者头像 李华
网站建设 2026/5/10 2:30:47

AI IDE账号统一管理:Cockpit Tools实现多平台一键切换与多开

1. 项目概述&#xff1a;为什么我们需要一个AI IDE账号管理器&#xff1f;如果你和我一样&#xff0c;是个重度依赖AI编程助手的开发者&#xff0c;那你手头肯定不止一个账号。可能是为了应对不同项目的配额限制&#xff0c;也可能是想体验不同模型的能力&#xff0c;又或者单纯…

作者头像 李华
网站建设 2026/5/10 2:30:41

基于Stream Chat构建AI聊天助手:完整技术栈与实时对话实现

1. 项目概述&#xff1a;一个为AI聊天助手提供完整技术栈的示例库如果你正在为你的应用&#xff08;无论是移动端还是Web端&#xff09;集成一个类似ChatGPT或Claude的智能聊天助手&#xff0c;并且希望它具备流畅的实时对话体验、完整的对话历史&#xff0c;以及一个开箱即用、…

作者头像 李华