目录
一、STL简介
(一)什么是STL
STL(standard template libaray - 标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
(二)STL的版本
① 原始版本:
Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 HP 版本--所有STL实现版本的始祖。
② P. J. 版本:
由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。
③ SGI版本:
由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版 本。被GCC(Linux)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。STL要阅读部分源代码,主要参考的就是这个版本。
(三)STL的六大组件
以容器为核心,其他组件围绕着容器进行使用。
① 容器(Containers):容器是用来存储和管理数据的对象(提供各种数据结构)。
② 算法(Algorithms):STL 中的算法是一些通用的函数模板,用于对容器中的数据进行各种操作,如排序(sort())、查找(find())、交换(swap())等操作。
③ 迭代器(Iterators):迭代器是一种对象,它提供了一种统一的方式来遍历容器中的元素,类似于指针,但具有更高级的功能。可以把迭代器想象成一个 “导航器”,它知道如何在容器中从一个元素移动到下一个元素。
④ 函数对象(Function objects):也称为仿函数(Functors),是一种可以像函数一样被调用的对象。它们是实现了operator()
(函数调用运算符)的类的实例。函数对象可以携带状态,这使得它们比普通函数更灵活。
⑤ 适配器(Adapters):适配器是一种机制,用于将一个类的接口转换为另一个接口,使得原本不兼容的类可以一起工作(对容器与算法进行调整)。
⑥ 空间配置器(Allocators):空间配置器负责管理内存的分配和回收。它们为容器提供了一种灵活的方式来获取和释放内存,使得容器可以更好地控制内存的使用。
二、string类
string-管理字符数组的类。
string的含义:一个字符char为8个比特位(1个字节),相当于每个字符char都是8个比特位(1个字节)的字符数组。另外,string与编码有关,因为编码本质就是字符串,string兼容ascii编码与utf-8编码。
(一)C语言中的字符串 vs C++string类
1、C语言中的字符串
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,如: strlen()
、strcpy()
和 strcat()
等;
但是这些库函数与字符串是分离开的,不太符合OOP的思想(面向对象编程【Object-Oriented Programming,OOP】是一种编程范式,它围绕对象和类的概念来组织程序结构。);
而且底层空间(字符数组长度)需要用户自己管理,稍不留神可能还会越界访问。例如:
int main() {
char str1[20] = "I like";
char str2[20] = " pizza!";
// 字符串连接
strcat(str1, str2);
printf("%s\n", str1); // 输出:I like pizza!
return 0;
}
2、C++string类
C++ 中的 string 类是一个封装好的类,它内部对字符串的存储进行了优化和管理。它可能使用动态内存分配等方式来存储字符串,用户不需要关心具体的存储细节(自己管理字符数组长度)。例如:
int main()
{
string s1("I like");
string s2(" pizza!");
//字符串连接
s1 += s2;
cout << s1 << endl; // 输出:I like pizza!
return 0;
}
(二)string类的常用接口
1、string类对象的常见构造
函数名称 | 功能说明 |
---|---|
string() (重点)
|
构造空的
string
类对象,即
空字符串
|
string(const char* s) (重点)
|
用
字符串S
来构造
string类对象
|
string(size_t n, char c)
|
构造包含
n个字符c
的
string类对象
|
string(const string&s) (重点)
|
拷贝构造
函数
|
使用例如下:
int main()
{
string s1; // 构造空的string类对象s1
string s2("I like pizza!"); // 用C格式字符串构造string类对象s2
string s3(5, 'x'); // 使用5个x字符构造string类对象s3
string s4(s2); // 拷贝构造s4
cout << s1 << endl; // 输出:空
cout << s2 << endl; // 输出:I like pizza!
cout << s3 << endl; // 输出:xxxxx
cout << s4 << endl; // 输出:I like pizza!
return 0;
}
2、string类对象的容量操作
函数名称 | 功能说明 |
---|---|
size()(重点)
|
返回字符串有效字符长度(不包括 '\0')
|
length()
|
返回字符串有效字符长度,
与 size() 等价
|
capacity()(重点) |
返回总分配空间大小
|
empty()
|
检测字符串是否为空串,
是空返回true
,不空返回false
|
clear()(重点)
| 清空有效字符 |
reserve()(重点)
| 为字符串预留空间,不改变有效字符个数(用于改善代码效率) |
resize()
|
改变字符串的
有效长度
,若空间增大则用默认字符填充
|
使用例如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int main()
{
string s1; // 构造空的string类对象s1
string s2("I like pizza!"); // 用C格式字符串构造string类对象s2
string s3(5, 'x'); // 使用5个x字符构造string类对象s3
string s4(s2); // 拷贝构造s4
cout << s2.size() << endl; // 输出:13
cout << s2.length() << endl; // 输出:13
cout << s2.capacity() << endl; // 输出:15
cout << s2.empty() << endl; // 输出:0(false表示不为空)
s2.clear();
cout << s2 << endl; // 输出:空
cout << s4.capacity() << endl; // 输出:15
s4.reserve(50);
cout << s4.capacity() << endl; // 输出:63
cout << s4 << endl; // 输出:I like pizza!
s4.resize(6); //需要为'\0'预留一位空间
cout << s4 << endl; // 输出:I like
cout << s4.size() << endl; // 输出:6
s4.resize(20);
cout << s4 << endl; // 输出:I like
cout << s4.size() << endl; // 输出:20
return 0;
}
注意:
① size() 与 length() 方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
int main()
{
string s5("pizza");
cout << s5.size() << endl; // 输出:5
cout << s5.length() << endl; // 输出:5
cout << endl;
return 0;
}
② clear()只是将string中有效字符清空,不改变底层空间大小。
int main()
{
string s5("pizza");
string s6(s5);
cout << s6.capacity() << endl; // 输出:15
s6.clear();
cout << s6.size() << endl; // 输出:0
cout << s6.capacity() << endl; // 输出:15
cout << endl;
return 0;
}
③ resize(size_t n) 与 resize(size_t n, char c) 都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用 '\0' 来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
int main()
{
string s5("pizza");
cout << s5.size() << endl; // 输出:5
cout << s5.capacity() << endl; // 输出:15
s5.resize(10);
cout << s5.size() << endl; // 输出:10
cout << s5.capacity() << endl; // 输出:15
cout << s5 << endl; // 输出:pizza
return 0;
}
int main()
{
cout << s5.size() << endl; // 输出:5
cout << s5.capacity() << endl; // 输出:15
s5.resize(20, 'x');
cout << s5.size() << endl; // 输出:20
cout << s5.capacity() << endl; // 输出:31
cout << s5 << endl; // 输出:pizzaxxxxxxxxxxxxxxx
return 0;
}
④ reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
int main()
{
string s5("pizza");
cout << s5 << endl; // 输出:pizza
cout << s5.size() << endl; // 输出:5
cout << s5.capacity() << endl; // 输出:15
s7.reserve(3);
cout << s5 << endl; // 输出:pizza
cout << s5.size() << endl; // 输出:5
cout << s5.capacity() << endl; // 输出:15
return 0;
}
⑤ string的扩容在vs2022中,除了第一次扩容,其他的基本以1.5倍扩容;string第一次储存的有效字符若小于15个,就会存储在一个buff的数组里面,等超过了这个数组的存储范围才会放入动态申请的空间中,并且进行二倍的扩容。(linux是二倍扩容)
3、string类对象的访问及遍历操作
函数名称
|
功能说明
|
---|---|
operator[ ](重点)
|
operator是string类的对象名
,
返回或更改 [ 下标 ] 位置的字符
|
at( ) | 与operator[ ]的作用一样,区别:at 会进行边界检查。如果传入的索引超出了字符串的有效范围出错了会抛异常,而operator[ ]在这种情况下会导致未定义行为。 |
begin() + end()(重点)
|
正向遍历:
begin()
获取第一个字符的
迭代器
+
end()
获取最后一个字符下一个位置的
迭代器
|
rbegin + rend(重点)
|
逆向遍历:
rbegin()
获取最后一个字符的
迭代器
+
rend()
获取第一个字符前一个位置的
迭代器
|
范围for
|
C++11支持更简洁的范围for的新遍历方式,
只适用于容器与数组(本质是迭代器)
|
(1)string类对象的访问
【operator[ ] 】与 【string对象.at() 】使用例:
int main()
{
string s1("I like pizza!");
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i];
}
// 输出:I like pizza!
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1.at(i);
}
// 输出:I like pizza!
return 0;
}
注意:operator[]有const版本与普通版本的接口,const版本不能写,而普通版本能读写。
(2)string类对象的遍历操作:
【 begin() + end() 】与 【 rbegin() + rend() 】以及 【 范围for 】,这些方法都与六大组件中的迭代器相关,需要先学习一下迭代器。
①、正向迭代器 iterator
在 C++ 中,string类里面有个iterator的自定义类型:string::iterator(使用时要指定类域,不然会报语法错误),它是一种迭代器类型,用于正向遍历string类型的对象。迭代器可以看作是一种广义的指针,它提供了一种统一的方式来访问容器(这里是字符串)中的元素,而不必关心容器的具体实现细节。
【 begin() + end() 】就是string对象获取正向迭代器的方法:
- begin() 获取第一个字符的正向迭代器
- end() 获取最后一个字符下一个位置的正向迭代器
使用例:
int main()
{
string s1("I like pizza!");
string::iterator i1 = s1.begin();// 获取字符'I'的迭代器
string::iterator i2 = s1.end();// 获取字符'!'的下一个字符的迭代器
while (i1 != i2) //开始和结尾的迭代器不相等时就循环
{
cout << *i1;// 使用方法与指针一样,需要*解引用获得字符
i1++; // 像指针一样进行指针++运算到下一个迭代器
}
// 输出:I like pizza!
return 0;
}
②、逆向迭代器 reverse_iterator
reverse_iterator是 C++ 中用于反向遍历string对象的迭代器类型。它提供了一种方便的方式来从字符串的末尾开始,朝着字符串开头的方向逐个访问字符。
【 rbegin() + rend() 】就是string对象获取逆向迭代器的方法:
- rbegin() 获取最后一个字符的逆向迭代器
- rend() 获取第一个字符前一个位置的逆向迭代器
使用例:
int main()
{
string s1("I like pizza!");
string::reverse_iterator i1 = s1.rbegin();// 获取字符'!'的迭代器
string::reverse_iterator i2 = s1.rend();// 获取字符'I'的前一个字符的迭代器
while (i1 != i2) //开始和结尾的迭代器不相等时就循环
{
cout << *i1;// 使用方法与指针一样,需要*解引用获得字符
i1++; // 这里的指针是先左边走的
}
// 输出:!azzip ekil I
return 0;
}
注意 i1++ 是向左边走的,因为是逆向遍历。
③、const迭代器 const_iterator
const对象无法使用普通迭代器,只能使用const迭代器。
使用例:
const string s2(s1);
string::const_iterator it1 = s2.begin();// 迭代器的通用写法
特点:不能对内容进行修改
也有逆向的迭代器:
const string s3(s1);
string::const_reverse_iterator it1 = s3.rbegin();// 迭代器的通用写法
④、迭代器小结
(①) 迭代器通用写法总结
正向迭代器通用写法:
//先定义某个容器的对象:
类型 对象;
类型::iterator 迭代器名 = 对象名.begin();//获取第一个元素的迭代器
while(迭代器名 != 对象名.end())//【第一个元素的迭代器】与【最后一个元素的下一个位置的迭代器】对比
{
cout << *迭代器名 << " "; // 遍历操作
迭代器名++;
}
cout << endl;
进行遍历时建议写成while循环。
逆向迭代器写法只需改动迭代器的名字以及调用的方法名即可。
迭代器与使用operator[]的区别:operator[]不是通用的方法,只适用于string和vector(底层为连续的空间),不再适用于更后面的容器;而迭代器是所有容器都使用的遍历方法。所以,在面对不同容器的时候,都可以使用迭代器,迭代器不管底层怎么实现的,都能进行访问。
(②) 类型名称的简化:auto关键字
auto关键字会在定义变量时根据赋值操作符右边的值自动推导类型,可以极大简化写起来长的类型,比如迭代器类型:string::iterator,可用auto关键字代替。使用例:
int a = 0;
auto b = i; // auto为int类型
string s1("love");
string::iterator it1 = s1.begin();// 正向迭代器通用写法
auto it1 = s1.begin(); // 类型写法简化,auto为迭代器类型
auto关键字并不会识别引用类型,只能推导被引用的那个类型,想要成为引用类型,还是要手动在auto后面加&,这样一来才会识别引用类型,这样的话就必须给初始值了。使用例:
int a = 5;
int& b = a;
auto c = b;
// auto的类型为int,不加&的话只能识别被引用的类型
auto& d = b;
// auto的类型为int&,能识别引用类型
注意:
- C++11不支持auto作函数参数,C++20开始支持,C++11支持auto作返回值类型,但并不好,会对代码阅读造成极大不良影响,谨慎使用 。
- 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。
- 当在同一行使用auto声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
- auto不能直接用来声明数组。
④、范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号( : )分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
范围for可以作用到数组和容器对象上进行遍历。
范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
使用例:
void Test01()
{
string s1("I like pizza!");
for (char c : s1) //范围内迭代的变量是字符类型的c,在s1对象的范围内遍历
{
cout << c;
}// 输出:I like pizza!
}
int main()
{
Test01();
return 0;
}
结合auto关键字进行使用:
void Test02()
{
string s1("I like pizza!");
for (auto c : s1)
{
cout << c;
}// 输出:I like pizza!
}
int main()
{
Test02();
return 0;
}
需要注意的是,在范围for中用于迭代的变量是从遍历对象中进行浅拷贝复制过来的,改变范围for中变量的值并不影响遍历对象中的值。若想修改,则迭代的变量要使用引用&。
void Test03()
{
string s1("I like pizza!");
for (auto c : s1)
{
cout << ++c;
}// 输出:J!mjlf!qj{{b"
cout << endl;
cout << s1 << endl;// 输出:I like pizza!
for (auto& c : s1)
{
cout << ++c;
}// 输出:J!mjlf!qj{{b"
cout << endl;
cout << s1 << endl;// 输出:J!mjlf!qj{{b"
}
int main()
{
Test03();
return 0;
}
(三)string类的高级操作
1、string类对象的插入与删除操作
(1)插入操作
函数名称 | 功能说明 |
---|---|
push_back(char c)
|
在字符串后尾插字符c
|
append()
|
在字符串后追加一个字符串
|
operator+= (重点)
| 在字符串后追加一个字符或字符串 |
insert() | 在字符串的指定下标位置插入字符或字符串 |
注意:
在 string 对象 s 尾部追加字符时, s.push_back(c) / s.append(1, c) / s += 'c' 三种的实现方式差不多, 一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串 。 对 string 操作时,如果能够大概预估到放多少字符, 可以先通过reserve把空间预留好,这样不用频繁扩容,提高代码效率 。
使用例:
void Test04()
{
string s1;
s1.push_back('X');
s1.append(" I like ");
s1 += 'p';
s1 += "pizza!";
s1.insert(2, "hello ");
cout << s1 << endl; // 输出:X hello I like ppizza!
}
int main()
{
Test04();
return 0;
}
(2)删除操作
函数名称 | 功能说明 |
---|---|
erase(pos,len) | 删除字符串中指定下标位置pos 开始的 len 个字符 |
注意: 当 len 不指定长度时,默认为npos,表示会删除到最后。
① npos
npos
是string
类中的一个静态成员常量。它是string
类中用于表示 “未找到” 或 “不存在的位置” 的值。其类型为std::string::size_type
,通常是一个无符号整数类型(如size_t
)。通常它被定义为一个足够大的值,以表示超出字符串范围的位置。例如,在很多实现中,npos
被定义为-1,转换为size_type
后的无符号值(42亿9千万...),适合表示 “不存在的位置”。void Test07() { string s1("I like pizza!"); if (s1.find("abc") == string::npos) //类中的静态成员,不需要具体对象引用, //因为静态成员不是类中真正的成员,自然实例化的对象中也不会存在,而是在全局中, //限定类域即可。 cout << "字符串不存在" << endl; // 输出:字符串不存在 } int main() { Test07(); return 0; }
删除操作使用例:
void Test05()
{
string s1("I like pizza!");
s1.erase(6, 5);
cout << s1 << endl; // 输出:I likea!
s1.erase(6);// 此时len为npos,意为把后面的都删除
cout << s1 << endl; // 输出:I like
}
int main()
{
Test05();
return 0;
}
(3)小结
string的insert插入和erase删除相对而言是比较少使用的,因为要挪动数据,效率比较低。
2、string类对象的获取、查找和截取操作
函数名称 | 功能说明 |
---|---|
c_str() (重点) |
返回
【调用该函数的string对象】的
C格式字符串
|
find
(重点)
|
从【
string对象的pos下标位置(默认从头开始找,pos = 0)
】开始往后找【
指定的字符或字符串
】,
返回其首次出现的下标位置,找不到则返回 string::npos
|
rfind
| 从【string对象的pos下标位置(默认从末尾开始找,pos = npos)】开始往后找【指定的字符或字符串】,返回最后一次出现子串或字符的下标位置 |
substr |
在string对象中
从pos位置开始,截取n个字符,然后将其返回
该方法常用在处理文件路径或URL。
|
使用例:
void Test06()
{
string s1("I like pizza!");
cout << s1.c_str() << endl; //输出:I like pizza!
cout << s1.find("like") << endl; //输出:2
cout << s1.rfind("I") << endl; //输出:0
cout << s1.substr(2, 4) << endl; //输出:like
}
int main()
{
Test06();
return 0;
}
c_str接口是为了与C进行兼容写出来的,返回的是const char*类型的字符串。
(四)string类的非成员函数
函数名称 | 功能声明 |
---|---|
operator+
| 加法运算符的重载,实现两个字符或字符串的拼接 |
operator>>
(重点)
| 输入运算符重载 |
operator<<
(重点)
| 输出运算符重载 |
getline
(重点)
| 从输入流(如std::cin 或者文件输入流std::ifstream )中读取一行字符且不会受到空格影响,并将其存储到一个std::string 对象中,直到遇到换行符\n。 |
relational operators
(重点)
| 比较大小 |
注意:
- operator+尽量少用,因为传值返回,导致深拷贝效率低。
- 流输入规定【换行】和【空格】是输入缓冲区的分割,若要输入英文句子,最后只会得到句子的第一个英文单词,需要使用getline函数来获得具有多个空格的字符串。
- getline函数是被包含在<string>头文件中的,使用时要包涵该头文件。
使用例:
void Test08()
{
string s1("I like pizza!");
cout << s1 + '!' << endl; // 输出:I like pizza!!
cout << s1 + "!!!" << endl; // 输出:I like pizza!!!!
cin >> s1; // 输入:Hello world
cout << s1 << endl; // 输出:Hello,输入会因为空格而终止
string s2;
std::getline(cin, s2);// 输入:Hello world
cout << s2 << endl; // 输出:Hello world
bool a = s2 > s1 ;// 输出:1
bool b = s2 < s1 ;// 输出:0
bool c = s2 == s1 ;// 输出:0
bool d = s2 != s1 ;// 输出:1
cout << a << " " << b << " " << c << " " << d << " " << endl;
}
int main()
{
Test08();
return 0;
}
一个注意点: const char* p的地址不能直接被打印,如果直接打印的话会打印出字符,想要打印出真正的地址,就要强转为(void*)打印。
const char* p1 = "xxxxx"; cout << p1 << endl; //打印xxxxx cout << (void*)p1 << endl; 打印地址
以上内容仅供分享,若有错误,请多指正。