本文将接着上文(03 黑马C++_泛型编程-CSDN博客),继续更新本人在学习黑马C++过程所整理的笔记(本文序号将继续承接上文)。
本文的使用方法:可以将本文作为一个工具书来使用,当编写程序或者阅读程序遇到不熟悉的知识点的时候,可以用 CTRL+F 按键 搜索关键词,查看该知识点。如:当忘记结构体定义方法,需不需要在括号后加分号,就可以 CTRL+F 搜索 “结构体”关键字,即可跳转到所需内容处。相较于使用 C++ Primer 感觉更加快速、便捷,希望对大家有所帮助。
为了建立数据结构和算法的一套标准,诞生了 STL,提高代码复用性
STL (Standard Template Library)
- STL 分为三大类 (容器和算法之间通过迭代器进行无缝连接)
- 容器(container)
- string:字符串
- vector:数组
- deque:双端数组、双向队列
- stack:栈(先进后出)
- queue:队列(先进先出)
- list:链表(随意插入)
- set/multiset:集合(自动排序)
- map/multimap:映射(键,值)高性能高效率
- 算法(algorithm)
- 迭代器(iterator)
6.1 基本概念
6.1.1 定义
-
容器
-
算法
-
迭代器 (便于访问元素,常用双向迭代器、随机迭代器)
6.1.2 迭代器
#include <vector>
// 创建容器
vector<int> v;
v.push_back(1);
v.push_back(2);
// 创建迭代器
vector<int>::iterator itBegin = v.begin(); // 指向第一个元素
vector<int>::iterator itEnd = v.end(); // 指向最后一个元素的下一个位置
6.1.3 输出显示
- for循环
for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << *it << endl; // 技巧:*it解出来的数据类型就是迭代器定义尖括号里的数据类型
}
- STL遍历算法
#include <algorithm> // 需要包含头文件
// 输出回调函数
void myPrint(int val)
{
cout << val << endl;
}
for_each(v.begin(), v.end(), myPrint);
6.1.4 容器的嵌套
// 定义二维容器
vector<int> a, b, c;
for (int i = 0; i < 3; i++) {
a.push_back(i+1);
b.push_back(i+2);
c.push_back(i+3);
}
vector<vector<int>> aa;
aa.push_back(a);
aa.push_back(b);
aa.push_back(c);
// 输出显示
for (vector<vector<int>>::iterator it = aa.begin(); it != aa.end(); it++) {
// 此时,(*it) 相当于 vector<int>
// 法一
for_each((*it).begin(), (*it).end(), myPrint);
// 法二
for (vector<int>::iterator vit = (*it).begin(); vit != (*it).end(); vit++) {
cout << *vit << " ";
}
cout << endl;
}
6.1.5 容器作为形参
- 函数传入参数为容器时,如果要防止传入实参在函数中被修改,可以加上 const,但是加上 const 的容器,在使用迭代器时需要使用 const_iterator,例:
void printVector(const vector<int>& v)
{
// const 防止实参被修改,此时 v.begin() 返回 const_iterator,所以需要定义 const_iterator 类型的迭代器
for (vector<int>::const_iterator it = v.begin(); it != v.end(); it++) {
cout << *it << " ";
}
cout << endl;
}
6.2 string 容器
6.2.1 构造函数
// 1. 默认构造
string s1;
// 2. 有参构造
const char* str = "hello world"; // 注意,字符串赋值的方法,前面加个 const
string s2(str);
// 3. 拷贝构造
string s3(s2);
// 4. n 个字符初始化
string s4(5, 'a');
注意:给 char* str 直接赋值字符串,需要加上 const !!!
- 给 char* 赋值的另外一种方法
char* str1 = NULL;
str1 = (char*)malloc(1024); // 申请一块 1024 的连续内存空间,并返回 char* 指针
for (int i = 0; i < 5; i++) { // 赋值
str1[i] = 'a'+i;
}
6.2.2 赋值操作
6.2.3 字符串拼接
6.2.4 string 查找和替换
- find :从左往右查找 ; rfind : 从右往左查找
- 若 find 没找到,则返回 string::npos
- 注意,替换输入了多少都会全替换上
string str3("hello");
// 从 1号位置起 3 个字符,都替换成 1111
str3.replace(1, 3, "1111"); // hello -> h1111o,四个1都加入
6.2.5 字符串比较
按照 ASCLL 码进行对比,相等:return 0; 大于: return 1; 小于: return -1;
6.2.6 字符存取
- 重载了[ ],可以通过 [ ] 进行索引
- 也可以通过 at 访问获取字符
注意:二者的区别在于,operator []不做边界检查, 哪怕越界了也会返回一个引用,当然这个引用是错误的引用,如果不小心调用了这个引用对象的方法,会直接导致应用退出。 而由于at会做边界检查,如果越界,会提示异常错误。
6.2.7 插入和删除
6.2.8 子串获取
6.3 vector 容器
也称为单端数组,不同于数组的是vector空间可以动态拓展(并不是在原有的空间之后继续接新空间,而是找更大的内存空间,然后将原数据拷贝到新空间)
- 迭代器是支持随机访问的迭代器
6.3.1 构造函数
// 默认构造
vector<int> v1;
// 利用 v1 区间中的元素拷贝
vector<int> v2(v1.begin(), v1.end());
// 10 个 100
vector<int> v3(10, 100);
// 拷贝构造
vector<int> v4(v3);
6.3.2 赋值操作
6.3.3 容量和大小
6.3.4 插入和删除
6.3.5 数据存取
6.3.6 互换容器
swap( vec ) 将 vec 与本身元素互换
实际用途:可以收缩内存空间
vector<int> v;
for (int i = 0; i < 100000; i++) { // 存入100000个数
v.push_back(i);
}
cout << v.size() << endl; // 100000
cout << v.capacity() << endl; // 138255
// 重新指定大小为3
v.resize(3);
cout << v.size() << endl; // 3
cout << v.capacity() << endl; // 138255,此时容量还是13万多,造成内存浪费
// 巧用 swap 收缩内存
vector<int>(v).swap(v);
// vector<int>(v) 利用v拷贝构造创建一个匿名对象,此时的匿名对象容量、大小均为 3 ;
// 再调用swap交换,使得v的大小和容量都为匿名对象的3、3;而匿名对象则为3、138255,但是匿名对象在当前行代码执行完自动回收
cout << v.size() << endl; // 3
cout << v.capacity() << endl; // 3
6.3.7 预留空间
减少 vector 在动态拓展容量时的拓展次数
- 统计 vector 开辟了多少次内存才把 10万 个数据存入
vector<int> v;
int count = 0; // 统计开辟次数
int* p = NULL;
for (int i = 0; i < 100000; i++) {
v.push_back(i);
// 当指针不指向第一个位置,即增大容量开辟了内存
if (p != &v[0]) {
count++;
p = &v[0]; // 指向新地址
}
}
cout << count << endl; // 30次
若直接在 v.reserve(100000); 则 count = 1,只需一开始开辟一次即可。
6.4 deque 容器
双端数组,可以对头端、尾端进行插入删除操作
- 迭代器是支持随机访问的迭代器
6.4.1 构造函数
6.4.2 赋值操作
6.4.3 容量和大小
- deque 没有容量的概念
6.4.4 插入和删除
6.4.5 数据存取
6.4.6 排序操作
- 需要包含标准算法头文件 algorithm
- 默认为升序,从小到大排序
- 对于支持随机访问的迭代器容器,都可以用 sort 排序,比如 vector 容器也有sort
6.5 stack 容器
6.5.1 基本概念
- 先进后出
- 栈不允许有遍历的行为,只能访问栈顶元素
6.5.2 常用接口
6.6 queue 容器
6.6.1 基本改概念
- 先进先出
- 队列中只有队头和队尾才可以被外界访问,不允许有遍历行为
6.6.2 常用接口
6.7 list 容器
- 物理存储非连续,数据元素的逻辑顺序是通过链表中的指针链接实现。
- 由一系列结点组成,结点的组成:存储数据元素的数据域 + 存储下一个结点地址的指针域
- 单链表:指针域只能指向节点的下一个节点;
- 双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点;
- 循环链表:链表首尾相连(是属于单链表,只有next)
6.7.1 基本概念
- 由于链表的存储方式并不是连续的内存空间,因此链表中的迭代器只支持前移和后移,不能随机访问,属于 双向迭代器
- 因为 vector 容器插入删除会重新分配内存空间,所以迭代器会失效, 而list不会
- STL 中 list 和 vector 是两个最常用的容器
C/C++的定义链表节点方式,如下所示:
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
6.7.2 构造函数
6.7.3 赋值和交换
6.7.4 容量和大小
6.7.5 插入和删除
- 新增了一个 remove,可以删除所有匹配的值
6.7.6 数据存取
- 注意:内存存放不是在连续的物理空间,且双向迭代器不支持随机访问,此时不可以通过 [ ] 和 at 跳跃性地访问元素
- 验证迭代器是否可以随机访问的方法:
list<int> l;
list<int>::iterator it = l.begin();
it++;it--; // 可以运行,双向迭代器支持前移后移
it = it + 1; // 报错,不可以随机访问
6.7.7 反转和排序
- 注意,只能使用 list 类的排序成员函数sort,而不能使用 algorithm 头文件中的 sort 排序函数,所有不支持随机访问迭代器的容器,不可以使用标准算法
vector<int> v;
sort(v.begin(), v.end()); // 调用 algorithm 标准函数
list<int> l;
//sort(l.begin(), l.end()); // 报错,不支持双向迭代器使用标准算法
l.sort(); // 只能使用内置的成员函数
- list 的 sort 函数默认升序排列, 若想改为降序, 可以提供函数或者仿函数来改变排序规则
// 排序的回调函数
bool myCompare(int v1, int v2)
{
return v1 > v2; // 让第一个数 大于 第二个数
}
l.sort(myCompare); // 此时排序结果为降序
6.7.8 排序案例
- 自定义数据类型的高级排序
// 排序回调函数
bool comparePerson(Person p1, Person p2)
{
if (p1.age != p2.age)
return p1.age < p2.age; // 年龄升序
else
return p1.height > p2.height; // 年龄相同则身高降序
}
list<Person> L;
Person p1("张三", 35, 175);
Person p2("李四", 32, 180);
Person p3("王五", 33, 170);
Person p4("赵六", 35, 195);
L.push_back(p1);
L.push_back(p2);
L.push_back(p3);
L.push_back(p4);
L.sort(comparePerson); // 排序
6.8 set/multiset 容器
关联式容器,在插入时自动排序(升序),底层结构式用二叉树实现的
set 容器配置的迭代器类型为==双向迭代器==
6.8.1 构造和复制
6.8.2 容量和大小
- 没有 resize,不允许重新指定大小
6.8.3 插入和删除
- 没有尾插 push_back 等等,只有一个 insert
6.8.4 查找和统计
- count 对于 set 而言没有意义,结果只能是 0/1
6.8.5 set 和 multiset 的区别
- 在使用时,直接包含** set 头文件**,即可使用这两个
- s.insert(10) 的返回值为对组 pair<iterator, bool>。第一个值为迭代器,表示插入值的位置;第二个值为插入数据是否成功的标志。
set<int> s;
// 定义一个对组来接收 insert 的返回值,从而判断是否插入成功
pair<set<int>::iterator, bool> ret = s.insert(10);
if (ret.second) { // 使用 second 索引第二个参数
cout << "插入成功!" << endl;
}
else {
cout << "插入失败!" << endl;
}
6.8.6 排序
利用仿函数(重载函数调用运算符),可以改变排序规则
- 内置类型排序
// 指定排序规则仿函数
class myCompare
{
public:
bool operator()(int v1, int v2) const { // 注意,要加上const
return v1 > v2; // 降序
}
};
// 修改排序规则从大到小,需要在插入数据前、创建容器时指定
set<int, myCompare> s1; // 传入仿函数
s1.insert(10);
s1.insert(30);
s1.insert(20);
// 声明迭代器,也需要传入仿函数创建专属迭代器
for (set<int, myCompare>::iterator it = s1.begin(); it != s1.end(); it++) {
cout << (*it) << " "; // 输出显示
}
- 自定义类型排序
// 自定义数据类型
class Person
{
public:
Person(string name, int age) {
this->name = name;
this->age = age;
}
string name;
int age;
};
// 指定排序规则仿函数
class comparePerson
{
public:
bool operator()(const Person& p1, const Person& p2) const { // 注意要加个 const
return p1.age > p2.age; // 年龄降序
}
};
// 自定义数据类型,需要指定排序规则,否则 insert 时报错
set<Person, comparePerson> s1; // 传入仿函数
Person p1("张三", 24);
Person p2("李四", 22);
Person p3("王五", 25);
Person p4("赵六", 12);
// 插入数据
s1.insert(p1);
s1.insert(p2);
s1.insert(p3);
s1.insert(p4);
// 输出显示 - 声明迭代器,也需要传入仿函数创建专属迭代器
for (set<Person, comparePerson>::iterator it = s1.begin(); it != s1.end(); it++) {
cout << it->name << " " << it->age << endl;
}
- 注意:自定义数据类型插入 set 容器中时,需要指定排序规则,否则 insert 时报错
6.9 map/multimap 容器
- 所有的元素都是 pair(对组),第一个元素为key(键值),起到索引作用;第二个元素为value,为实值。
- 所有的元素都会根据元素的key键值自动排序
- 本质: 属于关联式容器,底层结构采用 二叉树 实现
- 优点:可以根据 key 值快速找到 value 值,实现快速索引
- map/multimap 区别:map 不允许有重复的 key 值
6.9.1 构造和赋值
void printMap(const map<int, int>& m)
{
for (map<int, int>::const_iterator it = m.begin(); it != m.end(); it++) { // 用 first 和 second 索引
cout << it->first << " " << it->second << endl;
}
}
map<int, int> m;
// 1. 创建对组对象
pair<int, int> x1(1, 10);
m.insert(x1);
// 2. 匿名对组
m.insert(pair<int, int>(2, 20));
// 3. 直接用 mak_pair
m.insert(make_pair(3, 30));
// 4. value_type
m.insert(map<int, int>::value_type(4, 40));
// 5. [] - 不建议使用
m[4] = 40;
// 输出显示
printMap(m);
6.9.2 大小和容量
6.9.3 插入和删除
erase 之后,返回迭代器指向的下一个值 iter = vecInt.erase(iter);
6.9.4 查找和统计
6.9.5 排序
class myCompare
{
public:
bool operator()(int v1, int v2) const {
return v1 > v2; // 按照降序排列
}
};
map<int, int, myCompare> m; // 需要传入排序仿函数
m.insert(make_pair(1, 10));
m.insert(make_pair(2, 20));
m.insert(make_pair(3, 30));
m.insert(make_pair(4, 40));
// 输出显示
for (map<int, int, myCompare>::const_iterator it = m.begin(); it != m.end(); it++) {
cout << it->first << " " << it->second << endl;
}
6.10 其他
6.10.1 pair 对组
成对出现的数据,利用对组可以返回两个数据
- 创建及使用
// 1. 创建方法一
pair<int, double> p1(14, 3.14);
// 2. 创建方法二
pair<string, int> p2 = make_pair("Tom", 18);
// 使用方法
cout << p2.first << " " << p2.second << endl;
6.10.2 函数对象
函数重载调用操作符的类(本质是一个类),其对象成为函数对象
函数对象使用重载()时,行为类似函数调用,也叫仿函数
特点:
- 函数对象不同于普通函数的地方在于,函数对象可以有成员属性,可以用来记录一些内部状态,可以有自己的状态
- 函数对象作为一个类,还可以作为参数进行传递
class myPrint
{
public:
int operator()(string test) { // 重载 ()
cout << test << endl;
this->count++;
return count;
}
int count = 0;
};
6.10.3 谓词
返回 bool 类型的仿函数成为 谓词
- operator() 只有一个参数,称为一元谓词;
- operator() 由两个参数,称为二元谓词
- 一元谓词 - 用于查找 vector 容器 find_if 中大于 5 的值
// 一元谓词
class greaterFive
{
public:
bool operator()(int val) {
return val > 5; // 查找大于 5 的值
}
};
vector<int> v;
for (int i = 0; i < 10; i++) {
v.push_back(i);
}
// 查找容器中大于 5 的数据,若查找到返回迭代器
vector<int>::iterator it = find_if(v.begin(), v.end(), greaterFive());
if (it != v.end())
cout << "找到了大于 5 的数据:" << *it << endl;
- 二元谓词 - 用于 vector 容器中 sort 降序排列
// 二元谓词
class sortDown
{
public:
bool operator()(int v1, int v2) {
return v1 > v2; // 降序
}
};
vector<int> v;
v.push_back(1);
v.push_back(4);
v.push_back(2);
v.push_back(3);
v.push_back(5);
// 降序排流
sort(v.begin(), v.end(), sortDown());
6.10.4 内建函数对象
需要引入头文件 functional
- 算数仿函数
例:
#include <functional>
negate<int> n;
cout << n(50) << endl; // 取反仿函数(一元),输出 -50
plus<int> p;
cout << p(10, 20) << endl; // 相加函数(二元),输出 30
- 关系仿函数
例:(使用内建函数对象改变 sort 排序)
sort(v.begin(), v.end(), greater<int>()); // 使用内建函数对象实现降序排序
- 逻辑仿函数
例:(把 v1 容器取反放到 v2容器中)
vector<bool> v;
v.push_back(true);
v.push_back(false);
v.push_back(true);
v.push_back(false);
v.push_back(true);
vector<bool> v2;
v2.resize(v.size()); // 搬运前需要先指定 v2 大小空间
// 使用搬运函数
transform(v.begin(), v.end(), v2.begin(), logical_not<bool>());
6.11 常用算法
6.11.1 遍历算法
for_each
for_each(iterator beg, iterator end, _func);
例(遍历输出 vector 容器):
// 普通函数
void print01(int val)
{
cout << val << " ";
}
// 仿函数
class print02
{
public:
void operator()(int val) {
cout << val << " ";
}
};
vector<int> v;
for (int i = 0; i < 10; i++) {
v.push_back(i);
}
// 调用普通函数遍历输出
for_each(v.begin(), v.end(), print01);
// 调用仿函数遍历输出
for_each(v.begin(), v.end(), print02());
transform
搬运容器数据
例:
class Transform
{
public:
int operator()(int val) {
return val; // 把数据原封不动地返回去
} // 也可以加个100或者取个反啥的再搬过去
};
vector<int> v;
for (int i = 0; i < 10; i++) {
v.push_back(i);
}
vector<int> v2;
v2.resize(v.size()); // 在搬运前目标容器需要开辟空间
transform(v.begin(), v.end(), v2.begin(), Transform());
6.11.3 查找算法
find
例(查找自定义类型):
// 自定义数据类型
class Person
{
public:
Person(string name, int age) {
this->name = name;
this->age = age;
}
// 需要重载 ==,让底层的 find 知道如何对比数据
bool operator==(const Person& p) {
if ((this->name == p.name) && (this->age == p.age))
return true;
else
return false;
}
string name;
int age;
};
vector<Person> v2;
Person p1("张三", 18);
Person p2("李四", 15);
Person p3("王五", 20);
v2.push_back(p1);
v2.push_back(p2);
v2.push_back(p3);
// 查找,注意在自定义类中重载 ==
vector<Person>::iterator itt = find(v2.begin(), v2.end(), p3);
if (itt != v2.end())
cout << "找到了" << itt->name << endl;
find_if
例:
vector<int> v;
for (int i = 0; i < 10; i++) {
v.push_back(i);
}
// 找出所有大于 5 的数
vector<int>::iterator it;
vector<int>::iterator start = v.begin();
do {
it = find_if(start, v.end(), greaterFive());
if (it != v.end()) {
cout << "找到了大于5的数:" << *it << endl;
start = it+1; // 从该位置的下一个位置开始查找
}
} while (it != v.end());
adjacent_find
binary_search
二分查找
count
count_if
6.11.4 排序算法
sort
random_shuffle
merge
合并后的容器也是有序的
注意:在合并之前,需要为目标容器开辟内存空间(可以用resize函数)
reverse
6.11.5 拷贝和替换算法
copy
注意,拷贝前需要为目标容器开辟空间
replace
replace_if
swap
6.11.6 算术生成算法
属于小型算法,使用时包含头文件 numeric
accumulate
fill
![[Pasted image 20230823212924.png]]
6.11.7 集合算法
注意,目标容器需要提前开辟空间
![[Pasted image 20230823213053.png]]
set_intersection
#include <algorithm> // 使用 min 需要包含头文件
v.resize(min(v1.size(), v2.size())); // 开辟两个容器中的最小大小
set_union
set_difference
至此,黑马C++相关文章已更新完毕。