C++从入门到实战(十六)String(中)String的常用接口(构造接口,析构接口,迭代器,遍历修改,容量管理与数据访问)
C++从入门到实战(十六)String(中)详细讲解String的常用接口
- 前言
- 一、std::string
- 二、string的构造接口
- 1. 默认构造函数:创建空字符串
- 2. 拷贝构造函数:复制已有字符串
- 3. 从已有字符串截取部分
- 4. 用C风格字符串构造
- 5. 用C风格字符串的前n个字符构造
- 6. 用重复字符构造
- 7. 用迭代器构造(了解即可)
- 三、string的赋值与析构
- 1. 析构函数 ~string()
- 2. 赋值运算符 operator=
- 2.1 用另一个 string 对象赋值(常用)
- 2.2 用 C 风格字符串赋值(了解即可)
- 2.3 用单个字符赋值
- 四、string::iterator迭代器(重点掌握)
- 1. 为什么需要迭代器?
- 2. 迭代器的基本使用(像指针一样操作)
- 3. 迭代器的4种类型
- 4. 迭代器的边界
- 5. 修改字符串(通过迭代器写操作)
- 6. 迭代器与STL算法结合
- 7. 安全注意事项
- 五、string的遍历与修改
- 1. string的遍历(逐个访问字符)
- 2. string的修改(增删改查)
- 3. string的底层(了解即可)
- 六、String里的Capacity(容量管理)
- 1. size() 和 length()(获取字符串长度)
- 2. max_size()(最大容量)
- 3. resize(n) 和 resize(n, c)(调整大小)
- 4. capacity()(当前分配的容量)
- 5. reserve(n)(预分配容量)
- 6. clear()(清空字符串)
- 7. empty()(判断是否为空)
- 8. shrink_to_fit()(缩容)
- 总结
- 底层原理(了解即可)
- 八、string数据访问
- 1. operator[](下标访问)
- 2. at() 方法(安全访问)
- 3. back()(访问最后一个字符)
- 4. front()(访问第一个字符)
前言
- 在上一篇博客中,我们深入探讨了 STL 与 String 的内在联系,解析了 string 类的设计初衷以及它在 C++ 编程中的重要价值。
- 通过剖析 string 类在内存管理、字符操作和 STL 容器适配等方面的独特优势,我们理解了它作为 C++ 标准字符串处理工具的核心地位。
- 本篇博客将进入 string 的实战环节,重点解析 string 类的常用接口
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482
C++string的官方讲解网站
https://cplusplus.com/reference/string/string/?kw=string
一、std::string
typedef basic_string<char> string;
std::string
是 C++ 标准库中用于处理字符串的类,它让我们可以像操作普通变量一样方便地处理文本数据。
-
简单来说,string 就是 C++ 为程序员提供的「字符串神器」,不用再手动管理内存,也不用操心字符串长度,各种常用操作都已经帮你写好了。
-
string的接口有很多,我们先来看一看他的接口有哪些,后面会挨个详细讲
接口名 | 功能简述 |
---|---|
构造函数 | 创建字符串对象 |
析构函数 | 销毁字符串对象 |
operator= | 字符串赋值操作 |
迭代器 | |
begin() | 返回指向字符串起始位置的迭代器 |
end() | 返回指向字符串末尾(‘\0’ 之后)的迭代器 |
rbegin() | 返回反向迭代器,指向字符串末尾 |
rend() | 返回反向迭代器,指向字符串起始位置之前 |
cbegin() | 返回 const 迭代器,指向字符串起始位置 |
cend() | 返回 const 迭代器,指向字符串末尾 |
crbegin() | 返回 const 反向迭代器,指向字符串末尾 |
crend() | 返回 const 反向迭代器,指向字符串起始位置之前 |
容量操作 | |
size() / length() | 返回字符串长度(不包含终止符) |
max_size() | 返回字符串的最大可能长度 |
resize(n) | 调整字符串长度为 n ,超出部分截断,不足部分填充默认字符 |
capacity() | 返回当前分配的存储空间大小 |
reserve(n) | 请求至少能容纳 n 个字符的存储空间(可能扩大 capacity) |
clear() | 清空字符串内容 |
empty() | 判断字符串是否为空 |
shrink_to_fit() | 释放未使用的内存,使 capacity 等于 size |
元素访问 | |
operator[] | 通过下标访问字符(不检查越界) |
at() | 通过下标访问字符(越界时抛出异常) |
back() | 访问最后一个字符 |
front() | 访问第一个字符 |
修改操作 | |
operator+= | 追加内容到字符串末尾 |
append() | 追加内容到字符串末尾 |
push_back() | 追加单个字符到字符串末尾 |
assign() | 赋值新内容给字符串 |
insert() | 在指定位置插入内容 |
erase() | 删除指定位置的字符或子串 |
replace() | 替换指定位置的子串 |
swap() | 交换两个字符串的内容 |
pop_back() | 删除最后一个字符 |
字符串操作 | |
c_str() | 返回 C 风格字符串(以 ‘\0’ 结尾的 const char*) |
data() | 返回字符串数据的指针(C++11 起与 c_str() 相同) |
get_allocator() | 返回用于内存分配的 allocator 对象 |
copy() | 将字符串内容复制到字符数组中 |
find() | 查找子串或字符首次出现的位置 |
rfind() | 从后向前查找子串或字符首次出现的位置 |
find_first_of() | 查找字符串中第一个与指定字符集中任何字符匹配的位置 |
find_last_of() | 从后向前查找字符串中第一个与指定字符集中任何字符匹配的位置 |
find_first_not_of() | 查找字符串中第一个不与指定字符集中任何字符匹配的位置 |
find_last_not_of() | 从后向前查找字符串中第一个不与指定字符集中任何字符匹配的位置 |
substr() | 返回子串(从指定位置开始的指定长度的子串) |
compare() | 比较两个字符串的大小(字典序) |
二、string的构造接口
string
类提供了多种构造函数,让你可以用不同的方式创建字符串对象。下面是最常用的几种构造方式。
1. 默认构造函数:创建空字符串
string();
作用:创建一个空的字符串对象,不包含任何字符。
例子:
string s; // 创建空字符串
cout << s << endl; // 输出空行
2. 拷贝构造函数:复制已有字符串
string (const string& str);
作用:用一个已有的 string
对象创建新的字符串,内容完全相同。
例子:
string original = "hello";
string copy(original); // 拷贝构造
cout << copy << endl; // 输出: hello
3. 从已有字符串截取部分
string (const string& str, size_t pos, size_t len = npos);
作用:从 str
的 pos
位置开始,截取 len
个字符作为新字符串。
关键参数:
pos
:起始位置(从0开始)。len = npos
:npos
是一个特殊值,表示「直到字符串末尾」。
例子:
string original = "hello world";
string substr1(original, 6); // 从位置6开始到末尾,输出: world
string substr2(original, 0, 5); // 从位置0开始取5个字符,输出: hello
4. 用C风格字符串构造
string (const char* s);
作用:把 C 风格的字符串(以 '\0'
结尾的字符数组)转换为 string
对象。
例子:
const char* c_str = "hello";
string s(c_str); // 用C字符串构造
cout << s << endl; // 输出: hello
5. 用C风格字符串的前n个字符构造
string (const char* s, size_t n);
作用:取 C 风格字符串的前 n
个字符(不要求以 '\0'
结尾)。
例子:
const char* c_str = "abcdef";
string s(c_str, 3); // 取前3个字符
cout << s << endl; // 输出: abc
6. 用重复字符构造
string (size_t n, char c);
作用:创建包含 n
个重复字符 c
的字符串。
例子:
string s(5, 'a'); // 创建5个'a'的字符串
cout << s << endl; // 输出: aaaaa
7. 用迭代器构造(了解即可)
template <class InputIterator>
string (InputIterator first, InputIterator last);
作用:用迭代器范围内的字符构造字符串,常用于从其他容器(如 vector<char>
)创建字符串。
三、string的赋值与析构
1. 析构函数 ~string()
每个 string
对象在生命周期结束时,会自动调用析构函数,释放占用的内存。你不需要手动操作,C++ 会帮你搞定。
例子:
{string s = "hello"; // 创建对象
} // 离开作用域,s 自动销毁,内存自动释放
2. 赋值运算符 operator=
2.1 用另一个 string 对象赋值(常用)
string& operator= (const string& str);
作用:把一个 string
对象的内容复制到另一个 string
对象。
例子:
string s1 = "hello";
string s2 = "world";
s1 = s2; // s1 现在是 "world"
cout << s1 << endl; // 输出: world
2.2 用 C 风格字符串赋值(了解即可)
string& operator= (const char* s);
作用:把 C 风格字符串(如 "hello"
)赋值给 string
对象。
例子:
string s;
s = "hello"; // 等价于 string s = "hello";
cout << s << endl; // 输出: hello
2.3 用单个字符赋值
string& operator= (char c);
作用:让 string
对象只包含一个字符。
例子:
string s;
s = 'A'; // s 现在是 "A"
cout << s << endl; // 输出: A
四、string::iterator迭代器(重点掌握)
1. 为什么需要迭代器?
迭代器是C++中访问容器(如string、vector)元素的通用方式,它让我们可以:
- 统一语法:用相同的方式遍历不同类型的容器。
- 支持算法:STL算法(如
find
、sort
)依赖迭代器工作。 - 隐藏底层细节:无需关心容器内部如何存储数据。
类比:迭代器就像“容器的指针”,让你可以逐个访问元素。
2. 迭代器的基本使用(像指针一样操作)
定义迭代器:
string::iterator it; // 正向迭代器(从前往后)
string::reverse_iterator rit; // 反向迭代器(从后往前)
遍历字符串:
#include <iostream>
#include <string>
using namespace std;
int main()
{string s = "hello";// 正向遍历(从前往后)
for (string::iterator it = s.begin(); it != s.end(); ++it) {cout << *it; // *it 是当前字符,输出: hello
}cout << endl;
// 反向遍历(从后往前)
for (string::reverse_iterator rit = s.rbegin(); rit != s.rend(); ++rit) {cout << *rit; // 输出: olleh
}return 0;
}
简化写法(用auto
自动推导类型):
for (auto it = s.begin(); it != s.end(); ++it) {cout << *it; // 效果同上
}
3. 迭代器的4种类型
类型 | 功能 | 例子 |
---|---|---|
iterator | 正向遍历,可读写 | auto it = s.begin(); |
const_iterator | 正向遍历,只读(用于常量字符串) | const string cs = "hi"; auto cit = cs.begin(); |
reverse_iterator | 反向遍历,可读写 | auto rit = s.rbegin(); |
const_reverse_iterator | 反向遍历,只读 | auto crit = cs.rbegin(); |
4. 迭代器的边界
s.begin()
:指向第一个字符(如'h'
in"hello"
)。s.end()
:指向最后一个字符的下一个位置(即字符串结束标记'\0'
的位置),不包含该字符。s.rbegin()
:指向最后一个字符(如'o'
in"hello"
)。s.rend()
:指向第一个字符的前一个位置(反向的结束标记)。
为什么不访问'\0'
?
因为迭代器设计为只访问有效字符,而 '\0'
是C风格字符串的结束标记,string
类用 size()
明确记录长度,不需要 '\0'
。
5. 修改字符串(通过迭代器写操作)
string s = "hello";
for (auto it = s.begin(); it != s.end(); ++it) {*it = toupper(*it); // 转为大写
}
cout << s; // 输出: HELLO
6. 迭代器与STL算法结合
例子1:查找字符
string s = "hello";
auto it = find(s.begin(), s.end(), 'l');
if (it != s.end()) {cout << "找到 'l' 在位置: " << it - s.begin() << endl; // 输出: 2
}
例子2:反转字符串
string s = "hello";
reverse(s.begin(), s.end()); // 直接反转
cout << s; // 输出: olleh
7. 安全注意事项
- 迭代器失效:修改字符串可能导致迭代器失效,例如:
正确做法:for (auto it = s.begin(); it != s.end(); ) {if (*it == 'l') {s.erase(it); // 错误!删除元素后迭代器失效} else {++it;} }
erase()
返回下一个有效迭代器:for (auto it = s.begin(); it != s.end(); ) {if (*it == 'l') {it = s.erase(it); // 正确!更新迭代器} else {++it;} }
五、string的遍历与修改
1. string的遍历(逐个访问字符)
方法1:用下标 []
(最常用)
类似数组,用 s[i]
访问第 i
个字符(索引从0开始)。
例子:
string s = "hello";
for (size_t i = 0; i < s.size(); i++) {cout << s[i] << " "; // 输出: h e l l o
}
方法2:用迭代器(适合STL算法)
迭代器像「指针」,指向字符串中的字符。
例子:
string s = "hello";
for (auto it = s.begin(); it != s.end(); ++it) {cout << *it << " "; // 输出: h e l l o
}
方法3:范围for循环(C++11起,最简单)
直接遍历每个字符,无需索引。
例子:
string s = "hello";
for (char c : s) {cout << c << " ";
}
2. string的修改(增删改查)
修改单个字符
用下标或迭代器直接修改。
例子:
string s = "hello";
s[0] = 'H'; // s 变为 "Hello"
s.back() = '!'; // 修改最后一个字符,s 变为 "Hello!"
追加内容
用 +=
或 append()
方法。
例子:
string s = "hello";
s += " world"; // s 变为 "hello world"
s.append("!"); // s 变为 "hello world!"
插入字符
用 insert()
方法在指定位置插入。
例子:
string s = "hello";
s.insert(1, "X"); // 在位置1插入 'X',s 变为 "hXello"
删除字符
用 erase()
方法删除指定位置或范围。
例子:
string s = "hello";
s.erase(1, 2); // 从位置1开始删除2个字符,s 变为 "heo"
替换内容
用 replace()
方法替换指定范围的字符。
例子:
string s = "hello";
s.replace(1, 3, "XXX"); // 替换位置1~3的字符,s 变为 "hXXXo"
3. string的底层(了解即可)
string
的底层是一个动态字符数组,类似 char*
,但会自动管理内存:
- 自动扩容:当字符串变长时,
string
会自动申请更大的内存,并复制原有内容。 - 内存优化:现代C++对短字符串有特殊优化(SSO,小字符串优化),直接存储在对象内部,避免堆内存分配。
例子(帮助理解底层):
string s = "a"; // 可能直接存对象内部(SSO)
s += "bcdef"; // 内容太长,转为堆内存存储
注意事项
- 索引越界:访问
s[i]
时,确保i < s.size()
,否则会导致未定义行为(比如崩溃)。 - 迭代器失效:修改字符串可能导致迭代器失效,比如:
for (auto it = s.begin(); it != s.end(); ++it) {s.erase(it); // 错误!删除元素后迭代器失效 }
- 性能提示:频繁插入/删除可能导致内存频繁重新分配,性能较低。
六、String里的Capacity(容量管理)
1. size() 和 length()(获取字符串长度)
- 作用:返回字符串中实际字符的数量(不包含结尾的
'\0'
)。 - 区别:
size()
是所有容器(如vector
)通用的接口,length()
是专门为字符串设计的,两者功能完全相同。
例子:
string s = "hello";
cout << s.size() << endl; // 输出: 5
cout << s.length() << endl; // 输出: 5(和size()一样)
2. max_size()(最大容量)
作用:返回当前系统中 string
对象能存储的最大字符数(理论上限)。
注意:实际中几乎不可能达到这个值,因为内存会先耗尽。
例子:
string s;
cout << s.max_size() << endl; // 输出一个很大的数
3. resize(n) 和 resize(n, c)(调整大小)
作用:调整字符串长度为 n
:
- 若
n
小于当前长度,截断多余字符。 - 若
n
大于当前长度,用字符c
填充(默认用'\0'
填充)。
例子:
string s = "hello";
s.resize(3); // s 变为 "hel"
s.resize(5, '!'); // s 变为 "hel!!"
4. capacity()(当前分配的容量)
作用:返回字符串当前分配的内存能容纳的最大字符数(不包含 '\0'
)。
特点:容量通常大于等于实际长度,避免频繁重新分配内存。
例子:
string s = "hello";
cout << s.size() << endl; // 输出: 5(实际长度)
cout << s.capacity() << endl; // 输出: 15(不同系统可能不同)
5. reserve(n)(预分配容量)
作用:提前分配至少能容纳 n
个字符的内存,减少后续扩容次数。
例子:
string s;
s.reserve(100); // 预先分配100个字符的空间
cout << s.capacity() << endl; // 输出: 100 或更大(取决于实现)
6. clear()(清空字符串)
作用:清空字符串内容,使其长度为0,但不释放内存(容量不变)。
例子:
string s = "hello";
s.clear();
cout << s.size() << endl; // 输出: 0
cout << s.capacity() << endl; // 输出: 15(和clear前一样)
7. empty()(判断是否为空)
作用:检查字符串是否为空(长度是否为0)。
例子:
string s = "hello";
cout << s.empty() << endl; // 输出: 0(false)s.clear();
cout << s.empty() << endl; // 输出: 1(true)
8. shrink_to_fit()(缩容)
作用:将容量调整为当前实际长度,释放多余内存。
注意:缩容需要重新分配内存并复制数据,代价较高,建议少用。
例子:
string s;
s.reserve(100); // 容量变为100
s = "hello"; // 长度变为5,但容量仍为100s.shrink_to_fit(); // 容量缩小到5(或略大于5)
总结
方法 | 作用 | 例子 |
---|---|---|
size() | 返回字符串实际长度 | string s = "a"; s.size(); |
capacity() | 返回当前分配的容量 | s.capacity(); |
reserve(n) | 预分配至少 n 个字符的空间 | s.reserve(100); |
clear() | 清空字符串(长度为0,但容量不变) | s.clear(); |
empty() | 判断字符串是否为空 | if (s.empty()) { ... } |
shrink_to_fit() | 释放多余内存(慎用,代价高) | s.shrink_to_fit(); |
底层原理(了解即可)
string
的内存管理策略:
- 动态扩容:当字符串长度超过容量时,自动申请更大的内存(通常翻倍),例如:
string s; s += 'a'; // 容量可能变为1 s += 'b'; // 容量可能变为2 s += 'c'; // 容量可能变为4(翻倍)
- 小字符串优化(SSO):短字符串(如
"hello"
)直接存储在对象内部,不使用堆内存,提高性能。
注意事项
- 容量 vs 长度:
- 长度:实际字符数(
size()
)。 - 容量:已分配的内存大小(
capacity()
)。
- 长度:实际字符数(
- 性能提示:
- 提前
reserve()
可避免频繁扩容。 - 少用
shrink_to_fit()
,除非确实需要释放大量内存。
- 提前
- 清空字符串:
clear()
比s = ""
效率更高,因为不涉及内存分配。
八、string数据访问
1. operator[](下标访问)
作用:通过索引访问字符串中的字符,类似数组。
重载版本:
char& operator[] (size_t pos);
// 用于非const字符串(可修改)const char& operator[] (size_t pos) const;
// 用于const字符串(只读)
为什么需要两个版本?
为了区分读写操作:
- 对非const字符串(如
string s = "hello";
),s[0] = 'H';
可以修改字符。 - 对const字符串(如
const string cs = "hi";
),cs[0]
只能读,不能修改。
例子:
string s = "hello";
s[0] = 'H'; // 修改第一个字符,s 变为 "Hello"
cout << s[1]; // 输出: econst string cs = "hi";
// cs[0] = 'H'; // 错误!const版本不允许修改
cout << cs[0]; // 输出: h
2. at() 方法(安全访问)
作用:同 []
,但会检查索引是否越界(若越界则抛出 out_of_range
异常)。
例子:
string s = "hello";
try {cout << s.at(10); // 越界,抛出异常
} catch (const out_of_range& e) {cout << "错误: " << e.what(); // 输出错误信息
}
对比 []
和 at()
:
方法 | 越界检查 | 性能 | 建议场景 |
---|---|---|---|
[] | 不检查 | 快 | 已知索引合法时 |
at() | 检查 | 稍慢 | 不确定索引是否合法时 |
3. back()(访问最后一个字符)
作用:返回字符串的最后一个字符的引用。
例子:
string s = "hello";
cout << s.back(); // 输出: o
s.back() = '!'; // 修改最后一个字符,s 变为 "hell!"
4. front()(访问第一个字符)
作用:返回字符串的第一个字符的引用。
例子:
string s = "hello";
cout << s.front(); // 输出: h
s.front() = 'H'; // 修改第一个字符,s 变为 "Hello"
注意事项
- 空字符串风险:对空字符串调用
back()
或front()
会导致未定义行为(如崩溃),需先检查s.empty()
。 - 性能取舍:
at()
更安全但稍慢,[]
更快但需自行确保索引合法。 - 引用返回:
[]
、at()
、back()
、front()
返回的都是引用,可直接修改字符。
以上就是这篇博客的全部内容,下一篇我们将继续探索STL中String里更多精彩内容。
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343
我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482
非常感谢您的阅读,喜欢的话记得三连哦 |