学习STL的意义
STL即Standard Template Library(标准模板库),是C++的一部分 (C++ = C + 类 + 模板(STL), C++编译器都支持STL,不需要单独安装),不会STL不算C++程序员。
STL里面封装了很多经典的算法,再加上是基于模板的,适用于多种数据类型,某种程度上说是通用算法,所以 它在C++中的地位很高。
入门STL的痛点
1. 看书入门STL
像 《C++ primer plus》 这类书侧重于对语言语法知识的讲解,没有系统、全面讲解STL的具体用法、各个成员函数的使用。而且书籍贼厚,STL的知识点分布零散。想快速入门对新手来说不是很友好。 像 《STL源码剖析》 侧重讲解STL的底层原理,书中作者自序中就指出 此书不适合C++初学者, 不适合 Genericity(泛型技术)初学者,或STL初学者。
2. 看视频入门STL
像侯捷老师的 STL体系结构与内核分析 一类的视频,同样侧重于底层原理的讲解,不适合新手入门。
3. 通过在力扣、牛客等平台上做题入门STL
算法题需要低的时间复杂度和空间复杂度, STL是一种高效的模板库,如果你是主C++语言,STL的使用对于写算法题来说是很好的选择。如果不熟悉基本的STL的容器和STL的成员函数,连string、vector、list类都不会使用,你去刷算法题很可能就是连解题程序 数据是怎么装的,为什么能调用insert、sort等函数,这些成员函数怎么使用都不知道,当你几小时勉强搞懂一两道题的时候你可能连容器的空间结构都不知道。 花费的时间和收获不成正比,而且你会有挫败感,得到的是负反馈。
新手该如何正确、快速入门STL
1. 需要具备一定的基础
(1).C++语言基础(模板、类、运算符重载等)
(2).基本的数据结构基础(数组、链表、树、图等)
2. 需要对STL的各个容器的内存模型有所了解
(1).了解容器的内存模型有利于加深对各个容器的成员函数的理解,有利于对各个容器成员函数的记忆与使用。
(2).只有理解STL各个容器的内存模型才能在不同的场景下决定决定使用哪个容器效率高,使用哪个容器方便。
3. 对STL的各个容器进行测试,根据测试结果加深对STL的使用
(1).STL容器的构造函数,成员函数很多,并且有很多成员函数都有几个重载函数,如果不一一测试,进行对比分析,你很可能都不知道如何利用构造函数对容器进行初始化、成员函数怎么调用、常见的成员函数的参数都不知道怎么填。
(2).STL的容器的成员函数很多都类似,当深入了解一种以后再学习其他的容器就会发现成员函数的使用和容器的操作都类似,STL越学越简单。
学习STL的工具推荐
- 通过C++的参考网页能够很快查询到各个STL容器的成员函数的用法、函数的参数、返回值等,而且这些网站对于STL成员函数、用法都讲述的很全面。
参考网站1: cplusplus.com
参考网站2: cppreference.com
- C/C++参考: 这个手册拥有C/C++库,C++STL等,还是中文的文档,使用查询起来很方便。(缺点:不是很全面,只更新到了2006年,不支持C++11, 对新手入门足够)
STL的组成
STL可分为容器(containers)、迭代器(iterators)、空间配置器(allocator)、配接器(adapters)、算法(algorithms)、仿函数(functors)六个部分。它们之间的关系可通过下图来说明。
通过对STL的测试入门STL
1.String, 专门的字符串操作的一个类。内存模型为字符数组(只能存字符)。需要包含头文件<string> 。
下面从String的7个方面来测试String。
(1). String的定义。构造函数
#include <iostream>
#include <string>
using namespace std;
// 定义
void StrDefine()
{
string str; // 默认构造函数,创建string类型对象str
cout << str << "" << str.empty() << endl; // 输出str,empty函数测试str是否为空,为空时输出1,非空时输出0
string str1(5, 'a'); // 创建一个string类型为5个字符a的对象str1
cout << str1 << " " << str1.empty() << endl; // 输出str1,empty函数测试str1是否为空
string str2("abcdefg"); // 创建初值为"abcdefg"的对象str2,
cout << str2 << endl; // 输出str2
string str3("abcdefg", 3); // 创建"abcdefg"的长度为3str3
cout << str3 << endl; // 输出str3
string str4(str2, 2, 3); // 将str2从位置为2的地方拷贝3个字符到str4
cout << str4 << endl; // 输出str4
string str5(str2); // 拷贝构造函数,将string对象str2复制到str5
cout << str5 << endl; // 输出str5
}
int main()
{
StrDefine();
return 0;
}
(2). String的属性。size、resize、capacity、reserve、length
只给出测试函数,头文件和主函数略(后面的测试函数一样)
// 属性
void StrPro()
{
string str;
cout << str.size() << endl; // size函数用于返回字符串的字符的个数
string str1(17, 'a');
str1.reserve(78); // reserve函数用于改变string对象的容量,参数小于原容量时容量不变,大于原容量时,容量改变
cout << str1.size() << endl;
cout << str1.capacity() << endl;
string str2("abcdefg13215254345");
str2.resize(13); // resize函数改变字符串的大小,数值大于原长度时容量会扩大,小于原长不变
cout << str2.size() << endl;
cout << str2.length() << endl; // length函数用于返回string对象的字符串的长度
cout << str2.capacity() << endl;
cout << str2 << endl;
string str3("abcdefg", 3);
cout << str3.capacity() << endl; // 测试拷贝后的str3的容量
string str4(str2, 2, 3);
cout << str4.capacity() << endl; // 测试拷贝后的str4的容量
string str5(str2);
cout << str5.capacity() << endl; // 测试拷贝后的str5的容量
}
(3). String输出。直接输出、通过索引[]、通过at函数
// 输出
void StrCout()
{
string str2("abcdefg");
cout << str2 << endl; // 通过对象名直接输出
cout << str2.c_str() << endl; // 通过c_str()函数返回的字符串指针的输出
cout << str2[6] << endl; // 通过下标输出字符串中的元素
try
{
cout << str2.at(9) <<endl; // 通过at()函数输出,at()函数越界会输出异常
}
catch(out_of_range& a)
{
cout << "越界了" << endl;
}
}
(4). String的修改。通过下标运算[],与at函数,insert函数插入,append函数尾插入,assign赋值,erase删除
// 修改
void StrChange()
{
string str2("abcdefg");
str2[2] = 's'; // 通过下标运算修改字符串对象
cout << str2 << endl;
str2.at(3) = 'q'; // 通过at()函数修改字符串对象
cout << str2 << endl;
string str3("asfsa");
str2.insert(str2.length(), 3, '1'); // 通过insert函数插入3个字符1到str2的尾部
cout << str2 << endl;
str2 += str3; // 用+=运算将str2和str3两个字符串对象进行拼接
cout << str2 << endl;
str2 += "qwert"; // 用+=运算将str2和字符串常量"qwert"进行拼接
cout << str2 << endl;
str2.append(2, 'w'); // 通过append函数在str2尾部添加2个字符w
cout << str2 << endl;
str2 = str3; // 将str3赋值给str2(调用拷贝构造)
cout << str2 << endl;
str2.assign(str3); // 通过assign函数将str3赋值给str2
cout << str2 << endl;
str2.erase(2,2); // 通过erase函数删除str2索引为2长度为2的字符串
cout << str2 << endl;
}
(5). String的函数。find、substr、swap
// string的函数
void StrFunction()
{
string str2("abcefsd");
string str3("cefrwe");
char arry[6] = {0};
cout << (int)str2.find('b', 1) <<endl; // 通过find函数查找字符'b'是否在索引1的位置,是返回1,否返回0
cout << str2.substr(2, 3) << endl; // 通过substr函数返回索引为2开始长度为3的子串
str2.swap(str3); // 交换str2与str3
cout << str2 << endl;
cout << str3 << endl;
}
(6). String的迭代器。 本质为char* ,能够进行地址自加、*取值和下标运算
// string的迭代器
void FunIterator()
{
string str("abcdefg");
string::iterator ite; // 迭代器的类型为 string::iterator, 本质为char* ,能够进行地址自加、*取值和下标运算。
// char* a =str.c_str();
ite = str.begin(); // 对迭代器赋值,ite指向str的首元素
for(ite; ite != str.end()-2; ite++) // 通过迭代器对str内的字符进行遍历
{
*ite = 'a'; // 利用 *取值并对str内的元素进行赋值
cout << *ite << " "; // 通过 *取值输出str内的元素
}
str.append(18, 'a'); // 通过append函数在索引为18的地方添加字符a
ite[6] = 'w'; // 迭代器指向了索引为19的元素,通过迭代器索引赋值导致迭代器失效,会报错
cout << str;
for(int i=0; i<str.size(); i++) // 通过for循环遍历str内的元素,size函数返回str的字符个数
{
cout << *ite << " ";
ite++; // 迭代器向指str的索引向后移动一位
}
for(int i=0; i<str.size(); i++) // 通过for循环遍历str内的元素
{
cout << ite[i] << " "; // 通过下表运算输出str内的元素
}
}
(7). 测试String的迭代器。begin函数指向字符串的首元素指针,end函数返回指向字符串尾元素的后一个元素
// 测试string的迭代器
void Testiterator()
{
string str("abcfadffdfsafg");
string str1("efg");
str.append(str1.begin(), str1.begin()+3); // 通过append函数在str1尾部插入str1[0]到str1[3]这段字符串
cout << str << endl;
str.erase(str.begin()+2, str.begin()+5); // 通过erase函数删除str1[2]到str1[5]这段字符串
cout << str << endl;
string str2("zxcdfd");
str.insert(str.begin()+2, 3, 'A'); // 通过insert函数插入3个字符A到str[2]
cout << str << endl;
str.insert(str.begin()+2, str2.begin()+2, str2.begin()+5); // 通过insert函数从str[2]的位置插入str2[2]到str2[5]这段字符串
cout << str;
}
(8).测试string的两个算法。 for_each和sort函数(需包含头文件),可以用仿函数进行适配
// 普通函数
void fun(char c)
{
cout << c;
}
// 用string测试两个算法
void FunForeach()
{
string str("qwertyui");
for_each(str.begin(), str.end(), fun); // for_each函数用于遍历str的元素,并通过仿函数fun输出
cout << endl;
sort(str.begin(), str.end()); // sort函数对str内的字符进行排序(默认从小到大)
for_each(str.begin(), str.end(), fun);
cout << endl;
sort(str.begin(), str.end(), greater<char>()); // sort函数参数中添加了仿函数greater<char>()(需包含头文件<functional>)后,从大到小排序
for_each(str.begin(), str.end(), fun);
cout << endl;
}
2.Vector, Vector是一种可以存储任意类型的动态数组。内存模型为数组。需要包含头文件<vector>。
下面从5个方面来测试Vector。
(1).Vector的定义。
vector v1; vector保存类型为T的对象。默认构造函数v1为空。
vector v2(v1); v2是v1的一个副本。
vector v3(n,i); v3包含n个值为i的元素。
vector v4(n); v4含有值初始化的元素的n个副本。
(2).构造函数
void STLConstructor()
{
vector<int> vec;
// cout << vec[0]; // 错误,vector内无参数
vector<int> vec1(5, 2); // 创建一个存放整形数的vector容器vec1,初始化为5个整数2
vector<int> vec2(5, 23); // 创建一个存放整形数的vector容器vec2,初始化为5个整数23
vector<int> vec3(vec1); // 创建一个存放整形数的vector容器vec3,并将vec1的值赋值给它
/// =======================
vector<int>::iterator ite = vec2.begin(); //创建一个vector<int>类型的迭代器ite,并将其指向vec2的首元素
vector<int>::iterator ite1 = vec2.end(); //创建一个vector<int>类型的迭代器ite1,并将其指向vec2的尾元素的后一个元素
vector<int> vec4(ite, ite1); //创建一个vector容器vec4,并通过迭代器将vec2的数据赋值给vec4
for(int i=0; i<5; i++)
{
cout << vec4[i] << endl; //通过下标输出vec4内的元素
}
}
(3).Vector的属性
void STLCapacity()
{
vector<int> vec(3);
cout << vec.empty(); // empty函数的使用与String一样
vec.resize(13); // 重新设置vec元素的大小为13
vec.reserve(18); // 重新设置vec的内存空间大小为18
cout << vec.size() << endl;
cout << vec.capacity() << endl;
vector<int> vec1(5); // 分配给vec1的容量为5,默认的5个数值为0
cout << vec1.capacity() << endl; // 容量为5
vec1.push_back(1); // 向vec1尾部压入1
cout << vec1.capacity() << endl; // 输出容量为 10
vec1.push_back(1);
vec1.push_back(1);
vec1.push_back(1);
vec1.push_back(1);
vec1.push_back(1); // 向容器中再压入5个1
cout << vec1.capacity() << endl; // 输出容量为20, gun C vector存储满了之后,再次申请内存时容量将翻倍
}
(4).Vector的操作
void fun(int i)
{
cout << i << endl;
}
void STLDO()
{
vector<int> vec;
for(int i=0; i<10; i++)
{
vec.push_back(i);
}
vector<int>::iterator ite = vec.begin();
for(ite; ite != vec.end(); ite++) // 通过迭代器,输出vec内的元素
{
cout << *ite << endl;
}
for_each(vec.begin(), vec.end(), fun); // 通过for_each函数输出vec内的元素
for(int i=0; i<10; i++)
{
cout << vec[i] << endl; // 通过下标输出
}
for(int i=0; i<10; i++)
{
cout << vec.at(i) << endl; // 通过at函数输出
}
cout << vec.back() << endl; // back函数返回容器末尾的元素
}
(5).Vector添加元素
void STLADD()
{
vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);
vec.insert(vec.begin()+2, 12); // 在vec索引为2的地方插入12
vec.insert(vec.begin(), 5, 12); // 在vec索引的首元素前插入5个12
vector<int> vec1(5, 1);
vec.insert(vec.begin()+3, vec1.begin(), vec1.begin()+3); // 在vec索引的3的元素前插入vec1的索引为0-3的字符串
vec.pop_back(); // 输出vec的尾元素
vec.erase(vec.end()-1); // 删除尾元素
vec.swap(vec1); // 交换vec1与vec2
for_each(vec1.begin(), vec1.end(), fun); // 用for_each函数输出vec1中的元素
for_each(vec.begin(), vec.end(), fun); // 用for_each函数输出vec中的元素
cout << (vec > vec1) << endl; //输出vec与vec1比较后的值
}
3. 其他的list、deque、queue、map、unorderd_map、set、unorderd_set等的学习方法与此类似。
后续的学习
- 通过对STL容器的测试,我们达到能够使用STL境界,这时再去Leetcode做题就不至于到看不懂代码的地步。也能借LeetCode上的题目强化STL的使用。
- 现在可以看侯捷老师的视频 STL体系结构与内核分析 视频, 和 《STL源码剖析》, 能够加深对STL底层原理的理解。
- 可以自己动手写一个STL的模板库,可以在 github上搜索代码进行仿写。这同样能够写在简历上。