一 什么是STL?
STL,全称是标准模板库(Standard Template Library),是C++标准库的一部分。STL提供了一些常用的数据结构和算法,帮助开发者更方便、高效地编写代码。通过使用STL,开发者不需要重复造轮子,可以直接使用已经优化好的组件,提高了代码的可读性和可维护性。
二 STL的六大组件
1. 容器(Containers)
容器是用来存储和管理一组数据的集合。STL提供了几种不同类型的容器,适用于不同的场景:
-
序列式容器:存储数据的顺序很重要。
vector
:动态数组,可以自动调整大小。list
:双向链表,适用于频繁插入和删除操作。deque
:双端队列,支持在两端高效地插入和删除。
-
关联式容器:根据键值对数据进行管理。
set
:集合,存储不重复的元素,并自动排序。map
:映射,存储键值对,键不重复,自动排序。unordered_set
和unordered_map
:基于哈希表实现,不自动排序,但操作更快。
2. 迭代器(Iterators)
迭代器是一种对象,允许我们遍历容器中的元素,而无需了解容器的内部实现。可以将迭代器看作是指向容器中元素的指针。常见的迭代器类型有:
- 输入迭代器
- 输出迭代器
- 前向迭代器
- 双向迭代器
- 随机访问迭代器
3. 算法(Algorithms)
STL提供了许多常用的算法,可以直接作用于容器的数据上。这些算法大大简化了代码的编写和维护。例如:
sort
:排序find
:查找replace
:替换count
:计数for_each
:遍历每个元素
4. 函数对象(Function Objects 或仿函数 Functors)
函数对象是重载了operator()
的类。它们可以像函数一样调用,并且可以保存状态。例如,用于自定义排序规则的函数对象。
5. 适配器(Adapters)
适配器是一种修改容器或函数对象接口的工具,使其适应不同的需求。例如:
- 容器适配器:
stack
(栈)、queue
(队列)、priority_queue
(优先队列)。 - 函数适配器:可以将普通函数或函数对象转换为不同的形式,以适应特定的需求。
6. 分配器(Allocators)
分配器负责管理内存的分配和释放。STL容器默认使用标准的动态内存分配器,但可以根据需要自定义分配器。
三 STL的重要性
STL的设计哲学是通用性和高效性,它极大地提升了C++编程的效率和灵活性。通过使用STL,开发者可以避免重复造轮子,直接使用已经高度优化的组件,编写出更高效、更易维护的代码。
四 string
定义:
C++ 标准库提供了一个名为 std::string
的类,用于处理字符串。string
类是 std
命名空间的一部分。
string
类对象的常见构造
#include <string>
using namespace std;
int main()
{
// 1 string();
string s1;//默认构造函数
cout << s1 << endl;
// 2 string(const char* s);
string s2("hello world");//C-string 构造
cout << s2 << endl;
// 3 string(const string& s);
string s3(s2);//拷贝构造
cout << s3 << endl;
// 4 string(const string& str, size_t pos, size_t len = npos);
string s4(s3, 0, 30);//部分字符串构造
cout << s4 << endl;
// 5 string(const char* s, size_t n);
string s5("hello", 3);//前 N 个字符构造
cout << s5 << endl;
// 6 string(size_t n, char c);
string s6(10, 'x'); //重复字符构造
cout << s6 << endl;
return 0;
}
输出:
在上面我们已经知道怎么构造了,现在我想和C语言一样遍历字符数组那C++中的string类该怎么实现呢?
string对象遍历和修改:
string s6("World");// C-string 构造
for (int i = 0; i < s6.size(); i++)
{
cout << s6[i] << " ";
}
cout << endl;
在上面代码中我们靠size()成员函数来获取字符串个数(和C语言一样不包含'\0'),然后在for
循环中,使用了s1[i]
来访问字符串s1
中位置为i
的字符。这里用到了string
类的operator[]
操作符。这个操作符允许我们像访问数组元素一样访问字符串中的字符。
遍历方式:
1 重载操作符operator[]
在string
类中,operator[]
被重载用来访问字符串中的字符
char& operator[] (size_t pos);
这个重载操作符的声明表示它返回一个对char
类型的引用,参数pos
是一个size_t
类型的值,表示字符在字符串中的位置。
这时我就有个疑问,为什么我在for循环中传过去的是数字而返回来的确实字符呢?
因为 operator[]
用数字 0
查找字符串中第 0 个位置的字符 这个过程类似于数组的访问,但实际上是通过函数调用来实现的,而且operator[] 返回的是字符的引用,所以我们不仅可以读取这个字符,还可以对它进行修改(也就是说我返回的是W的引用 代码就是char& tmp = W,然后改变tmp就是改变w)。
总结:
- 数组访问 :访问数组元素时,是通过指针偏移和解引用来实现的。例如,
arr[i]
实际上是*(arr + i)
,即从指针arr
偏移i
个元素后得到的地址的值。 - 字符串访问 :在
string
类中,operator[]
是一个函数调用。每次使用s1[pos]
时,实际上是调用了这个函数来返回字符串中位置pos
的字符的引用。
2 迭代器 iterator
迭代器是一种对象,允许程序员遍历容器(如数组、链表、树等数据结构)中的元素,而无需了解容器的内部结构。它们类似于指针,可以指向容器中的元素,并提供访问和操作这些元素的方法。
#include <iostream>
#include <string> //允许你在代码中省略std::前缀来直接使用标准库中的名称
using namespace std;
int main() {
string s1("hello world");
string::iterator it1 = s1.begin(); //定义迭代器
while (it1 != s1.end()) {
cout << *it1 << " ";
it1++;
}
return 0;
}
为什么在上面我定义了using namespace std;可在定义迭代器是还是要写呢?
这是因为在C++中,迭代器(iterator)是容器(如std::vector
、std::string
等)的一部分。每个容器都有自己的迭代器类型定义在其类内。这意味着iterator
是std::string
类的一个嵌套类型,因此必须通过string::iterator
来访问它。
在主函数中**string::iterator it1
** :声明一个it1
变量,其类型是string::iterator
,它是一个迭代器类型,可以用于遍历std::string
对象的字符。然后begin的作用是返回第一个有效位置的迭代器,让it1指向第一个有效元素的迭代器,这就有点类似于C语言里面的指针,但是指针是一个内存地址,可以指向任何类型的变量但是迭代器是一个对象只能指向容器中的元素,通过解引用操作符*
,可以访问迭代器指向的元素。下面while循环也没什么好讲的,end就和begin是相对的,begin指向第一个那end就是指向'\0'。
总结:
迭代器提供了统一的接口来遍历不同类型的容器(如数组、链表、树等),不需要知道容器的内部实现。
迭代器可以用来访问和修改容器中的元素,支持向前和向后移动(在支持的容器中),使得遍历操作更加灵活。
对于链表等数据结构,无法使用操作符 []
进行访问,必须使用迭代器。
3 反向迭代器
反向迭代器是一种特殊的迭代器,用于从容器(如字符串、向量、列表等)的末尾向前迭代,而不是从头到尾。
#include <iostream>
#include <string>
using namespace std;
int main() {
string s1("hello world");
string::reverse_iterator rit = s1.rbegin(); //定义反向迭代器
while (rit != s1.rend()) {
cout << *rit << " ";
rit++;
}
return 0;
}
rbegin()
: 返回一个指向容器最后一个元素的反向迭代器。rend()
: 返回一个指向容器第一个元素之前位置的反向迭代器。
4 范围for
在讲范围for之前我们需要了解一下auto关键字
auto
关键字让编译器自动推断变量的类型。
string s1("hello world");
for (auto e : s1)
{
cout << e << " ";
}
cout << endl;
首先e是用来存储每次迭代时从s1中获取的元素而s1就是要遍历的范围。auto e
意味着e
的类型将由s1
的元素类型决定。
常见成员函数:
begin
虽然在上文中我们已经提到它的用法但是在这里还是要重新提起
用法:
string s1("hello world");
string::iterator it = s1.begin();
cout << *it << endl;
输出:
iterator与const_iterator和
const iterator的区别:
为了帮助你更好地理解 C++ 中的 iterator
、const_iterator,
我们可以借助 C 语言中的 const int* p2
和 int* const p2
的概念。让我们首先复习一下这两个指针的区别,然后再将其应用到 C++ 迭代器的理解上。
const int* p2
:指向常量整数的指针。你不能通过这个指针修改它所指向的值,但可以改变指针本身,使其指向另一个地址。
const int* p2;
int a = 10;
int b = 20;
p2 = &a; // 可以
*p2 = 30; // 错误,不允许通过 p2 修改 a 的值
p2 = &b; // 可以
int* const p2
:常量指针,指向整数。你可以通过这个指针修改它所指向的值,但不能改变指针本身使其指向另一个地址。
int* const p2;
int a = 10;
p2 = &a; // 需要在初始化时赋值
*p2 = 30; // 可以,通过 p2 修改 a 的值
p2 = &b; // 错误,不允许改变 p2 指向的地址
1: iterator
类似于 int* p2
,即普通的指针。你可以通过它修改容器中的元素,并且可以改变迭代器本身使其指向容器中的不同位置。
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int>::iterator it = vec.begin();
*it = 10; // 可以,修改元素
++it; // 可以,改变迭代器的位置
2: const_iterator
类似于 const int* p2
,即指向常量的指针。你不能通过它修改容器中的元素,但可以改变迭代器本身使其指向容器中的不同位置。
std::vector<int> vec = {1, 2, 3, 4, 5};
std::vector<int>::const_iterator cit = vec.begin();
// *cit = 10; // 错误,不能修改元素
++cit; // 可以,改变迭代器的位置
3: const iterator
类似于 int* const p2
,即常量指针。迭代器本身是常量,不能改变它指向的元素位置,但可以通过它修改容器中的元素。
const iterator 类似于 int* const p2,即常量指针。迭代器本身是常量,不能改变它指向的元素位置,但可以通过它修改容器中的元素。
总结:
当 const
出现在 *
左边时,它修饰的是指向的值。
当 const
出现在 *
右边时,它修饰的是指针本身。
iterator可修指可修值 , const_iterator只修指向不修值 , const iterator只修修值不修指向
end
用法:
string s1("hello world");
string::iterator it = s1.end() - 1;
cout << *it << endl;
输出:
size
用法:
string s1("hello world");
cout << s1.size() << endl;
输出:
capacity:
用法:
string s1("hello world");
cout << s1.capacity()<< endl;
输出:
size:告诉你箱子里目前有多少东西(字符串的实际长度)
capacity:告诉你这个箱子最多可以装多少东西而不需要换一个更大的箱子(不需要重新分配内存的最大容量)。
clear
用法:
清空字符串内容,使其变成一个空字符串。
string s1("hello world");
s1.clear();
empty
用法:
检查字符串是否为空。如果字符串为空,返回 true
,否则返回 false
。
string s1("hello world");
if (s1.empty())
{
cout << "ture" << endl;
}
else
cout << "flase" << endl;
输出:
reserve
用法:
预留一定的内存空间,以避免将来添加更多字符时频繁重新分配内存。
string s1("hello world");
s1.reserve(50); // 预留至少可以存储 50 个字符的空间
resize
用法:
改变字符串的大小。如果新大小比原来大,用指定字符填充新增加的部分;如果比原来小,字符串被截断。
std::string s1("hello");
s1.resize(10, 'x');
cout << s1 << endl;
s1.resize(3);
cout << s1 << endl;
输出:
push_back
用法:
在字符串末尾添加一个字符。
string s1("hello world");
s1.push_back('!');
cout << s1 << endl;
输出:
append
用法:
在字符串末尾添加另一个字符串。
string s1("hello world");
s1.append(" abcdefj");
cout << s1 << endl;
输出:
operate+=
用法:
在字符串末尾追加另一个字符串或字符。
string s1("hello world");
s1+= "s";
cout << s1 << endl;
string s2("hello");
s2 += " world";
cout << s2 << endl;
输出:
insert和erase
用法:
insert
: 在指定位置插入字符或字符串。
string s1("hello world");
s1.insert(5, "*beautiful");
cout << s1 << endl;
输出:
erase
: 删除指定位置的字符或一段字符串。
std::string s1("hello world");
s1.insert(5, ", beautiful"); // 在位置 5 插入 ", beautiful",现在 s1 是 "hello, beautiful world"
s1.erase(5, 11); // 从位置 5 开始删除 11 个字符,现在 s1 是 "hello world"
cout << s1 << endl;
输出:
replace
用法:
替换字符串中的一部分为另一个字符串。
std::string s1("hello world");
s1.replace(6, 1, "everyone");//从6开始的1个字符替换为字符串"everyone"
cout << s1 << endl;
输出:
find
用法:
查找字符串中第一次出现的子字符串或字符的位置。如果没有找到,返回 std::string::npos
。
std::string s1("hello world");
size_t pos = s1.find("n");
if (pos != std::string::npos)
{
std::cout << "找到了"<< pos << std::endl;
}
else
{
std::cout << "没找到" << std::endl;
}
std::string s1("hello world");
size_t pos = s1.find("world");
if (pos != std::string::npos)
{
std::cout << "找到了"<< pos << std::endl;
}
else
{
std::cout << "没找到"<< std::endl;
}
输出: