
/--------------C++初阶---------------/
【 C++发展史、命名空间、输入输出、缺省参数、函数重载 】
目录:
[① string 类是什么?](#① string 类是什么?)
[② string 类的核心特点](#② string 类的核心特点)
[③ 使用前的准备](#③ 使用前的准备)
[① 构造函数](#① 构造函数)
[<1> 子串构造](#<1> 子串构造)
[<2> 截取 C 串前 n 个字符构造](#<2> 截取 C 串前 n 个字符构造)
[<3> 重复字符填充构造](#<3> 重复字符填充构造)
[<4> 迭代器范围构造](#<4> 迭代器范围构造)
[<5> 单参数构造的隐式类型转换](#<5> 单参数构造的隐式类型转换)
[② string类对象的访问及遍历操作](#② string类对象的访问及遍历操作)
[<1> operator[]](#<1> operator[])
[<2> begin+ end](#<2> begin+ end)
[<3> rbegin + rend](#<3> rbegin + rend)
[③ string类对象的容量操作](#③ string类对象的容量操作)
[<1> size](#<1> size)
[<2> length](#<2> length)
[<3> capacity](#<3> capacity)
[<4> empty](#<4> empty)
[<5> clear](#<5> clear)
[<6> reserve(重要)](#<6> reserve(重要))
【问题】:string对象在容量不足时,不是会自动扩容,为啥会设计个扩容接口呢?
[<7> resize(重要)](#<7> resize(重要))
[<1> push_back](#<1> push_back)
[<2> append](#<2> append)
[<3> operator+=(重要)](#<3> operator+=(重要))
[<4> c_str](#<4> c_str)
[<5> find(重点)](#<5> find(重点))
[<6> rfind](#<6> rfind)
[<7> substr](#<7> substr)
[【find+substr 小案例:分割网址】](#【find+substr 小案例:分割网址】)
[【+= 小案例:所有空格替换成%20】](#【+= 小案例:所有空格替换成%20】)
[<8> insert](#<8> insert)
[<9> erase](#<9> erase)
[<1> operator+](#<1> operator+)
[<2> operator>>](#<2> operator>>)
[<3> getline](#<3> getline)
[<4> operator<<](#<4> operator<<)
[<5> relational operators](#<5> relational operators)
[○ vs 中string的结构](#○ vs 中string的结构)
[○ g++ 下string的结构](#○ g++ 下string的结构)
STL string 是 C++ 字符串处理的核心工具,它彻底摆脱了 char * 手动管理内存的繁琐与风险,同时也是开发场景与面试考察的高频重点。本文将围绕其核心概念展开介绍,并聚焦实际开发中最常用的接口用法,结合代码案例帮你快速掌握 string 的日常使用技巧。
Ⅰ、string介绍
① string 类是什么?
C++ 中string是专门表示字符串的类,本质是basic_string模板类以char实例化后的别名,底层定义为:
cpp
typedef basic_string<char> string;
② string 类的核心特点
接口兼容 + 专属扩展它的接口和 C++ 标准容器(比如数组)类似,但额外加了很多字符串专属操作(比如拼接、查找子串),用起来更贴合字符串场景。
单字节字符限制注意:
string是按字节处理数据的,不适合操作多字节 / 变长字符(比如 UTF-8 编码的中文、 emoji(比如😀、😂、👍))------ 这类场景下,它的长度、迭代器等功能会按字节计算,而不是实际的字符数。

也就是说上述字符每个占2个字节,当然具体的根据编码不同,所占字节数会有所不同,但是ASCLL中的所有字符都只占一个字节
③ 使用前的准备
用string类时,代码里必须加这两句:
cpp
#include <string> // 包含头文件
using namespace std; // 引入std命名空间
Ⅱ、string常用接口的介绍和使用
① 构造函数
| (constructor) 函数名称 | 功能说明 |
|---|---|
| string ()(无参默认构造) | 构造空的 string 类对象,即空字符串 |
| string (const string& str)(拷贝构造) | 拷贝构造函数,用已有的 string 对象构造新对象 |
| string (const string& str, size_t pos, size_t len = npos)(子串构造) | 从已有 string 对象的 pos 位置开始,截取 len 个字符构造新对象(len 默认取到末尾) |
| string (const char* s)(C 风格字符串构造) | 用 C 风格字符串(char*)构造 string 类对象 |
| string (const char* s, size_t n)(截取 C 串前 n 个字符构造) | 用 C 风格字符串的前 n 个字符构造 string 类对象 |
| string (size_t n, char c)(重复字符填充构造) | 构造包含 n 个字符 c 的 string 类对象 |
| template <class InputIterator>string (InputIterator first, InputIterator last)(迭代器范围构造) | 用迭代器 [first, last) 区间内的字符构造 string 类对象 |
简单的我就不详细介绍了,我只介绍几个复杂一点的:
<1> 子串构造

*string (const char* s)*用于通过 C 语言风格的字符串构造 string 对象;string (const string& str, size_t pos, size_t len = npos) 则是从已有 string 对象对应的字符串中,截取从 pos 下标开始的 len 个字符来构造新的 string 对象。
参数规则:
- 若 pos 超过字符串的最大下标,会触发断言错误;
- 若原字符串 str 长度不足,或 len 取值为
string::npos,则会从 pos 位置开始复制到字符串末尾。
pos越界:

使用npos缺省值:

【小测试】:string对应字符串后面有没有 '\0'

运行后控制台无输出,且未触发断言警告。
这个现象可以说明:string 内部会在存储的字符串末尾维护\0,但由于\0是不可打印字符,所以输出时不会显示;而我们从原字符串的末尾位置(下标 11)截取字符时,刚好取到了这个\0,因此控制台没有打印出可见内容。
【npos成员变量】

string::npos 定义为 -1,在赋值给 size_t 类型时,触发有符号 int 到无符号 size_t 的算数转换 ,-1的补码(全 1)被解释为size_t的最大值。
- 若平台是 32 位系统,
size_t是 32 位无符号整数,此时npos的值为2³² - 1;- 若平台是 64 位系统,
size_t是 64 位无符号整数,此时npos的值为2⁶⁴ - 1。
我之所以能直接用cout打印string对象,是因为标准库中为string重载了<<(配合cout)和>>(配合cin)这两个输入输出运算符(具体接口介绍后面讲)。
<2> 截取 C 串前 n 个字符构造

<3> 重复字符填充构造

<4> 迭代器范围构造
核心用法(先会用,细节后面讲):
用两个类似指针 的迭代器,划定原字符串的字符范围,构造新string。
- ***
s.begin():***类比 "指向字符串第一个字符的指针";- ***
s.end():***类比 "指向字符串最后一个字符的下一个位置的指针";- 迭代器支持
+/-偏移 (如s.begin() + 5),就像指针偏移一样。

这里有同学就会有疑问了,如果向指针那样去使用,s.begin()+5不应该指向空格吗,为啥没打印空格呢?
这是因为容器迭代器构造均遵循左闭右开规则:
begin()+5是终止标记,仅拷贝[begin(), begin()+5)区间内的元素(共 5 个),不会访问终止标记指向的位置,因此只输出 5 个字符。
<5> 单参数构造的隐式类型转换
单参数构造函数支持隐式类型转换

如果想禁止这种隐式转换,可以给单参数构造函数加explicit关键字(比如explicit string(const char* s)),这样就不能直接用string s = "张三",得显式写string s("张三")啦~
② string类对象的访问及遍历操作
<1> operator[]

功能: 返回pos位置的字符

<2> begin+ end


功能: begin 获取第一个字符的迭代器 + end 获取最后一个字符下一个位置的迭代器
迭代器是 C++ 容器(如 string、vector、map)的 "通用访问工具",本质是封装了指针的对象,作用是:✅ 遍历容器中的元素(读 / 写);✅ 屏蔽不同容器的底层实现差异(比如数组、链表遍历方式不同,但迭代器用法统一)。
核心特点:
- 用法像指针:支持
*it(取元素)、++it(移动)、it->(取成员)等操作;- 分类适配场景:比如
begin()返回指向第一个元素的迭代器 ,end()返回 "最后一个元素的下一个位置的迭代器";- 注意失效:容器扩容 / 缩容 / 删除元素时,迭代器可能失效(指向无效内存),需重新获取(后面实现时讲解)。
实际上在string这个容器中迭代器就是指针,begin返回的是指向第一个字符的指针,end返回的是指向最后一个字符下一个位置的指针,这是因为string容器存储的是字符串,可以直接通过指针对字符串进行接引用操作等

<3> rbegin + rend


***功能:***和begin/end的区别是,rbegin/rend是反向遍历,rbegin指向的是最后一个数据的位置,rend指向的是第一个元素

<4> const迭代器
这个和上述介绍的迭代器没本质区别,不同的就是const迭代器指向的对象不能被修改

③ string类对象的容量操作
<1> size
功能: 返回字符串有效大小(不包含末尾的\0)


<2> length

功能: 返回字符串有效长度(不包含末尾的\0)
注:length和size的功能完全一致 (返回结果相同),只是命名来源不同 ------length是早期为字符串设计的接口,size是后续为了统一所有容器(如vector)的接口风格新增的,二者在string中是等价的。

<3> capacity

功能: 返回空间总大小

string 的自动扩容规则在插入数据且当前空间不足时,string 会自动扩容,不同编译器的扩容策略不同:
- *VS 环境下:*第一次扩容为当前容量的 2 倍,后续扩容为当前容量的 1.5 倍;
- *g++ 环境下:*每次扩容均为当前容量的 2 倍。
<4> empty

功能: 检测字符串是否为空串,是返回true,否则返回false

<5> clear

功能: 清空有效字符

<6> reserve(重要)

***功能:***提前给字符串分配好指定大小的内存空间(不会改变size)
【场景1】:reverse要求100个字节空间,但却开辟了111个字节空间,为啥呢?

这是 VS 编译器的内存对齐 / 容量对齐策略 导致的:
reserve(100)只是要求 "至少 100 字节",VS 会在满足需求的基础上,额外多分配一些空间(比如按特定规则对齐),所以实际开辟了 111 字节。核心:reserve(n)保证容量≥n,具体大小由编译器的内存管理策略决定。
【场景2】:reserve 请求空间小于当前容量时的容量变化(缩容)

当
reserve(n)中n小于当前字符串容量时:标准将 "缩小容量" 定义为非约束请求------ 编译器 / 容器实现可以自主决定是否缩容,但通常会选择 "不缩容"(比如本示例中当前容量 15>请求的 10,容量保持 15 不变),最终保证容量不小于n即可。
【场景3】:clear+reserve 组合操作下的缩容行为

在最新版vs中,即使先clear()再reserve(n < 当前容量),reserve也不会触发缩容,字符串容量会保持原大小。
但是在老版本中,先clear()再reserve(n < 当前容量),reserve也会触发缩容
reserve在新版 VS 里的行为:
- ✅ 当 n > 当前容量:正常扩容(满足容量≥n);
- ❌ 当 n < 当前容量:完全不缩容,容量保持原样(仅保证不低于 n,但不会主动减小)。
***【问题】:***string对象在容量不足时,不是会自动扩容,为啥会设计个扩容接口呢?
当能提前确定字符串所需空间大小时,直接用
reserve(n)一次性分配好n大小的内存,就能避免后续添加字符时因空间不足触发的多次自动扩容(每次扩容都会涉及内存重新申请和数据拷贝),从而减少性能损耗,提升程序运行效率。
<7> resize(重要)

功能: 将有效字符的个数改成n个,多出的空间用 "字符c" 或者 "/0" 填充
**【场景1】:**n > oldsize


字符串会被扩展到 n 个字符,新增的位置默认填充 '\0'(空字符);如果显式传第二个参数(比如 resize(n, 'a')),则用指定字符填充。
【场景2】: n < oldsize

字符串会被截断,只保留前 n 个字符,超出部分直接丢弃(不可逆)。
Ⅲ、string类对象的修改操作
<1> push_back

功能: 在字符串后尾插字符c

<2> append

功能: 在字符串后追加一个字符串

<3> operator+=(重要)
前面这两个接口虽说用着还行,但我感觉都不如这个接口好用

***【功能】:***在字符串后追加字符串str

我觉得后面这两种重载函数其实没必要单独实现 ------ 第一个重载已经能兼容后两种场景了:不管传单个字符还是 C 风格字符串,都能通过单参数构造函数的隐式类型转换,自动生成对应的 string 对象来参与追加操作。
<4> c_str

***功能:***返回C格式的字符串

<5> find(重点)

功能: 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置

<6> rfind

***功能:***从指定位置开始,从后往前搜索目标内容在字符串中最后一次出现的位置

<7> substr

功能: 在str中从pos位置开始,截取n个字符,然后将其返回

【find+substr 小案例:分割网址】
cpp
int main()
{
string url = "ftp://www.baidu.com/?tn=65081411_1_oem_dg";
// http://www.baidu.com/?tn=65081411_1_oem_dg
// https://legacy.cplusplus.com/reference/string/string/
// 协议 域名 资源名
size_t pos1 = url.find("://");
//协议
string protocol;
if (pos1 != string::npos)
{
protocol = url.substr(0, pos1);
}
cout << protocol << endl;
//域名
string domain;
//资源名
string uri;
size_t pos2 = url.find('/', pos1 + 3);
if (pos2 != string::npos)
{
domain = url.substr(pos1 + 3, pos2 - (pos1 + 3));
uri = url.substr(pos2 + 1);
}
cout << domain << endl;
cout << uri << endl;
return 0;
}
【+= 小案例:所有空格替换成%20】
cpp
int main()
{
// 所有空格替换成20%
string s2("hello world hello bit");
string s3;
for (auto ch : s2)
{
if (ch != ' ')
{
s3 += ch;
}
else
{
s3 += "20%";
}
}
cout << s3;
return 0;
}
<8> insert

***功能:***任意位置插入内容


对于非法插入操作,程序一般会抛出异常来提示错误。不过我现在还没学到异常处理的相关内容,所以这里就不写异常捕获的代码了(之前用到的几个插入接口,遇到非法插入时也会以抛异常的方式报错)。
<9> erase

***功能:***删除字符串中任意位置的内容

这个接口用起来确实挺灵活,但得谨慎使用 ------ 它底层的效率比较低,会涉及大量的数据移动操作。
Ⅳ、string类非成员函数
<1> operator+

***功能:***拼接字符串(不会修改原字符串)

尽量少用,因为传值返回,导致深拷贝效率低
<2> operator>>

***功能:***流提取运算符重载

这个时候就有疑问了,为啥只打印出了前半段呢?
这是因为cin在从缓冲区读取字符串时,通常以换行和空格为结束标识,所以,只读取到hello
<3> getline
如果想读取包含空格的整行字符串,得用getline(cin, s)

***功能:***获取一行字符串(和>>不同的是,它仅仅以换行为结束标识)

<4> operator<<

***功能:***流插入运算符重载

<5> relational operators

功能: 大小比较


如果条件成立则返回真,否则返回假
以上是一些重要接口的使用案例,还有几个常用接口我暂未展开,等下篇讲解具体实现时,我会再详细介绍!!!
Ⅴ、vs和g++下string结构的说明
注意:下述结构是在 32 位平台下验证的(32 位平台中指针占 4 个字节)。
○ vs 中string的结构
vs 的string对象总占28 个字节 ,内部结构包含一个联合体(用于存储字符串),逻辑如下:
cpp
union _Bxty
{ // storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
- 当字符串长度小于 16时:直接用内部固定的字符数组(联合体中的
_Buf)存放,无需额外开辟堆空间,效率更高;- 当字符串长度大于等于 16时:从堆上开辟空间(用联合体中的指针
_Ptr指向堆内存)。
除此之外,还包含 3 个字段:
size_t字段:保存字符串的长度;size_t字段:保存堆空间的总容量;- 一个指针:用于其他辅助逻辑。
这几部分的内存占用计算:联合体(16 字节) + 长度(4 字节) + 容量(4 字节) + 指针(4 字节) = 28 字节。

○ g++ 下string的结构
g++ 的string是通过写时拷贝实现的,string对象本身只占4 个字节(仅包含一个指针)。
这个指针会指向一块堆空间,堆空间内部包含以下字段:
- 空间总大小(
_M_capacity); - 字符串有效长度(
_M_length); - 引用计数(
_M_refcount);
cpp
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
- 指向实际存储字符串的指针。
