C++ STL 与 std::string 入门:迭代器、范围 for 及字符串遍历方式
在进入sring之前,我先简单介绍一下STL
STL简介
1. 什么是STL
STL(standardtemplatelibaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
2. STL的版本
STL 并不是只有一个固定的"官方版本",它有多个历史实现版本。按时间线和影响力,主要如下:
- HP STL(原始始祖)
- Alexander Stepanov 和 Meng Lee 在惠普实验室开发
- 1994 年提交给 C++ 标准委员会,是所有 STL 的源头
- P.J. Plauger STL
- 由 P.J. Plauger(《C++ Standard Library》作者)实现
- 被 Microsoft Visual C++ (MSVC) 采用
- 可读性好,但有一些非标准扩展
- Rouge Wave STL
- 被早期 Borland C++ Builder 采用
- 后来被收购整合
- STLport
- 开源、跨平台、高度可移植的实现
- 目标是在不同编译器上提供一套统一的 STL
- SGI STL(影响力最大也是我们后面学习所接触最多的)
- Silicon Graphics 公司实现,Matt Austern 主导维护
- 被 GCC(Linux) 采用,侯捷《STL 源码剖析》就是基于此版本
- 设计精巧、注释丰富,对现代 STL 影响最深
3. STL的六大组件

这里的容器可以理解为数据结构
std::string 是 STL 的一部分,准确说是 STL 的扩展。
原始 STL(由 Stepanov 提出)的核心是三大件:容器 + 算法 + 迭代器,std::string 不在其中。但后来的 C++ 标准库在 STL 基础上扩展,把 std::string 也吸收了进来,它的接口完全遵循 STL 的约定
简单来说就是std::string 不是 STL 的原始成员,但它是按 STL 的设计哲学打造的字符串类,与 STL 容器无缝对接。
将stack、queue、priority_queue归类为配接器是因为它们三个是借用已有容器储存能力,限制并改造对外接口来实现的
好的,下面进入sring的学习部分
sring
std::string 是标准库提供的字符串类,需要包含头文件 ,全称 std::string,它在底层管理一个动态的 char 数组。
这里介绍三种遍历方式:
- 下标+\[\]
- 迭代器
- 范围for --C++11之后
迭代器
迭代器就是用来遍历容器中元素的一个"指针式"工具(不一定是指针)。对于 std::string,它指向字符串中的每个字符。
它是 STL 容器内部定义的一个类型名(嵌套类)。比如 string::iterator(容器) 就是std::string(容器内部类型)类里定义的迭代器类型。
c++
#include <string>
#include <iostream>
using namespace std;
int main() {
string s = "hello";
// 正向遍历(可修改元素)
for (string::iterator it = s.begin(); it != s.end(); ++it) {
*it = toupper(*it); // 解引用 it 来读/写字符
}
cout << s << endl; // "HELLO"
// 反向遍历
for (string::reverse_iterator rit = s.rbegin(); rit != s.rend(); ++rit) {
cout << *rit; // "OLLEH"
}
// C++11 起推荐用 auto 省去冗长类型
for (auto it = s.begin(); it != s.end(); ++it) { ... }
// 只读不写时用 const_iterator
for (string::const_iterator it = s.cbegin(); it != s.cend(); ++it) { ... }
return 0;
}
const_iterator 里的 const 修饰的是它指向的元素,迭代器本身照样能改,所以 ++it 可以走,只是 *it 不能赋值
在官网中也是使用const_iterator,它相当于 const T*:*it 不能赋值,++it 照样可以;普通的 iterator 则相当于 T*
迭代器的end返回的是字符串最后一个有效字符的下一个位置

迭代器的意义:
- 统一类似的方式遍历修改容器
- 算法脱离底层结构,算法独立模版实现对多个容器的处理
C++要求使用迭代器必须使用左闭右开区间即[begin, last)
除了上面这种,还有反向迭代器,指针指向的规律就反过来了
c++
#include <string>
#include <iostream>
using namespace std;
int main() {
string s = "12345";
// 反向迭代器:从尾到头输出
for (auto it = s.rbegin(); it != s.rend(); ++it)
{
cout << *it;
}
// 输出:54321
return 0;
}
//s.rbegin() --- 指向最后一个字符
//s.rend() --- 指向第一个字符的前一个位置
//++it --- 反向迭代器自增时向前移动(往开头方向走)
范围for
auto:自动推导对象类型
如
c++
auto x=1;
auto y=3.14;
//也可以引用
auto& m=y;
//指针就有两种方式
auto ptr=&x;
auto*ptr=&x;//这两种方式推导出来的类型都是int*
范围for也叫语法糖(甜的,比喻让代码"更好看、更好吃",相对的还有 "语法盐"(syntactic salt)------指那些故意让代码写起来不太舒服的设计,目的是防止程序员写出烂代码。不过这个术语用得少很多。):
- 自动取容器数据赋值
- 自动判断结束
- 自动迭代
- 本质底层也会被替换成迭代器,auto e = *迭代器
c++
char s1[]="Hello world";
for(auto ch:s1)
{
cout<<ch<<endl;
}

加上引用才会修改值
c++
for(auto& e : s1)
{
e--;
}
否则
c++
for(auto e : s1)
{
e--//并没有什么用,因为本质底层也会被替换成迭代器,auto e = *迭代器,迭代器拷贝给e
}
c++
// 语法糖:范围 for
for (char c : s)
cout << c;
// 脱糖后等价于:
for (auto it = s.begin(); it != s.end(); ++it)
cout << *it;
// 语法糖:s[i]
s[i] = 'a';
// 脱糖后等价于:
*(s.begin() + i) = 'a';
范围for严格来说是可以用具体类型来代替auto的
范围for不仅能够支持容器还能支持数组
c++
int Array[]={1,2,3,4,5};
for(auto e : Array)//想要修改仍然要加引用&
{
cout<<e<<" ";
}
cout<<endl;
基本使用
1. 构造方式
c++
string s1; // 空字符串
string s2 = "hello"; // C风格字符串初始化
string s3("world"); // 构造函数初始化
string s4(5, 'a'); // "aaaaa" (5个'a')
string s5 = s2; // 拷贝构造
2. 赋值
c++
s1 = "new value";
s1 = s2;
3. 常用操作
c++
string s = "Hello World";
s.length() // 长度: 11
s.size() // 同上, 两者等价
s.empty() // 是否为空
s[0] // 访问字符: 'H', 越界不检查
s.at(0) // 访问字符: 'H', 越界抛异常
// 拼接
s += "!!!"; // "Hello World!!!"
string s2 = s + " C++"; // "Hello World!!! C++"
// 比较 (直接用运算符)
if (s == s2) { }
if (s < s2) { } // 字典序比较
// 查找
s.find("World"); // 返回位置6, 找不到返回 string::npos
// 子串
s.substr(0, 5); // "Hello" (从位置0开始, 截5个)返回一个新的 string 对象,不是首元素地址
s.substr(6); // "World!!!" (从位置6到末尾)
// 插入/删除/替换
s.insert(5, " C++"); // 在位置5插入,插入单个字符时还要加一个参数说明插入个数
s.erase(5, 4); // 从位置5删除4个字符
s.replace(6, 5, "C++"); // 替换位置6开始的5个字符
//反转
std::reverse(s.begin(), s.end());// 需要 #include <algorithm>,reverse 是算法不是 string 的成员
4. 输入输出
c++
string name;
cin >> name; // 读取一个词 (遇空格停止)
getline(cin, name); // 读取一整行 (含空格),默认读到'\n'结束,并把'\n'丢弃
getline(cin,name,'!'); // 读取到'!'才结束,
cout << name << endl;
更多操作可以去这个网站看文档说明:basic_string - C++ Reference 打开页面如下

下面的内容可以了解一下:
shrink_to_fit缩容(C++11 起),以时间换空间,不同环境下缩容后的容量会不同,但保证大于等于sizereserve扩容,扩容量在不同环境下不同,但至少大于等于传递的参数值,用于提前知道要多少空间,提前开好,避免反复扩容,提高效率operator[]访问特定位置的数据,注意它本身不做越界检查 ,越界是未定义行为(C++11 起,pos == size()时返回'\0'的引用是合法的);我模拟实现时加的assert只是调试期的额外保护,不是标准行为at与operator主要功能相同,区别是at越界会抛异常append尾插,功能丰富,可以尾插单个字符,个数可定,也可以尾插字符串等等npos的值是size_t类型的-1,当然不可能是负数,那么就是该环境下该类型的最大值,是 C++ 中std::string类的一个静态常量,表示 "不存在的位置" 或 "未找到" 。主要用途是作为字符串查找函数(find、rfind、find_first_of 等)的返回值,表示未找到匹配项s.rfind倒着找- 流插入
模拟实现string底层
-
构造
-
带参构造,带参初始化不能直接用常量串来初始化,在string类中是可以插入修改和扩容的,这就要求这个空间是我们自己开辟的
-
c_str,暂时不写<<与>>的运算符重载,用str的,此时我们就要把C的用c_str -
1和2结合可写成全缺省
-
析构
-
size -
operator[]一个只读的一个写的 -
写个迭代器使用范围for
- 插入相关,push_back、append()、reserve
-
运算符重载+=,一个串的一个单个字符的(复用就行)
-
实现
insert一个字符的和一个串的和erase串的 -
pop_back尾删 -
find一个单个字符的一个子串的 -
substr, 注意临时对象销毁的问题,即写一个自定义类型的拷贝构造进行深拷贝
注意COW机制
注意:
-
在实现 string 底层时不能用
swap 交换:str 是 const char*,_str 是 new 出来的堆指针,swap 换的是指针的指向,换完之后再改字符或 delete[] 就打到常量区去了。这里要的是把内容拷进自己的空间,所以用 strcpy -
自定义类型不能直接用范围for,需要自己写迭代器、begin()和end()
-
拷贝构造的传统写法与现代写法
现代写法大致流程:

更简洁的方式:直接利用形参完成第一步代码

-
看看下面两个代码的区别
c++vector<int> a{1, 2, 3}; vector<int> b(1, 2);花括号与圆括号会影响编译器选择构造函数的过程
先简单理解vector有下面两个构造函数
c++// 构造函数1:指定元素个数和初始值 vector(int n, int value); // 构造函数2:接收一组初始元素 vector(initializer_list<int> list); //构造函数1 vector<int> b(1, 2);//创建一个元素并把它初始化为2 //构造函数2 vector<int> a{1, 2, 3}//使用1,2,3这三个数初始化vector使用圆括号:编译器查找可以接收两个参数的构造函数
使用花括号:按照列表初始化规则选择构造函数
c++
//string.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"mystring.h"
using namespace std;
namespace test_string {//同一个命名空间会被看做一个,这叫命名空间的合并或命名空间的扩展
/*string::string()//无参构造
:_str(new char[1] {'\0'})
, _size(0)
, _capacity(0)
{
}*/
string::string(const char* str)
:_str(new char[strlen(str) + 1])//这里多次使用strlen,我们可以尝试调换初始化顺序,但主要与变量声明顺序耦合,否则会有越界风险
, _size(strlen(str)) //更灵活一点就是只将_size初始化,其余变量在函数体内部利用_size进行初始化,这样既减少了多次使用strlen的负担,也防止有人看不懂方案一的代码导致随便更改初始化列表的顺序,增加了代码容错(鲁棒性)
, _capacity(strlen(str))
//:_size(strlen(str))
{
//_capacity(strlen(str))
//_str(new char[strlen(str) + 1])
strcpy(_str, str);//不能用swap,原因有2,1是两个变量类型不同,str有const修饰;2是swap更换的是指针指向,但我们的目的是将str的内容弄到_str开批的空间内
}
const char* string::c_str() const
{
return _str;
}
void string::swap(string& s)
{
std::swap(_size, s._size);
std::swap(_str, s._str);
std::swap(_capacity, s._capacity);
}
string::~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
string::string(const char* str, size_t size)//私有构造:按长度拷贝,不会在中间的'\0'处停下
:_str(new char[size + 1])
, _size(size)
, _capacity(size)
{
memcpy(_str, str, size + 1);
}
string::string(const string& s)
//传统写法
/* :_str(new char[s._capacity + 1])
,_capacity(s._capacity)
,_size(s._size)
{
memcpy(_str, s._str, s._size+1);
}*/
//现代写法
{
string tmp(s._str, s._size);//必须带上_size,写成string tmp(s._str)会按strlen截断,中间有'\0'就丢字符了
swap(tmp);
}
size_t string::size() const
{
return _size;
}
char& string::operator[](size_t i)
{
assert(i <= _size);//等于_size时返回的是结尾'\0'的引用,标准库允许这样访问
return _str[i];
}
const char& string::operator[](size_t i) const
{
assert(i <= _size);
return _str[i];
}
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* str = new char[n + 1];
//strcpy(str, _str);//这里不能使用strcpy的原因:strcpy遇到'\0'就停,而string中间可能有'\0'(比如"hello\0\0i"),只能按长度拷
memcpy(str, _str, _size+1);
delete[] _str; //最好使用memcpy,拷贝单位是字符
_str = str;
_capacity = n;
}
}
void string::pushback(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
void string::append(const char* str)
{
if (str >= _str && str <= _str + _size)//str指向自己的缓冲区时,reserve会把这块内存释放掉,必须先留一份副本
{
string tmp(str, strlen(str));
append(tmp.c_str());
return;
}
size_t len = _size + strlen(str);//不调strcat因为其要遍历找尾,效率较低
if (len > _capacity)
{
size_t newcapacity = 2 * _capacity > len ? 2 * _capacity : len;
reserve(newcapacity);
}
memcpy(_str + _size, str, strlen(str) + 1);//用_size定位结尾,不能写成strlen(_str):中间有'\0'时会被截断
_size += strlen(str);
}
string& string::operator=(string tmp)
{
swap(tmp);
return *this;
}
string& string::operator+=(char ch)
{
pushback(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
ostream& operator<<(ostream& out, const string& s)//按引用传,否则每输出一次就整串拷贝一份
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];
}
return out;
}
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;//插入了字符,size也要跟着加,否则后面全部错位
}
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
if (str >= _str && str <= _str + _size)//同append:reserve之后源指针就失效了,先抄一份
{
string tmp(str, strlen(str));
insert(pos, tmp.c_str());
return;
}
size_t len = strlen(str);
if (_size + len >= _capacity)
{
size_t newcapacity = (2 * _capacity <= _size + len) ? _size + len : 2 * _capacity;
reserve(newcapacity);
}
size_t end = _size + 1;
while (end > pos)//先--end再搬,写成_str[end - len]在pos很小时会减成负数,size_t回绕成巨大值直接越界
{
--end;
_str[end + len] = _str[end];
}
memcpy(_str + pos, str, len);
_size += len;
}
//npos已经在类内初始化(static const size_t npos = -1),这里不再需要额外定义
void string::erase(size_t pos, size_t len)
{
assert(pos <= _size);
if (len == npos || len >= (_size - pos))
{
_size = pos;
_str[_size] = '\0';
}
else
{
for (size_t i = pos+len; i <=_size; i++)
{
_str[pos] = _str[i];
++pos;
}
//或者直接用memmove(_str+pos, _str+pos+len, _size-pos-len+1);
_size -= len;
}
}
void string::pop_back()
{
assert(_size > 0);//空串调用时_size--会回绕成SIZE_MAX,下一行就写坏内存
_size--;
_str[_size] = '\0';
}
size_t string::find(char ch, size_t pos) const
{
if (pos > _size)//越界直接当作找不到,和标准库一致
return npos;
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
size_t string::find(const char* str, size_t pos) const
{
if (pos > _size)//不先挡住,_str + pos 就是野指针
return npos;
size_t len = strlen(str);
if (len == 0)
return pos;
for (size_t i = pos; i + len <= _size; i++)
{
if (memcmp(_str + i, str, len) == 0)//按长度比,strstr遇到'\0'就停,字符串中间有'\0'会找不到
return i;
}
return npos;
}
string string::substr(size_t pos, size_t len) const
{
assert(pos <= _size);
if (len == npos || len > _size - pos)
{
len = _size - pos;
}
string re;
re.reserve(len);
for (size_t i = 0; i < len; i++)
{
re += _str[pos + i];//直接尾插,不能用re[i],此时re的_size还是0,operator[]会断言失败
}
return re;
}
}
c++
//mystring.h
#pragma once
#include <cstring>
#include <cstddef>
#include <iostream>
#include <cassert>
namespace test_string {
class string {
public:
typedef char* iterator;
typedef const char* const_iterator;
static const size_t npos = -1;//必须在类内初始化:erase/substr 的默认参数要用它,默认参数要求是常量表达式
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
//string();
string(const char* str="");//利用全缺省合并两个构造函数.""字符串会默认自带一个\0,使用"\0"结果就是两个\0,不过没有影响
const char* c_str()const;
void swap(string& s);
~string();
string(const string& s);
size_t size() const;
char& operator[](size_t i);
const char& operator[](size_t i)const;
void reserve(size_t n);
void pushback(char ch);
void append(const char* str);
string& operator=(string tmp);
string& operator+=(char ch);
string& operator+=(const char* str);
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
void erase(size_t pos, size_t len = npos);
void pop_back();
size_t find(char ch, size_t pos) const;
size_t find(const char* str, size_t pos) const;//参数必须是const char*,否则传字符串字面量编译不过
string substr(size_t pos, size_t len = npos) const;//顺序是pos在前,和标准库一致
private:
string(const char* str, size_t size);//私有:按长度构造,保留字符串中间的'\0',给拷贝构造复用
char* _str;
size_t _size;
size_t _capacity;
};
std::ostream& operator<<(std::ostream& out, const string& s);//头文件里不写using namespace std,靠std::限定
}
完