文章目录
- 前言
- [第一章 STL初识:C++程序员的"屠龙刀"](#第一章 STL初识:C++程序员的“屠龙刀”)
-
- [1. 什么是 STL?](#1. 什么是 STL?)
- [2. STL 的版本流派](#2. STL 的版本流派)
- [3. STL 的六大组件(核心架构)](#3. STL 的六大组件(核心架构))
- [4. STL 的重要性](#4. STL 的重要性)
- [5. 如何高效学习 STL?](#5. 如何高效学习 STL?)
- [6. STL 的缺陷](#6. STL 的缺陷)
- [第二章 String类:为什么要学习string类?](#第二章 String类:为什么要学习string类?)
-
- [1. C 语言字符串的"三大原罪"](#1. C 语言字符串的“三大原罪”)
- [2. String 类:C++ 的优雅解决方案](#2. String 类:C++ 的优雅解决方案)
- [3. 为什么 string 是学习 STL 的"第一课"?](#3. 为什么 string 是学习 STL 的“第一课”?)
- [4. 实战中的地位:OJ 与 面试](#4. 实战中的地位:OJ 与 面试)
- [第三章 String类注意事项](#第三章 String类注意事项)
- 第四章:默认成员函数
-
- 构造函数
-
- [1\. 核心翻译:构造函数列表](#1. 核心翻译:构造函数列表)
- [2\. 代码示例解析](#2. 代码示例解析)
- [3\. 关键注意事项与异常安全(总结)](#3. 关键注意事项与异常安全(总结))
- 总结建议
- 析构函数
-
- [1\. 核心翻译](#1. 核心翻译)
- [2\. 技术细节参数](#2. 技术细节参数)
- [3\. 总结与专家解读](#3. 总结与专家解读)
-
- [A. 自动化内存管理 (RAII)](#A. 自动化内存管理 (RAII))
- [B. 它是如何释放内存的?](#B. 它是如何释放内存的?)
- [C. 为什么"绝不抛出异常"很重要?](#C. 为什么“绝不抛出异常”很重要?)
- 一句话总结
- std::string::operator=
-
- [1\. 核心翻译:赋值方式列表](#1. 核心翻译:赋值方式列表)
- [2\. 代码示例解析](#2. 代码示例解析)
- [3\. 技术细节总结(考试/面试重点)](#3. 技术细节总结(考试/面试重点))
-
- [A. 复杂度 (Complexity)](#A. 复杂度 (Complexity))
- [B. 迭代器有效性 (Iterator Validity)](#B. 迭代器有效性 (Iterator Validity))
- [C. 异常安全性 (Exception Safety)](#C. 异常安全性 (Exception Safety))
- [D. 移动后的源对象 (Moved-from state)](#D. 移动后的源对象 (Moved-from state))
- 一句话总结
- 第五章:迭代器
-
- 补充:iterator
-
- [1\. 核心概念:为什么需要迭代器?](#1. 核心概念:为什么需要迭代器?)
- [2\. 迭代器的基本操作](#2. 迭代器的基本操作)
- [3\. 迭代器的五大种类(重点)](#3. 迭代器的五大种类(重点))
- [4\. 迭代器失效 ------ 常见的坑](#4. 迭代器失效 —— 常见的坑)
-
- [示例:在 Vector 遍历时添加元素](#示例:在 Vector 遍历时添加元素)
- [5\. 底层实现原理(简化版)](#5. 底层实现原理(简化版))
- 总结
- [const _iterator](#const _iterator)
-
- [1. 为什么 `iterator` 无法遍历 `const` 对象?](#1. 为什么
iterator无法遍历const对象?) - [2. 正确姿势:使用 `const_iterator`](#2. 正确姿势:使用
const_iterator) - [3. C++11 的语法糖:`cbegin()` 和 `cend()`](#3. C++11 的语法糖:
cbegin()和cend()) - [4. 容易混淆的概念:`const_iterator` vs `const iterator`](#4. 容易混淆的概念:
const_iteratorvsconst iterator) - 总结
- [1. 为什么 `iterator` 无法遍历 `const` 对象?](#1. 为什么
- 3种迭代器遍历方式
- std::string::begin
-
- [1\. 核心翻译:功能描述](#1. 核心翻译:功能描述)
- [2\. C++ 版本差异 (重载)](#2. C++ 版本差异 (重载))
- [3\. 代码示例解析](#3. 代码示例解析)
- [4\. 技术细节总结(关键考点)](#4. 技术细节总结(关键考点))
- [5\. 常见误区与提示](#5. 常见误区与提示)
- std::string::end
-
- [1\. 核心翻译:功能描述](#1. 核心翻译:功能描述)
- [2\. C++ 版本与重载](#2. C++ 版本与重载)
- [3\. 形象化图解](#3. 形象化图解)
- [4\. 代码示例解析](#4. 代码示例解析)
- [5\. 技术细节总结](#5. 技术细节总结)
- [6\. 关键警告 (新手常犯错误)](#6. 关键警告 (新手常犯错误))
- std::string::rbegin
-
- [1\. 核心翻译:功能描述](#1. 核心翻译:功能描述)
- [2\. C++ 版本与重载](#2. C++ 版本与重载)
- [3\. 形象化图解 (关键理解)](#3. 形象化图解 (关键理解))
- [4\. 代码示例解析](#4. 代码示例解析)
- [5\. 技术细节总结](#5. 技术细节总结)
- [6\. 专家点拨](#6. 专家点拨)
- std::string::rend
-
- [1\. 核心翻译:功能描述](#1. 核心翻译:功能描述)
- [2\. 形象化图解 (核心考点)](#2. 形象化图解 (核心考点))
- [3\. C++ 版本与重载](#3. C++ 版本与重载)
- [4\. 代码示例解析](#4. 代码示例解析)
- [5\. 技术细节总结](#5. 技术细节总结)
- [6\. 总结对比表](#6. 总结对比表)
- [带 `c` 版本的迭代器](#带
c版本的迭代器)
- 第六章:容量
-
- std::string::size
-
- [1\. 函数原型](#1. 函数原型)
- [2\. 核心总结](#2. 核心总结)
- [3\. 技术规格 (Technical Specs)](#3. 技术规格 (Technical Specs))
- [4\. 代码示例](#4. 代码示例)
- 总结
- std::string::length
-
- [1\. 函数原型](#1. 函数原型)
- [2\. 核心总结](#2. 核心总结)
- [3\. 技术规格](#3. 技术规格)
- [4\. 代码示例](#4. 代码示例)
- 总结
- std::string::max_size
-
- [1\. 函数原型](#1. 函数原型)
- [2\. 核心总结](#2. 核心总结)
- [3\. 返回值解析](#3. 返回值解析)
- [4\. 代码示例分析](#4. 代码示例分析)
- [5\. 技术规格](#5. 技术规格)
- 总结
- std::string::resize
-
- [1\. 函数原型](#1. 函数原型)
- [2\. 核心总结](#2. 核心总结)
- [3\. 参数与返回值](#3. 参数与返回值)
- [4\. 代码示例解析](#4. 代码示例解析)
- [5\. 关键技术细节](#5. 关键技术细节)
- [6\. 避坑指南:`resize` vs `reserve`](#6. 避坑指南:
resizevsreserve)
- std::string::capacity
-
- [1\. 函数原型](#1. 函数原型)
- [2\. 核心总结](#2. 核心总结)
- [3\. 三大"大小"概念对比](#3. 三大“大小”概念对比)
- [4\. 代码示例解析](#4. 代码示例解析)
- [5\. 技术规格 (Technical Specs)](#5. 技术规格 (Technical Specs))
- 总结
- std::string::reserve
-
- [1\. 函数原型](#1. 函数原型)
- [2\. 核心总结](#2. 核心总结)
- [3\. 为什么需要 `reserve`?(性能优化的秘密)](#3. 为什么需要
reserve?(性能优化的秘密)) - [4\. 示例代码深度解析](#4. 示例代码深度解析)
- [5\. `reserve` vs `resize` (一定要分清)](#5.
reservevsresize(一定要分清)) - [补充:reserve为什么不能访问 `s[n-1]` (越界)](#补充:reserve为什么不能访问
s[n-1](越界)) - [6\. 技术规格](#6. 技术规格)
- 总结
- std::string::clear
-
- [1\. 函数原型](#1. 函数原型)
- [2\. 核心总结](#2. 核心总结)
- [3\. 代码示例解析](#3. 代码示例解析)
- [4\. 技术规格](#4. 技术规格)
- [5\. 避坑指南:如何真的释放内存?](#5. 避坑指南:如何真的释放内存?)
- 总结
- std::string::empty
-
- [1\. 函数原型](#1. 函数原型)
- [2\. 核心总结](#2. 核心总结)
- [3\. 最佳实践:为什么不用 `size() == 0`?](#3. 最佳实践:为什么不用
size() == 0?) - [4\. 代码示例解析](#4. 代码示例解析)
- [5\. 技术规格 (Technical Specs)](#5. 技术规格 (Technical Specs))
- 总结
- 第七章:元素访问
-
- std::string::operator[]
-
- [1\. 中文翻译](#1. 中文翻译)
-
- [**`std::string::operator[]` 公有成员函数**](#
std::string::operator[]公有成员函数)
- [**`std::string::operator[]` 公有成员函数**](#
- [2\. 技术总结与核心考点](#2. 技术总结与核心考点)
-
- [**1. 访问与修改**](#1. 访问与修改)
- [**2. 边界检查**](#2. 边界检查)
- [**3. 特殊的 `\0` 处理**](#3. 特殊的
\0处理) - [**4. 效率**](#4. 效率)
- std::string::at
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
-
- [**`std::string::at` 公有成员函数**](#
std::string::at公有成员函数)
- [**`std::string::at` 公有成员函数**](#
- [2\. 技术总结与核心对比](#2. 技术总结与核心对比)
-
- [**1. 核心区别:安全性 vs 性能**](#1. 核心区别:安全性 vs 性能)
- [**2. 最佳实践场景**](#2. 最佳实践场景)
- [**3. 异常处理示例**](#3. 异常处理示例)
- std::string::back
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
-
- [**`std::string::back` 公有成员函数**](#
std::string::back公有成员函数)
- [**`std::string::back` 公有成员函数**](#
- [2\. 技术总结与核心考点](#2. 技术总结与核心考点)
-
- [**1. 等价形式**](#1. 等价形式)
- [**2. 致命陷阱:空字符串**](#2. 致命陷阱:空字符串)
- [**3. 与 `pop_back()` 的配合**](#3. 与
pop_back()的配合) - [**4. 总结对比表 (访问方式)**](#4. 总结对比表 (访问方式))
- std::string::front
-
- [1\. 中文翻译](#1. 中文翻译)
-
- [**`std::string::front` 公有成员函数**](#
std::string::front公有成员函数)
- [**`std::string::front` 公有成员函数**](#
- [2\. 技术总结与核心考点](#2. 技术总结与核心考点)
-
- [**1. 等价形式**](#1. 等价形式)
- [**2. `front()` vs `begin()`**](#2.
front()vsbegin()) - [**3. 致命陷阱:空字符串**](#3. 致命陷阱:空字符串)
- [**4. 为什么需要 `front()`?**](#4. 为什么需要
front()?)
- [总结:C++ String 元素访问全家桶](#总结:C++ String 元素访问全家桶)
- 第八章:修改器(Modifiers)
-
- std::string::operator+=
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
-
- [**`std::string::operator+=` 公有成员函数**](#
std::string::operator+=公有成员函数)
- [**`std::string::operator+=` 公有成员函数**](#
- [2\. 技术总结与核心考点](#2. 技术总结与核心考点)
-
- [**1. 极高的易用性**](#1. 极高的易用性)
- [**2. 内存管理与迭代器失效**](#2. 内存管理与迭代器失效)
- [**3. 返回值 `*this` 的妙用**](#3. 返回值
*this的妙用) - [**4. 与 `append()` 的区别**](#4. 与
append()的区别)
- std::string::append
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
-
- [**`std::string::append` 公有成员函数**](#
std::string::append公有成员函数)
- [**`std::string::append` 公有成员函数**](#
- [2\. 技术总结与核心考点](#2. 技术总结与核心考点)
-
- [**1. `append` vs `operator+=`**](#1.
appendvsoperator+=) - [**2. 性能优化技巧:子串处理**](#2. 性能优化技巧:子串处理)
- [**3. 缓冲区与二进制安全**](#3. 缓冲区与二进制安全)
- [**4. 内存重分配\**](#**4. 内存重分配**)
- [**1. `append` vs `operator+=`**](#1.
- std::string::push_back
-
- [1\. 中文翻译](#1. 中文翻译)
-
- [**`std::string::push_back` 公有成员函数**](#
std::string::push_back公有成员函数)
- [**`std::string::push_back` 公有成员函数**](#
- [2\. 技术总结与核心考点](#2. 技术总结与核心考点)
-
- [**1. 容器语义**](#1. 容器语义)
- [**2. 摊销常数时间**](#2. 摊销常数时间)
- [**3. 与 `operator+=` 的对比**](#3. 与
operator+=的对比) - [**4. 最佳实践**](#4. 最佳实践)
- std::string::assign
-
- [1\. 中文翻译](#1. 中文翻译)
-
- [**`std::string::assign` 公有成员函数**](#
std::string::assign公有成员函数)
- [**`std::string::assign` 公有成员函数**](#
- [2\. 技术总结与核心考点](#2. 技术总结与核心考点)
-
- [**1. `assign` vs `operator=`**](#1.
assignvsoperator=) - [**2. 移动语义 (Move Semantics - C++11)**](#2. 移动语义 (Move Semantics - C++11))
- [**3. 彻底重置**](#3. 彻底重置)
- [**4. `append` 与 `assign` 的参数一致性**](#4.
append与assign的参数一致性)
- [**1. `assign` vs `operator=`**](#1.
- std::string::insert
-
- [1\. 中文翻译](#1. 中文翻译)
-
- [**`std::string::insert` 公有成员函数**](#
std::string::insert公有成员函数)
- [**`std::string::insert` 公有成员函数**](#
- [2\. 技术总结与核心考点](#2. 技术总结与核心考点)
-
- [**1. 性能代价:移动与拷贝**](#1. 性能代价:移动与拷贝)
- [**2. 两套接口:下标 vs 迭代器**](#2. 两套接口:下标 vs 迭代器)
- [**3. 迭代器失效 (Iterator Invalidation)**](#3. 迭代器失效 (Iterator Invalidation))
- [**4. `insert` 与 `append` 的关系**](#4.
insert与append的关系)
- [补充:如何提高insert效率--- [std::reverse](https://legacy.cplusplus.com/reference/algorithm/reverse/?kw=reverse)](#补充:如何提高insert效率--- std::reverse)
- std::string::erase
-
- [1\. 中文翻译](#1. 中文翻译)
-
- [**`std::string::erase` 公有成员函数**](#
std::string::erase公有成员函数)
- [**`std::string::erase` 公有成员函数**](#
- [2\. 技术总结与核心考点](#2. 技术总结与核心考点)
-
- [**1. 三种模式的对比**](#1. 三种模式的对比)
- [**2. 性能代价:数据搬移**](#2. 性能代价:数据搬移)
- [**3. 陷阱:在循环中删除 (The Loop Trap)**](#3. 陷阱:在循环中删除 (The Loop Trap))
- [**4. 常用简写技巧**](#4. 常用简写技巧)
- std::string::replace
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
-
- [**`std::string::replace` 公有成员函数**](#
std::string::replace公有成员函数)
- [**`std::string::replace` 公有成员函数**](#
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [**1. 核心逻辑**](#1. 核心逻辑)
- [**2. 参数记忆技巧**](#2. 参数记忆技巧)
- [**3. 性能考量**](#3. 性能考量)
- [**4. 实战场景**](#4. 实战场景)
- std::string::swap
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
-
- [**`std::string::swap` 公有成员函数**](#
std::string::swap公有成员函数)
- [**`std::string::swap` 公有成员函数**](#
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [**1. 极致的效率:指针交换 vs 数据拷贝**](#1. 极致的效率:指针交换 vs 数据拷贝)
- [**2. 异常安全:Copy-and-Swap 惯用语**](#2. 异常安全:Copy-and-Swap 惯用语)
- [**3. 与排序算法的关系**](#3. 与排序算法的关系)
- [**4. 最佳实践:`std::swap` vs `str.swap`**](#4. 最佳实践:
std::swapvsstr.swap)
- std::string::pop_back
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
-
- [**`std::string::pop_back` 公有成员函数**](#
std::string::pop_back公有成员函数)
- [**`std::string::pop_back` 公有成员函数**](#
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [**1. 极简的实现机制**](#1. 极简的实现机制)
- [**2. 致命陷阱:空字符串 (UB)**](#2. 致命陷阱:空字符串 (UB))
- [**3. 历史演变 (C++98 vs C++11)**](#3. 历史演变 (C++98 vs C++11))
- [**4. 常用组合拳**](#4. 常用组合拳)
- 第九章:字符串操作
-
- std::string::c_str
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. 核心考点:指针失效 (Pointer Invalidation)](#1. 核心考点:指针失效 (Pointer Invalidation))
- [2\. 只读属性 (Read-Only)](#2. 只读属性 (Read-Only))
- [3\. 与 `data()` 的对比 (面试常问)](#3. 与
data()的对比 (面试常问)) - [4\. 常见用途](#4. 常见用途)
- std::string::data
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. 版本演进(必考点)](#1. 版本演进(必考点))
- [2\. 为什么示例中使用 `memcmp`?](#2. 为什么示例中使用
memcmp?) - [3\. 现代 C++ (C++17) 的高级用法](#3. 现代 C++ (C++17) 的高级用法)
- [4\. `c_str()` vs `data()` 该选谁?](#4.
c_str()vsdata()该选谁?)
- std::string::get_allocator
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
-
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. 为什么需要这个函数?](#1. 为什么需要这个函数?)
- [2\. 代码演示](#2. 代码演示)
- [3\. 核心考点:Stateful vs Stateless](#3. 核心考点:Stateful vs Stateless)
- std::string::copy
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. 最大的坑:没有 `\0`](#1. 最大的坑:没有
\0) - [2\. 与 `c_str()` / `strcpy` 的应用场景对比](#2. 与
c_str()/strcpy的应用场景对比) - [3\. 安全性检查](#3. 安全性检查)
- [4\. 返回值的妙用](#4. 返回值的妙用)
- [1\. 最大的坑:没有 `\0`](#1. 最大的坑:没有
- std::string::find
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. 判空检查:必不可少的 `npos`](#1. 判空检查:必不可少的
npos) - [2\. 循环查找模式](#2. 循环查找模式)
- [3\. `find` vs `find_first_of` (易混淆)](#3.
findvsfind_first_of(易混淆)) - [4\. 效率提示](#4. 效率提示)
- [1\. 判空检查:必不可少的 `npos`](#1. 判空检查:必不可少的
- std::string::rfind
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. `pos` 参数的逆向逻辑(易错点)](#1.
pos参数的逆向逻辑(易错点)) - [2\. 经典应用场景:文件路径解析](#2. 经典应用场景:文件路径解析)
- [3\. `rfind` vs `find_last_of`](#3.
rfindvsfind_last_of) - [4\. 性能提示](#4. 性能提示)
- [1\. `pos` 参数的逆向逻辑(易错点)](#1.
- std::string::find_first_of
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. 核心区别:`find` vs `find_first_of`](#1. 核心区别:
findvsfind_first_of) - [2\. 常见应用场景](#2. 常见应用场景)
- [3\. 它的孪生兄弟:`find_first_not_of`](#3. 它的孪生兄弟:
find_first_not_of) - [4\. 性能陷阱](#4. 性能陷阱)
- [1\. 核心区别:`find` vs `find_first_of`](#1. 核心区别:
- std::string::find_last_of
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点](#2. 技术总结与核心考点)
-
- [1\. 核心对比:`rfind` vs `find_last_of`](#1. 核心对比:
rfindvsfind_last_of) - [2\. 经典场景:跨平台路径分割](#2. 经典场景:跨平台路径分割)
- [3\. 边界情况处理](#3. 边界情况处理)
- [4\. 配合 `substr`](#4. 配合
substr)
- [1\. 核心对比:`rfind` vs `find_last_of`](#1. 核心对比:
- [std::string::find_first_not_of 和 std::string::find_last_not_of](#std::string::find_first_not_of 和 std::string::find_last_not_of)
-
- [1\. `find_first_not_of` (查找第一个不匹配的字符)](#1.
find_first_not_of(查找第一个不匹配的字符)) - [2\. `find_last_not_of` (从末尾查找第一个不匹配的字符)](#2.
find_last_not_of(从末尾查找第一个不匹配的字符)) - [3\. 技术总结与核心考点](#3. 技术总结与核心考点)
-
- [1\. 逻辑取反 (Inverted Logic)](#1. 逻辑取反 (Inverted Logic))
- [2\. Trim 的完整实现 (笔记必备)](#2. Trim 的完整实现 (笔记必备))
- [3\. 性能提示](#3. 性能提示)
- [1\. `find_first_not_of` (查找第一个不匹配的字符)](#1.
- std::string::substr
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. 参数行为差异 (重要考点)](#1. 参数行为差异 (重要考点))
- [2\. 性能代价:深拷贝 (Deep Copy)](#2. 性能代价:深拷贝 (Deep Copy))
- [3\. 现代替代方案:`std::string_view` (C++17)](#3. 现代替代方案:
std::string_view(C++17)) - [4\. 常用简写](#4. 常用简写)
- std::string::compare
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. 性能杀手锏:避免 `substr` 产生的临时拷贝](#1. 性能杀手锏:避免
substr产生的临时拷贝) - [2\. 三路比较 (Three-way Comparison)](#2. 三路比较 (Three-way Comparison))
- [3\. 记忆口诀:减法逻辑](#3. 记忆口诀:减法逻辑)
- [4\. 参数重载的识别技巧](#4. 参数重载的识别技巧)
- [1\. 性能杀手锏:避免 `substr` 产生的临时拷贝](#1. 性能杀手锏:避免
- 第十章:成员常量
-
- std::string::npos
-
- [📝 翻译 (Translation)](#📝 翻译 (Translation))
- [💡 核心总结 (Summary)](#💡 核心总结 (Summary))
- 第十一章:非成员函数重载
-
- [[std::operator+ (string)](https://legacy.cplusplus.com/reference/string/string/operator+/)](#std::operator+ (string))
-
- [📝 文档翻译 (Translation)](#📝 文档翻译 (Translation))
-
- [2\. 功能描述](#2. 功能描述)
- [3\. 参数与复杂度 (Parameters & Complexity)](#3. 参数与复杂度 (Parameters & Complexity))
- [4\. 安全性与异常 (Safety & Exceptions)](#4. 安全性与异常 (Safety & Exceptions))
- [💡 核心总结 (Summary)](#💡 核心总结 (Summary))
- [[relational operators (string)](https://legacy.cplusplus.com/reference/string/string/operators/)](#relational operators (string))
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. 字典序比较 (Lexicographical Comparison)](#1. 字典序比较 (Lexicographical Comparison))
- [2\. 与 `compare()` 的关系](#2. 与
compare()的关系) - [3\. 性能优化细节 (`operator==` vs `operator<`)](#3. 性能优化细节 (
operator==vsoperator<)) - [4\. 混合类型比较的便利性](#4. 混合类型比较的便利性)
- [[std::swap (string)](https://legacy.cplusplus.com/reference/string/string/swap-free/)](#std::swap (string))
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. 性能核心:指针交换 (Pointer Swapping)](#1. 性能核心:指针交换 (Pointer Swapping))
- [2\. 迭代器失效 (Iterator Invalidation)](#2. 迭代器失效 (Iterator Invalidation))
- [3\. 全局 vs 成员函数](#3. 全局 vs 成员函数)
- [4\. 异常安全性](#4. 异常安全性)
- [[std::operator>> (string)](https://legacy.cplusplus.com/reference/string/string/operator>>/)](#std::operator>> (string))
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点](#2. 技术总结与核心考点)
-
- [1\. 空白字符陷阱 (The Whitespace Trap)](#1. 空白字符陷阱 (The Whitespace Trap))
- [2\. 覆盖行为 (Overwrite)](#2. 覆盖行为 (Overwrite))
- [3\. 内存自动管理](#3. 内存自动管理)
- [4\. 对比 `std::getline`](#4. 对比
std::getline)
- [[std::operator<< (string)](https://legacy.cplusplus.com/reference/string/string/operator<</)](#std::operator<< (string))
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. 为什么它存在? (The "Why")](#1. 为什么它存在? (The "Why"))
- [2\. 链式调用的原理 (Chaining)](#2. 链式调用的原理 (Chaining))
- [3\. 与 `printf` 的主要区别](#3. 与
printf的主要区别) - [4\. 对比 Input (`>>`)](#4. 对比 Input (
>>))
- [[std::getline (string)](https://legacy.cplusplus.com/reference/string/string/getline/)](#std::getline (string))
-
- [1\. 中文翻译 (Translation)](#1. 中文翻译 (Translation))
- [2\. 技术总结与核心考点 (Summary & Key Points)](#2. 技术总结与核心考点 (Summary & Key Points))
-
- [1\. 核心特性:吃掉分隔符 (Consume and Discard)](#1. 核心特性:吃掉分隔符 (Consume and Discard))
- [2\. 与 `std::cin >>` 的对比 (必考点)](#2. 与
std::cin >>的对比 (必考点)) - [3\. 经典陷阱:混合使用的"幽灵空行"](#3. 经典陷阱:混合使用的“幽灵空行”)
- [4\. 常见应用:按自定义分隔符读取](#4. 常见应用:按自定义分隔符读取)
- 补充:[std::sort](https://legacy.cplusplus.com/reference/algorithm/sort/)
-
-
- [1. 核心翻译与功能总结](#1. 核心翻译与功能总结)
- [2. 代码示例解析](#2. 代码示例解析)
- [3. 深度解析:`<algorithm>` 中的细节](#3. 深度解析:
<algorithm>中的细节) -
- [A. 为什么强调 "Move-Constructible" (可移动构造)?](#A. 为什么强调 "Move-Constructible" (可移动构造)?)
- [B. 什么是 "Strict Weak Ordering" (严格弱序)?](#B. 什么是 "Strict Weak Ordering" (严格弱序)?)
- [C. 数据竞争 (Data Races)](#C. 数据竞争 (Data Races))
- [D. 视觉化理解](#D. 视觉化理解)
-
前言
本文介绍初步介绍STL和对C++的string类文档进行简单的解读,由于该文篇幅过长,string类模拟实现放在下一篇博客
(【由浅入深】是一个系列文章,它记录了我个人作为一个小白,在学习c++技术开发方向计相关知识过程中的笔记,欢迎各位彭于晏刘亦菲从中指出我的错误并且与我共同学习进步,作为该系列的第三部曲-c++,大部分知识会根据本人所学和我的助手------通义,gimini等以及合并网络上所找到的相关资料进行核实编写,每一篇文章都可能会因为一些错误在后续时间增删改查,因为该系列会按照我在互联网中的学习笔记形式编写,我会使用绝大多数人使用的讲解顺序编写,所以基础框架和大部分内容案例会与他人一样,基础知识不会过于详细讲述)
第一章 STL初识:C++程序员的"屠龙刀"
在 C++ 的世界里,流传着这样一句话:"不懂 STL,不要说你会 C++"。
如果说 C 语言是手工打造每一个零件,那么 C++ 的 STL 就是为你提供了一座设备齐全的现代化工厂。从今天开始,我们将正式进入 C++ 的标准模板库 章节。这一篇,我们不写代码,我们来谈谈 STL 的前世今生与宏观架构。
1. 什么是 STL?
STL(Standard Template Library),即标准模板库,是 C++ 标准库的重要组成部分,也是 C++ 标准库的核心。
从代码层面看,它是一套功能强大、基于模板的复用组件库;从思想层面看,它是泛型编程(Generic Programming) 的集大成者。
核心理念 :
STL 的设计目标是将 数据容器(Data Structures) 和 算法(Algorithms) 分离,二者通过 迭代器(Iterators) 进行粘合。这种设计使得算法可以独立于具体的数据结构,实现了极高的复用性。
stl库把所有的实现都放在 std 命名空间里面
2. STL 的版本流派
STL 并非只有一个版本。就像 Linux 有 Ubuntu、CentOS 一样,STL 也有不同的实现版本。了解这些有助于我们在调试和阅读源码时有的放矢。
- HP STL :
- STL 的鼻祖,由 Alexander Stepanov 和 Meng Lee 在惠普实验室完成。它是所有后续 STL 实现的蓝本。
- P.J. Plauger STL :
- 继承自 HP STL,被 Visual Studio (MSVC) 采用。它的特点是变量命名非常怪异(充斥着下划线),可读性较差,不适合初学者阅读源码。
- RW STL :
- 由 Rogue Wave 公司开发,主要用于一些早期的 C++ 编译器(如 C++ Builder)。
- SGI STL ✨(重点 ):
- 由 Silicon Graphics Computer Systems 公司开发。
- 特点:被 GCC (Linux) 采用,代码风格极佳,变量命名规范,且在标准之上增加了很多实用的扩展(如哈希容器、空间配置器)。
- 学习建议 :我们后续深入剖析 STL 源码时,主要参考的就是 SGI STL 版本(也是经典书籍《STL源码剖析》的蓝本)。
你在 Linux 下写 C/C++ 代码,默认调用的就是 GCC(命令是 gcc 或 g++)。
3. STL 的六大组件(核心架构)
这是 STL 最重要的宏观概念。STL 不仅仅是容器,它是一个紧密协作的生态系统。
- 容器 (Containers) :
- 本质 :各种数据结构,如
vector,list,deque,set,map,string。 - 作用:负责存储数据。
- 本质 :各种数据结构,如
- 算法 (Algorithms) :
- 本质 :各种常用算法,如
sort,find,copy,for_each。 - 作用:负责处理数据。
- 本质 :各种常用算法,如
- 迭代器 (Iterators) :
- 本质:扮演"指针"的角色。
- 作用 :胶水。它是算法访问容器中元素的工具,算法通过迭代器遍历容器,而不需要知道容器底层的实现细节。
- 仿函数 (Functors) :
- 本质 :行为类似函数的对象(重载了
operator()的类)。 - 作用 :作为算法的某种"策略"或"规则",例如告诉
sort算法是升序还是降序。
- 本质 :行为类似函数的对象(重载了
- 适配器 (Adapters) :
- 本质:一种设计模式。
- 作用 :用于修饰容器或仿函数接口。例如
stack和queue其实并非独立的容器,而是由deque适配(包装)而来的。
- 空间配置器 (Allocators) :
- 本质:内存管理的底层实现。
- 作用 :负责空间的配置与管理。它隐藏在容器背后,默默地进行
new和delete的工作,通常使用内存池技术来提高效率。
六大组件的关系图:
容器存储数据 -> 空间配置器提供内存 -> 算法通过迭代器访问数据 -> 仿函数协助算法制定策略 -> 适配器转换接口。
4. STL 的重要性
为什么面试必问?为什么工作必用?
- 避免重复造轮子:你不需要手写链表、红黑树或快排,STL 提供了经过工业级测试的高效实现。
- 高性能:STL 的源码经过了无数大牛的优化(例如 SGI STL 的二级空间配置器、红黑树的各种旋转优化),通常比普通程序员手写的代码效率更高。
- 标准化:使用 STL 编写的代码具有良好的跨平台性,Windows 能跑,Linux 也能跑。
- 进阶基石:熟练掌握 STL 是阅读大型 C++ 项目(如 Chromium, TensorFlow)源码的前提。
5. 如何高效学习 STL?
STL 的学习可以分为三个境界,建议按部就班:
- 第一重境界:熟练使用(User Level)
- 掌握常用容器(
string,vector,list,map等)的接口。 - 知道在什么场景下选择什么容器(例如:需要频繁查找用
map,需要频繁随机访问用vector)。
- 掌握常用容器(
- 第二重境界:了解原理(Concept Level)
- 理解底层数据结构。例如:
vector是动态数组,list是双向链表,map是红黑树。 - 理解迭代器失效的问题。
- 理解底层数据结构。例如:
- 第三重境界:模拟实现与源码剖析(Hacker Level)
- 尝试自己手写一个简易版的
vector或string。 - 阅读 SGI STL 源码,理解空间配置器 的内存池技术、Traits 编程技巧 以及模板偏特化的黑魔法。
- 尝试自己手写一个简易版的
6. STL 的缺陷
任何技术都不是完美的,STL 也有它的痛点,面试中不仅要夸它,也要能客观评价它:
- 代码膨胀:由于大量使用模板,编译时会生成大量的实例化代码,导致可执行文件体积变大。
- 编译速度慢:模板的解析和编译非常耗时。
- 报错信息极其难懂:一旦模板使用出错,编译器会抛出几百行的错误信息,被戏称为"天书",极难调试。
- 线程安全问题 :STL 容器本身通常不是线程安全的。在多线程环境下并发读写同一个容器,需要开发者手动加锁。
- 内存碎片:虽然有空间配置器,但在频繁进行小块内存分配和释放时,仍可能产生内存碎片(取决于具体的实现)。
结语 :
STL 是 C++ 的灵魂。从这一篇博客开始,我们将从最简单的
string类入手,一步步拆解这个庞大的武器库。准备好开启 C++ 的进阶之旅了吗?
第二章 String类:为什么要学习string类?
在 C 语言的初阶阶段,我们无数次被字符数组(
char[])和字符指针(char*)折磨过:忘记加'\0'导致的乱码、strcpy造成的越界访问、繁琐的字符串拼接......C++ 标准库告诉我们:这种苦日子,该结束了。
今天我们正式进入 STL 的预热章节------
string类。它是 C++ 给你的一份礼物,让你从此告别底层内存管理的泥潭,专注于业务逻辑。
1. C 语言字符串的"三大原罪"
要明白为什么要学 string,首先得回忆一下我们在 C 语言中使用字符串时的痛点。C 语言中的字符串并不是一种真正的"类型",而是以空字符 \0 结尾的字符数组。这种设计带来了极大的不便:
- 内存管理地狱 :
- 你需要手动计算字符串长度,手动
malloc开辟空间,用完还得记得free。一旦忘记释放就是内存泄漏,算错大小就是缓冲区溢出。
- 你需要手动计算字符串长度,手动
- 功能极度匮乏 :
- 想拼接两个字符串?你得用
strcat,还战战兢兢地担心目标空间够不够。 - 想查找子串?
strstr返回的是指针,处理起来非常麻烦。 - 想比较字符串?不能直接用
==,必须用strcmp。
- 想拼接两个字符串?你得用
- 安全性极低 :
- C 风格字符串是很多安全漏洞(如栈溢出攻击)的万恶之源。
举个例子 :仅仅是简单的"将 s2 拼接到 s1 后面",C 语言需要你考虑 s1 的容量是否足够,如果不够还要重新
realloc... 而在 C++ 中,你只需要写:s1 += s2;。
2. String 类:C++ 的优雅解决方案
C++ 标准库中的 std::string 是一个类(Class)。它将字符数组封装在内部,自动管理内存,并提供了一系列丰富的方法。
学习 string 的核心理由如下:
- 自动内存管理(RAII) :
你只管往里塞字符,扩容、缩容、释放内存全部由string类在底层自动完成。你再也不用写malloc和free。 - 直观的操作符重载 :
- 赋值直接用
=。 - 比较直接用
==,>,<。 - 拼接直接用
+或+=。 - 获取某个字符直接用
[]下标访问。
这使得处理字符串就像处理int一样自然。
- 赋值直接用
- 丰富的接口 :
查找(find)、替换(replace)、插入(insert)、截取子串(substr)等功能应有尽有,直接调用即可。 - 与 STL 的无缝衔接 :
string虽然早于 STL 诞生,但它在设计上与 STL 容器高度一致。它拥有迭代器(Iterator) ,可以配合 STL 算法(如sort,reverse)使用。
3. 为什么 string 是学习 STL 的"第一课"?
很多教材直接讲 vector,但我建议先学 string,原因有二:
- 平滑过渡 :
string可以看作是专门管理char的vector(动态数组)。它的内部结构(连续存储、动态扩容、迭代器)与vector几乎一模一样。 - 降低认知负担 :
相比于模板容器(需要理解template<class T>),字符串是我们最熟悉的数据类型。先通过string熟悉了迭代器、接口设计风格,后面学习真正的 STL 容器时,你会发现一切都是熟悉的味道。
4. 实战中的地位:OJ 与 面试
- 在工作开发中 :现代 C++ 开发中,除非涉及极底层的驱动或嵌入式开发,否则严禁 使用 C 风格字符串。
std::string是行业标准。 - 在笔试面试中 :
LeetCode 或牛客网上的算法题,涉及字符串处理的题目占比极大。- 如果你用 C 语言写:可能需要写 50 行代码来处理内存和拼接,稍不注意就
Segmentation Fault。 - 如果你用
string:核心逻辑可能只需要 5 行。
为了在面试中节省宝贵的时间,熟练掌握string是必修课。
- 如果你用 C 语言写:可能需要写 50 行代码来处理内存和拼接,稍不注意就
结语 :
学习
string类,不只是为了处理文本,更是为了适应 C++ "对象管理资源" 的编程思想。既然
string这么好用,它的内部拥有哪些接口?它是如何自动扩容的?我们该如何高效地遍历它?下一篇文章,我们将正式深入 String 类的常用接口与核心用法,带你玩转 C++ 字符串!
第三章 String类注意事项
既然 C++ 的
string是个类,那么学习它无非就是学习它的成员函数 。
std::string的接口非常多(有一百多个),初学者容易迷失。但实际上,高频使用的核心接口只有几十个。本文将这些接口分为四大类:默认基础 、容量操作 、增删查改 、非成员操作,带你彻底玩转 C++ 字符串。
注意:
1.在使用string类时,必须包含#include 以及using namespace std(或者std::string),string 类是被包裹在 std(Standard)这个命名空间里的。
2.C++ 标准库(Standard Library,STL 是其中的核心部分)中定义的所有类、函数、对象和模板,都放在了 std 这个命名空间或者其子命名空间中。
4.string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。在 C++ 标准库头文件 中,std::string 大致是这样定义的:using string = std::basic_string<char, std::char_traits<char>, std::allocator<char>>;
5.ASCII:std::string 是最佳选择。UTF-8 (Unicode):可以用 std::string 存储和传递(这是目前的行业主流做法),但在涉及计算长度、截取子串、遍历字符时,不能直接调成员函数,需要使用专门处理 UTF-8 的库(如 ICU 库)或者自己写算法来处理多字节逻辑。
怎么造出一个字符串?这是第一步。
cpp
void TestConstructor() {
// 1. 默认构造:空字符串 ""
string s1;
// 2. 字符数组构造:最常用的方式
string s2("Hello World");
// 3. 拷贝构造:用一个已有的 string 初始化另一个
string s3(s2);
// 4. 填充构造:用 n 个字符 c 初始化
string s4(10, 'x'); // "xxxxxxxxxx"
// 5. 子串构造 (重点细节)
// string(const string& str, size_t pos, size_t len = npos);
// 从 s2 的下标 6 开始,拷贝 5 个字符
string s5(s2, 6, 5); // "World"
}
注意:
std::string重载了流插入<<和流提取>>运算符,所以我们可以直接cout << s1或cin >> s1。cin >> s遇到空格或换行会停止读取。
第四章:默认成员函数
构造函数
1. 核心翻译:构造函数列表
std::string 提供了多种构造函数,以适应不同的初始化场景:
| 编号 | 名称 | 原型示例 | 功能描述 |
|---|---|---|---|
| (1) | 默认构造 (Default) | string(); |
创建一个空字符串,长度为 0。 |
| (2) | 拷贝构造 (Copy) | string(const string& str); |
创建 str 的副本。 |
| (3) | 子串构造 (Substring) | string(const string& str, size_t pos, size_t len = npos); |
复制 str 中从 pos 位置开始的 len 个字符。 (如果 len 是 npos,则一直复制到末尾)。 |
| (4) | C风格字符串 (From C-string) | string(const char* s); |
复制以空字符结尾的 C 风格字符串 s。 |
| (5) | 缓冲区构造 (From buffer) | string(const char* s, size_t n); |
复制字符数组 s 中的前 n 个字符(即使中间有空字符也会复制)。 |
| (6) | 填充构造 (Fill) | string(size_t n, char c); |
创建一个包含 n 个字符 c 的字符串。 |
| (7) | 范围构造 (Range) | string(InputIterator first, InputIterator last); |
复制迭代器范围 [first, last) 内的字符序列。 |
| (8) | 初始化列表 (Init List) | (C++11) | 使用初始化列表构造,如 string s = {'a', 'b', 'c'}; |
| (9) | 移动构造 (Move) | (C++11) string(string&& str); |
窃取 str 的资源,避免深拷贝。str 变为空或未定义状态。 |
2. 代码示例解析
以下是文档中提供的代码示例的中文注释版,展示了上述构造函数的实际用法:
cpp
#include <iostream>
#include <string>
int main ()
{
// 先定义一个初始字符串用于后续拷贝
std::string s0 ("Initial string");
// 1. 默认构造:空字符串
std::string s1;
// 2. 拷贝构造:完全复制 s0
std::string s2 (s0);
// 3. 子串构造:从 s0 的第 8 个字符开始,复制 3 个字符 ("str")
std::string s3 (s0, 8, 3);
// 4. C风格字符串构造:直接用双引号的字符串字面量初始化
std::string s4 ("A character sequence");
// 5. 缓冲区构造:复制该字符串的前 12 个字符
std::string s5 ("Another character sequence", 12);
// 6. 填充构造:
std::string s6a (10, 'x'); // 10个 'x'
std::string s6b (10, 42); // 10个 ASCII 42 (即 '*')
// 7. 范围构造:使用迭代器复制 s0 的前 7 个字符
std::string s7 (s0.begin(), s0.begin()+7);
// 输出结果
std::cout << "s1: " << s1 << "\ns2: " << s2 << "\ns3: " << s3;
std::cout << "\ns4: " << s4 << "\ns5: " << s5 << "\ns6a: " << s6a;
std::cout << "\ns6b: " << s6b << "\ns7: " << s7 << '\n';
return 0;
}
输出结果:
text
s1:
s2: Initial string
s3: str
s4: A character sequence
s5: Another char
s6a: xxxxxxxxxx
s6b: **********
s7: Initial
3. 关键注意事项与异常安全(总结)
在使用这些构造函数时,必须注意以下潜在的错误和异常:
-
越界错误 (
std::out_of_range):- 仅在 (3) 子串构造 时发生。
- 如果你指定的起始位置
pos大于源字符串的长度,会抛出此异常。
-
未定义行为 (Undefined Behavior, UB) ------ 程序会崩溃或乱码:
- 空指针: 如果传入的 C 风格字符串指针
s是NULL(空指针)。 - 读取越界: 在 (5) 缓冲区构造 中,如果
n大于实际字符数组的长度。 - 无效范围: 在 (7) 范围构造 中,如果
[first, last)不是一个有效的区间(例如first在last之后)。
- 空指针: 如果传入的 C 风格字符串指针
-
内存分配失败 (
std::bad_alloc):- 如果系统内存不足,无法为新字符串分配空间,任何构造函数都可能抛出此异常。
-
特殊的参数:
string::npos- 它是一个非常大的无符号整数(通常是
-1强转后的值)。 - 在 (3) 子串构造 中,它表示"直到字符串末尾"。
- 它是一个非常大的无符号整数(通常是
总结建议
- 最常用: (1) 默认, (2) 拷贝, (4) C风格字符串。
- 初始化固定长度缓冲区: 使用 (6) 填充构造,例如
string s(1024, '\0');用作接收数据的 buffer。 - 截取字符串: 使用 (3) 子串构造。
- 安全隐患: 永远不要把
NULL传给 string 的构造函数。
析构函数
1. 核心翻译
- 函数原型:
~string(); - 功能描述:
- 销毁 string 对象。
- 通过该对象的分配器(allocator),释放(Deallocate)所有分配给该字符串的存储容量(内存)。
2. 技术细节参数
| 属性 | 描述 |
|---|---|
| 时间复杂度 | 未指定,但通常是常数时间 O(1) 。 (释放一块连续内存通常很快,与字符串长度无关,除非使用了复杂的自定义分配器)。 |
| 迭代器有效性 | 全部失效 。 对象被销毁后,所有指向该字符串内容的迭代器、指针和引用都会变成"悬空指针",再使用会导致未定义行为。 |
| 数据竞争 | 对象会被修改(被销毁)。 |
| 异常安全性 | 无异常保证 (No-throw guarantee) 。 析构函数保证绝不抛出异常。这是 C++ 析构函数的黄金法则。 |
3. 总结与专家解读
虽然这段文档很短,但它体现了 C++ 内存管理的核心机制:
A. 自动化内存管理 (RAII)
你不需要(也不应该)手动调用这个函数。
- 当
std::string对象超出其作用域(例如函数结束、if块结束)时,或者包含它的对象被销毁时,编译器会自动 插入对~string()的调用。 - 这与 C 语言不同,你不需要手动写
free()。
B. 它是如何释放内存的?
std::string 内部通常维护一个指向堆内存(Heap)的指针。
- 析构时:
~string()会将这块堆内存归还给操作系统。 - 短字符串优化 (SSO): 现代 C++ 编译器(如 GCC, Clang, MSVC)通常有 SSO 机制。如果字符串很短(例如 15 字节以内),它直接存放在栈上对象内部,不分配堆内存。这种情况下,析构函数几乎什么都不用做,速度极快。
C. 为什么"绝不抛出异常"很重要?
如果析构函数抛出异常,而此时程序正因为另一个异常在进行"栈展开"(Stack Unwinding),C++ 运行时会直接调用 std::terminate() 导致程序立即崩溃 。因此,std::string 的析构函数必须是安全的。
一句话总结
~string() 是 std::string 的清理工,它在字符串生命周期结束时自动运行,负责退还内存,且保证过程安全、不报错。
std::string::operator=
这是一份关于 std::string::operator= (赋值运算符重载) 的翻译与技术总结。
这个函数的作用是将新值赋给字符串对象,替换其原有的内容。
1. 核心翻译:赋值方式列表
std::string 重载了 = 运算符,支持 5 种不同的赋值来源:
| 编号 | 名称 | 原型示例 | 功能描述 |
|---|---|---|---|
| (1) | 字符串拷贝赋值 (String) | string& operator= (const string& str); |
将另一个 string 对象 str 的内容复制给当前对象。 |
| (2) | C风格字符串赋值 (C-string) | string& operator= (const char* s); |
将 s 指向的 C 风格字符串(以 null 结尾)复制给当前对象。 |
| (3) | 单字符赋值 (Character) | string& operator= (char c); |
将当前字符串的内容变成单个字符 c(长度变为 1)。 |
| (4) | 初始化列表赋值 (Initializer list) | (C++11) string& operator= (initializer_list<char> il); |
使用初始化列表赋值,如 str = {'a', 'b'}; |
| (5) | 移动赋值 (Move) | (C++11) string& operator= (string&& str) noexcept; |
窃取 str 的资源。当前对象获得内容,str 变为空或未定义状态。效率极高。 |
返回值:
所有重载都返回 *this(即当前对象的引用)。这允许链式赋值 ,例如:str1 = str2 = str3;。
2. 代码示例解析
以下是文档中示例代码的中文注释版,展示了最常用的几种赋值方式:
cpp
#include <iostream>
#include <string>
int main ()
{
std::string str1, str2, str3;
// 调用 (2):C风格字符串赋值
str1 = "Test string: ";
// 调用 (3):单字符赋值
str2 = 'x';
// 调用 (1):字符串拷贝赋值
// 解释:str1 + str2 产生一个临时 string 对象,然后赋值给 str3
str3 = str1 + str2;
std::cout << str3 << '\n';
return 0;
}
输出:
text
Test string: x
3. 技术细节总结(考试/面试重点)
A. 复杂度 (Complexity)
- 拷贝赋值 (1, 2, 3, 4): O(N),线性复杂度。因为需要申请新内存并逐个复制字符(N 是新字符串的长度)。
- 移动赋值 (5): O(1),常数复杂度。仅仅是交换了内部指针,没有发生数据复制。
B. 迭代器有效性 (Iterator Validity)
- 全部失效。
- 一旦执行了赋值操作,之前指向该字符串的所有迭代器、指针、引用都不能再使用了,否则会导致程序崩溃。
C. 异常安全性 (Exception Safety)
这是 C++ 中非常重要的概念:
- 移动赋值 (5): No-throw guarantee(不抛出异常) 。标记为
noexcept,非常安全且高效。 - 其他赋值 (1-4): Strong guarantee(强保证) 。如果赋值过程中发生错误(比如内存不足
bad_alloc),原字符串的内容保持不变 ,不会出现数据只改了一半的情况。- 注:如果新字符串太长超过
max_size,会抛出length_error。
- 注:如果新字符串太长超过
D. 移动后的源对象 (Moved-from state)
在执行 strA = std::move(strB) 后,strB 处于"未指定但有效"的状态。
- 这意味着你不能依赖
strB原来的内容(它通常变为空了)。 - 但你可以安全地销毁
strB或者给它赋新值。
一句话总结
operator= 用于给字符串"换血",支持从字符串、字符数组、字符等多种类型赋值;在 C++11 中应尽量利用移动赋值(Move Assignment)来提升性能。
第五章:迭代器
补充:iterator
iterator(迭代器)是 C++ 标准模板库 (STL) 的灵魂。iterator 是一个类型(Type)。
简单来说,迭代器是一个泛型的指针 。它提供了一种统一的方式来访问容器中的元素,而不需要你关心容器底层的内存结构(是数组、链表还是树)。
通常情况下,迭代器(Iterator)是定义在容器类(Container Class)内部的。
1. 核心概念:为什么需要迭代器?
想象一下,你面前有两个数据结构:
- 数组 (
vector):内存是连续的。访问下一个元素只需要内存地址 +1。 - 链表 (
list) :内存是分散的。访问下一个元素需要读取next指针跳转。
如果没有迭代器,你需要为每种数据结构写两套遍历代码。
有了迭代器 ,你只需要告诉它"我要下一个 (++)"或者"我要读数据 (*)",迭代器会在内部自动处理具体的跳转逻辑。
它的地位: 它是算法 和容器 之间的桥梁。
2. 迭代器的基本操作
虽然迭代器底层实现千差万别,但对外表现得像一个指针。绝大多数迭代器支持以下操作:
- 解引用 (
*it):获取迭代器当前指向的元素的值。 - 成员访问 (
it->):如果你存的是对象,访问对象的成员。 - 递增 (
++it):移动到下一个元素。 - 比较 (
it != end):判断两个迭代器是否相等(通常用于判断是否遍历结束)。
基础代码示例
cpp
#include <iostream>
#include <vector>
#include <list>
int main() {
// 1. 对于 Vector (底层是数组)
std::vector<int> vec = {1, 2, 3};
std::vector<int>::iterator it_vec = vec.begin();
// 像指针一样使用
std::cout << *it_vec << std::endl; // 输出 1
it_vec++; // 移动到 2
// 2. 对于 List (底层是双向链表)
std::list<int> lst = {10, 20, 30};
std::list<int>::iterator it_lst = lst.begin();
// 即使底层结构完全不同,用法却一模一样!
std::cout << *it_lst << std::endl; // 输出 10
it_lst++; // 移动到 20
return 0;
}
3. 迭代器的五大种类(重点)
不是所有迭代器生来都是平等的。根据容器底层的能力,迭代器被分为 5 个等级(能力由弱到强):
| 迭代器类型 | 能力描述 | 支持的操作 | 典型容器 |
|---|---|---|---|
| 1. 输入迭代器 (Input) | 只读,只能向前,一次性 | ++, * (只读), == |
istream_iterator (cin) |
| 2. 输出迭代器 (Output) | 只写,只能向前,一次性 | ++, * (只写) |
ostream_iterator (cout) |
| 3. 前向迭代器 (Forward) | 可读写,只能向前 | ++, *, -> |
forward_list, 哈希表 (unordered_map) |
| 4. 双向迭代器 (Bidirectional) | 可读写,可后退 | ++, -- |
list, map, set |
| 5. 随机访问迭代器 (Random Access) | 最强王者,可跳跃 | it + n, it[n], < |
vector, deque, array, string |
为什么这个分类很重要?
这决定了你可以对容器使用什么算法。
- 例子 :
std::sort需要随机访问迭代器 。- 你可以对
vector使用std::sort。 - 你不能 对
list使用std::sort(因为链表不能瞬间跳转到中间位置),所以list自带了一个特殊的list.sort()成员函数。
- 你可以对
4. 迭代器失效 ------ 常见的坑
迭代器本质上指向内存中的某个位置。如果容器的内存发生了变化,你手里的迭代器可能会变成"悬空指针",继续使用会导致程序崩溃。
示例:在 Vector 遍历时添加元素
cpp
std::vector<int> v = {1, 2, 3};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 2) {
// 危险!!push_back 可能会导致 vector 扩容(搬家),
// 旧的内存被释放,此时 it 变成野指针。
v.push_back(4);
}
} // 程序崩溃或未定义行为
规则总结:
- Vector/String :插入/删除可能会导致所有迭代器失效(因为可能发生了内存重新分配)。
- List/Map/Set :插入/删除通常只影响被删除那个节点的迭代器,其他迭代器依然有效(因为节点在内存中是独立的)。
5. 底层实现原理(简化版)
你可能会好奇,迭代器是怎么写出来的?其实它通常是一个嵌套类。
对于 std::vector(数组),迭代器可能只是一个简单的指针别名:
cpp
// 伪代码
using iterator = T*;
对于 std::list(链表),迭代器是一个类,包裹了节点指针:
cpp
// 伪代码,List 的迭代器实现
template <typename T>
struct ListIterator {
Node<T>* current; // 持有当前节点的指针
// 重载 ++ 操作符
ListIterator& operator++() {
current = current->next; // 具体的链表跳转逻辑
return *this;
}
// 重载 * 操作符
T& operator*() {
return current->data; // 取出数据
}
// ... 其他重载
};
总结
- 统一接口:让不同的容器拥有一致的遍历方式。
- 分离算法 :
std::sort不需要知道它排的是数组还是什么,只要你有迭代器就行。 - 注意层级:知道你的迭代器能不能后退(双向)或跳跃(随机)。
- 小心失效:修改容器结构时,手里的迭代器可能已经"过期"了。
const _iterator
普通的 iterator 确实无法用来遍历 const 容器,因为那样会破坏"只读"的承诺。
但是,C++ 并没有因此禁止你遍历 const 对象,它给出了专门的解决方案:const_iterator。
这就好比:
iterator:是"管理员权限"的指针,既能看也能改。const容器:是一个"上了锁"的房间。- 冲突 :你不能拿着"管理员钥匙"(
iterator)去开"普通游客"的门,因为编译器怕你进去乱改东西。
解决办法就是给你一张"游客通行证" ------ const_iterator。
1. 为什么 iterator 无法遍历 const 对象?
这是 C++ 类型安全机制在起作用。
如果你有一个 const std::vector<int>,里面的元素就变成了 const int。
- 普通
iterator解引用后得到的是int&(可读可写引用)。 - 如果你能用
iterator指向const数据,你就可以通过*it = 10修改它,这违背了const的初衷。
所以,编译器直接报错,禁止这种转换。
2. 正确姿势:使用 const_iterator
const_iterator 是一种只读迭代器。
- 能做的事 :
++it(移动位置),*it(读取数值)。 - 不能做的事 :
*it = 5(修改数值)。
代码演示
cpp
#include <iostream>
#include <vector>
// 这个函数接收 const 引用,保证不会修改 vec
void printVector(const std::vector<int>& vec) {
// 错误写法:
// std::vector<int>::iterator it = vec.begin();
// 报错!无法将 const_iterator 转换为 iterator
// 正确写法 1:显式声明 const_iterator
for (std::vector<int>::const_iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
// *it = 100; // 错误!这是只读的,不能修改
}
std::cout << "\n";
}
int main() {
std::vector<int> nums = {1, 2, 3};
printVector(nums);
return 0;
}
3. C++11 的语法糖:cbegin() 和 cend()
为了避免你每次都要写很长的 const_iterator,或者为了避免 auto 自动推导时混淆,C++11 引入了强制获取只读迭代器的方法。
即使你的容器不是 const 的,你也可以强制获取一个只读的迭代器来保证安全。
cpp
std::vector<int> v = {10, 20, 30};
// 1. 自动推导:
// 因为 v 是非 const 的,v.begin() 默认返回 iterator (可写)
auto it1 = v.begin();
*it1 = 99; // 允许修改
// 2. 强制只读:
// 使用 cbegin() (const begin),强制返回 const_iterator
auto it2 = v.cbegin();
// *it2 = 99; // 编译报错!it2 是只读的
4. 容易混淆的概念:const_iterator vs const iterator
这就像指针中的 const int* 和 int* const 的区别,是 C++ 面试的经典陷阱:
| 写法 | 对应指针概念 | 含义 | 能移动吗 (++it)? |
能改数据吗 (*it=x)? |
|---|---|---|---|---|
const_iterator |
const int* |
指向常量的迭代器 (最常用) | ✅ 能 | ❌ 不能 |
const iterator |
int* const |
常迭代器 (指针本身不能动) | ❌ 不能 | ✅ 能 |
极少 会用到 const iterator(因为不能 ++,甚至没法遍历),99% 的情况你需要的是 const_iterator。
总结
- 无法遍历 const :是因为你试图用"可写"的
iterator去指"只读"的数据。 - 解决方案 :使用
const_iterator。 - 最佳实践 :如果你的函数参数是
const &,或者你明确不需要修改数据,请尽量使用const_iterator或cbegin(),这样代码更安全。
3种迭代器遍历方式
这是一个非常基础且重要的 C++ 知识点。在 C++ 中,遍历容器(如 std::vector 或 std::string)主要有这三种经典方式。
为了方便演示,假设我们要遍历一个 std::vector<int>:
cpp
std::vector<int> nums = {10, 20, 30, 40, 50};
以下是这三种方式的详细实现与对比:
1. 利用下标 [] 遍历
这是最像 C 语言数组的写法,适合需要知道当前元素索引位置的情况。
- 语法 :利用
for循环配合.size()和[]操作符。 - 适用容器 :
vector,string,deque,array(必须支持随机访问)。注意:list,map,set不能用这种方式。
cpp
// 方式 1:下标遍历
for (size_t i = 0; i < nums.size(); ++i) {
// nums[i] 获取元素,既可以读也可以写
nums[i] += 1; // 修改元素
std::cout << "Index " << i << ": " << nums[i] << std::endl;
}
注意 :这里推荐使用
size_t而不是int作为索引类型,以避免与.size()返回的无符号整数比较时产生警告。
2. 范围 for 循环 ------ 推荐
这是 C++11 引入的语法糖,代码最简洁,可读性最高。
- 语法 :
for (类型 变量名 : 容器) - 适用容器 :所有标准 STL 容器(包括
list,map等)以及原生数组。 - 三种写法细节:
cpp
// 写法 A:按值遍历 (Copy) - 修改 x 不会影响原数组
for (int x : nums) {
std::cout << x << " ";
}
// 写法 B:引用遍历 (Reference) - 可以修改原数组
for (auto& x : nums) {
x *= 2; // 原数组的值变成了 20, 40...
}
// 写法 C:常引用遍历 (Const Reference) - 只读,且避免大对象拷贝
for (const auto& x : nums) {
// x = 100; // 错误!不能修改
std::cout << x << " ";
}
3. 迭代器遍历
这是 STL 的"正统"遍历方式。在 C++11 之前是主流,现在通常用于需要在遍历过程中删除元素 或操作特定位置的场景。
- 语法 :使用
.begin()和.end()。 - 原理 :迭代器像是一个指针,
*it取值,it->访问成员。
cpp
// 方式 3:迭代器遍历
// 使用 auto 简化类型声明 (完整类型是 std::vector<int>::iterator)
for (auto it = nums.begin(); it != nums.end(); ++it) {
// *it 代表当前元素
*it += 5; // 修改元素
std::cout << *it << " ";
}
三种方式对比总结
| 特性 | 下标 [] |
范围 for |
迭代器 iterator |
|---|---|---|---|
| 代码简洁度 | 中等 | 最高 (最推荐) | 最繁琐 |
| 获取索引 | 方便 (i 就是索引) |
麻烦 (需要额外计数器) | 麻烦 (需用 it - begin()) |
| 通用性 | 差 (仅限 vector/string/deque) | 强 (所有容器) | 强 (所有容器) |
| 删除元素 | 容易出错 | 不能 (会导致迭代器失效) | 安全 (配合 erase 返回值) |
| 核心用途 | 需要索引操作时 | 仅需遍历读写所有元素时 | 需要精细控制(如删除)时 |
完整演示代码
你可以直接复制运行这段代码来感受区别:
cpp
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
// 1. 下标 []
std::cout << "下标遍历: ";
for (size_t i = 0; i < v.size(); ++i) {
std::cout << v[i] << " ";
}
std::cout << "\n";
// 2. 范围 for
std::cout << "范围 for: ";
for (const auto& val : v) {
std::cout << val << " ";
}
std::cout << "\n";
// 3. 迭代器
std::cout << "迭代器: ";
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
std::cout << "\n";
return 0;
}
std::string::begin
函数原型
cpp
iterator begin() noexcept;
const_iterator begin() const noexcept;
1. 核心翻译:功能描述
- 功能 :返回一个指向字符串第一个字符的迭代器。
- 参数:无。
- 返回值 :
- 如果字符串对象是普通的(非
const),返回iterator(可读写)。 - 如果字符串对象是常量(
const),返回const_iterator(只读)。
- 如果字符串对象是普通的(非
- 迭代器类型 :属于随机访问迭代器 (Random Access Iterator) 。这意味着你可以像操作数组下标一样操作它(例如
it + 5或it[3])。
2. C++ 版本差异 (重载)
文档中列出了两个原型,这是 C++ 的重载机制:
iterator begin() noexcept;- 当你的字符串不是
const时调用。 - 作用 :你可以通过返回的这个迭代器修改 字符(例如
*it = 'A')。
- 当你的字符串不是
const_iterator begin() const noexcept;- 当你的字符串是
const时调用。 - 作用 :返回的迭代器只能读取,不能修改字符。
- 当你的字符串是
3. 代码示例解析
cpp
// string::begin/end
#include <iostream>
#include <string>
int main ()
{
std::string str ("Test string");
// 使用迭代器从头(begin)遍历到尾(end)
// std::string::iterator 是类型名,C++11 后通常简写为 auto
for ( std::string::iterator it = str.begin(); it != str.end(); ++it )
std::cout << *it; // 解引用迭代器,获取字符
std::cout << '\n';
return 0;
}
输出:
text
Test string
4. 技术细节总结(关键考点)
| 属性 | 描述 |
|---|---|
| 时间复杂度 | 常数时间 O(1)。获取起始位置非常快,不随字符串长度变化。 |
| 异常安全性 | No-throw guarantee (无异常保证)。调用此函数绝不会报错,非常安全。 |
| 迭代器有效性 | 调用 begin() 本身不会导致其他迭代器失效。 |
| 数据竞争 | 调用此函数是线程安全的。同时通过不同的迭代器读写字符串中不同的字符也是安全的。 |
5. 常见误区与提示
-
空字符串的情况:
如果字符串是空的(string s = "";),那么s.begin()会直接等于s.end()。这是判断容器是否为空的一种底层方式。text[ ] <-- 内存为空 ^ begin() 和 end() 指向同一个位置 -
不仅仅是 ++:
因为它是"随机访问迭代器",所以str.begin() + 3会直接跳到第 4 个字符,不需要循环 3 次++。
std::string::end
它是 begin() 的孪生函数,两者必须配合使用才能定义一个完整的范围。
1. 核心翻译:功能描述
- 功能 :返回一个指向字符串末尾之后 位置的迭代器。
- 核心概念 :
- 它指向的不是 字符串的最后一个字符,而是最后一个字符后面那个"理论上"的位置。
- 严禁解引用 :你不能对
end()返回的迭代器进行解引用操作(即不能写*str.end()),因为那个位置没有有效数据。
- 用途 :
- 标准库函数通常使用"半开区间"
[begin, end),这意味着包含begin指向的元素,但不包含end指向的元素。 - 它主要用作循环或算法的结束标志。
- 标准库函数通常使用"半开区间"
- 空字符串 :如果字符串为空,
end()的返回值与begin()相同。
std::string::end 在物理地址上指向 \0。但是不能通过 *s.end() 来读取。要读 \0 请使用 s[s.size()] 或 s.c_str()。
2. C++ 版本与重载
与 begin() 一样,end() 也有两个版本:
iterator end() noexcept;- 当字符串非 const 时调用。返回可读写迭代器。
const_iterator end() const noexcept;- 当字符串是 const 时调用。返回只读迭代器。
3. 形象化图解
假设有一个字符串 std::string s = "ABC";
text
位置(Index): 0 1 2 3
字符(Data): A B C <无数据/边界>
迭代器: ^ ^
| |
s.begin() s.end()
s.begin()指向 'A'。s.end()指向 'C' 后面的位置(位置 3)。- 当循环执行到
it == s.end()时,意味着已经处理完了 'C',循环应当终止。
4. 代码示例解析
cpp
#include <iostream>
#include <string>
int main ()
{
std::string str ("Test string");
// 循环条件:只要 it 不等于 end(),就继续执行
for ( std::string::iterator it = str.begin(); it != str.end(); ++it ) {
std::cout << *it;
}
// 注意:我们从未访问过 end() 指向的内容,只是用它来做比较。
std::cout << '\n';
return 0;
}
5. 技术细节总结
| 属性 | 描述 |
|---|---|
| 时间复杂度 | 常数时间 O(1)。 |
| 异常安全性 | No-throw guarantee (无异常保证)。绝不抛出异常。 |
| 迭代器类型 | 随机访问迭代器。 |
| 数据竞争 | 访问此函数是线程安全的。 |
6. 关键警告 (新手常犯错误)
千万不要这样做:
cppstring s = "Hello"; auto it = s.end(); char c = *it; // 错误!解引用 end() 是未定义行为,程序可能会崩溃或读取乱码。如果你想访问最后一个字符,应该使用
s.back()或者*(s.end() - 1)(前提是字符串不为空)。
std::string::rbegin
它标志着反向遍历的起点。
1. 核心翻译:功能描述
- 功能 :返回一个指向字符串最后一个字符 (即反向序列的开头)的反向迭代器。
- 反向机制 :
- 反向迭代器的工作方向是向后的。
- 当你对反向迭代器执行 自增操作 (
++) 时,它实际上会向字符串的开头移动。
- 位置关系 :
rbegin()指向的字符,正好位于普通迭代器end()指向位置的前一个。
2. C++ 版本与重载
与 begin() 类似,根据字符串是否为常量,有两个版本:
reverse_iterator rbegin() noexcept;- 非 const 字符串调用,返回的迭代器可以修改字符。
const_reverse_iterator rbegin() const noexcept;- const 字符串调用,只读。
3. 形象化图解 (关键理解)
这是理解 rbegin 最重要的一点:逻辑上的"开始",是物理上的"末尾"。
假设字符串 string s = "ABC";
text
物理位置: 0 1 2 (3)
字符内容: A B C <边界>
^
正向迭代器: s.end()
反向迭代器: s.rbegin()
^
|
这是反向遍历的第1个元素
s.begin()指向 'A'。s.end()指向 'C' 后面的虚无。s.rbegin()指向 'C' (即最后一个有效字符)。
4. 代码示例解析
文档中的示例展示了利用反向迭代器翻转字符串的输出:
cpp
#include <iostream>
#include <string>
int main ()
{
std::string str ("now step live...");
// 初始化 rit 指向最后一个字符 ('.')
// 只要 rit 不等于 rend() (反向的终点),就继续
// ++rit 实际上是让指针向前移动 (从 '.' -> 'e' -> 'v'...)
for (std::string::reverse_iterator rit=str.rbegin(); rit!=str.rend(); ++rit)
std::cout << *rit;
return 0;
}
输出:
text
...evil pets won
(注:这是一个回文文字游戏,"live" 倒过来是 "evil","step" 倒过来是 "pets","now" 倒过来是 "won")
5. 技术细节总结
| 属性 | 描述 |
|---|---|
| 时间复杂度 | 常数时间 O(1)。 |
| 异常安全性 | No-throw guarantee (无异常保证)。绝不抛出异常。 |
| 迭代器类型 | 随机访问迭代器 。支持 rit + 5 这种操作(实际上是向左移动5格)。 |
| 数据竞争 | 线程安全(同 begin)。 |
6. 专家点拨
- 为什么要用
rbegin而不是end() - 1?
虽然end() - 1也指向最后一个字符,但它是一个正向迭代器 。如果你对它做++,它会跑到end()变成无效。
而对rbegin()做++,它会自动帮你走向倒数第二个字符。它封装了"倒着走"的逻辑,让你的代码写起来像顺着走一样自然。
std::string::rend
它是反向遍历的终点哨兵。
1. 核心翻译:功能描述
- 功能 :返回一个指向字符串反向结尾的反向迭代器。
- 具体位置 :它指向字符串第一个字符前面 的那个"理论上"的元素。
- 这就好比正向迭代器的
end()指向最后一个字符的后面。 - 反向迭代器的
rend()指向第一个字符的前面。
- 这就好比正向迭代器的
- 范围 :
string::rbegin到string::rend构成的区间包含了字符串中的所有字符(按逆序排列)。
2. 形象化图解 (核心考点)
理解 rend 的关键在于明白它指向的是一个不存在的、位于开头之前的位置。
假设字符串 string s = "ABC";
text
理论位置: (-1) 0 1 2
字符内容: <边界> A B C
^ ^
| |
s.rend() s.rbegin()
(反向终点) (反向起点)
- 当我们反向遍历时,从 'C' (
rbegin) 开始,接着是 'B',然后是 'A'。 - 处理完 'A' 之后,迭代器再次
++,就会变成rend()。 - 此时循环判断
it != s.rend()失败,循环结束。
3. C++ 版本与重载
同样有两个版本:
reverse_iterator rend() noexcept;(非 const)const_reverse_iterator rend() const noexcept;(const)
4. 代码示例解析
cpp
#include <iostream>
#include <string>
int main ()
{
std::string str ("now step live...");
// 循环条件:只要 rit 没有走到 rend() (即没有走到 'n' 的前面),就继续
for (std::string::reverse_iterator rit=str.rbegin(); rit!=str.rend(); ++rit)
std::cout << *rit;
return 0;
}
输出:
text
...evil pets won
5. 技术细节总结
| 属性 | 描述 |
|---|---|
| 时间复杂度 | 常数时间 O(1)。 |
| 异常安全性 | No-throw guarantee (无异常保证)。绝不抛出异常。 |
| 迭代器类型 | 随机访问迭代器。 |
| 严禁操作 | 严禁解引用 rend()。因为它指向的位置(索引 -1)没有数据,解引用会导致未定义行为(程序崩溃或乱码)。 |
6. 总结对比表
为了帮你彻底理清这四个概念:
| 迭代器 | 方向 | 指向哪里? | 能解引用吗? | 物理含义 |
|---|---|---|---|---|
begin() |
正向 | 第 1 个字符 | ✅ 能 | index 0 |
end() |
正向 | 最后一个字符的后面 | ❌ 这里的空间是空的 | index Size |
rbegin() |
反向 | 最后一个字符 | ✅ 能 | index Size-1 |
rend() |
反向 | 第 1 个字符的前面 | ❌ 这里的空间是空的 | index -1 |
带 c 版本的迭代器
带 c 的版本就是为了在非 const 容器上,强行获取一个只读的"视图",防止手滑改了数据。除此之外,它们的行为逻辑一模一样。
cbegin -const_iterator cbegin() const noexcept;
cend -const_iterator cend() const noexcept;
crbegin -const_reverse_iterator crbegin() const noexcept;
crend -const_reverse_iterator crend() const noexcept;
这四个函数是 C++11 引入的,专门用于获取 只读(常量)迭代器 (const_iterator)。
核心区别:
- 普通的
begin()/end():如果容器本身不是const的,它们返回的是可读写的iterator。 - 带
c前缀的 (cbegin,cend...):无论容器是否为常量 ,强制返回const_iterator,确保你只能读取元素,不能修改。
1. 正向常量迭代器:cbegin 和 cend
这两个函数用于从前向后 遍历容器,但保证数据的只读性。
cbegin(): 返回指向容器第一个元素的常量迭代器。cend(): 返回指向容器最后一个元素之后(Past-the-end)位置的常量迭代器。
代码示例
cpp
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {10, 20, 30, 40};
// 使用 cbegin 和 cend 获取常量迭代器
// 显式类型是 std::vector<int>::const_iterator
for (auto it = v.cbegin(); it != v.cend(); ++it) {
std::cout << *it << " ";
// *it = 99; // ❌ 错误!不能修改,因为它是 const_iterator
}
std::cout << std::endl;
return 0;
}
2. 反向常量迭代器:crbegin 和 crend
这两个函数用于从后向前 遍历容器,同样保证数据的只读性。
crbegin(): 返回指向容器最后一个元素的常量反向迭代器。crend(): 返回指向容器第一个元素之前(理论上的位置)的常量反向迭代器。
注意方向: 对反向迭代器执行
++操作,实际上是向容器的"头部"移动。
代码示例
cpp
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {10, 20, 30, 40};
std::cout << "反向遍历 (只读): ";
// 从最后一个元素 (40) 开始,直到第一个元素之前
for (auto it = v.crbegin(); it != v.crend(); ++it) {
std::cout << *it << " "; // 输出: 40 30 20 10
// *it = 50; // ❌ 错误!试图给只读迭代器赋值
}
std::cout << std::endl;
return 0;
}
3. 核心总结对比表
为了方便记忆,可以将它们与普通的迭代器进行对比:
| 函数名 | 返回类型 | 指向位置 | 可否修改元素? | 典型用途 |
|---|---|---|---|---|
cbegin() |
const_iterator |
第一个元素 | 否 | 安全读取数据 |
cend() |
const_iterator |
尾后 (Past-the-end) | N/A | 循环结束条件 |
crbegin() |
const_reverse_iterator |
最后一个元素 | 否 | 安全反向读取 |
crend() |
const_reverse_iterator |
首前 (Before-begin) | N/A | 反向循环结束条件 |
为什么要用带 c 的版本?
即使你的容器变量 v 没有被声明为 const,使用 cbegin() 可以向阅读代码的人(以及编译器)明确传达意图:"在这个循环中,我不打算,也不应该修改任何数据。" 这是一种防御性编程的好习惯。
第六章:容量
std::string::size
1. 函数原型
cpp
size_t size() const noexcept; // C++11 及以后
const: 表示这是一个只读函数,不会修改字符串的内容。noexcept: 表示该函数保证绝不抛出异常。
2. 核心总结
功能:
返回字符串当前的长度 (以字节为单位),不计算\0。
关键细节:
-
字节 vs 字符 (重点):
- 该函数统计的是字节数 (Bytes),而不是人类理解的"字符数 (Characters)"。
std::string不感知编码(如 UTF-8)。对于多字节字符集(例如中文),一个汉字可能占用 3 个字节(UTF-8)或 2 个字节(GBK)。- 示例 :
std::string s = "你好";(UTF-8编码),s.size()返回的是 6,而不是 2。
-
Size vs Capacity:
size()返回的是实际内容的大小。- 这通常不等于
capacity()(容量),后者是预分配的内存空间,通常大于或等于 size。
-
完全同义词:
string::size()和string::length()是完全一样的,它们互为同义词,返回相同的值。设计两个名字主要是为了配合 STL 容器的统一接口(STL 容器通常用size,而传统的字符串操作习惯用length)。
-
返回值:
- 类型是
size_t(无符号整数)。
- 类型是
3. 技术规格 (Technical Specs)
| 属性 | 描述 |
|---|---|
| 时间复杂度 | 常数时间 O ( 1 ) O(1) O(1)。读取这个值非常快,不随字符串长度增加而变慢。 |
| 异常安全性 | No-throw guarantee。保证绝不抛出异常。 |
| 迭代器有效性 | 调用此函数不会导致任何迭代器失效。 |
| 数据竞争 | 此函数被视为"读取"操作(Access),多线程同时读是安全的,但如果同时有线程在写,则需加锁。 |
4. 代码示例
cpp
#include <iostream>
#include <string>
int main ()
{
// 1. 普通 ASCII 字符串
std::string str ("Test string");
std::cout << "str 的字节大小是: " << str.size() << " bytes.\n";
// 输出: 11 (因为包含空格,正好11个字母/符号,每个占1字节)
// 2. 补充示例:中文字符串 (假设是 UTF-8 环境)
std::string cn_str = "你好";
std::cout << "cn_str 的字节大小是: " << cn_str.size() << " bytes.\n";
// 输出: 6 (每个汉字占3字节)
return 0;
}
总结
这就是最常用的获取字符串长度的方法。唯一需要警惕的就是处理中文等多字节字符时,它返回的是字节数而非字数。
std::string::length
1. 函数原型
cpp
size_t length() const noexcept; // C++11 标准
const: 表示这是一个只读函数,不修改字符串内容。noexcept: 保证绝对不会抛出异常。
2. 核心总结
功能:
返回字符串当前的长度 ,单位是字节 (Bytes)。
关键技术点:
-
完全同义词 (Synonym):
length()和size()是完全一样的。- 它们返回相同的值,实现逻辑也通常一致。
- 为什么有两个? 为了接口统一。
length是字符串操作的传统术语(像 Java/C#),而size是 STL 容器(如 vector, list)的标准术语。C++ 为了照顾两边的习惯,两个都提供了。
-
字节 vs 字符:
std::string不懂编码。它只把字符串看作一串char(字节)。- 如果你存的是多字节字符(如 UTF-8 编码的中文),返回值可能大于你看到的字符个数。
- 例子 :
"A"(1字节),"你好"(通常6字节)。
-
Length vs Capacity:
length()是实际装了多少水。capacity()是瓶子能装多少水(分配的内存)。- 通常
capacity>=length。
3. 技术规格
| 属性 | 描述 |
|---|---|
| 参数 | 无 (None) |
| 返回值 | size_t (无符号整数),表示字节数。 |
| 时间复杂度 | 常数时间 O ( 1 ) O(1) O(1)。非常快,不随字符串变长而变慢。 |
| 异常安全性 | No-throw guarantee。绝不抛出异常。 |
| 数据竞争 | 被视为"读取"操作。多线程并发读取是安全的。 |
4. 代码示例
cpp
#include <iostream>
#include <string>
int main ()
{
std::string str ("Test string");
// length() 是成员函数,必须加括号 ()
std::cout << "str 的长度是 " << str.length() << " 字节。\n";
// 输出: 11 字节
// 对比 size()
std::cout << "size() 的结果也是: " << str.size() << "\n";
return 0;
}
总结
length() 和 size() 在 C++ std::string 中是一回事。
- 如果你习惯写普通业务逻辑或来自 Java 背景,可能更喜欢用
length()。 - 如果你习惯写 STL 算法或泛型编程,通常更喜欢用
size()。 - 切记 :它数的是字节,不是字数。
std::string::max_size
1. 函数原型
cpp
size_t max_size() const noexcept; // C++11 及以后
const: 只读函数。noexcept: 保证绝不抛出异常。
2. 核心总结
功能:
返回字符串理论上能达到的最大长度(字符数/字节数)。
核心含义:
这是一个由系统架构 (如 32位 vs 64位)和库实现 决定的硬性上限。它代表了 std::string 这个容器在设计上允许的最大寻址范围,而不是你电脑实际能提供的内存。
关键区别(重点):
size(): 现在有多少个字符。capacity(): 现在申请好的内存能装多少字符(不触发重新分配)。max_size(): 这个容器的设计极限是多少。通常是一个极大的数字。
重要警告:
- 理论 vs 现实 :返回值只是一个理论上限。并不代表你真的能创建一个这么大的字符串。
- 限制因素 :实际能创建多大的字符串,受限于你的 物理内存 (RAM) 。如果你试图创建一个接近
max_size的字符串,通常早就因为内存不足(bad_alloc)而失败了。
3. 返回值解析
返回值通常与机器字长有关:
- 32位系统 :通常接近 2 32 2^{32} 232 (约 4GB),例如
4294967291。 - 64位系统 :通常接近 2 64 2^{64} 264 (天文数字),是一个大到你永远用不完的数值。
4. 代码示例分析
cpp
#include <iostream>
#include <string>
int main ()
{
std::string str ("Test string");
// 1. 实际大小 (11)
std::cout << "size: " << str.size() << "\n";
// 2. 实际大小同义词 (11)
std::cout << "length: " << str.length() << "\n";
// 3. 当前已分配内存 (例如 15, 取决于编译器策略)
std::cout << "capacity: " << str.capacity() << "\n";
// 4. 理论极限 (通常是几十亿或者几百亿亿)
std::cout << "max_size: " << str.max_size() << "\n";
return 0;
}
典型输出 (64位 Linux/GCC 环境):
text
size: 11
length: 11
capacity: 15
max_size: 9223372036854775807 <-- 这是一个巨大的数字 (2^63 - 1)
5. 技术规格
| 属性 | 描述 |
|---|---|
| 时间复杂度 | 常数时间 O ( 1 ) O(1) O(1)。通常直接返回一个硬编码的常量计算值。 |
| 异常安全性 | No-throw guarantee。绝不抛出异常。 |
| 用途 | 通常用于在分配大内存前进行合法性检查 。如果你想分配的长度 > max_size(),应该抛出 std::length_error。 |
总结
max_size() 是容器的天花板。在日常编程中很少直接用到,除非你在编写通用的容器分配器或者需要检查输入数据长度是否由于溢出而变得非法。对于普通用户,只需要知道它是一个"非常非常大的数字"即可。
std::string::resize
1. 函数原型
cpp
void resize(size_t n); // 版本 1:默认填充
void resize(size_t n, char c); // 版本 2:指定字符填充
2. 核心总结
功能:
强制改变字符串的有效长度 (即 size() 的返回值)。
核心行为逻辑(分两种情况):
-
变短(截断):
- 如果
n小于 当前长度:字符串会被截断。 - 保留前
n个字符,第n个字符之后的所有内容都会被删除。
- 如果
-
变长(扩展):
- 如果
n大于 当前长度:字符串会变长,新增加的空间需要填充数据。 - 版本 1 (
resize(n)) :用 空字符 (\0) 填充新增的位置。(注意:这可能会导致字符串中间出现不可见的空字符)。 - 版本 2 (
resize(n, c)) :用指定的字符c填充新增的位置。
- 如果
3. 参数与返回值
n: 新的字符串长度(字符个数)。c: (可选)如果发生了扩展,用来填充新空间的字符。- 返回值 : 无 (
void)。
4. 代码示例解析
cpp
#include <iostream>
#include <string>
int main ()
{
std::string str ("I like to code in C");
std::cout << str << '\n'; // 输出: I like to code in C (长度 19)
unsigned sz = str.size();
// 1. 变长:长度变为 19+2=21,新增的位置用 '+' 填充
str.resize (sz + 2, '+');
std::cout << str << '\n';
// 输出: I like to code in C++
// 2. 变短:长度变为 14,截断后面内容
str.resize (14);
std::cout << str << '\n';
// 输出: I like to code (截断了 " in C++")
return 0;
}
5. 关键技术细节
| 属性 | 描述 |
|---|---|
| 迭代器失效 | 高风险 。如果 resize 导致字符串变长且超过了当前的 capacity,内存会重新分配,所有 指向该字符串的迭代器、指针、引用都会失效(变成悬空指针)。 |
| 异常安全性 | 强保证 (Strong Guarantee)。如果过程中发生错误(如内存分配失败),字符串会保持调用前的状态,不会被"改坏"。 |
| 时间复杂度 | 通常是线性时间 O ( N ) O(N) O(N)(取决于新长度),如果只是截断则非常快。 |
| 容量变化 | resize 可能会改变 capacity(如果变长),但也可能不变(如果变短,通常不回收内存)。 |
6. 避坑指南:resize vs reserve
这是初学者最容易混淆的一对概念:
-
resize(n):- 改
size,也可能改capacity。 - 会有真实的数据被创建 (即你可以直接访问
s[n-1])。 - 比喻 :不仅把瓶子做大,还往里面灌水 (水不够就灌空气
\0)。
- 改
-
reserve(n):- 只改
capacity,不改size。 - 没有真实数据(不能访问下标)。
- 比喻 :只是把瓶子换个大的,但里面的水没变多。
- 只改
std::string::capacity
1. 函数原型
cpp
size_t capacity() const noexcept; // C++11 及以后
const: 只读,不修改对象。noexcept: 保证绝不抛出异常。
2. 核心总结
功能:
返回字符串当前已分配的内存空间大小(以字节为单位)。
核心比喻(瓶子与水):
size()/length():瓶子里实际装了多少水。capacity():这个瓶子当前总共能装多少水(包括有水的和没水的空间)。max_size():理论上能制造出的最大瓶子。
关键特性:
-
Capacity ≥ \ge ≥ Size:
capacity通常大于或等于size。- 多出来的空间是预留空间(Buffer)。
- 目的 :为了性能优化。当你向字符串追加字符(
append或push_back)时,如果有预留空间,只需写入数据即可, O ( 1 ) O(1) O(1) 时间复杂度;如果没有预留空间,就需要重新申请更大的内存块并搬运数据(Reallocation),非常耗时。
-
自动扩容:
capacity不是字符串长度的硬性上限。如果你的size超过了当前的capacity,string会自动申请更大的内存(通常是成倍增长,如 1.5倍或2倍),capacity也会随之变大。
-
与 Vector 的区别(细微差别):
- 文档特别提到:
string的capacity可能会在修改字符串时改变,哪怕是字符串变短了。这与std::vector不同(vector通常保证在元素减少时capacity不变,除非显式调用shrink_to_fit)。不过在现代编译器优化(如 SSO - 小字符串优化)下,这通常是透明的。
- 文档特别提到:
-
手动控制:
- 你可以通过调用
reserve(n)来显式调整capacity。
- 你可以通过调用
3. 三大"大小"概念对比
初学者最容易混淆这三个概念,请看下表:
| 概念 | 函数名 | 含义 | 备注 |
|---|---|---|---|
| 实际内容 | size() / length() |
当前有多少个字符 | 水的体积 |
| 当前容量 | capacity() |
当前分配了多少内存 | 当前瓶子的容积 |
| 理论极限 | max_size() |
系统允许的最大长度 | 地球上最大的瓶子 |
4. 代码示例解析
cpp
#include <iostream>
#include <string>
int main ()
{
std::string str ("Test string"); // 11 个字符
std::cout << "size: " << str.size() << "\n"; // 输出 11
std::cout << "length: " << str.length() << "\n"; // 输出 11
// capacity 通常会比 11 大,具体取决于编译器的分配策略
// 比如可能是 15 (因为 16字节对齐,扣掉1个字节存 \0)
std::cout << "capacity: " << str.capacity() << "\n";
std::cout << "max_size: " << str.max_size() << "\n"; // 天文数字
return 0;
}
5. 技术规格 (Technical Specs)
| 属性 | 描述 |
|---|---|
| 时间复杂度 | 常数时间 (Unspecified but generally constant)。 |
| 异常安全性 | No-throw guarantee。绝不抛出异常。 |
| 用途 | 当你需要预估内存占用,或者在大量拼接字符串前,检查是否需要手动调用 reserve 以避免多次内存分配。 |
总结
capacity() 是用来查看内存分配策略的窗口。它告诉你为了避免频繁向操作系统要内存,你的字符串对象偷偷"私藏"了多少预留空间。
std::string::reserve
1. 函数原型
cpp
void reserve(size_t n = 0);
- 参数
n: 你计划让字符串容纳的字符数量。 - 返回值: 无。
2. 核心总结
功能:
请求修改字符串的 容量 (capacity) ,以适应未来可能达到的长度 n。它主要用于性能优化。
核心行为逻辑:
- 扩容(主要用途) :
- 如果
n大于 当前的capacity:字符串容器必须 重新分配内存,将容量提升到至少n(通常会更多以保持对齐或优化)。
- 如果
- 缩容(非强制) :
- 如果
n小于或等于 当前的capacity:这是一个非绑定性请求。 - 意思是:你告诉编译器"我用不了这么多空间了",但编译器和标准库实现可以忽略这个请求,保持大容量不变(为了避免未来又要扩容)。
- 注意:如果你真心想缩减内存,C++11 提供了专门的
shrink_to_fit()函数。
- 如果
关键约束:
- 此函数绝不 改变字符串的
size(长度)。 - 此函数绝不 修改字符串中已有的 内容。
3. 为什么需要 reserve?(性能优化的秘密)
想象一下你要往字符串里追加 1000 个字符。
-
没有
reserve:你每次
push_back或+=,如果容量不够了,string就得:- 找一块更大的新内存。
- 把旧数据搬运(复制)过去。
- 销毁旧内存。
- 插入新字符。
这个"搬家"的过程非常耗时。如果频繁触发,性能会极其低下。
-
使用了
reserve(1000):你预先告诉
string:"我要装 1000 个字,请一次性把地皮批给我"。string一次性申请好能装 1000 个字的内存。- 之后你的 1000 次追加操作,完全不需要 重新分配内存,全是 O ( 1 ) O(1) O(1) 的直接写入。
性能大幅提升。
4. 示例代码深度解析
文档中提供的示例非常经典:读取整个文件。
cpp
// 1. 打开文件
std::ifstream file ("test.txt", std::ios::in | std::ios::ate); // ate: 指针直接跳到文件末尾
if (file) {
// 2. 获取文件大小
std::ifstream::streampos filesize = file.tellg();
// 3. 【关键步骤】预留空间
// 我们已经知道文件有多大了,直接一次性要把内存申请好
// 这样下面的 while 循环中,str += file.get() 就永远不会触发"搬家"操作
str.reserve(filesize);
// 4. 回到文件头开始读
file.seekg(0);
while (!file.eof()) {
str += file.get(); // 疯狂追加字符,但因为有 reserve,效率极高
}
std::cout << str;
}
5. reserve vs resize (一定要分清)
这是 C++ 面试和编程中最容易混淆的一对:
| 特性 | reserve(n) |
resize(n) |
|---|---|---|
| 修改对象 | 只改 capacity (容量) |
改 size (长度),也可能改 capacity |
| 实际元素 | 不创建任何实际元素 | 创建 n 个元素 (用默认值填充) |
| 能否访问 | 不能访问 s[n-1] (越界) |
可以访问 s[n-1] (合法) |
| 用途 | 为了省时间 (预留坑位) | 为了改逻辑 (填充/截断数据) |
| 比喻 | 把空瓶子换成大瓶子,但不倒水进去 | 把瓶子里的水灌满到指定刻度 |
补充:reserve为什么不能访问 s[n-1] (越界)
resize(n) 做两件事:
- 分配内存(如果没有的话)。
- 构造对象/初始化数据(比如填充 \0 或默认字符)。
- 结果:数据是干净的,合法的。
reserve(n) 只做一件事:
- 分配原始内存。
- 它不做初始化。这块新申请的内存里可能存着上一个程序留下的乱码。
- 结果:内存是脏的。
6. 技术规格
| 属性 | 描述 |
|---|---|
| 迭代器失效 | 非常危险 。如果 reserve 导致了扩容(内存地址变了),所有指向该字符串的迭代器、指针、引用会立即失效。 |
| 异常安全性 | 强保证 。如果内存分配失败(抛出 bad_alloc),字符串会保持调用前的原样,数据不会丢失。 |
| 异常抛出 | 如果 n > max_size(),抛出 length_error。 |
总结
当你能预知字符串大概会变得多长时(比如读文件、大量拼接),务必先调用 reserve。这是一行代码就能带来的巨大性能提升。
std::string::clear
1. 函数原型
cpp
void clear() noexcept; // C++11 及以后
noexcept: 保证绝不抛出异常。这是一个非常安全的操作。
2. 核心总结
功能:
清空字符串的内容。
核心行为逻辑:
- 变为空串 :执行后,字符串变成空字符串
""。 - 长度归零 :
size()和length()变为 0。 - 容量通常不变 (关键点) :
clear()通常不会 释放内存 (capacity保持不变)。- 目的:为了性能。如果你马上又要往这个字符串里写数据,保留内存可以避免重新分配。
- 比喻 :把瓶子里的水倒光,但瓶子本身还是那么大,随时准备装新水。
3. 代码示例解析
文档中的示例展示了一个经典的缓冲器 (Buffer) 用法:
cpp
#include <iostream>
#include <string>
int main ()
{
char c;
std::string str;
std::cout << "请输入文本,输入点号 (.) 结束:\n";
do {
c = std::cin.get();
str += c; // 一个字一个字地追加到 buffer
// 如果读到了换行符,说明这一行结束了
if (c == '\n')
{
std::cout << str; // 1. 处理数据(打印)
// 2. 【关键】清空 buffer,准备接收下一行
// 此时内存没有释放,下一次 += 操作非常快
str.clear();
}
} while (c != '.');
return 0;
}
4. 技术规格
| 属性 | 描述 |
|---|---|
| 时间复杂度 | 通常是常数时间 O ( 1 ) O(1) O(1) 。对于 string 这种存储简单字符的容器,它只需要把 size 变量设为 0,并在开头写入 \0 即可,非常快。 |
| 迭代器失效 | 全部失效 。因为元素在逻辑上被删除了,指向原内容的迭代器、指针、引用都不能再使用了。特别是 end() 迭代器也会发生变化。 |
| 异常安全性 | No-throw guarantee。绝不抛出异常。 |
| 数据竞争 | 修改了对象,非线程安全。 |
5. 避坑指南:如何真的释放内存?
既然 clear() 只倒水不扔瓶子,如果你真的想释放内存 (把 capacity 也变成 0),该怎么办?
-
方法 A (C++11 推荐) :
使用
str.shrink_to_fit()。cppstr.clear(); // size 变 0,capacity 不变 str.shrink_to_fit(); // 请求将 capacity 缩小到适配 size (即 0) -
方法 B (C++98 老派写法) :
使用"交换惯用手法" (Swap Idiom)。
cppstd::string().swap(str); // 创建一个临时的空字符串,然后和 str 交换。 // str 拿到了空字符串的内存(0),临时对象拿到了 str 的大内存并随即销毁。
总结
clear() 是重置 字符串状态的最快方法。记住它清空数据但不退还内存,这通常是你想要的(为了复用内存提升性能)。
std::string::empty
1. 函数原型
cpp
bool empty() const noexcept; // C++11 及以后
const: 只读,不修改字符串。noexcept: 保证绝不抛出异常。
2. 核心总结
功能:
检测字符串是否为空。
返回值:
true: 如果字符串长度为 0(即length() == 0)。false: 如果字符串里哪怕有一个字符。
关键区别(容易搞混):
empty()是一个疑问句 ("你是空的吗?")。它只检查状态,不改变任何东西。clear()是一个祈使句 ("变空!")。它执行清空操作。
3. 最佳实践:为什么不用 size() == 0?
虽然 str.size() == 0 和 str.empty() 在逻辑上是等价的,但在 C++ 开发中,强烈推荐使用 empty()。
理由:
- 可读性 (Readability) :
if (str.empty())读起来像英语句子,比数学表达式if (str.size() == 0)更直观。 - 通用性 (Generic Programming) :对于
std::string和std::vector,两者效率一样(都是 O ( 1 ) O(1) O(1))。但是对于某些特殊的容器(如 C++11 之前的std::list),size()可能是 O ( N ) O(N) O(N) 的,而empty()永远保证是 O ( 1 ) O(1) O(1) 的。养成用empty()的习惯,换了容器也不怕掉坑里。
4. 代码示例解析
cpp
#include <iostream>
#include <string>
int main ()
{
std::string content;
std::string line;
std::cout << "请输入文本。输入空行结束:\n";
do {
getline(std::cin, line); // 读取一行
content += line + '\n';
// 使用 empty() 作为循环结束条件
// 如果 line 里有字符,!line.empty() 为真,继续循环
// 如果 line 是空行,!line.empty() 为假,退出循环
} while (!line.empty());
std::cout << "你输入的文本是:\n" << content;
return 0;
}
5. 技术规格 (Technical Specs)
| 属性 | 描述 |
|---|---|
| 时间复杂度 | 常数时间 O ( 1 ) O(1) O(1) 。它只需要检查一下内部的 size 变量是不是 0。 |
| 异常安全性 | No-throw guarantee。绝不抛出异常。 |
| 数据竞争 | 读取操作,线程安全。 |
总结
这是一个非常简单但使用频率极高的函数。
口诀: 想清空用 clear(),想检查有没有东西用 empty()。不要被名字误导以为它会把字符串变空。
第七章:元素访问
std::string::operator[]
1. 中文翻译
std::string::operator[] 公有成员函数
函数原型:
cpp
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
功能:获取字符串中的字符
返回字符串中 pos 位置的字符的引用。
-
版本差异说明 (C++98 / C++11):
C++98:仅保证 const 字符串在访问 str[size()] 时返回 \0。对非 const 字符串访问 size() 处是未定义行为 (Undefined Behavior)。
C++11 及以后:保证所有情况(无论是 const 还是非 const),访问 str[size()] 都必须返回 \0。
参数: -
pos:字符在字符串中的位置数值。
- 注意 :字符串的第一个字符位置为
0(而不是 1)。 size_t是一种无符号整数类型(等同于成员类型string::size_type)。
- 注意 :字符串的第一个字符位置为
返回值 (Return value):
- 返回位于字符串指定位置的字符。
- 如果字符串对象是
const限定的,函数返回const char&。 - 否则,它返回
char&(这意味着你可以通过该引用修改原来的字符)。
示例 :
cpp
// string::operator[]
#include <iostream>
#include <string>
int main ()
{
std::string str ("Test string");
for (int i=0; i<str.length(); ++i)
{
std::cout << str[i];
}
return 0;
}
输出: 此代码使用偏移运算符(即下标 [])逐个打印 str 的内容:
Test string
复杂度 :
- 未指定(通常为常数时间 O ( 1 ) O(1) O(1))。
迭代器有效性:
- 通常没有变化。
- 注意 :在某些实现中(通常指旧标准的 Copy-On-Write 实现),非
const版本在对象构造或修改后首次访问字符串字符时,可能会使所有迭代器、指针和引用失效。
数据竞争 (Data races):
- 对象被访问。在某些实现中,非
const版本在构造或修改后首次访问字符时可能会修改对象内部状态。 - 返回的引用可用于访问或修改字符。
异常安全性:
- 如果
pos小于字符串长度,函数从不抛出异常。 - 如果
pos等于字符串长度且为const版本,函数从不抛出异常。 - 重要 :除此之外的情况(即越界访问),会导致 未定义行为。
- 注意 :使用返回的引用去修改越界元素(包括
pos位置的字符)也会导致未定义行为。
2. 技术总结与核心考点
以下是关于 operator[] 最核心的几个知识点:
1. 访问与修改
operator[]返回的是引用 (reference) 。- 对于非
const字符串,你可以直接通过下标修改字符,例如:str[0] = 'A';。 - 对于
const字符串,返回只读引用,不可修改。
- 对于非
2. 边界检查
- 不检查边界 :这是
operator[]与at()函数最大的区别。str[pos]:不检查pos是否越界。如果越界,后果自负(未定义行为,可能导致程序崩溃或数据损坏)。但速度稍快。str.at(pos):会检查边界。如果越界,会抛出std::out_of_range异常。更安全,但有极微小的性能损耗。
3. 特殊的 \0 处理
- 虽然通常索引是从
0到length()-1,但在 C++ 标准中,对于const字符串,允许访问str[str.length()],它会返回字符串结尾的空字符\0。这主要是为了兼容 C 风格字符串的习惯。 - 警告 :对于非
const字符串,虽然某些实现可能允许读取str[length()],但根据标准,写入该位置通常是未定义行为。
4. 效率
- 由于不涉及异常处理和边界检查,
operator[]是访问字符串字符最高效的方式。
std::string::at
1. 中文翻译 (Translation)
std::string::at 公有成员函数
函数原型:
cpp
char& at (size_t pos);
const char& at (size_t pos) const;
功能:获取字符串中的字符
返回字符串中 pos 位置的字符的引用。
- 核心特性 :该函数会自动检查
pos是否为字符串中的有效位置(即pos是否小于字符串长度)。如果不是有效位置,它会抛出一个out_of_range异常。
参数:
- pos :字符在字符串中的位置数值。
- 注意 :字符串的第一个字符位置为
0(而不是 1)。 - 如果该值不是一个有效的字符位置,则抛出
out_of_range异常。 size_t是一种无符号整数类型(等同于成员类型string::size_type)。
- 注意 :字符串的第一个字符位置为
返回值 (Return value):
- 返回位于字符串指定位置的字符。
- 如果字符串对象是
const限定的,函数返回const char&。 - 否则,它返回
char&。
示例 (Example):
cpp
// string::at
#include <iostream>
#include <string>
int main ()
{
std::string str ("Test string");
for (unsigned i=0; i<str.length(); ++i)
{
std::cout << str.at(i);
}
return 0;
}
输出:
Test string
复杂度 :
- 未指定(通常为常数时间 O ( 1 ) O(1) O(1),但包含额外的检查开销)。
迭代器有效性:
- 通常没有变化。
- 注意 :在某些实现中,非
const版本在对象构造或修改后首次访问字符时,可能会使所有迭代器、指针和引用失效。
数据竞争 (Data races):
- 对象被访问。在某些实现中,非
const版本可能会在首次访问时修改对象。 - 返回的引用可用于访问或修改字符。
异常安全性:
- 强异常保证 (Strong guarantee):如果抛出异常,字符串的内容不会发生任何改变。
- 如果
pos不小于字符串长度(即pos >= length),则抛出out_of_range异常。
2. 技术总结与核心对比
at() 和 operator[] 放在一起对比,这是 C++ 面试和实际开发中非常经典的考点。
1. 核心区别:安全性 vs 性能
| 特性 | operator[] (下标运算符) |
at() (成员函数) |
|---|---|---|
| 边界检查 | 无 。不检查 pos 是否越界。 |
有 。自动检查 pos < length()。 |
| 越界后果 | 未定义行为。可能导致程序崩溃、数据篡改或读取脏数据。 | 抛出异常 (std::out_of_range)。程序可以捕获并处理。 |
| 性能 | 极快。直接内存偏移。 | 稍慢 。包含一个额外的 if 判断分支。 |
| 异常安全性 | 无异常保证(针对越界情况)。 | 强异常保证。 |
2. 最佳实践场景
- 使用
operator[]的场景 :- 当你通过循环变量访问,且循环条件已经严格限制在
str.length()范围内时(例如标准的for循环)。 - 在对性能要求极高的底层算法中(避免每次访问都进行 check)。
- 当你通过循环变量访问,且循环条件已经严格限制在
- 使用
at()的场景 :- 当索引
pos来自外部输入、用户输入或不确定的计算结果时。 - 在调试阶段,或者当程序的稳定性比微小的性能损耗更重要时。
- 当索引
3. 异常处理示例
如果你想展示 at() 的优势,可以在笔记中加入这个代码片段:
cpp
std::string s = "abc";
try {
char c = s.at(10); // 肯定越界
} catch (const std::out_of_range& e) {
std::cerr << "捕获到越界错误: " << e.what() << std::endl;
// 程序可以继续运行,不会直接崩溃
}
std::string::back
1. 中文翻译 (Translation)
std::string::back 公有成员函数
函数原型:
cpp
char& back();
const char& back() const;
功能:访问最后一个字符
返回字符串最后一个字符的引用。
- 重要限制 :此函数不得在空字符串上调用。
参数:
- 无 (none)。
返回值:
- 返回字符串中最后一个字符的引用。
- 如果字符串对象被
const修饰,函数返回const char&。 - 否则,返回
char&(允许修改)。
示例 (Example):
cpp
// string::back
#include <iostream>
#include <string>
int main ()
{
std::string str ("hello world.");
str.back() = '!'; // 将最后一个字符 '.' 修改为 '!'
std::cout << str << '\n';
return 0;
}
输出:
hello world!
复杂度 :
- 常数时间 O ( 1 ) O(1) O(1)。
迭代器有效性 :
- 无变化。
数据竞争:
- 容器被访问(无论是 const 还是非 const 版本都不会修改容器的结构,只会访问或修改元素内容)。
- 返回的引用可用于访问或修改字符。并发访问或修改不同的字符是安全的。
异常安全性:
- 如果字符串非空,该函数从不抛出异常(No-throw guarantee)
- 警告 :如果字符串为空,调用此函数会导致 未定义行为。
2. 技术总结与核心考点
back() 函数虽然简单,但在代码可读性和安全性上有一些值得注意的细节
1. 等价形式
str.back() 本质上是以下代码的简化写法(语法糖):
str[str.length() - 1]*str.rbegin()(使用反向迭代器)
优势:代码更简洁,意图更明确("我要最后一个字符" vs "我要长度减一位置的字符")。
2. 致命陷阱:空字符串
这是使用 back() 时最大的风险点。
-
文档明确指出 :在空字符串上调用
back()是未定义行为 (UB) 。它不会 像at()那样抛出异常,而是可能导致程序崩溃或读取乱码。 -
防御性编程 :在调用
back()之前,通常需要检查非空:cppif (!str.empty()) { str.back() = 'x'; }
3. 与 pop_back() 的配合
-
back()只负责访问(看)。 -
pop_back()负责删除(删)。 -
如果你想获取并删除最后一个字符,通常需要两步:
cppchar c = str.back(); // 获取 str.pop_back(); // 删除
4. 总结对比表 (访问方式)
| 函数 | 功能 | 针对位置 | 空字符串安全性 |
|---|---|---|---|
str.front() |
访问第一个字符 | 等同于 str[0] |
UB (未定义行为) |
str.back() |
访问最后一个字符 | 等同于 str[len-1] |
UB (未定义行为) |
str[i] |
访问任意字符 | 任意索引 | UB (如果越界) |
str.at(i) |
访问任意字符 | 任意索引 | 抛出异常 (如果越界) |
std::string::front
1. 中文翻译
std::string::front 公有成员函数
函数原型:
cpp
char& front();
const char& front() const;
功能:访问第一个字符
返回字符串第一个字符的引用。
- 对比说明 :成员函数
string::begin返回的是指向同一个字符的迭代器 (iterator) ,而本函数直接返回该字符的引用。 - 重要限制 :此函数不得在空字符串上调用。
参数:
- 无 (none)。
返回值:
- 返回字符串中第一个字符的引用。
- 如果字符串对象被
const修饰,函数返回const char&。 - 否则,返回
char&(这意味着你可以修改它)。
示例 (Example):
cpp
// string::front
#include <iostream>
#include <string>
int main ()
{
std::string str ("test string");
str.front() = 'T'; // 将第一个字符 't' 修改为 'T'
std::cout << str << '\n';
return 0;
}
输出:
Test string
复杂度:
- 常数时间 O ( 1 ) O(1) O(1)。
迭代器有效性:
- 无变化。
数据竞争:
- 容器被访问(const 和非 const 版本都不修改容器结构)。
- 返回的引用可用于访问或修改字符。并发访问或修改不同的字符是安全的。
异常安全性:
- 如果字符串非空,该函数从不抛出异常(No-throw guarantee)。
- 警告 :如果字符串为空,调用此函数会导致 未定义行为 (Undefined Behavior)。
2. 技术总结与核心考点
front() 补全了字符串首尾访问的接口。在整理笔记时,可以将其与 begin() 和 operator[] 进行关联记忆。
1. 等价形式
str.front() 在逻辑上等价于:
str[0]*str.begin()(对 begin 返回的迭代器进行解引用)
2. front() vs begin()
这是面试或概念理解中容易混淆的点:
str.front():返回的是char&(字符本身)。你可以直接用它赋值:str.front() = 'A';str.begin():返回的是iterator(类似指针的对象)。你需要先解引用才能操作字符:*str.begin() = 'A';
3. 致命陷阱:空字符串
与 back() 一样,front() 不检查字符串是否为空。
-
错误示范 :
cppstd::string s = ""; char c = s.front(); // 未定义行为!程序可能崩溃。 -
正确做法 :始终保证
!s.empty()后再调用。
4. 为什么需要 front()?
既然有了 str[0],为什么还要引入 front()?
- 语义对称性 :为了和
back()对应,同时也为了和 C++ 标准库的其他容器(如std::vector,std::list)保持接口一致性。这使得编写泛型代码(Templates)更加容易。 - 可读性 :
str.front()比str[0]更直观地表达了"我要第一个元素"的意图,尤其是在处理队列逻辑时。
总结:C++ String 元素访问全家桶
你现在已经整理完了 C++ string 所有的单字符访问方式。我为你做了一个最终的速查表,可以直接复制到你的博客总结部分:
| 方法 | 语法示例 | 边界检查? | 越界后果 | 典型用途 |
|---|---|---|---|---|
| 下标 | str[i] |
无 | 未定义行为 | 追求极致性能,确定索引有效时 |
| at() | str.at(i) |
有 | 抛出异常 | 索引来源不可靠,需要安全性时 |
| front() | str.front() |
无 | 未定义行为 | 语义化访问第一个字符 |
| back() | str.back() |
无 | 未定义行为 | 语义化访问最后一个字符 |
第八章:修改器(Modifiers)
std::string::operator+=
这是 std::string 最常用的修改器函数之一:operator+=。它是实现字符串拼接最直观的方式。
1. 中文翻译 (Translation)
std::string::operator+= 公有成员函数
函数原型:
cpp
// C++98 / C++11
string& operator+= (const string& str); // (1) 追加 string 对象
string& operator+= (const char* s); // (2) 追加 C 风格字符串
string& operator+= (char c); // (3) 追加单个字符
// 文档参数部分提到了 initializer_list (C++11),原型通常为:
string& operator+= (initializer_list<char> il);
功能:追加到字符串
通过在当前值的末尾追加额外的字符来扩展 string。
(更多追加选项,请参阅成员函数 append)。
参数:
- str :一个
string对象,其值会被拷贝并追加到末尾。 - s:指向以空字符结尾的字符序列(C-string)的指针。该序列会被拷贝并追加到末尾。
- c:一个字符,会被追加到字符串当前值的末尾。
- il :一个
initializer_list对象(C++11)。- 这些对象由初始化列表声明符自动构造。
- 字符会按顺序追加到字符串中。
返回值 :
*this(返回对象自身的引用,支持链式调用)。
示例:
cpp
// string::operator+=
#include <iostream>
#include <string>
int main ()
{
std::string name ("John");
std::string family ("Smith");
name += " K. "; // 追加 C-string
name += family; // 追加 string 对象
name += '\n'; // 追加 字符 (character)
std::cout << name;
return 0;
}
输出:
John K. Smith
复杂度 :
- 未指定,但通常与新字符串的长度成线性关系(Linear, O ( N ) O(N) O(N))。
迭代器有效性:
- 重要 :任何与此对象相关的迭代器、指针和引用都可能失效。
- (译注:这是因为追加操作可能导致内存重新分配)
数据竞争:
- 对象会被修改。
异常安全性:
- 强保证 (Strong guarantee):如果抛出异常,字符串的内容不会发生任何改变。
- 如果结果字符串的长度超过
max_size,抛出length_error异常。 - 如果函数需要分配存储空间但失败,抛出
bad_alloc异常。
2. 技术总结与核心考点
operator+= 是 C++ 中最"重载"操作符的经典案例之一,它极大地简化了代码编写。以下是你在笔记中需要重点记录的内容:
1. 极高的易用性
它统一了三种不同的追加场景:
- 对象拼接 :
str1 += str2; - C 串拼接 :
str1 += "hello"; - 字符拼接 :
str1 += 'A';
相比于 C 语言中使用strcat、strncat并手动管理缓冲区,这是 C++std::string最大的优势之一。
2. 内存管理与迭代器失效
这是该函数最底层的考点。
- 扩容机制 :当你追加字符时,如果当前字符串的
capacity(容量)不足以容纳新内容,string会申请一块更大的内存块(通常是原来的 1.5 倍或 2 倍),将旧数据拷贝过去,然后释放旧内存。 - 失效风险 :一旦发生上述的"内存搬家",之前指向该字符串的所有迭代器 (Iterators) 、指针 (Pointers) 和 引用 (References) 都会瞬间变成"悬空指针",再次使用会导致程序崩溃。
3. 返回值 *this 的妙用
函数返回 string&,这意味着你可以进行链式调用:
cpp
str += "A";
str += "B";
// 等同于:
(str += "A") += "B";
4. 与 append() 的区别
operator+=:语法糖,简单直观,适合大多数场景。append():功能更强大。例如,+=只能追加整个字符串,而append()可以只追加字符串的一部分(例如"只追加str2的前 3 个字符")。
std::string::append
这是 std::string 中功能最强大的拼接函数:append。
与简单的 operator+= 相比,append 提供了极高的灵活性,允许你精确控制 "追加什么" 以及 "追加多少"。
1. 中文翻译 (Translation)
std::string::append 公有成员函数
版本支持: C++98 / C++11 / C++14
函数重载原型:
- string (整体) :
string& append (const string& str); - substring (子串) :
string& append (const string& str, size_t subpos, size_t sublen); - c-string (C 风格串) :
string& append (const char* s); - buffer (缓冲区) :
string& append (const char* s, size_t n); - fill (填充) :
string& append (size_t n, char c); - range (范围) :
template <class InputIterator> string& append (InputIterator first, InputIterator last); - initializer list (初始化列表) : 追加
initializer_list中的字符 (C++11)。
功能:追加到字符串
通过在当前值的末尾追加额外的字符来扩展 string:
- (1) string : 追加
str的副本。 - (2) substring : 追加
str的一个子串 。- 子串从
str的subpos位置开始,跨越sublen个字符【如果str太短(你要的太多了,字符串给不出来了)或sublen为string::npos,则直到str的末尾】。
- 子串从
- (3) c-string : 追加由
s指向的以空字符结尾的字符序列(C-string)的副本。 - (4) buffer : 追加由
s指向的字符数组中的前n个字符的副本。 - (5) fill : 追加
n个字符c的副本。 - (6) range : 按顺序追加范围
[first, last)内的字符序列的副本。
参数:
- str : 另一个被追加的
string对象。 - subpos :
str中要作为子串复制的第一个字符的位置。- 如果该值大于
str的长度,抛出out_of_range异常。 - 注意:位置索引从 0 开始。
- 如果该值大于
- sublen : 要复制的子串长度。
- 值
string::npos表示直到str的末尾。
- 值
- s: 指向字符数组(如 C-string)的指针。
- n: 要复制的字符数。
- c: 重复填充的字符值。
- first, last : 输入迭代器,指向范围的起始和终止位置
[first, last)。- 如果
InputIterator是整数类型,参数会被转换为适当的类型,从而调用签名 (5)(即填充版本)。
- 如果
返回值:
*this(返回自身引用,支持链式调用)。
示例 :
cpp
// appending to string
#include <iostream>
#include <string>
int main ()
{
std::string str;
std::string str2="Writing ";
std::string str3="print 10 and then 5 more";
// 按上述顺序使用不同的重载:
str.append(str2); // 追加整个对象 -> "Writing "
str.append(str3,6,3); // 追加子串(从idx 6开始, 长3) -> "10 "
str.append("dots are cool",5); // 追加buffer的前5个字符 -> "dots "
str.append("here: "); // 追加C-string -> "here: "
str.append(10u,'.'); // 追加10个点 -> ".........."
str.append(str3.begin()+8,str3.end()); // 追加迭代器范围 -> " and then 5 more"
// 显式指定模板参数为 int,强制调用重载(5)而非(6)
str.append<int>(5,0x2E); // 再次追加5个点 -> "....."
std::cout << str << '\n';
return 0;
}
输出:
Writing 10 dots here: .......... and then 5 more.....
复杂度 :
- 未指定,但通常与新字符串的长度成线性关系(Linear)。
迭代器有效性:
- 警告:任何与此对象相关的迭代器、指针和引用都可能失效(因为可能发生内存重分配)。
异常安全性:
- 强保证 (Strong guarantee):如果抛出异常,字符串内容不改变。
- Undefined Behavior : 如果
s指向的数组不够长,或迭代器范围无效。 - out_of_range : 如果
subpos > str.length()。 - length_error : 如果结果长度超过
max_size。 - bad_alloc: 内存分配失败。
2. 技术总结与核心考点
append 是 C++ string 操作中最核心的修改器之一。在整理笔记时,建议将其描述为 "operator+= 的增强版"。
1. append vs operator+=
这是面试常考题,也是实际编程选择的关键。
| 特性 | operator+= |
append() |
|---|---|---|
| 易用性 | 高(语法糖),代码简洁。 | 中等,参数较多。 |
| 灵活性 | 低。只能追加"整体"。 | 极高。可以追加子串、前缀、范围。 |
| 性能 | 相同(底层机制一致)。 | 在追加"子串"时性能更好(见下文)。 |
| 场景 | 简单的全量拼接。 | 需要精细控制(如只取文件流的一部分)时。 |
2. 性能优化技巧:子串处理
如果你想把 strB 的一部分拼接到 strA 后面:
-
低效做法 :
cppstrA += strB.substr(pos, len); // 原因:substr() 会创建一个临时的 string 对象,涉及一次额外的 malloc 和 copy。 -
高效做法 :
cppstrA.append(strB, pos, len); // 原因:直接将 strB 的内存数据拷贝到 strA 的末尾,无临时对象。
3. 缓冲区与二进制安全
重载版本 (4) buffer (append(const char* s, size_t n)) 非常适合处理非以 null 结尾 的字符数组,或者包含 \0 的二进制数据流。
- 例如:从网络 socket 读取了一个 buffer,其中包含
\0,你不能用+=(因为它遇到\0就停了),必须用append(buf, len)。
1. 如果你追加的是 C 风格字符串 (const char*) ,比如 "Hello" 或者 char str[] = "..."。
- noperator+=:遇到 \0 就停止。它内部相当于先对输入执行了一次 strlen,一旦遇到 \0 就会认为字符串结束了,后面的内容会被丢弃。
- append(const char*):遇到 \0 就停止。这种用法和 += 一模一样。
- append(const char*, n):数够 n 个字符才停止(无视 \0)。这是 append 的特权。它可以强制读取 n 个字节,哪怕中间有 \0,它也会把 \0 当作普通数据追加进去(常用于处理二进制数据)。
2. 如果你追加的是 C++ 字符串对象 (std::string)
- operator+=:读完对方所有的 size() 就停止。它不看内容,只看对方记录的长度。如果对方字符串中间包含 \0,也会照单全收。
- append(string):读完对方所有的 size() 就停止。同上。
*4. 内存重分配*
和 += 一样,append 可能会触发底层数组的扩容。
- 当
size() + new_chars > capacity()时,会申请新内存 -> 拷贝旧数据 -> 释放旧内存。 - 考点 :这会导致所有指向该字符串内部字符的指针、引用和迭代器失效。
std::string::push_back
这是 std::string 的最后一个追加类 函数:push_back。
它来源于标准模板库(STL)容器(如 std::vector)的通用接口。对于 std::string 来说,它专门用于追加单个字符。
1. 中文翻译
std::string::push_back 公有成员函数
函数原型:
cpp
void push_back (char c);
功能:追加字符到字符串
将字符 c 追加到字符串的末尾,使其长度(length)增加 1。
参数 :
- c:要添加到字符串中的字符。
返回值 :
- 无(
void)。
示例:
cpp
// string::push_back
#include <iostream>
#include <fstream>
#include <string>
int main ()
{
std::string str;
std::ifstream file ("test.txt",std::ios::in); // 打开文件
if (file) {
// 只要没到文件末尾,就读取一个字符并 push_back 到 str 中
while (!file.eof()) str.push_back(file.get());
}
std::cout << str << '\n';
return 0;
}
说明:
该示例逐字符读取整个文件,并使用 push_back 将每个字符追加到字符串对象中。
复杂度 :
- 未指定;通常是 摊销常数时间。
- 但在最坏情况下(即需要重新分配内存时),复杂度随新字符串长度呈线性关系。
迭代器有效性 :
- 任何与此对象相关的迭代器、指针和引用都可能失效(如果发生内存重分配)。
数据竞争:
- 对象被修改。
异常安全性:
- 强保证 (Strong guarantee):如果抛出异常,字符串内容不会改变。
- 如果结果字符串长度超过
max_size,抛出length_error异常。 - 如果函数需要分配存储空间但失败,抛出
bad_alloc异常。
2. 技术总结与核心考点
push_back 是一个非常基础但涉及深刻计算机科学概念(如摊销分析)的函数。重点应放在它与内存管理的关系上。
1. 容器语义
push_back是 C++ STL 序列容器(如std::vector,std::list,std::deque)的标准接口。std::string在很多方面可以被视为std::vector<char>。提供push_back是为了让string能够像其他容器一样通过std::back_inserter等迭代器适配器进行操作,增加了泛型编程的兼容性。
2. 摊销常数时间
这是该函数最重要的考点。
- 为什么不是单纯的 O ( 1 ) O(1) O(1)?
- 当字符串的
capacity(容量)还有剩余时,追加字符只需要简单的内存写入,速度是 O ( 1 ) O(1) O(1)。 - 当
capacity满了,push_back会触发内存重分配 :申请一块更大的内存(通常是 2 倍),将旧数据拷贝过去,释放旧内存。这个操作是 O ( N ) O(N) O(N) 的。
- 当字符串的
- 摊销 (Amortized) :
- 虽然偶尔会有一次昂贵的 O ( N ) O(N) O(N) 操作,但随着容量呈几何级数增长(1, 2, 4, 8...),昂贵操作发生的频率越来越低。
- 平均下来,每次追加操作的成本依然是常数时间。
3. 与 operator+= 的对比
str.push_back('a')str += 'a'- 区别 :
- 功能上几乎完全一致。
push_back只能 接受一个char。+=可以接受char、char*(C-string) 或string。- 在极其严格的编译器优化下,
push_back可能因为没有函数重载解析的开销而微乎其微地快一点点(通常可忽略)。
4. 最佳实践
如果你预先知道大概要 push_back 多少次(例如在循环中),强烈建议先调用 reserve()。
cpp
std::string str;
str.reserve(1000); // 预先分配内存
for(int i=0; i<1000; ++i) {
str.push_back('a'); // 此时绝对是 O(1),且不会发生内存重分配
}
std::string::assign
这是 std::string 的核心修改器函数之一:assign。
如果说 append 是 += 的增强版,那么 assign 就是 = (赋值运算符) 的增强版。它允许你在赋值的同时进行数据清洗、截取或格式转换。
1. 中文翻译
std::string::assign 公有成员函数
版本支持: C++98 / C++11 / C++14
函数重载原型:
- string (整体) :
string& assign (const string& str); - substring (子串) :
string& assign (const string& str, size_t subpos, size_t sublen = npos); - c-string (C 风格串) :
string& assign (const char* s); - buffer (缓冲区) :
string& assign (const char* s, size_t n); - fill (填充) :
string& assign (size_t n, char c); - range (范围) :
template <class InputIterator> string& assign (InputIterator first, InputIterator last); - initializer list (初始化列表) :
string& assign (initializer_list<char> il); - move (移动) :
string& assign (string&& str) noexcept;
功能:为字符串赋值
为字符串赋予一个新值,替换其当前内容。
- (1) string : 复制
str。 - (2) substring : 复制
str的一部分。从subpos开始,长度为sublen(如果str太短或sublen为string::npos,则直到str结尾)。 - (3) c-string : 复制由
s指向的以空字符结尾的字符序列。 - (4) buffer : 复制由
s指向的字符数组的前n个字符。 - (5) fill : 将当前值替换为
n个字符c的副本。 - (6) range : 按顺序复制范围
[first, last)内的字符序列。 - (7) initializer list : 按顺序复制
il中的每个字符。 - (8) move : 获取(窃取)
str的内容。str被置于一种"未指定但有效"的状态。
参数:
- str : 另一个
string对象,其值被复制或移动。 - subpos :
str中要作为子串复制的起始位置。- 如果大于
str的长度,抛出out_of_range异常。
- 如果大于
- sublen : 要复制的子串长度。
string::npos表示直到末尾。 - s: 指向字符数组(如 C-string)的指针。
- n: 要复制的字符数。
- c: 重复填充的字符值。
- first, last : 输入迭代器,表示范围
[first, last)。 - il :
initializer_list对象。
返回值 (Return Value):
*this(支持链式调用)。
示例:
cpp
// string::assign
#include <iostream>
#include <string>
int main ()
{
std::string str;
std::string base="The quick brown fox jumps over a lazy dog.";
// 按上述描述顺序使用:
str.assign(base);
std::cout << str << '\n';
str.assign(base,10,9); // 取 base 的子串 (从索引10开始, 长9)
std::cout << str << '\n'; // "brown fox"
str.assign("pangrams are cool",7); // 取 C-string 的前7个字符
std::cout << str << '\n'; // "pangram"
str.assign("c-string"); // 普通 C-string 赋值
std::cout << str << '\n'; // "c-string"
str.assign(10,'*'); // 填充 10 个 '*'
std::cout << str << '\n'; // "**********"
str.assign<int>(10,0x2D); // 模板转换 (填充 10 个 '-')
std::cout << str << '\n'; // "----------"
str.assign(base.begin()+16,base.end()-12); // 迭代器范围赋值
std::cout << str << '\n'; // "fox jumps over"
return 0;
}
复杂度 :
- 移动版本 (Move version) :常数时间 O ( 1 ) O(1) O(1)。
- 其他版本 :未指定,但通常与新字符串的长度成线性关系 O ( N ) O(N) O(N)。
迭代器有效性:
- 任何与此对象相关的迭代器、指针和引用都可能失效(因为旧内容被擦除,新内容可能导致重新分配)。
数据竞争:
- 对象被修改。
- 移动赋值 (8) 也会修改参数
str。
异常安全性:
- 移动赋值 (8) :不抛出异常保证。
- 其他情况 :强保证 (Strong guarantee)。
- 如果
subpos > str.length(),抛出out_of_range。 - 如果结果长度超过
max_size,抛出length_error。 - 内存分配失败抛出
bad_alloc。
2. 技术总结与核心考点
assign 是需要重点标记的函数,因为它在性能优化 和灵活性 上都优于简单的 = 赋值。
1. assign vs operator=
这是最核心的对比点。
operator=:简单直接,只能进行"全量复制"。str1 = str2;(全部拷贝)
assign():提供了细粒度的控制,避免了中间临时对象的产生 。- 场景 :你想把
str2的一部分赋值给str1。 - 低效写法 :
str1 = str2.substr(5, 10);(substr会创建一个临时的 string 对象,涉及一次 malloc/copy/free)。 - 高效写法 :
str1.assign(str2, 5, 10);(直接从str2拷贝数据到str1,零临时对象)。
- 场景 :你想把
2. 移动语义 (Move Semantics - C++11)
注意文档中的第 (8) 个重载:string& assign (string&& str) noexcept;
- 这是 C++11 引入的移动赋值。
- 当你调用
str1.assign(std::move(str2))时,str1不会复制str2的数据,而是直接接管 (窃取)str2内部的指针。 - 这是一个 O ( 1 ) O(1) O(1) 的操作,非常快。
str2会变成空或者其他有效状态,但不再持有原来的数据。
3. 彻底重置
- 调用
assign会完全丢弃 字符串当前的内容。这与append(保留当前内容并在后面追加)完全相反。 - 内存复用 :如果
assign的新内容长度小于当前capacity,string通常会复用已有的内存块,而不会重新分配(reallocate),这有助于提高性能。
4. append 与 assign 的参数一致性
你会发现 assign 的 8 个重载和 append 的重载几乎是一模一样的(子串、buffer、fill、range...)。这是 STL 接口设计的一致性体现,记忆了一个就能掌握另一个。
std::string::insert
这是 std::string 中操作成本较高但不可或缺的函数:insert。
它允许你在字符串的任意位置 插入新内容,而不仅仅是像 append 那样只能在末尾添加。
1. 中文翻译
std::string::insert 公有成员函数
版本支持: C++98 / C++11 / C++14
函数重载原型(按功能分类):
- string (整体) :
string& insert (size_t pos, const string& str); - substring (子串) :
string& insert (size_t pos, const string& str, size_t subpos, size_t sublen = npos); - c-string (C 风格串) :
string& insert (size_t pos, const char* s); - buffer (缓冲区) :
string& insert (size_t pos, const char* s, size_t n); - fill (填充) :
string& insert (size_t pos, size_t n, char c);(基于下标)iterator insert (const_iterator p, size_t n, char c);(基于迭代器)
- single character (单字符) :
iterator insert (const_iterator p, char c); - range (范围) :
template <class InputIterator> iterator insert (iterator p, InputIterator first, InputIterator last); - initializer list (初始化列表) :
string& insert (const_iterator p, initializer_list<char> il);
功能:插入到字符串中
在 pos 指示的字符(或 p 指向的字符)之前插入额外的字符。
- (1) string : 插入
str的副本。 - (2) substring : 插入
str的一个子串(从subpos开始,长sublen)。 - (3) c-string : 插入
s指向的 C 风格字符串。 - (4) buffer : 插入
s指向的字符数组的前n个字符。 - (5) fill : 插入
n个字符c的副本。 - (6) single character : 插入单个字符
c。 - (7) range : 按顺序插入
[first, last)范围内的字符序列。 - (8) initializer list : 按顺序插入
il中的每个字符。
参数 :
- pos : 插入点下标。新内容插入在该位置的字符之前 。
- 如果
pos > length,抛出out_of_range异常。
- 如果
- str : 另一个
string对象。 - subpos, sublen: 子串的起始位置和长度。
- s: 指向字符数组的指针。
- n: 插入字符的数量。
- c: 字符值。
- p : 指向插入点的迭代器 。新内容插入在
p指向的字符之前。 - first, last : 输入迭代器范围
[first, last)。 - il :
initializer_list对象。
返回值 (Return value):
- 返回
string&的版本 :返回*this(支持链式调用)。 - 返回
iterator的版本 :返回一个指向第一个被插入字符的迭代器。
示例 (Example):
cpp
// inserting into a string
#include <iostream>
#include <string>
int main ()
{
std::string str="to be question";
std::string str2="the ";
std::string str3="or not to be";
std::string::iterator it;
// (1) 在下标 6 插入 str2
str.insert(6,str2);
// str 变成: "to be (the )question" -> "to be the question"
// (2) 在下标 6 插入 str3 的子串 (从 idx 3 开始取 4 个字符 -> "not ")
str.insert(6,str3,3,4);
// str 变成: "to be (not )the question" -> "to be not the question"
// (4) 在下标 10 插入 buffer 前 8 个字符
str.insert(10,"that is cool",8);
// str 变成: "to be not (that is )the question"
// (3) 在下标 10 插入 C-string
str.insert(10,"to be ");
// str 变成: "to be not (to be )that is the question"
// (5) 在下标 15 插入 1 个冒号
str.insert(15,1,':');
// str 变成: "to be not to be(:) that is the question"
// (6) 使用迭代器:在 begin()+5 的位置插入逗号,返回新迭代器
it = str.insert(str.begin()+5,',');
// str 变成: "to be(,) not to be: that is the question"
// (5 迭代器版) 在末尾插入 3 个点
str.insert (str.end(),3,'.');
// str 变成: "to be, not to be: that is the question(...)"
// (7) 在 it+2 的位置插入范围
str.insert (it+2,str3.begin(),str3.begin()+3);
// str 变成: "to be, (or )not to be: that is the question..."
std::cout << str << '\n';
return 0;
}
输出:
to be, or not to be: that is the question...
复杂度 (Complexity):
- 未指定,但通常与新字符串的长度成线性关系。
- 更准确地说 :复杂度 = O ( N + M ) O(N + M) O(N+M),其中 N N N 是插入后的字符串总长度(因为需要移动插入点之后的所有现有字符), M M M 是插入的字符数。
迭代器有效性 (Iterator validity):
- 任何与此对象相关的迭代器、指针和引用都可能失效。
异常安全性 (Exception safety):
- 强保证 (Strong guarantee)。
- 越界检查与
append/assign类似。
2. 技术总结与核心考点
insert 是字符串修改中最"昂贵"的操作之一,也是面试中考察对底层数据结构理解的好问题。
1. 性能代价:移动与拷贝
- 不同于
append(只在末尾操作),insert发生在字符串中间。 - 数据搬移:为了给新数据腾出空间,插入点之后的所有现有字符都必须向后移动。
- 因此,频繁在字符串头部或中间使用
insert效率极低。如果需要频繁在中间插入,考虑使用std::list或std::deque(尽管对于字符处理,通常还是std::string配合预留空间最快)。
2. 两套接口:下标 vs 迭代器
insert 混合了两套风格的 API,这是初学者容易困惑的地方:
- 下标风格 (Index-based) :
- 参数用
size_t pos。 - 返回
string&(对象自身)。 - 类似:
str.insert(0, "Hello");
- 参数用
- 迭代器风格 (Iterator-based) :
- 参数用
iterator p。 - 返回
iterator(指向新插入内容的第一个字符)。 - 类似:
str.insert(str.begin(), 'H'); - 注意 :STL 算法(如
std::inserter)通常依赖这个版本。
- 参数用
3. 迭代器失效 (Iterator Invalidation)
这是 insert 最危险的地方。
- 内存重分配 :如果插入后总长度超过
capacity,整个字符串会迁移到新内存,所有迭代器瞬间失效。 - 无需重分配 :即使容量足够,不发生内存迁移,插入点之后的所有迭代器也会失效(因为它们指向的元素在物理内存中被向后移动了)。
4. insert 与 append 的关系
str.append("abc") 在逻辑上严格等同于 str.insert(str.length(), "abc")。但 append 的意图更清晰,且编译器可能对其进行微小的优化。
补充:如何提高insert效率--- std::reverse
普通 insert:移动 N N N 个元素。复杂度 O ( N ) O(N) O(N)。尾插时间复杂度小,头插效率低,
但是头插=尾插+逆置
cpp
#include <iostream>
#include <string>
int main() {
std::string s_slow = "";
int N = 100000;
// 模拟:想把 'A', 'B', 'C'... 倒序插进去变成 "...CBA"
for (int i = 0; i < N; ++i) {
char c = 'A' + (i % 26);
// 【极慢的操作】
// 每次都要把 s_slow 里已有的几万个字符往后挪,腾出第0个位置
// 或者创建临时 string 对象进行拷贝
s_slow.insert(0, 1, c);
// 写法二:s_slow = c + s_slow; // 也是一样的慢
}
return 0;
}
改进后
cpp
#include <iostream>
#include <string>
#include <algorithm> // 必须包含,用于 std::reverse
int main() {
std::string s_fast = "";
int N = 100000;
// 1. 关键优化:预留内存(可选,但推荐)
// 既然知道大概要存多少,先分配好,避免中途多次扩容
s_fast.reserve(N);
// 2. 全部用尾插 (Tail Insert)
// 这是一个 O(1) 的操作
for (int i = 0; i < N; ++i) {
char c = 'A' + (i % 26);
s_fast += c; // 或者 s_fast.push_back(c);
}
// 3. 最后一次性逆置
// 这是一个 O(N) 的操作
std::reverse(s_fast.begin(), s_fast.end());
return 0;
}
1. 核心翻译与总结
cpp
template <class BidirectionalIterator>
void reverse (BidirectionalIterator first, BidirectionalIterator last);
功能描述
- 用途 :将范围
[first, last)内的元素顺序完全颠倒(反转)。 - 实现原理 :函数内部调用
std::iter_swap将元素交换到新的位置。 - 本质上就是首尾对调:第 1 个和倒数第 1 个换,第 2 个和倒数第 2 个换......直到中间相遇。
参数详解 (Parameters)
-
**
first,last**: -
表示要反转的区间(左闭右开)。
-
关键要求 :迭代器类型必须是 BidirectionalIterator(双向迭代器)。
-
这点很关键 :相比于
std::sort要求"随机访问迭代器",std::reverse的要求更低。 -
这意味着:
std::vector、std::deque、std::string、数组可以反转。甚至std::list(双向链表)也可以直接用这个函数反转! (但std::forward_list单向链表不行)。 -
类型要求 :元素必须支持
swap操作。
复杂度 (Complexity)
- 时间复杂度:线性复杂度 O(N)。
- 具体操作数:它是"距离的一半"(Linear in half the distance)。
- 如果有 N 个元素,它只需要进行 N/2 次交换操作。
2. 核心代码逻辑解析
文档中给出了等价实现的伪代码,非常有助于理解它的工作方式(双指针法):
cpp
template <class BidirectionalIterator>
void reverse (BidirectionalIterator first, BidirectionalIterator last) {
// 当 first 和 last 还没有相遇时
while ((first != last) && (first != --last)) {
// 1. last 先自减 (--last),因为 last 指向的是"最后一个元素的后面"
// 2. 交换首尾
std::iter_swap (first, last);
// 3. first 向后移动
++first;
}
}
图解过程:
假设 vector = {1, 2, 3, 4, 5}
first指向 1,last指向 5。交换 ->{5, 2, 3, 4, 1}first指向 2,last指向 4。交换 ->{5, 4, 3, 2, 1}first指向 3,last指向 3。相遇,停止。
3. 结合你之前的疑问:为什么它快?
结合之前讨论的 swap 和 insert:
- 对于
std::string或vector<int>**:
std::reverse进行的是 原地交换**。它不需要申请新的内存空间,也不需要像insert(begin)那样发生大规模的数据搬移。它只是单纯的内存值互换,对 CPU 缓存非常友好。 - 对于
vector<string>(存对象的数组) :
还记得我们讨论的swap吗?
std::iter_swap内部会调用std::swap。
如果vector里存的是大字符串:
std::reverse只会交换这些字符串内部的指针 ,而不会发生深拷贝。
这意味着:即使是反转一个包含 100 万个长字符串的数组,也是瞬间完成的,因为没有产生任何字符的复制。
总结
std::reverse 是实现"尾插法后逆序"策略的核心工具。它利用双向迭代器从两头向中间逼近,通过 swap 高效交换元素,是将 O(N^2) 的头部插入优化为 O(N) 的关键一步。
std::string::erase
这是 std::string 中用于删除内容 的核心函数:erase。
它是 insert 的逆操作。建议重点关注它的三种重载形式 以及迭代器版本返回值的用法(这在遍历删除时非常关键)。
1. 中文翻译
std::string::erase 公有成员函数
版本支持: C++98 / C++11
函数重载原型:
- sequence (序列/下标版) :
string& erase (size_t pos = 0, size_t len = npos); - character (迭代器单字符版) :
iterator erase (iterator p); - range (迭代器范围版) :
iterator erase (iterator first, iterator last);
功能:从字符串中删除字符
删除字符串的一部分,从而减小其长度 (length):
- (1) sequence : 删除从字符位置
pos开始,跨度为len个字符的部分。- 如果
str剩余部分太短或len为string::npos,则一直删除到字符串末尾。 - 注意 :该函数的默认参数会删除字符串中的所有字符(效果等同于成员函数
clear)。
- 如果
- (2) character : 删除迭代器
p指向的字符。 - (3) range : 删除区间
[first, last)内的字符序列。
参数 (Parameters):
- pos : 要删除的第一个字符的位置。
- 如果该值大于字符串长度,抛出
out_of_range异常。 - 注意:索引从 0 开始。
- 如果该值大于字符串长度,抛出
- len : 要删除的字符数。
string::npos表示直到字符串末尾。
- p: 指向要被移除字符的迭代器。
- first, last : 指定要移除范围的迭代器
[first, last)。包含first指向的字符,但不包含last指向的字符。
返回值 (Return value):
- 序列版本 (1) :返回
*this(引用),支持链式调用。 - 其他版本 (2, 3) :返回一个迭代器 ,指向被删除的最后一个字符之后 的那个位置。如果不存在这样的字符(即删除了末尾),则返回
string::end。
示例 (Example):
cpp
// string::erase
#include <iostream>
#include <string>
int main ()
{
std::string str ("This is an example sentence.");
std::cout << str << '\n';
// 输出: "This is an example sentence."
// (1) 下标版:从索引 10 开始,删除 8 个字符 ("example ")
str.erase (10,8);
std::cout << str << '\n';
// 输出: "This is an sentence."
// (2) 迭代器版:删除第 10 个字符 (索引 9, 即 'n')
str.erase (str.begin()+9);
std::cout << str << '\n';
// 输出: "This is a sentence."
// (3) 范围版:删除从索引 5 到倒数第 9 个字符之间的内容 ("is a ")
str.erase (str.begin()+5, str.end()-9);
std::cout << str << '\n';
// 输出: "This sentence."
return 0;
}
复杂度 (Complexity):
- 未指定,但通常与新字符串的长度成线性关系(即 O ( N ) O(N) O(N),因为删除点之后的所有字符都需要向前移动)。
迭代器有效性 (Iterator validity):
- 任何与此对象相关的迭代器、指针和引用都可能失效。
- 译注:具体来说,被删除点之后的所有迭代器都会失效。
异常安全性 (Exception safety):
- (1) 和 (3):强保证 (Strong guarantee),如果抛出异常,字符串不改变。
- (2):无异常保证 (No-throw guarantee),从不抛出异常。
- 如果
pos > length,抛出out_of_range。 - 如果 (3) 中的范围无效,导致未定义行为 (Undefined Behavior)。
2. 技术总结与核心考点
erase 是 C++ 字符串处理中必须掌握的函数,特别是涉及到循环删除时有常见的陷阱。
1. 三种模式的对比
在笔记中,建议用表格区分这三种用法:
| 模式 | 参数类型 | 返回值 | 典型用途 |
|---|---|---|---|
| 截断/区间删除 | (pos, len) |
string& |
str.erase(5); (保留前5个,后面全删) |
| 单点删除 | (iterator) |
iterator |
删除特定字符,常用于算法配合。 |
| 范围删除 | (iter, iter) |
iterator |
删除一段逻辑区间。 |
2. 性能代价:数据搬移
与 insert 一样,erase 也是 O ( N ) O(N) O(N) 的操作。
- 当你删除中间一段字符时,被删除段后面的所有字符都需要向前移动来填补空缺。
- 优化建议 :如果你需要大量且频繁地随机删除字符,
std::string可能不是最佳选择,考虑std::list;或者使用 Erase-Remove Idiom (先用std::remove移动元素,再一次性erase)。
3. 陷阱:在循环中删除 (The Loop Trap)
这是面试和实战中最大的坑。由于 erase 会使当前迭代器失效,直接 it++ 会导致崩溃。
-
错误写法 :
cppfor (auto it = str.begin(); it != str.end(); ++it) { if (*it == 'a') str.erase(it); // 错误!erase 后 it 失效,下轮 ++it 未定义行为 } -
正确写法 (利用
erase的返回值):cppfor (auto it = str.begin(); it != str.end(); ) { if (*it == 'a') { it = str.erase(it); // 更新 it 为下一个有效位置 } else { ++it; } }
4. 常用简写技巧
- 清空字符串 :
str.erase()等同于str.clear()。 - 截断字符串 :
str.erase(pos)等同于str.erase(pos, string::npos)。例如str = "12345"; str.erase(3);结果为"123"。
std::string::replace
这是 std::string 修改器(Modifiers)章节的 Boss:replace。
它之所以复杂,是因为它同时完成了"删除"和"插入"两个动作,而且它的重载函数非常多(多达 10 种以上),参数组合极其灵活。
1. 中文翻译 (Translation)
std::string::replace 公有成员函数
版本支持: C++98 / C++11 / C++14
函数重载原型(按功能分类):
主要分为两类接口:
- 基于下标 (Index-based) :
replace(pos, len, ...) - 基于迭代器 (Iterator-based) :
replace(i1, i2, ...)
具体重载:
- string (1) :
string& replace (size_t pos, size_t len, const string& str);string& replace (const_iterator i1, const_iterator i2, const string& str);
- substring (2) :
string& replace (size_t pos, size_t len, const string& str, size_t subpos, size_t sublen = npos);
- c-string (3) :
string& replace (size_t pos, size_t len, const char* s);string& replace (const_iterator i1, const_iterator i2, const char* s);
- buffer (4) :
string& replace (size_t pos, size_t len, const char* s, size_t n);string& replace (const_iterator i1, const_iterator i2, const char* s, size_t n);
- fill (5) :
string& replace (size_t pos, size_t len, size_t n, char c);string& replace (const_iterator i1, const_iterator i2, size_t n, char c);
- range (6) :
template <class InputIterator> string& replace (const_iterator i1, const_iterator i2, InputIterator first, InputIterator last);
- initializer list (7) :
string& replace (const_iterator i1, const_iterator i2, initializer_list<char> il);
功能:替换字符串的一部分
用新内容替换字符串中从 pos 开始、跨度为 len 个字符的部分(或者范围 [i1, i2) 内的部分):
- (1) string : 复制
str的内容来替换。 - (2) substring : 复制
str的子串(从subpos开始,长sublen)来替换。 - (3) c-string : 复制 C 风格字符串
s来替换。 - (4) buffer : 复制字符数组
s的前n个字符来替换。 - (5) fill : 用
n个字符c来替换。 - (6) range : 复制
[first, last)范围内的字符序列来替换。 - (7) initializer list : 复制初始化列表
il中的字符来替换。
参数 (Parameters):
- str : 用于替换的源
string对象。 - pos : 要被替换的起始字符位置。
- 如果大于字符串长度,抛出
out_of_range。
- 如果大于字符串长度,抛出
- len : 要被替换(删除)的字符数量。
string::npos表示直到字符串末尾。
- i1, i2 : 指向要被替换范围的迭代器
[i1, i2)。 - subpos, sublen : 源字符串
str中要取用的子串位置和长度。 - s: 指向字符数组的指针。
- n: 复制/填充的字符数量。
- c: 重复填充的字符。
- first, last: 输入迭代器范围。
- il :
initializer_list对象。
返回值 (Return Value):
*this(支持链式调用)。
示例 (Example):
cpp
// replacing in a string
#include <iostream>
#include <string>
int main ()
{
std::string base="this is a test string.";
std::string str2="n example";
std::string str3="sample phrase";
std::string str4="useful.";
// --- 使用下标位置 (Using positions) ---
std::string str=base; // "this is a test string."
// (1) 从索引9开始,替换5个字符("test ") -> 换成 str2
str.replace(9,5,str2); // "this is an example string."
// (2) 从索引19开始,替换6个字符("string") -> 换成 str3 的子串(从7开始取6个 -> "phrase")
str.replace(19,6,str3,7,6); // "this is an example phrase."
// (3) 从索引8开始,替换10个字符("an example") -> 换成 C-string
str.replace(8,10,"just a"); // "this is just a phrase."
// (4) 从索引8开始,替换6个字符("just a") -> 换成 buffer的前7个字符("a short")
str.replace(8,6,"a shorty",7); // "this is a short phrase."
// (5) 从索引22开始,替换1个字符(".") -> 换成 3个 '!'
str.replace(22,1,3,'!'); // "this is a short phrase!!!"
// --- 使用迭代器 (Using iterators) ---
// (1) 替换从 begin 到 end-3 之间的内容 -> 换成 str3
str.replace(str.begin(),str.end()-3,str3); // "sample phrase!!!"
// (3) 替换前6个字符 -> 换成 "replace"
str.replace(str.begin(),str.begin()+6,"replace"); // "replace phrase!!!"
// ... (更多迭代器示例,逻辑同上)
std::cout << str << '\n';
return 0;
}
输出:
replace is useful.
复杂度 (Complexity):
- 未指定,但通常与新字符串的长度成线性关系( O ( N ) O(N) O(N))。
迭代器有效性 (Iterator validity):
- 任何与此对象相关的迭代器、指针和引用都可能失效。
异常安全性 (Exception safety):
- 强保证 (Strong guarantee)。
- 越界检查同
erase/insert。
2. 技术总结与核心考点 (Summary & Key Points)
replace 是 C++ 字符串修改功能的集大成者。在笔记中,可以将其理解为 "Erase + Insert" 的原子操作。
1. 核心逻辑
replace 的行为可以分解为两步,但它在内部实现上可能更高效:
- 删除 指定区间(由
pos, len或i1, i2指定)。 - 插入新内容到该位置。
关键点 :被删除的长度 和插入的长度不需要相等!
- 如果
len > new_content.length(),字符串变短。 - 如果
len < new_content.length(),字符串变长(会自动扩容)。 - 如果
len == new_content.length(),仅在原地覆盖(效率最高,无数据移动)。
2. 参数记忆技巧
由于重载太多,死记硬背不可取。建议记住以下模式:
- 目标参数(前两个) :永远指定"被替换掉的旧地盘"。
- 要么是
(pos, len) - 要么是
(iter1, iter2)
- 要么是
- 源参数(后面一堆) :指定"新来的内容",与
append和insert的参数完全一致。- 可以是
str,char*,(char*, n),(n, c)等。
- 可以是
3. 性能考量
- 数据搬移 :除非新旧内容长度完全相等,否则
replace必然涉及后续字符的移动(Move)。 - 内存重分配 :如果替换后总长度超过
capacity,会触发 Reallocation,导致所有迭代器失效。
4. 实战场景
什么时候用 replace?
- 字符串模板替换 :例如将 "Hello, {{name}}!" 中的
{``{name}}替换为 "Gemini"。- 你需要先
find到{``{name}}的位置 (pos)。 - 然后
replace(pos, 8, "Gemini")。
- 你需要先
std::string::swap
这是 std::string 修改器(Modifiers)章节的最后一个重要函数:swap。
虽然它看起来很简单,但在 C++ 的性能优化 和异常安全 编程中,它的地位极高。对于你感兴趣的排序算法 来说,swap 更是核心操作。
1. 中文翻译 (Translation)
std::string::swap 公有成员函数
函数原型:
cpp
void swap (string& str);
功能:交换字符串的值
将当前容器的内容与 str(另一个 string 对象)的内容进行交换。两个字符串的长度可以不同。
- 调用后结果 :调用此成员函数后,当前对象的原本值变成了
str之前的值,而str的值变成了当前对象之前的值。 - 注意 :存在一个同名的非成员函数
std::swap,它重载了通用算法,并进行了优化,使其行为与此成员函数一致。
参数 (Parameters):
- str :另一个
string对象,其值将与当前string进行交换。
返回值 (Return value):
- 无 (none)。
示例 (Example):
cpp
// swap strings
#include <iostream>
#include <string>
int main ()
{
std::string buyer ("money");
std::string seller ("goods");
std::cout << "Before the swap, buyer has " << buyer;
std::cout << " and seller has " << seller << '\n';
// 执行交换
seller.swap (buyer);
std::cout << " After the swap, buyer has " << buyer;
std::cout << " and seller has " << seller << '\n';
return 0;
}
输出:
text
Before the swap, buyer has money and seller has goods
After the swap, buyer has goods and seller has money
复杂度 (Complexity):
- 常数时间 O ( 1 ) O(1) O(1)。
迭代器有效性 (Iterator validity):
- 任何与当前对象和
str相关的迭代器、指针和引用都可能失效。 - (注:这是标准库比较保守的说法,实际上对于长字符串,通常只是指针交换,迭代器可能指向交换后的新宿主,但在 SSO 短字符串优化场景下必然失效。为了安全,交换后应认为它们失效)。
数据竞争 (Data races):
- 当前对象和
str都会被修改。
异常安全性 (Exception safety):
- 无异常保证 (No-throw guarantee):此成员函数从不抛出异常。
2. 技术总结与核心考点 (Summary & Key Points)
不要小看 swap,它是 C++ 高效编程的基石之一。在你的博客中,这点非常值得深入讲解。
1. 极致的效率:指针交换 vs 数据拷贝
这是 swap 最大的考点。
- 普通赋值 (
a = b) :通常涉及深拷贝(Deep Copy),即申请新内存 -> 复制每一个字符 -> 释放旧内存。如果字符串很长,这是 O ( N ) O(N) O(N) 的操作,很慢。 - 交换 (
a.swap(b)) :对于长字符串,它仅仅交换了两个对象内部的控制指针 。就像两个人交换名片,而不是交换房子。无论字符串多长,它都是 O ( 1 ) O(1) O(1) 的瞬间操作。
2. 异常安全:Copy-and-Swap 惯用语
这是 C++ 中级/高级面试题。
- 编写赋值运算符(
operator=)时,为了保证"如果不成功,原对象不被破坏",程序员常使用swap。 - 逻辑 :先创建一个临时的副本(Copy),如果内存不足,这里就会抛出异常(原对象未动,安全)。一旦副本创建成功,再用
swap将副本的内容交换给当前对象(swap绝不抛异常,安全)。
3. 与排序算法的关系
swap 是大多数原址排序(In-place sort)的核心:
- 冒泡排序 (Bubble Sort) :
if (str[j] > str[j+1]) str[j].swap(str[j+1]); - 快速排序 (Quick Sort) :在 Partition 过程中大量使用
swap。 - 由于
std::string::swap是 O ( 1 ) O(1) O(1) 的,这使得vector<string>的排序速度非常快,因为移动元素的代价极低。
4. 最佳实践:std::swap vs str.swap
-
虽然文档介绍了成员函数
str.swap(other),但在泛型编程(Generic Programming)中,推荐使用全局函数:cppusing std::swap; swap(str1, str2);
这种写法更通用,且标准库已经针对 std::string 特化了 std::swap,它内部会自动调用高效的成员函数版本。
伪代码如下:
cpp
// ✅ 真实的情况:std::swap 针对 string 的特化版本
namespace std {
template<>
void swap<std::string>(std::string& a, std::string& b) {
// 它直接调用了成员函数 swap!
a.swap(b);
}
}
std::string::pop_back
它与之前介绍的 push_back 是一对"搭档",分别用于在字符串末尾增加 和删除字符。
1. 中文翻译 (Translation)
std::string::pop_back 公有成员函数
版本支持: C++11 (注意:C++98 没有这个函数)
函数原型:
cpp
void pop_back();
功能:删除最后一个字符
擦除字符串的最后一个字符,实际上将其长度(length)减一。
参数 (Parameters):
- 无 (none)。
返回值 (Return value):
- 无 (none)。
示例 (Example):
cpp
// string::pop_back
#include <iostream>
#include <string>
int main ()
{
std::string str ("hello world!");
str.pop_back(); // 删除最后一个字符 '!'
std::cout << str << '\n';
return 0;
}
输出:
hello world
复杂度 (Complexity):
- 未指定,但通常是常数时间 O ( 1 ) O(1) O(1)。
迭代器有效性 (Iterator validity):
- 任何与此对象相关的迭代器、指针和引用都可能失效。
- (译注:主要是指向末尾的迭代器
end()会发生变化,指向被删除字符的迭代器也会失效)。
数据竞争 (Data races):
- 对象被修改。
异常安全性 (Exception safety):
- 如果字符串为空,导致未定义行为 (Undefined Behavior)。
- 否则,该函数从不抛出异常 (No-throw guarantee)。
2. 技术总结与核心考点 (Summary & Key Points)
pop_back 是 C++11 为了统一容器接口(如 vector, deque)而引入的语法糖。在笔记中,它非常简单,但有一个致命的安全隐患。
1. 极简的实现机制
pop_back的操作成本极低。通常它只需要做一件事:将字符串的size计数器减 1。- 它通常不会 释放内存(Capacity 保持不变),也不会移动数据。因此它是严格的 O ( 1 ) O(1) O(1) 操作。
2. 致命陷阱:空字符串 (UB)
这是面试和实战中唯一的考点。
-
文档明确指出 :在空字符串上调用
pop_back()是 未定义行为 (UB)。 -
这与
vector::pop_back的行为一致,但很多人容易忽略。 -
防御性编程 :
cppstd::string s = ""; // s.pop_back(); // 危险!程序可能崩溃或破坏内存 if (!s.empty()) { s.pop_back(); // 安全 }
3. 历史演变 (C++98 vs C++11)
如果你在阅读旧代码,可能会发现没有 pop_back。
-
C++98 写法 :
cppstr.erase(str.length() - 1); // 或者 str.erase(str.end() - 1); -
C++11 写法 :
cppstr.pop_back(); // 更简洁,语义更清晰
4. 常用组合拳
在处理栈(Stack)结构的逻辑时,string 经常被当作字符栈使用:
back():查看栈顶(最后一个字符)。push_back(c):入栈。pop_back():出栈。
第九章:字符串操作
std::string::c_str
1. 中文翻译 (Translation)
std::string::c_str 公有成员函数
版本支持: C++98 / C++11
函数原型:
cpp
const char* c_str() const;
功能:获取 C 风格字符串等价物
返回一个指向数组的指针,该数组包含一个以空字符(null-terminated)结尾的字符序列(即 C 风格字符串),其内容代表了当前 string 对象的值。
该数组包含与 string 对象值相同的字符序列,并在末尾附加一个终止空字符 (\0)。
规范 (C++98 / C++11):
- 程序不得更改该序列中的任何字符。
- 如果调用了其他会修改该
string对象的成员函数,返回的指针可能会失效。
参数 (Parameters):
无 (none)
返回值 (Return Value):
一个指向 string 对象值的 C 风格字符串表示形式的指针。
示例 (Example):
cpp
// strings and c-strings
#include <iostream>
#include <cstring>
#include <string>
int main ()
{
std::string str ("Please split this sentence into tokens");
// 1. 分配新内存:str.length() 不包含 \0,所以需要 +1
char * cstr = new char [str.length()+1];
// 2. 拷贝内容:c_str() 返回的是 const,不能直接修改,必须拷贝到 cstr
std::strcpy (cstr, str.c_str());
// cstr 现在包含了 str 的 C 风格字符串副本
// 3. 使用 strtok 分割字符串(strtok 会修改传入的字符串)
char * p = std::strtok (cstr," ");
while (p!=0)
{
std::cout << p << '\n';
p = std::strtok(NULL," ");
}
delete[] cstr; // 记得释放内存
return 0;
}
输出:
text
Please
split
this
sentence
into
tokens
复杂度与异常 (Complexity & Exceptions):
- 复杂度:
- C++11 及以后: O ( 1 ) O(1) O(1) (常数时间)。
- C++98:未完全指定,可能需要 O ( N ) O(N) O(N) 进行拷贝(虽然很少见)。
- 异常安全性:
- C++11:
noexcept(保证不抛出异常)。 - C++98:通常不抛出异常。
- C++11:
2. 技术总结与核心考点 (Summary & Key Points)
c_str 是连接现代 C++ (std::string) 与传统 C 语言 API (printf, fopen, strtok 等) 的桥梁。重点生命周期 和只读属性。
1. 核心考点:指针失效 (Pointer Invalidation)
这是使用 c_str() 时最致命的陷阱。
c_str() 返回的指针直接指向 std::string 内部管理的内存。一旦 string 对象发生改变(扩容、修改字符、析构),这块内存可能被释放或移动。
-
错误写法:
cppconst char* p = str.c_str(); str += " append"; // str 可能重新分配内存 cout << p; // 错误!p 变成了悬空指针 (Dangling Pointer) -
正确习惯: 不要长期保存
c_str()返回的指针,随用随取,或者立即拷贝(如文档示例中的strcpy)。
2. 只读属性 (Read-Only)
- 返回类型是
const char*。 - 这意味着你不能通过这个指针修改字符串的内容。
- 场景分析: 文档中的
strtok函数要求传入char*(可修改),而c_str()返回const char*。因此,示例代码必须先new一个数组并strcpy内容,才能传给strtok使用。
3. 与 data() 的对比 (面试常问)
- C++11 之前:
c_str()保证以\0结尾;data()仅返回字符数组,不保证 以\0结尾。 - C++11 及之后:
c_str()和data()都会返回以\0结尾的字符串。 - C++17:
data()提供了一个非const重载,允许通过返回的指针修改字符串内容,而c_str()永远只返回const。
4. 常见用途
- 文件操作:
fstream fs(str.c_str());(注:C++11 起 fstream 构造函数已支持直接传 string,不再必须调 c_str) - 系统调用:
system(cmd.c_str()); - 输出调试:
printf("String is: %s", str.c_str());
std::string::data
1. 中文翻译 (Translation)
std::string::data 公有成员函数
版本支持: C++98 / C++11(注:后续 C++17 有重大更新)
函数原型:
cpp
const char* data() const;
功能:获取字符串数据
返回一个指向数组的指针,该数组包含与 string 对象值相同的字符序列。
C++98 特别说明(关键):
- 访问
data() + size()处的指会导致 未定义行为 (Undefined Behavior)。 - 不保证 返回的字符序列以空字符 (
\0) 结尾。如果需要保证以\0结尾,请使用string::c_str。
通用规范:
- 程序不得更改该序列中的任何字符。
- 如果调用了其他会修改该
string对象的成员函数,返回的指针可能会失效。
参数 (Parameters):
无 (none)
返回值 (Return Value):
一个指向字符串内容的指针。
示例 (Example):
cpp
// string::data
#include <iostream>
#include <string>
#include <cstring>
int main ()
{
int length;
std::string str = "Test string";
char* cstr = "Test string";
// 比较长度
if ( str.length() == std::strlen(cstr) )
{
std::cout << "str and cstr have the same length.\n";
// 比较内容:这里使用了 memcmp 而不是 strcmp
// 理由:在 C++98 中,data() 不保证有 \0,所以不能用 strcmp
if ( memcmp (cstr, str.data(), str.length() ) == 0 )
std::cout << "str and cstr have the same content.\n";
}
return 0;
}
输出:
text
str and cstr have the same length.
str and cstr have the same content.
复杂度与异常 (Complexity & Exceptions):
- 未指定或规范不统一(但在现代实现中通常为 O ( 1 ) O(1) O(1) 且不抛出异常)。
2. 技术总结与核心考点 (Summary & Key Points)
data() 是 C++ 标准演进中变化最大的成员函数之一。
1. 版本演进(必考点)
| 特性 | C++98 | C++11 | C++17 |
|---|---|---|---|
| 结尾符 | 不保证 以 \0 结尾 |
保证 以 \0 结尾 |
保证以 \0 结尾 |
| 等价性 | data() != c_str() |
data() == c_str() |
data() == c_str() |
| 返回类型 | const char* (只读) |
const char* (只读) |
提供 char* 重载 (可读写) |
| 用途 | 仅用于处理原始字节流 | 同 c_str() |
直接修改底层内存 (如作为 socket 接收缓冲) |
2. 为什么示例中使用 memcmp?
文档中的示例代码非常经典,体现了 C++98 的局限性。
- 因为 C++98 的
data()可能返回一个没有\0的数组。 - 如果使用
strcmp(cstr, str.data()),strcmp会一直向后读取直到遇到内存中的某个随机\0,这会导致越界访问和崩溃。 - 因此,必须使用
memcmp并指定明确的长度str.length()来比较。
3. 现代 C++ (C++17) 的高级用法
虽然文档未提及,但强烈建议 了解 C++17 的特性。
C++17 允许 string::data() 返回非 const 指针 (char*)。这意味着你可以直接向 string 的内存中写入数据。
场景:调用 C 语言 API 填充数据
cpp
std::string buffer;
buffer.resize(100); // 预分配空间
// C++17 之前:必须先用 vector<char> 或 new char[],然后再转存给 string
// C++17 之后:可以直接传 data()
c_style_read_function(buffer.data(), buffer.size());
4. c_str() vs data() 该选谁?
- 需要传给
printf,fopen等需要 C 字符串的函数: 首选c_str()(语义更清晰)。 - 需要处理二进制数据(可能包含
\0的内容): 首选data()。 - 需要修改字符串底层内存(C++17): 必须用
data()。
std::string::get_allocator
在 C++ 字符串的学习中,这是一个非常冷门 且高级的接口,通常只有在涉及自定义内存管理或编写泛型库时才会用到。
1. 中文翻译 (Translation)
std::string::get_allocator 公有成员函数
版本支持: C++98 / C++11
函数原型:
cpp
allocator_type get_allocator() const;
功能:获取分配器
返回与该 string 对象关联的分配器对象的副本。
string 默认使用 allocator<char> 类型,该类型没有状态(无状态,stateless)。因此,返回的值与一个默认构造的分配器是相同的。
参数 (Parameters):
无 (none)
返回值 (Return Value):
一个分配器对象。
在 string 中,成员类型 allocator_type 是 allocator<char> 的别名。
复杂度 (Complexity):
未指定,但通常是常数时间。
迭代器有效性 (Iterator validity):
无变化(不会导致迭代器失效)。
数据竞争 (Data races):
访问该对象。
异常安全性 (Exception safety):
无异常保证 (No-throw guarantee):此成员函数从不抛出异常。
拷贝任何默认分配器的实例也保证不抛出异常。
2. 技术总结与核心考点 (Summary & Key Points)
对于大多数应用层开发人员来说,这个函数几乎不会被用到,但在理解 STL 容器设计哲学时,它是一个很好的切入点。
1. 为什么需要这个函数?
- 统一接口: 所有 STL 容器(
vector,list,map等)都遵循标准的容器概念,都必须提供get_allocator()接口。这使得编写可以操作任意容器的泛型算法成为可能。 - 状态传递(针对自定义分配器):
虽然标准的std::string使用的std::allocator是无状态 的(即两个默认分配器实例完全一样),但在高级用法中,如果你使用std::basic_string并配合有状态的自定义分配器(例如:一个从特定内存池分配内存的分配器,持有内存池的指针),那么这个函数就至关重要了。它允许你获取当前字符串正在使用的那个特定的内存池句柄。
2. 代码演示
由于默认分配器没什么可展示的,这里演示如何获取并验证其类型:
cpp
#include <iostream>
#include <string>
#include <memory> // for std::allocator
int main () {
std::string str("Hello C++");
// 获取分配器
std::allocator<char> myAlloc = str.get_allocator();
// 使用获取到的分配器手动分配内存(仅演示,日常不要这样做)
char* p = myAlloc.allocate(5);
// 释放内存
myAlloc.deallocate(p, 5);
std::cout << "Allocator retrieved successfully." << std::endl;
return 0;
}
3. 核心考点:Stateful vs Stateless
- 默认情况:
std::string=std::basic_string<char, std::char_traits<char>, std::allocator<char>>。这里的std::allocator是无状态的。 - 常见误区: 很多初学者认为
get_allocator会返回整个内存堆的信息,其实它只返回一个用来执行allocate和deallocate动作的"工头"对象。
std::string::copy
这个函数与 c_str() 和 data() 形成鲜明对比:后两者是获取 内部指针,而 copy 是主动导出 数据到外部缓冲区。手动添加终止符是绝对的考点。
1. 中文翻译 (Translation)
std::string::copy 公有成员函数
函数原型:
cpp
size_t copy (char* s, size_t len, size_t pos = 0) const;
功能:从字符串复制字符序列
将当前 string 对象的一部分子串复制到由 s 指向的数组中。该子串包含从位置 pos 开始的 len 个字符。
关键注意: 该函数不会 在复制内容的末尾追加空字符 (\0)。
参数 (Parameters):
- s: 指向字符数组的指针。该数组必须有足够的存储空间来容纳被复制的字符。
- len: 要复制的字符数(如果字符串剩余长度小于该值,则复制尽可能多的字符)。
- pos : 第一个被复制字符的位置(索引)。
- 如果该值大于字符串长度,抛出
out_of_range异常。 - 注:字符串第一个字符的位置为 0。
- 如果该值大于字符串长度,抛出
返回值 (Return value):
实际复制到数组 s 中的字符数量。
该值可能等于 len,也可能等于 length() - pos(如果字符串在 pos + len 之前就结束了)。
示例 (Example):
cpp
// string::copy
#include <iostream>
#include <string>
int main ()
{
char buffer[20];
std::string str ("Test string...");
// 从索引 5 开始 ('s'),复制 6 个字符 ("string") 到 buffer
std::size_t length = str.copy(buffer, 6, 5);
// 必须手动添加结束符,否则打印 buffer 会导致乱码或崩溃
buffer[length] = '\0';
std::cout << "buffer contains: " << buffer << '\n';
return 0;
}
输出:
text
buffer contains: string
复杂度与异常 (Complexity & Safety):
- 复杂度: 与复制的字符数呈线性关系 O ( N ) O(N) O(N)。
- 异常安全性: 强保证 (Strong guarantee)。如果抛出异常(如
pos越界),原字符串不会改变。 - 未定义行为: 如果
s指向的数组不够长,会导致未定义行为(缓冲区溢出)。
2. 技术总结与核心考点 (Summary & Key Points)
copy 是一个非常"C 风格"的 C++ 成员函数,它要求程序员手动管理内存边界和结束符。
1. 最大的坑:没有 \0
这是面试和实际开发中 string::copy 最容易出错的地方。
- 区别:
strcpy会自动复制\0,c_str()返回的指针带\0。 - copy: 只复制指定数量的字符,绝不自动补零。
- 后果: 如果你把复制后的 buffer 直接传给
printf或cout,程序会继续读取内存直到遇到随机的0,导致乱码或 Segfault。 - 修正: 必须利用返回值手动添加:
buffer[ret_val] = '\0';
2. 与 c_str() / strcpy 的应用场景对比
在笔记中可以整理这样一个对比表:
| 特性 | str.c_str() |
str.copy(buf, len, pos) |
|---|---|---|
| 内存位置 | 指向 string 内部管理的内存 |
复制到用户提供的外部 buffer |
| 修改权限 | 只读 (const char*) |
可读写 (用户拥有 buffer) |
| 生命周期 | 随 string 变动而失效 |
独立于 string 存在 |
| 子串支持 | 只能获取完整字符串 | 原生支持截取子串 (通过 pos 和 len) |
| 典型用途 | 快速读取,传递给 C API | 网络包填充,序列化,固定长度结构体赋值 |
3. 安全性检查
- Input Check: 函数会自动检查
pos是否越界(抛出异常)。 - Output Check: 函数不会 检查
buffer是否够大。这是调用者的责任。- 建议: 使用
copy时,确保buffer大小至少为len + 1(为了放最后的\0)。
- 建议: 使用
4. 返回值的妙用
不要忽略返回值。它告诉你到底复制了多少个字符。
size_t written = str.copy(buf, 100, 0);
即使你请求复制 100 个字符,如果字符串只有 5 个字符,返回值就是 5。这对于后续的指针偏移或长度计算非常重要。
std::string::find
这是 C++ 字符串处理中最常用的查找 函数。其为"高频使用",并了解如何配合 pos 参数进行全量查找 (查找所有出现的位置),以及它与 find_first_of 的本质区别。
1. 中文翻译 (Translation)
std::string::find 公有成员函数
版本支持: C++98 / C++11
函数重载原型:
cpp
// -----------------------------------------------------------
// 这里的 "当前字符串" 指的是调用该函数的对象 (也就是 *this)
// 返回值:找到的第一个字符的下标 (index);如果没找到,返回 std::string::npos
// -----------------------------------------------------------
// (1) 【查找 std::string 对象】
// 功能:在当前字符串中,查找子串 str 第一次出现的位置。
// 参数 pos:从当前字符串的下标 pos 处开始向后搜索 (默认从头 0 开始)。
size_t find (const string& str, size_t pos = 0) const;
// (2) 【查找 C 风格字符串】
// 功能:在当前字符串中,查找 C 风格字符串 s 第一次出现的位置。
// 参数 s:必须是以 '\0' 结尾的字符数组。函数会自动计算它的长度 (strlen)。
// 参数 pos:从当前字符串的下标 pos 处开始向后搜索。
size_t find (const char* s, size_t pos = 0) const;
// (3) 【查找字符数组的前 n 个字符】(最容易有歧义的一个)
// 功能:在当前字符串中,查找由 s 指向的前 n 个字符组成的序列。
// 参数 s:一个字符数组(不需要以 '\0' 结尾,因为长度由 n 决定)。
// 参数 pos:从当前字符串的下标 pos 处开始向后搜索。
// 参数 n:我们要找的那个"目标子串"的长度。注意:n 不是限制搜索范围,而是定义要找的东西有多长。
size_t find (const char* s, size_t pos, size_t n) const;
// (4) 【查找单个字符】
// 功能:在当前字符串中,查找字符 c 第一次出现的位置。
// 参数 c:要查找的那个特定字符。
// 参数 pos:从当前字符串的下标 pos 处开始向后搜索。
size_t find (char c, size_t pos = 0) const;
功能:在字符串中查找内容
搜索字符串,寻找参数指定的序列第一次出现的位置。
当指定了 pos 时,搜索仅从位置 pos 开始(包含 pos)向后进行,忽略 pos 之前的所有内容。
注意: 与 find_first_of 成员函数不同,当查找目标包含多个字符时,find 要求整个序列必须完全匹配,而不仅仅是匹配其中任意一个字符。
参数 (Parameters):
- str : 包含要查找内容的另一个
string对象。 - pos : 开始搜索的起始位置索引。
- 如果该值大于字符串长度,函数将无法找到匹配项。
- 注意:第一个字符的索引为 0。值为 0 表示搜索整个字符串。
- s : 指向字符数组的指针。
- 如果指定了参数
n(版本 3),则匹配该数组的前n个字符。 - 否则 (版本 2),期望一个以 null 结尾的序列:要匹配的长度由第一个 null 字符决定。
- 如果指定了参数
- n: 要匹配的字符序列的长度。
- c: 要搜索的单个字符。
返回值 (Return Value):
返回第一个匹配子串的首字符位置(索引)。
如果未找到匹配项,函数返回 string::npos。
注:size_t 是一种无符号整数类型。
示例 (Example):
cpp
// string::find
#include <iostream> // std::cout
#include <string> // std::string
int main ()
{
std::string str ("There are two needles in this haystack with needles.");
std::string str2 ("needle");
// 1. 查找 string 对象
std::size_t found = str.find(str2);
if (found != std::string::npos)
std::cout << "first 'needle' found at: " << found << '\n';
// 2. 查找 C 风格字符串,并指定起始位置 (found+1)
// 这演示了如何查找第二个匹配项
found = str.find("needles are small", found+1, 6);
if (found != std::string::npos)
std::cout << "second 'needle' found at: " << found << '\n';
// 3. 查找普通 C 字符串
found = str.find("haystack");
if (found != std::string::npos)
std::cout << "'haystack' also found at: " << found << '\n';
// 4. 查找单个字符
found = str.find('.');
if (found != std::string::npos)
std::cout << "Period found at: " << found << '\n';
// 组合使用:先 find 再 replace
// 把第一个 "needle" 替换为 "preposition"
str.replace(str.find(str2), str2.length(), "preposition");
std::cout << str << '\n';
return 0;
}
输出:
text
first 'needle' found at: 14
second 'needle' found at: 44
'haystack' also found at: 30
Period found at: 51
There are two prepositions in this haystack with needles.
复杂度 (Complexity):
未指定,但通常最坏情况下为线性关系: O ( length ( ) × pattern_length ) O(\text{length}() \times \text{pattern\_length}) O(length()×pattern_length)。
异常安全性 (Exception safety):
如果 s 指向的数组不够长(导致越界读取),会产生未定义行为。
否则,保证不抛出异常。
2. 技术总结与核心考点 (Summary & Key Points)
find 是字符串处理的基石。除了基本用法,建议着重强调以下几个陷阱 和设计模式。
1. 判空检查:必不可少的 npos
这是新手最常犯的错误。find 失败时返回的不是 -1(因为 size_t 是无符号的),而是 std::string::npos(通常是 -1 的补码,即最大的无符号整数)。
- 错误写法:
if (str.find("abc") >= 0) { ... }// 永远为真! - 正确写法:
if (str.find("abc") != std::string::npos) { ... }
2. 循环查找模式
文档中的示例展示了如何查找第二个元素,但在实际开发中,我们通常使用 while 循环来查找所有出现的位置。这是面试中的常见考点。
cpp
std::string text = "abbaabba";
std::string pattern = "ba";
size_t pos = text.find(pattern);
while (pos != std::string::npos) {
std::cout << "Found at: " << pos << std::endl;
// 关键:下一次查找从 pos + 1 (或 pos + pattern.length()) 开始
// 使用 pos + 1 允许查找重叠子串(如 "aaa" 中找 "aa")
// 使用 pos + len 则是查找不重叠子串
pos = text.find(pattern, pos + 1);
}
3. find vs find_first_of (易混淆)
文档中特别提到了这一点,值得在笔记中用对比表突出:
| 函数 | 行为描述 | 示例:在 "hello world" 中找 "ole" |
|---|---|---|
find |
全字匹配。寻找子串 "ole"。 | 找不到 (因为没有连续的 "ole") |
find_first_of |
字符集匹配 。寻找 "o" 或 "l" 或 "e" 中任意一个字符第一次出现的位置。 | 返回 1 (即 'e' 的位置) |
find: 相当于 "Search String"find_first_of: 相当于 "Search Set"
4. 效率提示
std::string::find 通常实现为朴素的字符串匹配算法。
- 对于极长的文本和模式串,它的效率可能不如 KMP 算法或 Boyer-Moore 算法。
- 但在日常业务逻辑和短字符串处理中,标准库的
find已经足够快且经过了高度优化(通常利用了 CPU 的 SIMD 指令)。
std::string::rfind
rfind (Reverse Find) 是 find 的反向版本,常用于从后往前查找。在整理笔记时,建议将其标记为"文件路径处理神器"(常用于分离文件名和后缀)。
1. 中文翻译 (Translation)
std::string::rfind 公有成员函数
版本支持: C++98 / C++11
函数重载原型:
cpp
// (1) string: 查找 string 对象
size_t rfind (const string& str, size_t pos = npos) const;
// (2) c-string: 查找 C 风格字符串 (以 \0 结尾)
size_t rfind (const char* s, size_t pos = npos) const;
// (3) buffer: 查找字符数组的前 n 个字符
size_t rfind (const char* s, size_t pos, size_t n) const;
// (4) character: 查找单个字符
size_t rfind (char c, size_t pos = npos) const;
功能:查找字符串中内容的最后一次出现
搜索字符串,寻找参数指定序列的最后一次出现的位置。
当指定了 pos 时,搜索仅包含那些起始位置在 pos 处或之前 的字符序列,忽略任何起始位置在 pos 之后的匹配项。
参数 (Parameters):
- str : 包含要查找内容的另一个
string对象。 - pos : 字符串中被视为匹配起点的最后一个字符位置。
- 任何大于或等于字符串长度的值(包括默认值
string::npos)都意味着搜索整个字符串。 - 注意:索引从 0 开始。
- 任何大于或等于字符串长度的值(包括默认值
- s / n / c : 与
find函数含义相同(C 风格字符串 / 长度 / 单字符)。
返回值 (Return Value):
返回最后一次匹配的子串的首字符位置(索引)。
如果未找到匹配项,函数返回 string::npos。
示例 (Example):
cpp
// string::rfind
#include <iostream>
#include <string>
#include <cstddef>
int main ()
{
std::string str ("The sixth sick sheik's sixth sheep's sick.");
std::string key ("sixth");
// 查找最后一个 "sixth"
std::size_t found = str.rfind(key);
if (found != std::string::npos) {
// 将其替换为 "seventh"
// 注意:这里只会替换最后一个,前面的 "sixth" 不受影响
str.replace (found, key.length(), "seventh");
}
std::cout << str << '\n';
return 0;
}
输出:
text
The sixth sick sheik's seventh sheep's sick.
复杂度 (Complexity):
未指定,但通常最坏情况下为线性关系:与(字符串长度 或 pos)乘以(匹配序列长度)成正比。
异常安全性 (Exception safety):
同 find。如果 s 指向的数组有效,则保证不抛出异常。
2. 技术总结与核心考点 (Summary & Key Points)
rfind 的核心在于方向性。虽然它的名字叫 Reverse Find,但它查找的子串本身顺序是不变的(比如找 "abc" 还是找 "abc",不是找 "cba"),只是扫描方向从右向左。
1. pos 参数的逆向逻辑(易错点)
这是 rfind 最难理解的部分。
find(str, pos): 从pos向后看。rfind(str, pos): 从pos向前 看。- 确切地说,它限制的是匹配项的起始索引 不能超过
pos。 - 例如:
str = "hello world",str.rfind("world", 5)。 - 虽然 "world" 的长度是 5,但它的起始索引是 6。
- 因为
6 > 5(pos),所以这个匹配会被忽略。
- 确切地说,它限制的是匹配项的起始索引 不能超过
2. 经典应用场景:文件路径解析
这是 rfind 在实际工程中最常见的用途。
场景 A:获取文件扩展名
找最后一个 .,后面的就是扩展名。
cpp
std::string filename = "archive.tar.gz";
size_t pos = filename.rfind('.');
if (pos != std::string::npos) {
std::cout << "Extension: " << filename.substr(pos); // 输出: .gz
}
场景 B:从全路径中获取文件名
找最后一个路径分隔符(Windows 下是 \,Linux 下是 /)。
cpp
std::string path = "/usr/local/bin/gcc";
size_t pos = path.rfind('/');
if (pos != std::string::npos) {
std::cout << "Filename: " << path.substr(pos + 1); // 输出: gcc
}
3. rfind vs find_last_of
就像 find 和 find_first_of 的关系一样,这两个函数也经常混淆:
| 函数 | 行为描述 | 示例:在 "banana" 中 |
|---|---|---|
rfind("an") |
全字匹配。找最后一次出现的 "an"。 | 返回 3 (banana) |
find_last_of("an") |
字符集匹配 。找最后一次出现的 'a' 或 'n'。 | 返回 5 (banana) |
4. 性能提示
由于内存缓存(Cache)通常针对顺序读取进行了优化,在超长字符串上,rfind(逆向遍历)的性能理论上可能略低于 find(正向遍历),但在绝大多数业务场景中,这种差异可以忽略不计。
std::string::find_first_of
这个函数是处理字符集合 (Character Set)查找的核心工具,常被误认为是查找子串,整理笔记时请务必区分它与 find 的本质不同。
1. 中文翻译 (Translation)
std::string::find_first_of 公有成员函数
版本支持: C++98 / C++11
函数重载原型:
cpp
// (1) string: 查找 string 对象中包含的任意字符
size_t find_first_of (const string& str, size_t pos = 0) const;
// (2) c-string: 查找 C 风格字符串中包含的任意字符
size_t find_first_of (const char* s, size_t pos = 0) const;
// (3) buffer: 查找字符数组前 n 个字符中包含的任意字符
size_t find_first_of (const char* s, size_t pos, size_t n) const;
// (4) character: 查找单个字符
size_t find_first_of (char c, size_t pos = 0) const;
功能:在字符串中查找字符
搜索字符串,寻找第一个 与参数中指定的任意字符相匹配的字符。
当指定了 pos 时,搜索仅包含位置 pos 或之后的字符,忽略 pos 之前可能出现的匹配项。
注意: 只需要序列中的某一个 字符匹配即可(而不是全部匹配)。如果需要匹配整个序列,请参阅 string::find。
参数 (Parameters):
- str : 包含要搜索字符集合的另一个
string对象。 - pos : 搜索起始位置的索引。
- 如果大于字符串长度,则无法找到匹配项。
- 注意:第一个字符索引为 0。
- s : 指向字符数组的指针。
- 如果是版本 (3),搜索数组的前
n个字符。 - 否则 (2),期望一个以 null 结尾的序列:要匹配的字符集合长度由第一个 null 字符决定。
- 如果是版本 (3),搜索数组的前
- n: 要搜索的字符数值的数量。
- c: 要搜索的单个字符。
返回值 (Return Value):
返回第一个匹配字符的位置(索引)。
如果未找到匹配项,函数返回 string::npos。
示例 (Example):
该示例展示了如何将句子中的所有元音字母替换为星号。
cpp
// string::find_first_of
#include <iostream> // std::cout
#include <string> // std::string
#include <cstddef> // std::size_t
int main ()
{
std::string str ("Please, replace the vowels in this sentence by asterisks.");
// 查找第一个元音 ('a','e','i','o','u' 任意一个)
std::size_t found = str.find_first_of("aeiou");
while (found != std::string::npos)
{
str[found] = '*'; // 替换找到的元音
// 继续查找下一个,注意要从 found+1 开始,否则会死循环
found = str.find_first_of("aeiou", found+1);
}
std::cout << str << '\n';
return 0;
}
输出:
text
Pl**s*, r*pl*c* th* v*w*ls *n th*s s*nt*nc* by *st*r*sks.
复杂度 (Complexity):
未指定,但通常最坏情况下为线性关系: O ( length ( ) × number_of_target_chars ) O(\text{length}() \times \text{number\_of\_target\_chars}) O(length()×number_of_target_chars)。
异常安全性 (Exception safety):
同其他查找函数,若 s 有效则不抛出异常。
2. 技术总结与核心考点 (Summary & Key Points)
find_first_of 是处理词法分析 、分词 和数据清洗 的神器。在笔记中,一定要将其与 find 进行对比,这是面试和实战中最容易混淆的概念。
1. 核心区别:find vs find_first_of
这是本节最重要的知识点。
| 特性 | str.find("abc") |
str.find_first_of("abc") |
|---|---|---|
| 搜索逻辑 | 全字匹配 (Sequence Match) | 集合匹配 (Set Match) |
| 含义 | 找 "abc" 这个连续的子串 | 找 'a' 或 'b' 或 'c' |
| 示例 | 在 "track" 中找 "rac" -> Found | 在 "track" 中找 "rac" -> Found 'r' at 1 |
| 示例 | 在 "table" 中找 "rac" -> Not Found | 在 "table" 中找 "rac" -> Found 'a' at 1 |
2. 常见应用场景
-
敏感字符过滤/替换: 如示例代码所示,把所有 "aeiou" 替换成
*,或者把所有标点符号",.;!?"找出来删掉。 -
查找分隔符: 在解析 CSV 或命令行参数时,用来查找下一个分隔符(如空格、逗号、分号)的位置。
cpp// 查找下一个空格或逗号 size_t pos = text.find_first_of(" ,"); -
路径处理: 在跨平台路径处理中,查找
/或\。cpp// 查找任意一种路径分隔符 size_t split_pos = path.find_first_of("/\\");
3. 它的孪生兄弟:find_first_not_of
虽然这篇文档没提,但建议在笔记中顺带提及 find_first_not_of。
find_first_of: 找属于集合的字符。find_first_not_of: 找不属于集合的字符。- 用途: 常用于跳过空格(Trim)。例如,从字符串开头找第一个不是空格的字符,就是 Trim Left 的起始位置。
4. 性能陷阱
虽然通常很快,但如果你在长字符串中搜索一个巨大的字符集(例如 str 长度 100万,搜索集长度 1万),性能会下降。
- 底层实现: 简单的实现是双重循环。
- 优化: 对于 ASCII 字符集,标准库通常会使用 Lookup Table (查表法) 优化,将复杂度降为 O ( N ) O(N) O(N)。
std::string::find_last_of
这个函数是文件路径处理和字符串解析中的"多面手"。它结合了 倒序查找 和 集合匹配 两个特性。
1. 中文翻译 (Translation)
std::string::find_last_of 公有成员函数
版本支持: C++98 / C++11
函数重载原型:
cpp
// (1) string: 从末尾查找 string 对象中包含的任意字符
size_t find_last_of (const string& str, size_t pos = npos) const;
// (2) c-string: 从末尾查找 C 风格字符串中包含的任意字符
size_t find_last_of (const char* s, size_t pos = npos) const;
// (3) buffer: 从末尾查找字符数组前 n 个字符中包含的任意字符
size_t find_last_of (const char* s, size_t pos, size_t n) const;
// (4) character: 从末尾查找单个字符
size_t find_last_of (char c, size_t pos = npos) const;
功能:从末尾查找字符串中的字符
搜索字符串,寻找最后一次 出现的、且与参数指定字符集合中任意字符匹配的字符。
当指定了 pos 时,搜索仅包含位置 pos 或之前的字符,忽略 pos 之后可能出现的匹配项。
参数 (Parameters):
- str : 包含要搜索字符集合的另一个
string对象。 - pos : 搜索范围内最后一个字符的位置(即搜索的截止点,从后往前看是起始点)。
- 任何大于或等于字符串长度的值(包括
string::npos)都表示搜索整个字符串。 - 注意:索引从 0 开始。
- 任何大于或等于字符串长度的值(包括
- s / n / c : 与
find_first_of含义相同(C 风格字符串 / 长度 / 单字符)。
返回值 (Return Value):
返回最后一个匹配字符的位置(索引)。
如果未找到匹配项,函数返回 string::npos。
示例 (Example):
该示例展示了最经典用法:分离文件名与路径 。特别注意它如何同时处理 Windows (\) 和 Linux (/) 的路径分隔符。
cpp
// string::find_last_of
#include <iostream> // std::cout
#include <string> // std::string
#include <cstddef> // std::size_t
void SplitFilename (const std::string& str)
{
std::cout << "Splitting: " << str << '\n';
// 核心逻辑:从后往前找,找到第一个是 '/' 或者 '\' 的字符位置
std::size_t found = str.find_last_of("/\\");
// 提取路径:从开头到分隔符位置
std::cout << " path: " << str.substr(0, found) << '\n';
// 提取文件名:从分隔符后一位开始到末尾
std::cout << " file: " << str.substr(found+1) << '\n';
}
int main ()
{
std::string str1 ("/usr/bin/man");
std::string str2 ("c:\\windows\\winhelp.exe");
SplitFilename (str1);
SplitFilename (str2);
return 0;
}
输出:
text
Splitting: /usr/bin/man
path: /usr/bin
file: man
Splitting: c:\windows\winhelp.exe
path: c:\windows
file: winhelp.exe
复杂度 (Complexity):
未指定,但通常最坏情况下为线性关系: O ( length ( ) × pattern_size ) O(\text{length}() \times \text{pattern\_size}) O(length()×pattern_size)。
异常安全性 (Exception safety):
若 s 有效则不抛出异常。
2. 技术总结与核心考点
find_last_of 是处理路径 、URL 或 特定格式字符串(如倒序寻找标点符号)时的首选工具。
1. 核心对比:rfind vs find_last_of
这是面试中关于"倒序查找"的常见陷阱:
| 特性 | str.rfind("abc") |
str.find_last_of("abc") |
|---|---|---|
| 匹配逻辑 | 全字匹配 (Sequence) | 集合匹配 (Set) |
| 方向 | 从右向左 | 从右向左 |
| 含义 | 找最后一次出现的 "abc" | 找最后一次出现的 'a' 或 'b' 或 'c' |
| 场景 | 查找特定单词后缀 | 查找任意分隔符 |
2. 经典场景:跨平台路径分割
如文档示例所示,这是该函数的杀手级应用。
- Windows 路径使用
\,但也经常兼容/。 - Linux/Unix 使用
/。 - 如果使用
rfind("/"),无法处理 Windows 的\。 - 如果使用
find_last_of("/\\"),则可以一次性兼容两种分隔符,找到最后一个分隔符的位置,从而完美切割文件名。
3. 边界情况处理
在示例代码中,有一个隐含的假设:分隔符一定存在。在实际开发中,需要处理 found == string::npos 的情况:
- 如果
found == npos: 说明没有路径分隔符,整个字符串就是文件名,路径为空。 - 如果不处理:
str.substr(0, npos)会抛出异常(如果长度超限)或返回整个字符串(取决于实现),逻辑可能不符合预期。
4. 配合 substr
find_last_of 通常不单独使用,它总是作为 substr(提取子串)或 erase(删除)的定位器。
str.substr(0, pos):获取左半部分(目录)。str.substr(pos + 1):获取右半部分(文件名)。
std::string::find_first_not_of 和 std::string::find_last_not_of
这两个函数是之前学习的 find_first_of 和 find_last_of 的互补版本 (逻辑取反)。它们在处理Trim(去除空格) 、数据校验(检查是否只包含数字)等场景中是核心工具。
1. find_first_not_of (查找第一个不匹配的字符)
函数原型 (C++98 / C++11):
cpp
// (1) string
size_t find_first_not_of (const string& str, size_t pos = 0) const;
// (2) c-string
size_t find_first_not_of (const char* s, size_t pos = 0) const;
// (3) buffer
size_t find_first_not_of (const char* s, size_t pos, size_t n) const;
// (4) character
size_t find_first_not_of (char c, size_t pos = 0) const;
功能:
搜索字符串,寻找第一个 与参数中指定字符集合中任何字符都不匹配 的字符。
简单来说:它跳过所有匹配的字符,停在第一个"局外人"身上。
参数与返回值:
- 参数 :与
find_first_of完全一致。 - 返回值 :第一个不属于 指定集合的字符的位置(索引)。如果所有字符都匹配(即找不到不匹配的),则返回
string::npos。
典型场景:Trim Left (去除左侧空白)
cpp
std::string str = " \t Hello World";
std::string whitespaces = " \t\f\v\n\r";
// 从左往右找,跳过所有空白符,找到第一个不是空白符的字符 'H'
size_t found = str.find_first_not_of(whitespaces);
if (found != std::string::npos) {
str.erase(0, found); // 删除 'H' 之前的所有空白
} else {
str.clear(); //全是空白,直接清空
}
// str 现在是 "Hello World"
2. find_last_not_of (从末尾查找第一个不匹配的字符)
函数原型 (C++98 / C++11):
cpp
// (1) string
size_t find_last_not_of (const string& str, size_t pos = npos) const;
// (2) c-string
size_t find_last_not_of (const char* s, size_t pos = npos) const;
// (3) buffer
size_t find_last_not_of (const char* s, size_t pos, size_t n) const;
// (4) character
size_t find_last_not_of (char c, size_t pos = npos) const;
功能:
搜索字符串,寻找倒数第一个 与参数中指定字符集合中任何字符都不匹配 的字符。
或者理解为:从后往前找,跳过所有匹配字符,停在第一个"局外人"身上。
参数与返回值:
- 参数 :与
find_last_of完全一致。pos默认为npos(字符串末尾)。 - 返回值 :最后一个不属于 指定集合的字符的位置。如果全都是匹配字符,返回
string::npos。
典型场景:Trim Right (去除右侧空白)
cpp
std::string str = "Hello World \t";
std::string whitespaces = " \t\f\v\n\r";
// 从右往左找,跳过空白,找到最后一个不是空白的字符 'd'
size_t found = str.find_last_not_of(whitespaces);
if (found != std::string::npos) {
// 保留 'd' (索引 found),删除它之后的所有字符
str.erase(found + 1);
} else {
str.clear(); // 全是空白
}
// str 现在是 "Hello World"
3. 技术总结与核心考点
这两个函数最核心的应用就是实现字符串的 Trim(去除首尾字符)和数据格式校验。
1. 逻辑取反 (Inverted Logic)
find_first_of("0123456789"): 找第一个数字。find_first_not_of("0123456789"): 找第一个非数字 。- 应用: 检查字符串是否是纯数字整数。如果
find_first_not_of返回npos,说明整个字符串里全是数字。
- 应用: 检查字符串是否是纯数字整数。如果
2. Trim 的完整实现 (笔记必备)
C++ 标准库(直到 C++20)都没有提供直接的 trim() 函数。你需要组合这两个函数来实现:
cpp
#include <string>
#include <iostream>
int main() {
std::string s = " \t 12345 \n ";
std::string whitespace = " \t\n\r";
// 1. Trim Left
size_t start = s.find_first_not_of(whitespace);
// 如果 start == npos,说明全是空白
if (start == std::string::npos) {
s = "";
} else {
// 2. Trim Right (只有在非全空时才需要做)
size_t end = s.find_last_not_of(whitespace);
// 3. 提取子串
// 长度 = 结束位置 - 开始位置 + 1
s = s.substr(start, end - start + 1);
}
std::cout << "[" << s << "]" << std::endl; // 输出: [12345]
return 0;
}
3. 性能提示
与 find_first_of 一样,如果搜索的字符集合很大(例如搜索所有非 ASCII 字符),效率会受影响。但对于常见的"空白字符集"或"数字集",标准库实现通常非常高效(查表法)。
std::string::substr
作为 C++ 中最常用的子串生成 函数,它 的核心考点是:异常安全性 (pos 越界会抛出异常)以及它与现代 C++ string_view 的区别(substr 会发生深拷贝)。
1. 中文翻译 (Translation)
std::string::substr 公有成员函数
函数原型:
cpp
string substr (size_t pos = 0, size_t len = npos) const;
功能:生成子串
返回一个新构造的 string 对象,其值初始化为当前对象子串的副本。
该子串是从字符位置 pos 开始,包含 len 个字符的部分(或者直到字符串末尾,以先到者为准)。
参数 (Parameters):
- pos : 要作为子串复制的第一个字符的位置。
- 如果该值等于字符串长度,函数返回一个空字符串。
- 如果该值大于字符串长度,抛出
out_of_range异常。 - 注意:第一个字符位置为 0。
- len : 子串中包含的字符数。
- 如果字符串剩余长度小于该值,则使用尽可能多的字符。
- 值
string::npos表示直到字符串末尾的所有字符。
返回值 (Return Value):
一个包含当前对象子串的 string 对象。
示例 (Example):
cpp
// string::substr
#include <iostream>
#include <string>
int main ()
{
std::string str="We think in generalities, but we live in details.";
// (引用自 Alfred N. Whitehead)
// 1. 指定位置和长度:从索引 3 开始,取 5 个字符 -> "think"
std::string str2 = str.substr (3,5);
// 2. 配合 find 使用
std::size_t pos = str.find("live"); // 找到 "live" 的位置
// 3. 仅指定起始位置:从 pos 开始一直取到末尾 -> "live in details."
std::string str3 = str.substr (pos);
std::cout << str2 << ' ' << str3 << '\n';
return 0;
}
输出:
text
think live in details.
复杂度与异常 (Complexity & Exceptions):
- 复杂度: 未指定,但通常与返回对象的长度 成线性关系 ( O ( K ) O(K) O(K),K 为子串长度)。
- 异常安全性: 强保证 (Strong guarantee)。
- 如果
pos大于字符串长度,抛出out_of_range异常。 - 如果内存分配失败,抛出
bad_alloc异常。
- 如果
2. 技术总结与核心考点 (Summary & Key Points)
substr 是初学者最常用的函数,但也是导致 C++ 程序性能隐形下降的常见原因之一。
1. 参数行为差异 (重要考点)
pos 和 len 对"越界"的处理方式完全不同:
pos(起始位置):严格校验。- 如果
pos > str.length(),抛出异常。
- 如果
len(长度):宽容处理。- 如果
pos + len > str.length(),不会抛出异常,而是自动截断到字符串末尾。 - 这意味着
str.substr(pos, 99999)是安全的截取后缀的方法。
- 如果
2. 性能代价:深拷贝 (Deep Copy)
substr总是 返回一个新的std::string对象。- 这意味着它会申请新的内存堆空间,并将字符复制过去。
- 性能陷阱: 如果你在一个循环中对长字符串频繁调用
substr(例如解析大文本),会产生大量的内存分配和复制开销。
3. 现代替代方案:std::string_view (C++17)
在 C++17 及以后,如果你只需要读取 子串而不需要修改它,千万不要用 substr 。
请使用 std::string_view。它是一个轻量级的"窗口",不分配内存,不拷贝数据,只保存指针和长度。
-
传统写法 (C++98/11):
cppstd::string s = "hello world"; std::string sub = s.substr(0, 5); // 发生内存分配和拷贝 -
现代写法 (C++17):
cppstd::string s = "hello world"; std::string_view sub = std::string_view(s).substr(0, 5); // 零拷贝,极快
4. 常用简写
-
取后缀:
str.substr(pos)等同于str.substr(pos, string::npos)。 -
安全截取: 如果不确定
pos是否越界,必须先检查:cppif (pos < str.length()) { result = str.substr(pos); }
std::string::compare
这个函数是 C++ 字符串处理中用于高效比较 和字典序排序的核心工具,重点关注它如何避免创建临时子串对象(这一点对性能优化至关重要)。
1. 中文翻译 (Translation)
std::string::compare 公有成员函数
版本支持: C++98 / C++11 / C++14
函数重载原型(按功能分类):
cpp
// (1) string: 比较整个字符串
int compare (const string& str) const noexcept;
// (2) substrings: 比较当前对象的子串 vs 另一个字符串(或其子串)
int compare (size_t pos, size_t len, const string& str) const;
int compare (size_t pos, size_t len, const string& str,
size_t subpos, size_t sublen = npos) const;
// (3) c-string: 比较当前对象 vs C 风格字符串
int compare (const char* s) const;
int compare (size_t pos, size_t len, const char* s) const;
// (4) buffer: 比较当前对象子串 vs 字符数组的前 n 个字符
int compare (size_t pos, size_t len, const char* s, size_t n) const;
功能:比较字符串
将 string 对象的值(或其子串)与参数指定的字符序列进行比较。
- 被比较字符串 (Compared string) :指调用该函数的
string对象本身。如果使用了pos和len参数,则指从位置pos开始、跨度为len个字符的子串。 - 比较字符串 (Comparing string):指传入函数的参数,用来与前者进行对比。
参数 (Parameters):
- str : 另一个
string对象,全部(或部分)用作比较字符串。 - pos : 被比较字符串 (即
*this)中第一个字符的位置。如果大于字符串长度,抛出out_of_range异常。 - len : 被比较字符串的长度(如果字符串剩余部分较短,则取尽可能多的字符)。
- subpos, sublen : 上述
pos和len的对应参数,但用于 str(参数对象)。 - s: 指向字符数组的指针。
- n: 要比较的字符数量。
返回值 (Return Value):
返回一个有符号整数,指示字符串之间的关系(类似 C 语言的 strcmp):
| 返回值 | 关系描述 |
|---|---|
| 0 | 两个字符串相等。 |
| < 0 | 被比较字符串 在字典序上小于 比较字符串。 (即第一个不匹配的字符值较小,或字符都匹配但被比较字符串更短)。 |
| > 0 | 被比较字符串 在字典序上大于 比较字符串。 (即第一个不匹配的字符值较大,或字符都匹配但被比较字符串更长)。 |
示例 (Example):
cpp
// comparing apples with apples
#include <iostream>
#include <string>
int main ()
{
std::string str1 ("green apple");
std::string str2 ("red apple");
// 1. 整体比较
if (str1.compare(str2) != 0)
std::cout << str1 << " is not " << str2 << '\n';
// 2. 子串比较:比较 str1 从索引 6 开始的 5 个字符 ("apple") 是否等于 "apple"
// 注意:这里没有创建新的 string 对象,效率很高
if (str1.compare(6,5,"apple") == 0)
std::cout << "still, " << str1 << " is an apple\n";
// 3. 复杂比较:str2 的最后 5 个字符 vs "apple"
if (str2.compare(str2.size()-5,5,"apple") == 0)
std::cout << "and " << str2 << " is also an apple\n";
// 4. 双子串比较:str1 的 "apple" (6,5) vs str2 的 "apple" (4,5)
if (str1.compare(6,5,str2,4,5) == 0)
std::cout << "therefore, both are apples\n";
return 0;
}
输出:
text
green apple is not red apple
still, green apple is an apple
and red apple is also an apple
therefore, both are apples
复杂度与异常 (Complexity & Exceptions):
- 复杂度: 线性 O ( N ) O(N) O(N)。
- 异常安全性: 强保证。若
pos或subpos越界,抛出out_of_range。
2. 技术总结与核心考点 (Summary & Key Points)
compare 是 C++ 中比 ==, <, > 运算符更底层、更强大的比较工具。
1. 性能杀手锏:避免 substr 产生的临时拷贝
这是 compare 最重要的用途。
如果你想检查字符串的某一部分是否等于另一个词,初学者常写:
cpp
// 低效写法:substr 会分配新内存并拷贝字符,产生临时对象
if (str.substr(0, 5) == "Hello") { ... }
高手(或追求性能的嵌入式代码)会写:
cpp
// 高效写法:原地比较,零拷贝,零内存分配
if (str.compare(0, 5, "Hello") == 0) { ... }
考点: 当你需要只读 访问子串进行比较时,永远优先选择 compare 而不是 substr。
2. 三路比较 (Three-way Comparison)
与运算符返回 bool 不同,compare 返回 int (-1, 0, 1)。
- 这对于编写排序算法(如
std::sort的自定义比较器)非常有用,因为它一次调用就能通过正负值告诉你谁前谁后,而不需要分别调用<和==。
3. 记忆口诀:减法逻辑
如何记住返回值 <0 还是 >0?
想象返回值等于:(*this) - (argument)
- 如果
*this(当前串) 是 "a" (ASCII 97),参数是 "b" (ASCII 98)。 97 - 98 = -1(小于 0)。- 所以:当前串 < 参数串 -> 返回负值。
4. 参数重载的识别技巧
虽然重载看起来很多,其实就分三类:
- 跟谁比? (String 对象 / C 风格字符串 / 字符数组)
- 比多少? (全部 / 对方的子串)
- 我出多少? (全部 / 我的子串
pos, len)
第十章:成员常量
std::string::npos
📝 翻译 (Translation)
std::string::npos
静态常量 size_t npos = -1;
size_t 的最大值
npos 是一个静态成员常量,其数值为 size_t 类型元素所能表示的最大可能值。
- 作为参数时: 当这个值在
string的成员函数中被用作len(长度)或sublen(子长度)参数时,它的意思是 "直到字符串的末。 - 作为返回值时: 它通常用来表示 "未找到匹配项"。
- 原理: 这个常量被定义为
-1。因为size_t是无符号整型(unsigned integral type),在计算机补码表示法中,-1会被转换为该类型能表示的最大整数值。
💡 核心总结 (Summary)
std::string::npos 是 C++ 字符串处理中非常重要的一个常量,主要包含以下三个核心知识点:
-
数值定义 (Value Definition)
- 它在代码中定义为
-1。 - 由于类型是
size_t(无符号整数),-1会发生下溢(underflow),变成全1的二进制位,从而代表该类型可表示的最大正整数 (在 64 位系统中通常是 2 64 − 1 2^{64}-1 264−1)。
- 它在代码中定义为
-
用途一:表示"全部" (Input Parameter)
- 当你调用像
substr或erase这样的函数时,如果你想表达"从某个位置开始一直操作到字符串结束",你不需要计算剩余长度,直接传递npos即可。
- 当你调用像
-
用途二:表示"未找到" (Return Value)
- 这是最常见的错误检查方式。当你使用
find系列函数(如find,rfind)搜索子串或字符时,如果函数返回npos,则说明目标不存在。
- 这是最常见的错误检查方式。当你使用
示例代码片段
cpp
// 用途一:从索引 5 截取直到末尾
std::string sub = str.substr(5, std::string::npos);
// 用途二:检查是否找到
if (str.find("abc") == std::string::npos) {
std::cout << "未找到 'abc'" << std::endl;
}
第十一章:非成员函数重载
std::operator+ (string)
📝 文档翻译 (Translation)
cpp
string (1)
string operator+ (const string& lhs, const string& rhs);
string operator+ (string&& lhs, string&& rhs);
string operator+ (string&& lhs, const string& rhs);
string operator+ (const string& lhs, string&& rhs);
c-string (2)
string operator+ (const string& lhs, const char* rhs);
string operator+ (string&& lhs, const char* rhs);
string operator+ (const char* lhs, const string& rhs);
string operator+ (const char* lhs, string&& rhs);
character (3)
string operator+ (const string& lhs, char rhs);
string operator+ (string&& lhs, char rhs);
string operator+ (char lhs, const string& rhs);
string operator+ (char lhs, string&& rhs);
该运算符重载了多种形式,支持不同类型的混合拼接:
- String + String:
(1)string + string(包括 C++11 针对右值引用的优化版本)
- String + C-String (及反向):
(2)string + const char*(2)const char* + string
- String + Character (及反向):
(3)string + char(3)char + string
2. 功能描述
连接字符串
返回一个新的 string 对象,其内容是左操作数 (lhs) 的字符序列后面紧跟右操作数 (rhs) 的字符序列。
C++11 特性(重要优化)
在接受至少一个右值引用 (rvalue reference,即 string&&)作为参数的签名中,返回的对象是通过移动构造(move-constructed)生成的:
- 它会复用传入的临时对象(右值)的内存资源,而不是重新分配和复制。
- 被移动的参数将处于"未指定但有效"的状态。
- 如果两个参数都是右值引用,只有其中一个会被移动(具体哪一个未指定),另一个保持原值。
3. 参数与复杂度 (Parameters & Complexity)
lhs,rhs: 分别是运算符左侧和右侧的参数。- 注意:如果是
char*类型,必须指向以空字符(null-terminated)结尾的序列。
- 注意:如果是
- 复杂度 :
- 通常与结果字符串的长度成线性关系 O ( N + M ) O(N+M) O(N+M)。
- 优化 :对于包含右值引用的签名,复杂度仅与非移动 参数的长度成线性关系(因为移动操作本身接近 O ( 1 ) O(1) O(1))。
4. 安全性与异常 (Safety & Exceptions)
- 异常安全性 : 强保证 (Strong guarantee)。如果抛出异常(如内存分配失败),原有的字符串对象不会发生改变。
- 迭代器有效性: 涉及右值引用的签名可能会使与被移动字符串相关的迭代器、指针和引用失效。
- 数据竞争: 涉及右值引用的签名会修改被移动的字符串。
- 潜在错误 :
- 如果结果长度超过
max_size,抛出length_error。 - 如果内存不足,抛出
bad_alloc。 - 如果
char*指针没有以 null 结尾,会导致未定义行为。
- 如果结果长度超过
💡 核心总结 (Summary)
std::operator+ 是 C++ 中最自然的字符串拼接方式,适合用于表达式中。
1. 语法灵活性 (Syntactic Sugar)
它允许你自由混合 std::string、const char*(字符串字面量)和 char。
注意 :必须至少有一个操作数是
std::string对象。你不能写"Hello " + "World"(这是两个指针相加,是非法的),但可以写string("Hello ") + "World"。
2. C++11 的性能飞跃
在 C++98 中,a + b 总是意味着:分配新内存 -> 复制 a -> 复制 b -> 返回新对象。
在 C++11 中,如果 a 或 b 是临时对象(例如 str + "suffix"),编译器会使用移动语义。它会直接"窃取"临时对象的缓冲区,并在其后追加内容,避免了昂贵的内存重新分配和深拷贝。
3. 对比 operator+=
operator+:创建并返回新对象 。适合表达式,如string full = part1 + part2;。operator+=:修改当前对象 。适合追加模式,如str += " more";。- 一般建议 :如果你是在循环中拼接字符串,使用
+=通常比+更高效,因为它避免了产生大量的临时对象(尽管编译器优化可能缓解这一点)。
代码示例解析
cpp
// 示例展示了混合类型的拼接
hostname = "www." + secondlevel + '.' + firstlevel;
// 执行顺序(从左到右):
// 1. "www." (char*) + secondlevel (string) -> 产生临时 string A
// 2. A (temp string) + '.' (char) -> 产生临时 string B (C++11 会移动 A)
// 3. B (temp string) + firstlevel (string) -> 产生最终结果 (C++11 会移动 B)
relational operators (string)
这一部分虽然简单,但它是我们在 if 语句中最常用的字符串操作。在整理笔记时,建议将其与上一节的 compare 函数联系起来:运算符是 compare 的语法糖。
1. 中文翻译 (Translation)
std::string 关系运算符 (全局函数重载)
版本支持: C++98 / C++14 (增加了 noexcept)
函数原型 (简化版):
这些运算符定义在 <string> 头文件中,涵盖了所有 string 与 string、string 与 C-string 的组合。
cpp
(1)
bool operator== (const string& lhs, const string& rhs);
bool operator== (const char* lhs, const string& rhs);
bool operator== (const string& lhs, const char* rhs);
(2)
bool operator!= (const string& lhs, const string& rhs);
bool operator!= (const char* lhs, const string& rhs);
bool operator!= (const string& lhs, const char* rhs);
(3)
bool operator< (const string& lhs, const string& rhs);
bool operator< (const char* lhs, const string& rhs);
bool operator< (const string& lhs, const char* rhs);
(4)
bool operator<= (const string& lhs, const string& rhs);
bool operator<= (const char* lhs, const string& rhs);
bool operator<= (const string& lhs, const char* rhs);
(5)
bool operator> (const string& lhs, const string& rhs);
bool operator> (const char* lhs, const string& rhs);
bool operator> (const string& lhs, const char* rhs);
(6)
bool operator>= (const string& lhs, const string& rhs);
bool operator>= (const char* lhs, const string& rhs);
bool operator>= (const string& lhs, const char* rhs);
功能:字符串关系比较
在字符串对象 lhs(左操作数)和 rhs(右操作数)之间执行相应的比较操作。
这些函数内部通常调用 string::compare 来实现比较逻辑。
参数 (Parameters):
- lhs, rhs: 运算符左侧和右侧的参数。
- 如果参数类型是
char*,它必须指向一个以 null 结尾的字符序列(C 风格字符串)。
返回值 (Return Value):
如果条件成立返回 true,否则返回 false。
示例 (Example):
cpp
// string comparisons
#include <iostream>
#include <vector>
#include <string> // 记得包含这个
int main ()
{
std::string foo = "alpha";
std::string bar = "beta";
// 字典序比较:alpha 排在 beta 之前,所以 foo < bar
if (foo==bar) std::cout << "foo and bar are equal\n";
if (foo!=bar) std::cout << "foo and bar are not equal\n";
if (foo< bar) std::cout << "foo is less than bar\n";
if (foo> bar) std::cout << "foo is greater than bar\n";
if (foo<=bar) std::cout << "foo is less than or equal to bar\n";
if (foo>=bar) std::cout << "foo is greater than or equal to bar\n";
return 0;
}
输出:
text
foo and bar are not equal
foo is less than bar
foo is less than or equal to bar
复杂度与异常 (Complexity & Exceptions):
- 复杂度: 线性 O ( N ) O(N) O(N)(取决于较短字符串的长度)。
- 异常安全性:
- C++14 起:两个
string对象之间的比较保证不抛出异常 (noexcept)。 - 涉及
char*的比较:如果char*指针无效(如未以 null 结尾),会导致未定义行为。
- C++14 起:两个
2. 技术总结与核心考点 (Summary & Key Points)
虽然这些运算符用起来很简单,但在底层原理和性能优化上有一些值得记录的细节。
1. 字典序比较 (Lexicographical Comparison)
- 核心逻辑: 字符串的大小比较不是比"谁长",而是比"字典顺序"(ASCII 码值)。
- 规则: 从第一个字符开始逐个比较 ASCII 值。
"apple" < "banana"(因为 'a' < 'b')"apple" < "applepie"(前缀相同,越长的越大)"Apple" < "apple"(因为大写 'A' (65) < 小写 'a' (97)) ------ 注意大小写敏感性。
2. 与 compare() 的关系
- 语法糖:
operator<本质上就是对compare()函数的封装。s1 < s2等价于s1.compare(s2) < 0s1 == s2等价于s1.compare(s2) == 0
- 可读性优先: 在不需要三路比较结果(-1, 0, 1),只需要知道
true/false时,优先使用运算符 ,代码可读性远高于compare。
3. 性能优化细节 (operator== vs operator<)
operator==的优化: 标准库实现通常会先检查 长度 (size) 。- 如果
s1.size() != s2.size(),直接返回false。这是一个 O ( 1 ) O(1) O(1) 的快速失败检查。
- 如果
operator<的开销: 必须逐个字符比较(或者调用memcmp),直到发现不匹配的字符或遇到末尾。
4. 混合类型比较的便利性
标准库提供了重载,使得我们不需要手动构造临时对象就能比较:
cpp
std::string s = "hello";
// 高效:直接调用 operator==(const string&, const char*)
if (s == "hello") { ... }
// 低效:显式构造临时 string 对象(虽然现代编译器可能优化掉)
if (s == std::string("hello")) { ... }
std::swap (string)
"指针交换" vs "数据拷贝" 是核心考点。这是 C++ 高效处理大对象的基石之一。
1. 中文翻译 (Translation)
函数原型:
cpp
void swap (string& x, string& y);
功能:交换两个字符串的值
交换字符串对象 x 和 y 的值。调用此函数后,x 的值变为调用前 y 的值,而 y 的值变为调用前 x 的值。
这是泛型算法 std::swap 的一个重载版本 ,旨在通过互相转移内部数据的所有权 来提高性能(即:两个字符串交换指向其数据的引用/指针,而不是实际拷贝字符)。
它的行为等同于调用了成员函数 x.swap(y)。
参数 (Parameters):
- x, y : 需要交换的
string对象。
返回值 (Return Value):
无 (none)
示例 (Example):
cpp
// swap strings
#include <iostream>
#include <string>
main ()
{
std::string buyer ("money");
std::string seller ("goods");
std::cout << "Before the swap, buyer has " << buyer;
std::cout << " and seller has " << seller << '\n';
// 交换买家和卖家的内容
std::swap (buyer, seller);
std::cout << " After the swap, buyer has " << buyer;
std::cout << " and seller has " << seller << '\n';
return 0;
}
输出:
text
Before the swap, buyer has money and seller has goods
After the swap, buyer has goods and seller has money
复杂度与安全性 (Complexity & Safety):
- 复杂度: 常数时间 O ( 1 ) O(1) O(1)。
- 迭代器有效性: 与
x和y相关的任何迭代器、指针和引用都可能失效。 - 异常安全性: 无异常保证 (No-throw guarantee),此函数从不抛出异常。
2. 技术总结与核心考点 (Summary & Key Points)
std::swap 看似简单,却是 C++ 性能优化的关键点,特别是对于像 std::string 这样管理动态内存的类。
1. 性能核心:指针交换 (Pointer Swapping)
这是该函数存在的最大意义。
- 普通交换 (Naive Swap): 如果没有特化,交换两个字符串需要:
拷贝构造(temp = a)->赋值(a = b)->赋值(b = temp)。这涉及三次大规模的内存分配和字符拷贝, O ( N ) O(N) O(N)。 - std::swap (特化版):
std::string内部维护了指向堆内存的指针。swap仅仅是交换了这两个指针(以及大小、容量等元数据)。 - 结果: 无论字符串有一百万个字符还是一个字符,交换操作都在瞬间完成 ( O ( 1 ) O(1) O(1))。
2. 迭代器失效 (Iterator Invalidation)
这是一个高频面试陷阱。
- 虽然
vector::swap通常保证迭代器继续指向原来的元素(只是归属的容器变了),但string::swap会导致迭代器失效。 - 原因:SSO (Small String Optimization) 。现代 C++ 的 string 实现通常包含"短字符串优化"。
- 如果字符串很短(例如 < 15 字符),数据是直接存储在对象栈内存中的,而不是堆上。
- 交换时,数据被直接搬运到了另一个对象里。原来的迭代器指向的是旧对象的栈地址,现在那个地址里存放的是交换后的新数据,因此迭代器逻辑上失效了。
3. 全局 vs 成员函数
str1.swap(str2):成员函数。std::swap(str1, str2):非成员(全局)函数。- 最佳实践: 在编写泛型代码(模板)时,建议使用
std::swap。因为它会利用 ADL (Argument Dependent Lookup) 机制自动找到最高效的实现(即本特化版本)。
4. 异常安全性
标记为 noexcept。这意味着 swap 操作绝不会失败(因为它只涉及整数和指针的赋值,不涉及内存分配)。这也是为什么在实现赋值运算符 operator= 时,我们常使用 Copy-and-Swap Idiom 的原因。
std::operator>> (string)
这是 C++ 中最基础的输入方式,但在处理带空格的字符串 时,它是新手最容易"踩坑"的地方。重点标记其 "以空白为界" 的特性。
1. 中文翻译 (Translation)
函数原型:
cpp
istream& operator>> (istream& is, string& str);
功能:从流中提取字符串
从输入流 is 中提取一个字符串,并将其存储在 str 中。str 的原有内容会被覆盖(之前的字符被替换)。
该函数重载了 operator>>,使其行为类似于 C 风格字符串的输入,但应用于 string 对象。
提取的每个字符都会被追加到 string 中(就像调用了 push_back 一样)。
关键注意:
istream 的提取操作使用空白字符 (空格、换行、制表符等)作为分隔符。
因此,该操作只能从流中提取被视为 "单词" 的内容。
若要提取整行 文本(包含空格),请参阅全局函数 getline 的 string 重载版本。
参数 (Parameters):
- is : 输入流对象(如
std::cin),从中提取字符。 - str : 用于存储提取内容的
string对象。
返回值 (Return Value):
返回参数 is 本身(支持链式调用,如 cin >> a >> b;)。
状态标志 (State Flags):
以下情况可能会设置流的内部状态标志:
eofbit: 操作过程中到达了输入源的末尾。failbit: 获取的输入无法解释为该类型的有效文本表示(对于 string 很少见,除非流本身已损坏)。badbit: 发生了上述之外的错误(如 IO 错误)。
示例 (Example):
cpp
// extract to string
#include <iostream>
#include <string>
int main ()
{
std::string name;
std::cout << "Please, enter your name: ";
// 如果用户输入 "John Doe",只有 "John" 会被读入
// " Doe" 会残留在缓冲区中
std::cin >> name;
std::cout << "Hello, " << name << "!\n";
return 0;
}
复杂度与安全性 (Complexity & Safety):
- 复杂度: 未指定,但通常与结果字符串的长度成线性关系。
- 迭代器有效性: 与
str相关的任何迭代器、指针和引用都可能失效(因为涉及内存重分配)。 - 异常安全性: 基本保证 (Basic guarantee)。如果抛出异常,
is和str仍处于有效状态。
2. 技术总结与核心考点
std::cin >> str 是最常用的输入方式,但它的行为逻辑必须清晰理解,否则会导致"输入跳过"或"读取不全"的 Bug。
1. 空白字符陷阱 (The Whitespace Trap)
这是该函数最大的考点。
- 分隔符: 它视空格 (Space) 、制表符 (Tab) 、换行符 (Newline) 为结束标志。
- 自动跳过前导空白: 在读取有效字符前,它会自动忽略所有的前导空白(Skip Leading Whitespace)。
- 残留问题:
- 输入:
"Hello World\n" cin >> str;str变成"Hello"。- 关键: 空格
" World\n"仍然留在输入缓冲区(Buffer)里,等待下一次读取。
- 输入:
2. 覆盖行为 (Overwrite)
文档明确指出:str is overwritten。
- 这意味着你不需要在读取前手动调用
str.clear()。 - 每次
>>操作都会先清空str,再填入新内容。
3. 内存自动管理
与 C 语言的 scanf("%s", buffer) 相比,std::cin >> str 是安全的。
- C 语言: 必须确保
buffer足够大,否则缓冲区溢出。 - C++ string: 会根据输入长度自动
resize和分配内存,不会溢出。
4. 对比 std::getline
这是面试常问的对比:
| 特性 | std::cin >> str |
std::getline(std::cin, str) |
|---|---|---|
| 分隔符 | 任意空白字符 (空格/Tab/换行) | 仅 换行符 (\n) (或自定义字符) |
| 读取内容 | 一个"单词" | 一整"行" |
| 前导空白 | 自动跳过 | 不跳过 (保留行首空格) |
| 换行符处理 | 留在缓冲区 | 读取并丢弃 (Consumed but discarded) |
std::operator<< (string)
这是我们最熟悉的 "Print" 操作,虽然用起来最简单,但理解其链式调用 和格式化支持是写出优雅 C++ 代码的基础。
1. 中文翻译 (Translation)
函数原型:
cpp
ostream& operator<< (ostream& os, const string& str);
功能:将字符串插入到流
将符合 str 值的字符序列插入(输出)到输出流 os 中。
该函数重载了 operator<<,使其行为类似于 C 风格字符串在 ostream::operator<< 中的表现,但应用于 string 对象。
参数 (Parameters):
- os : 字符要被插入到的
ostream对象(如std::cout)。 - str : 包含要插入内容的
string对象。
返回值 (Return Value):
返回参数 os 本身。
(注:这允许连续的链式调用,例如 cout << s1 << s2;)
错误处理:
如果在输出操作期间发生错误,流的 badbit 标志会被设置。此时,如果通过 ios::exceptions 设置了相应的标志,则会抛出异常。
示例 (Example):
cpp
// inserting strings into output streams
#include <iostream>
#include <string>
main ()
{
std::string str = "Hello world!";
// 将字符串 str 和换行符插入到标准输出流
std::cout << str << '\n';
return 0;
}
复杂度与安全性 (Complexity & Safety):
- 复杂度: 未指定,但通常与
str的长度成线性关系 O ( N ) O(N) O(N)。 - 迭代器有效性: 无变化。
- 数据竞争:
os对象会被修改(写入数据)。 - 异常安全性: 基本保证 (Basic guarantee)。如果抛出异常,流对象
os和字符串str仍处于有效状态。
2. 技术总结与核心考点 (Summary & Key Points)
std::cout << str 是 C++ 程序员写的每一行代码的基础。
1. 为什么它存在? (The "Why")
在 C 语言中,我们必须用 printf("%s", str.c_str()) 来打印。
在 C++ 中,因为有了这个重载函数,我们可以直接把 string 对象"扔"给 cout。
- 本质: 它是一个非成员函数(全局函数),位于
<string>头文件中。它遍历string对象的所有字符,并逐个(或批量)调用os.rdbuf()->sputn写入流缓冲区。
2. 链式调用的原理 (Chaining)
为什么我们可以写 std::cout << "A" << "B" << std::endl;?
- 因为
operator<<的返回值是ostream&(即os的引用)。 - 执行流程:
(cout << "A")执行打印,并返回cout。- 代码变成
cout << "B" << endl; (cout << "B")执行打印,并返回cout。- 代码变成
cout << endl; - ...
3. 与 printf 的主要区别
-
类型安全: 不需要像
printf那样手动指定%s、%d,编译器会自动匹配重载函数。 -
格式化控制:
operator<<支持流操作符(Manipulators)。- 例如,要设置打印宽度和对齐:
cpp#include <iomanip> std::cout << std::setw(10) << std::left << str; // 输出: "Hello " (左对齐,占10位)
4. 对比 Input (>>)
这是一个对称的设计,但行为相反:
>>(Input): 遇到空白字符就停止。<<(Output): 遇到空白字符照常输出 。它会忠实地输出字符串中的每一个字节,包括空格、换行和\0(如果字符串中间包含了空字符,不过通常string不建议中间含\0)。=
std::getline (string)
这是 C++ 中读取整行文本 (包含空格)的标准方法,也是解决 cin >> str 无法读取带空格字符串问题的终极方案。在整理笔记时,请务必重点标记它与 cin >> 混合使用时的**"换行符残留"陷阱**。
1. 中文翻译 (Translation)
std::getline 全局函数
版本支持: C++98 / C++11 (支持右值引用重载)
函数重载原型:
cpp
// (1) 自定义分隔符
istream& getline (istream& is, string& str, char delim);
// (2) 默认分隔符 (换行符 '\n')
istream& getline (istream& is, string& str);
功能:从流中获取一行到字符串
从输入流 is 中提取字符并存储到 str 中,直到找到分隔符 delim(对于版本 (2),分隔符默认为换行符 \n)。
如果遇到文件结束符 (EOF) 或在输入操作期间发生其他错误,提取也会停止。
关键行为:
- 如果找到分隔符,它会被提取并丢弃 (即:它不会被存储在
str中,并且下一次输入操作将从它之后开始)。 - 注意:调用前
str中的任何内容都会被替换为新提取的序列。
参数 (Parameters):
- is : 输入流对象(如
std::cin或std::ifstream),从中提取字符。 - str : 用于存储提取行的
string对象。调用前的内容(如果有)会被丢弃并被替换。 - delim: 指定的分隔符。
返回值 (Return Value):
返回参数 is 本身(这允许在循环条件中直接使用,如 while(getline(...)))。
状态标志 (State Flags):
eofbit: 到达文件末尾。failbit: 未能提取到有效数据(极少见)或流已损坏。badbit: 发生了严重的 IO 错误。
示例 (Example):
cpp
// extract to string
#include <iostream>
#include <string>
int main ()
{
std::string name;
std::cout << "Please, enter your full name: ";
// 读取整行,允许包含空格,例如输入 "John Doe"
std::getline (std::cin, name);
std::cout << "Hello, " << name << "!\n";
return 0;
}
复杂度与安全性 (Complexity & Safety):
- 复杂度: 线性 O ( N ) O(N) O(N),取决于结果字符串的长度。
- 迭代器有效性:
str相关的迭代器可能失效(因为涉及内存重分配)。 - 数据竞争:
is和str都会被修改。 - 异常安全性: 基本保证。
2. 技术总结与核心考点 (Summary & Key Points)
std::getline 是 C++ 文本处理的基石,主要用于处理以行为单位的数据(如读取配置文件、CSV、或用户输入)。
1. 核心特性:吃掉分隔符 (Consume and Discard)
这是与 get() 或其他读取方法最大的区别:
- 读取: 它会从流中读取直到遇到
\n。 - 丢弃: 它把
\n从流缓冲区中拿走,但不放进str里。 - 结果: 流指针停在
\n的后面,准备读取下一行。str里面没有换行符。
2. 与 std::cin >> 的对比 (必考点)
| 特性 | std::cin >> str |
std::getline(std::cin, str) |
|---|---|---|
| 分隔符 | 空格、Tab、换行 | 仅换行符 \n (或自定义) |
| 空格处理 | 遇到空格就停止 | 读取并保存空格 |
| 换行符处理 | 留在缓冲区 (不读取) | 读取并丢弃 (Consumed) |
| 用途 | 读取单词 | 读取整行句子 |
3. 经典陷阱:混合使用的"幽灵空行"
这是新手最容易遇到的 Bug。
场景: 先用 cin >> 读一个数,再用 getline 读字符串。
cpp
int age;
string name;
cout << "Enter age: ";
cin >> age; // 假设输入 "18\n" -> 读走了 18,留下了 "\n"
cout << "Enter name: ";
getline(cin, name); // 灾难发生!
问题分析:
cin >> age读走了18,但在输入缓冲区里留下了用户敲的回车符\n。getline开始工作,一眼就看到了缓冲区开头的\n。getline认为"哦,这一行结束了",于是读取了一个空字符串,并消耗掉了那个\n。- 程序没有让用户输入名字就直接继续执行了。
解决方案:
在 getline 之前清除缓冲区中的换行符。
cpp
cin >> age;
cin.ignore(); // 忽略缓冲区中的下一个字符(通常是 \n)
getline(cin, name); // 现在正常了
4. 常见应用:按自定义分隔符读取
getline 不仅仅能按行读取,还能用来解析简单的格式,比如 CSV(逗号分隔值)。
cpp
std::string data = "name,age,score";
std::stringstream ss(data);
std::string segment;
// 按逗号分割读取
while(std::getline(ss, segment, ',')) {
std::cout << segment << endl;
}
// 输出:
// name
// age
// score
补充:std::sort
1. 核心翻译与功能总结
功能描述
cpp
default (1)
template <class RandomAccessIterator>
void sort (RandomAccessIterator first, RandomAccessIterator last);
custom (2)
template <class RandomAccessIterator, class Compare>
void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);
-
用途 :将指定范围
[first, last)内的元素按升序(从小到大)排列。 -
比较方式:
-
默认版 (1) :使用元素自带的
<运算符进行比较。 -
自定义版 (2) :使用传入的比较函数或对象
comp进行比较。 -
稳定性 :不稳定 (Not Guaranteed) 。这意味着如果两个元素相等(Equivalent),排序后它们的相对前后顺序可能会改变。(如果必须要保持原顺序,请使用
std::stable_sort)。
参数详解
first,last(迭代器):
-
定义了要排序的序列范围。包含
first指向的元素,但不包含last指向的元素(即左闭右开区间)。 -
关键要求 :必须是 RandomAccessIterator(随机访问迭代器)。
-
这意味着你可以对
std::vector、std::deque或普通数组使用std::sort。 -
不能 对
std::list(链表)使用此函数(链表迭代器只能逐个移动,无法随机访问)。 -
类型要求 :迭代器指向的元素类型必须支持 Swap(交换) ,且必须是 可移动构造 (Move-Constructible) 和 可移动赋值 (Move-Assignable) 的。
-
(注:这正好呼应了我们刚才讨论的:如果元素支持移动语义,排序速度会大幅提升。)
comp(比较器):
- 这是一个二元函数(接受两个参数),返回
bool。 - 规则 :返回
true表示第一个参数应该排在第二个参数前面。 - 它定义了一种"严格弱序"(Strict Weak Ordering)。简单理解就是:如果
comp(a,b)为 true,则a < b。
复杂度
- 时间复杂度:平均为 O(N \log N),其中 N 是元素数量。
- 操作:执行大约 N \log_2 N 次比较,以及最多同样数量的元素交换(swaps)或移动(moves)。
2. 代码示例解析
文档中的代码展示了三种不同的 sort 调用方式,非常经典:
- 默认排序 (使用
<):
cpp
// 仅排序前 4 个元素
std::sort (myvector.begin(), myvector.begin()+4);
// 结果: (12 32 45 71) 26 80 53 33 -> 括号内有序,后面不动
- 使用函数指针 (
myfunction):
cpp
bool myfunction (int i,int j) { return (i<j); }
// ...
std::sort (myvector.begin()+4, myvector.end(), myfunction);
// 对后半部分排序
- 使用函数对象/仿函数 (
myobject):
cpp
struct myclass {
bool operator() (int i,int j) { return (i<j);}
} myobject;
// ...
std::sort (myvector.begin(), myvector.end(), myobject);
// 全部重新排序
3. 深度解析:<algorithm> 中的细节
结合你之前关于 swap 和底层机制的兴趣,这里有几个文档中隐含的重要知识点:
A. 为什么强调 "Move-Constructible" (可移动构造)?
文档中特别提到:RandomAccessIterator shall point to a type... which is both move-constructible and move-assignable.
这是因为 std::sort 内部通常使用的是 快速排序 (QuickSort) 的变体(通常是 Introsort)。在排序过程中,算法需要频繁地调整元素位置。
- 如果在 C++98 时代,调整位置意味着大量的 Copy(深拷贝)。
- 在现代 C++ 中,算法会优先使用 Move(移动)。
- 如果你的对象很大(比如
std::vector<string>),但支持移动语义,std::sort就会极快(因为它只是交换内部指针,类似我们刚才讨论的swap)。 - 如果你的对象不支持移动(只有拷贝构造),
std::sort会被迫进行深拷贝,性能会显著下降。
B. 什么是 "Strict Weak Ordering" (严格弱序)?
这是写 comp 函数时最容易出错的地方。
如果 comp(a, b) 返回 true,意味着 a 必须在 b 前面。
陷阱 :绝对不能让 comp(a, b) 和 comp(b, a) 同时为 true,也不能在 a == b 时返回 true。
- 正确 :
return a < b; - 错误 :
return a <= b;(当 a 等于 b 时,这会导致逻辑崩溃,可能会导致程序死循环或崩溃)。
C. 数据竞争 (Data Races)
文档提到 The objects in the range [first,last) are modified.
这看似废话,但在多线程环境下很重要:**不要在两个线程中同时对同一个容器进行 sort**,也不要一边 sort 一边在另一个线程读取它,除非你加了锁。
D. 视觉化理解
std::sort 的底层通常是 Introsort(内省排序)。它结合了快速排序、堆排序和插入排序的优点。
- 快速排序 (QuickSort):用于在大规模数据上快速划分。
- 堆排序 (HeapSort):当快速排序递归太深(可能遇到最坏情况)时,切换到堆排序以保证 O(N \log N)。
- 插入排序 (InsertionSort):当数据块很小(例如少于 16 个元素)时,切换到插入排序,因为此时它比快排更高效。