目录
1.2 string (const string& str)
1.3 string (const string& str, size_t pos, size_t len = npos);
一 STL简介
1.什么是STL
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
STL并不是C++官方进行编写管理的内容,而是官方给出一个标准,由其他企业或实验室进行完成。所以不同编译器或者不同操作系统底层下实现可能并不相同,例如 String类中的扩容 capaccity(),在VS编译器下是1.5倍扩容,但是在Linux的g++下却是两倍扩容。
STL的使用在日常中的使用很频繁,比如 vector,string 等容器,sort(),reverse(),swap()等函数。使用过的同学能感受到这些工具的便利性,比如在算法比赛中STL库便是经常会被使用。灵活的使用STL无论是对于工作还是算法竞赛来说都是非常重要的。
2.STL的版本
原始版本 Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许 任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 HP 版本--所有STL实现版本的始祖。
P.J.版本
由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。
RW版本
由Rouge Wage公司开发,继承自HP版本,被C+ + Builder 采用,不能公开或修改,可读性一般。
SGI版本
由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版 本。被GCC(Linux)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。我们后面学习STL要阅读部分源代码,主要参考的就是这个版本。
3.STL的六大组件
1. 容器(Containers)
定义:用于存储和管理数据的数据结构模板类。
分类:
顺序容器:元素按线性顺序排列(如vector,list,duque)。
关联容器:通过键(Key)高效查找元素(如 map,set,unordered_set)。
容器适配器:基于其他容器实现的特定接口(如 stack,queue)。
特点:提供统一的数据管理接口(如 push_back(), insert(), erase())
2. 算法(Algorithms)
定义:独立于容器的通用函数模板,用于操作数据(如排序、查找、遍历)。
常见算法:
sort():对元素排序。
find():查找指定元素。
transform():对元素进行转换。
copy():复制元素到另一容器。
特点:通过迭代器与容器解耦,不直接操作容器本身。
3. 迭代器(Iterators)
定义:类似指针的对象,用于遍历容器中的元素,是算法与容器之间的桥梁。
分类(按功能递增):
输入迭代器:只读,单向移动(如 istream_iterator)。
输出迭代器:只写,单向移动(如 ostream_iterator)。
前向迭代器:可读写,单向移动(如 forward_list 的迭代器)。
双向迭代器:可双向移动(如 list, map 的迭代器)。
随机访问迭代器:支持随机访问(如 vector, deque 的迭代器)。
作用:统一容器的访问方式,使算法无需关心容器底层结构。
4. 仿函数(Functors,函数对象)
定义:重载了 operator() 的类对象,可像函数一样调用。
用途:
作为算法的策略(如自定义排序规则)。
与适配器结合(如 bind2nd 绑定参数)。
5. 适配器(Adapters,配接器)
定义:通过封装现有组件,改变其接口或行为。
分类:
容器适配器:如 stack(基于 deque 或 list 实现栈)、queue。
迭代器适配器:如 reverse_iterator(反向遍历容器)。
函数适配器:如 bind1st、bind2nd(绑定函数参数)。
6. 空间配置器(Allocators)
定义:负责内存分配与释放的模板类,封装底层内存管理细节。
作用:控制容器的内存分配策略(如内存池优化)。
支持自定义内存管理(如共享内存、对齐分配)
默认配置器:std::allocator<T>。
六大组件的协作关系
容器通过空间配置器管理内存。
算法通过迭代器操作容器的元素。
仿函数和适配器定制算法或容器的行为。
适配器基于现有组件扩展功能(如将 deque 适配为 stack)
4.STL的重要性
1.笔试中经常会用到STL,使用STL对于写代码来说会有很大便利性,STL里帮你实现了很多现成的功能,STL中的算法已经经过了优化,会比手写的代码效率更高。
2.在面试中通常面试官也会考察STL相关的知识点。
3.网上有句话说:“不懂STL,不要说你会C++”。STL是C++中的优秀作品,有了它的陪伴,许多底层的数据结构以及算法都不需要自己重新造轮子,站在前人的肩膀上,健步如飞的快速开发。
5.如何学习STL
学习STL可以分为三个境界
第一境界:熟用STL。
第二境界:了解STL底层是如何实现的,了解泛型技术的内涵。
第三境界:扩充STL,即能自己编写STL库内容。
二 String类
讲解之前先推荐一个很使用的工具 cplusplus.com 。对于我们日常使用的函数等,这个网站里能很系统的给出分类和讲解。
1 为什么学习 String 类
1.1 C语言中的字符串
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
1.2 面试中的题目
字符串转整形数字
2 标准库中的String类
2.1 String类
string类的文档介绍 这个文档就是从前面的 cplusplus 中找出来的,依次往下翻看能看到对于每一类功能都有进行系统的分类,对于系统性的学习和比较 来说更加友好。
std::string的本质是标准库提供的一个类模板的特化版本,它的底层实现动态字符数组。
内部通过动态分配的字符数组 char[] 存储字符串内容
2.2 auto 和 范围for
在学习String 类之前先来学习下这两个内容,auto 和 范围for 在C++中也被叫做语法糖,可以简单理解为使用较为友好,便利性很高。语法糖让更加复杂冗长的代码变得更为简洁并且更易于理解,但是底层的逻辑保持不变。就像糖衣一样,让代码变得更甜,但本质上来说,并没有增加任何功能。比如 += 来说也是一个语法糖。
auto
在早期的C/C++中auto的含义是:使用 auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto不能作为函数的参数,可以做返回值,但是建议谨慎使用。
auto不能直接用来声明数组。
1.auto能自动推导参数类型。
void text()
{
// auto会自动推导 a 参数类型
auto a = 10;
}
但是参数必须要初始化,否则编译器无法得知参数的类型到底是什么。
// 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项
2. auto可以作为返回值,但不能作为参数。
int func1()
{
return 10;
}
// 不能做参数
void func2(auto a)
{
}
// 可以做返回值,但是建议谨慎使用,此时编译器会根据返回值自动推导返回值类型
auto func3()
{
return 3;
}
3.auto声明指针类型
int x = 10; // x 已经指定为 int 类型
auto y = &x; // auto 推导类型,引用类型
auto* z = &x; // 此时指定为指针类型,右边必须是指针
auto& m = x; // 指定为引用类型
在声明指针类型时,使用auto 与 auto* 是一样的,形式上auto*可以更明显的告诉你这是个指针类型。可以理解为auto* 的右边必须是指针。
4.auto必须始终推导为同一类型
auto x = 1, y = 2;
// 类型相同,编译通过
auto cc = 3, dd = 4.0;
// 编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
5.auto不能直接用来声明数组
// 编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型
auto array[] = { 4, 5, 6 };
范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围 内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
范围for可以作用到数组和容器对象上进行遍历。
范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
范围for()表示
for(元素类型 element : 容器) {
// 迭代代码
}
// 容器是想要遍历的目标,可以是数组,字符串等
// 元素类型 是容器中一个元素的类型
// element可以任意命名,表示为容器中的一个元素
范围for()的使用
#include<iostream>
#include <string>
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
// C++98的遍历
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
array[i] *= 2;
}
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
cout << array[i] << endl;
}
// C++11的遍历
for (auto e : array)
cout << e << " " << endl;
string str("hello world");
for (auto ch : str)
{
cout << ch << " ";
}
cout << endl;
}
加&与不加&的区别
假设对于数组内的内容我们只需要进行读操作,就不需要加&。
如果还要进行写操作,需要加&。
同时这点还与下面的是否调用拷贝构造有关。
int arr[]={1,2,3,4,5};
// 只对数据进行读操作
for(auto e : arr)
{
cout << e << " " ;
}
cout << endl;
//改变数组内的数据值,加引用
for(auto& e : arr)
{
e *= 2;
}
是否调用拷贝构造
值类型声明
当循环变量声明是值类型时, 每次迭代时,容器中的元素会被 拷贝 到循环变量中。这会调用元素类型的拷贝构造函数(若存在且未被优化)。
int arr[] = {1,2,3};
for(auto e : arr)
{
....
}
数组arr的元素会被拷贝到e中去,这时会调用拷贝构造。
引用类型声明
当循环变量声明为引用类型时:循环变量直接引用容器中的元素,不会触发拷贝操作。不会调用拷贝构造函数。
这点原理是类的默认成员函数中拷贝构造函数内容。类的默认成员函数-优快云博客
这里拷贝构造的好处就是对象比较大的时候,节省空间。
2.3 String类的常用接口说明
1.string类对象常见构造
先了解什么是构造函数,不清楚的可以看这篇博客 类的默认成员函数-优快云博客
这里根据cplusplus.com - The C++ Resources Network的内容来讲解:
1.1 string()
string()是默认构造,可以不传任何参数。
#include<iostream>
using namespace std;
int main()
{
string a;
return 0;
}
1.2 string (const string& str)
string (const string& str) 可以传入一个string类型的对象。
#include<iostream>
using namespace std;
int main()
{
string a = "hello";
string b(a);
return 0;
}
1.3 string (const string& str, size_t pos, size_t len = npos);
这个构造的功能是 从str 串的pos 开始,往后截取len个字符长度的元素。
#include<iostream>
using namespace std;
int main()
{
string s = "hello";
string b(s, 1, 2);
return 0;
}
如果len的长度超出了字符串原有的长度会如何?
string b(s, 2, 1000);
显然字符串s肯定没有1000个字符的,遇到这样的情况,会读取到字符串s的最后一个字符。
假若不填最后一个参数也是没问题的,size_t len = npos是一个缺省值,它也会读取到最后一个字符停止。
npos
这时可能有人会问,这个 npos 是什么,结合官方文档来看
表面上它是一个静态类型的 -1,实则并不然。size_t是无符号类型的整数,但是-1是有符号整数,我们可以知道-1的补码是全1,全1在无符号整数中是整数的最大值,所以这里的npos是整数所能表示的最大值,所以当npos作为缺省值时,他必定会读取到字符的最后一个位置。
总结
void Teststring()
{
string s1; // 构造空的string类对象s1
string s2("hello bit"); // 用C格式字符串构造string类对象s2
string s3(s2); // 拷贝构造s3
}
2. string 类对象的访问和遍历操作
遍历总结来说共有三类:1.下标+[ ] 2.迭代器 3.范围for()
1.下标+[ ]
对于数组元素的读取,我们通常使用 数组名[下标] 的方式进行访问,这样访问的便捷性不用多说,而C++又有运算符重载的功能,因此String类中给出了 [] 的重载,使得字符串能够像数组一样使用下标进行访问,这无疑大大增加了便利性。
string::operator[ ]重载了两种类型,第一种是能进行读写,第二种是只读的。
// 1、下标+[]
for (size_t i = 0; i < s2.size(); i++)
{
s2[i] += 1;
//s2.operator[](i) += 1;
}
//for (size_t i = 0; i < s2.length(); i++)
for (size_t i = 0; i < s2.size(); i++)
{
cout << s2[i] << " ";
}
const string s5("xxxxxx");
//s5[0] = 'y';
// s5是const 类型的,只读,因此不能进行修改
2.迭代器
迭代器是一个左闭右开的区间 :[ )
在行为上可以将迭代器理解为像指针一样的对象,它有 begin() 和 end() 两个值,分别指向开始到结束的位置,使用迭代器时它会自动从 begin 进行遍历,到 end 时结束。
注意它是一个左闭右开的空间,end()指向最后一个元素的下一个位置。
如何使用
迭代器的类型根据你使用的容器来定 ,比如这里使用的string类型的字符串
string::iterator it = s.begin()
但是这样来说类型的名称就太冗长了,可以使用 auto 来自动生成类型
auto it = s2.begin()
auto it = s2.begin();
while (it != s2.end())
{
*it += 1;
cout << *it << " ";
++it;
}
cout << endl;
因为迭代器是行为类似于指针的对象,所以要访问内容时要使用 * (注意这里的 * 不一定是解引用的意思,C++可以对 * 进行重载)
还有一点要注意的是,这里的便利方式使用的是 while,而不是 for()循环,字符串和数组这里也可以使用for 循环来遍历,因为它的底层是数组,后一个元素的地址肯定要大于前一个,但是对于链表等来说,它的物理地址并不是连续的,因此建议使用while()遍历。
list<int> lt1 = { 1,2,3,4,5 };
list<int>::iterator it1 = lt1.begin();
while (it1 != lt1.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
begin() / end()
前面已经说过这两个分别指向开始和结束。
C++里给出了两个重载,可以根据const来进行区分,const这里缩小了权限,只能进行读操作。
这里会使用即可。
rbegin() / rend()
rbegin()/rend()是用来反向遍历的。
string::reverse_iterator rit = s2.rbegin();
while (rit != s2.rend())
{
//*rit += 1;
cout << *rit << " ";
++rit;
}
cout << endl;
注意观察这里使用的是反向迭代器,但是往后遍历的时候,使用的是 ++it,这也说明了 * 不是指针的解引用。
3.范围for()
这个前面已经讲解过,这里不做过多阐述,范围for()的底层是迭代器
// 范围for
for (auto ch : s)
{
cout << ch << " ";
}
4.at() 与 [ ]
at()与 opreator[ ] 的功能相同,他们的不同点在于二者对于越界访问的处理不同
operator[ ] 是断言处理,会直接终止程序
at() 是try-catch捕获异常
5.back() / front()
front()返回字符串的首字符,back()返回字符串的结尾字符。
3.String类对象的容量操作
3.1 size() / length()
size() 与 lenght() 都是返回字符串有效字符的长度,即从字符串开始到 '/0' 前的有效字符个数,注意这两个都不包含 '/0' 。
string s = "abcdef";
cout << s.size() << endl;
cout << s.length() << endl;
运行结果,显而易见二者的功能相同。
size()和length() 是等价的,二者可以用来获取字符串的长度。二者返回相同的结果,但是size()更符合标准库的容器类。
string这里可以使用length(),但是STL中的容器类有很多,根据字面意思,length更适合长度,但是后面有树这样的非线性的数据结果,使用length多少有点不合适,所以size()更符合标准库的容器类。
3.2 capacity()
capacity()用于返回当前字符串分配的空间大小。即其能够容纳的字符数量而不必立即重新分配内存
字符串当前分配的空间大小,并不一定等于字符串当前的字符长度。同时二者均不计算最后一个字符后的 '/0' 。capacity() 返回字符串当前分配的内存空间大小,单位为字符数。即使字符串为空,该值也可能大于零,因为字符串通常预先分配内存以提高效率。
string s = "abcdef";
cout << s.size() << endl;
cout << s.capacity() << endl;
自动扩容
当size()达到capacity()时,字符串会自动扩展内存,此时的capacity()会增加。但是不同的编译器实现的机制不同,扩容的方式也不同。比如 VS 是 1. 5 倍扩容,但是Linux下的g++是 2 倍扩容。
3.3 empty()
用于判断字符串是否为空。如果字符串中没有任何字符,包括没有终止空字符(
'\0'
),则返回true
。不需要参数直接调用。
这里只了解如何使用即可。
3.4 clear()
用于清空字符串内容,但一般情况下不会清理内存空间。
string s = "abcdef";
cout << s.size() << endl;
cout << s.capacity() << endl;
s.clear();
cout << s.size() << endl;
cout << s.capacity() << endl;
3.5 shrink_to_fit()
shrink_to_fit()
会尝试将字符串的内存容量(capacity()
)调整为刚好等于当前字符串的大小(size()
)。这样可以释放之前预留的未使用的内存空间,优化内存使用。同时shrink_to_fit 是 异地缩容,即它缩容时会另外开辟一块新的空间。因为释放空间不能从字符串中间开始释放,只能从起始指针处释放。
3.6 reserve()
reserve(n)
请求将字符串的容量(capacity()
)调整为至少n
个字符,以容纳未来的扩展,避免频繁的内存重新分配。调用此函数不会改变字符串的内容或长度(size()
),仅调整底层内存的容量。
reserve()扩容分为三种情况:
1. n < size() < capacity()
2. size() < n < capacity()
这两种情况不一定会缩容,主要看编译器如何解释。
3.size() < capacity() < n
一定会扩容。·
注意:无论是哪种情况,都不会改变字符串的内容。
string s = "abcdef";
cout << s.capacity() << endl;
s.reserve(1);
cout << s.capacity() << endl;
cout << s << endl;
3.7 resize()
resize()会将字符串的长度size()显式调整为 n 。
// resizing string
#include <iostream>
#include <string>
int main ()
{
std::string str ("I like to code in C");
std::cout << str << '\n';
unsigned sz = str.size();
str.resize (sz+2,'+');
std::cout << str << '\n';
str.resize (14);
std::cout << str << '\n';
return 0;
}
COUT
I like to code in C
I like to code in C++
I like to code
这里跟reserve()一样分多种情况。
4. string类对象的修改操作
1. push_back()
在字符串后尾插一个元素。
代码演示
string s = "abcdef";
s.push_back('1');
cout << s << endl;
注意这里的参数只有一个char 类型的字符,写入 '12'只会保留最后一个字符。
2.append()
将指定内容追加到字符串的末尾,它会修改字符串的长度,返回自身(*this)的引用。
这里的append()有多种类型重载,使用时查看文档即可
代码演示
// 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";
// used in the same order as described above:
str.append(str2); // "Writing "
str.append(str3,6,3); // "10 "
str.append("dots are cool",5); // "dots "
str.append("here: "); // "here: "
str.append(10u,'.'); // ".........."
str.append(str3.begin()+8,str3.end()); // " and then 5 more"
str.append<int>(5,0x2E); // "....."
std::cout << str << '\n';
return 0;
}
3.operator+=()
在字符串后追加内容,这个使用比较常见。
比如我们常见的数值 += 操作,此处功能相同。
// 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;
}
4.c_str()
返回C格式的字符串。
这里的功能主要与语言的兼容有关,比如平时完成C++项目工程时,一般来说不是全部使用C++完成,会有一部分内容使用C语言完成 ,如果C内容处想从C++内容处获取某些字符串,直接获取就不行的,这里便要使用c_str()来完成转换。
5.find()
查看指定内容在字符串中的位置。根据重载函数不同功能也不同。
代码演示
// 从pos位置开始查看 s1 在 s中的位置
string s = "abcdefgh";
string s1 = "ab";
cout << s.find(s1, 0);
如果说指定内容没找到会怎么样?
// 从pos位置开始查看 s1 在 s中的位置
string s = "abcdefgh";
string s1 = "ab";
cout << s.find(s1, 1);
返回了 npos。
6.rfind()
与find()功能相同,不过是从后往前进行查找。
rfind()的使用场景举例:要得到某个文件的后缀,这时从后往前查找更快。
7.substr()
从指定位置开始,截取字符串 len长度的目标串
代码演示
// string::substr
#include <iostream>
#include <string>
int main ()
{
std::string str="We think in generalities, but we live in details.";
// (quoting Alfred N. Whitehead)
std::string str2 = str.substr (3,5); // "think"
std::size_t pos = str.find("live"); // position of "live" in str
std::string str3 = str.substr (pos); // get from "live" to the end
std::cout << str2 << ' ' << str3 << '\n';
return 0;
}
注意:
1. 在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差
不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可
以连接字符串。
2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留
好。
5 string类的非成员函数
1.getline()
获取一行字符串。
平时我们使用 cin 时,遇到空格和换行就是遇到了终止符,如果我们想要输入的字符串带有空格等就会比较麻烦。
getline()就解决了这一问题,两个函数的第一个参数都是输入流对象,即 cin ,第二个是指定字符串,第一个式子中的第三个参数是指定终止符,不填的话默认是 ' /n'。
// extract to string
#include <iostream>
#include <string>
int main ()
{
std::string name;
std::cout << "Please, enter your full name: ";
std::getline (std::cin,name);
std::cout << "Hello, " << name << "!\n";
return 0;
}
2.relational operators
即是把字符串相关的运算符进行了全局类型的重载,这里不重载为成员函数的原因是成员函数自带一个自身的this指针,这样就定死了一个操作数是string类对象,而且使用时格式上也会比较怪异。
想了解这方面的知识 类的默认成员函数-优快云博客