STL容器

STL广义上分为:容器(container)、算法(algorithm)、迭代器(iterator),容器和算法之间通过迭代器进行无缝连接

STL 六大组件:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器

容器:各种数据结构,如vector、list、deque、set、map等,从实现角度上看,STL容器是一种class template

算法:各种常用算法,如sort、find、copy、for_each等。从实现角度上看,STL算法是一种function template

迭代器:扮演容器与算法之间的粘合剂,共有五种类型,从实现角度上看,迭代器是一种将operator*, operator→, operator++, operator—等指针相关操作给予重载的class template。所有的STL容器都附带有自己专属的迭代器,只有容器的设计者才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。

仿函数:行为类似函数,可以作为算法的某种策略。从实现角度上看,仿函数是一种重载了operator()的class或者class template

适配器:一种用来修饰容器或者仿函数或者迭代器接口的东西

空间配置器:负责空间的配置与管理,从实现角度看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template

六大组件的交互关系:

容器通过空间配置器取的数据存储空间,算法通过迭代器存储容器中的内容,仿函数可以协助算法完成不同的策略的变化,适配器可以修饰仿函数

优点:

  • STL是C++内在的一部分,不需要安装什么

  • STL的一个重要特性是将数据和操作分离。数据由容器类别加以管理,操作则由可制定的算法定义。迭代器在两者之间充当粘合剂,使得算法和容器可以交互运作。

  • 高可重用性,高性能,高移植性,跨平台

    高可重用性:STL几乎所有的代码都采用了模板类和模板函数进行实现的

    高性能:如map可以高效地在十万条记录中找出指定的记录,map采用的是红黑树的变体实现的

    高移植性:项目之间可以快速移植

    跨平台:

STL 三大组件

容器

序列式容器

每个元素有固定的位置,除非用删除或者插入进行改变。比如vector, deque, list等

关联式容器

各元素没有严格的物理上的顺序关系,元素在容器中并没有保存元素插入容器时的逻辑顺序,比如set multiset map multimap等。另一个特定是使用关键字进行索引。

算法

质变算法

会改变区间元素中的内容,比如拷贝、替换、删除等

非质变算法

不会改变区间的元素内容,比如查找,计数,遍历等

迭代器

iterator定义如下:提供一种方法,使之能够依序寻访某个容器中所含的各个元素,而又无需暴露该容器的内部表示方式。

分为五类,输入、输出、前向、双向、随机访问迭代器。

vector<int> v;
v.push_back(10); // 尾插法

vector<int>::iterator itBegin = v.begin(); // 声明一个迭代器, 指向容器的起始位置
vector<int>::iterator itEnd = v.end(); // 指向最后一个元素

*itBegin // 取元素
itBegin++ // 指向下一个元素

// 遍历
while(itBegin!=itEnd) {
    cout << *itBegin << endl;
    itBegin++;
}

// 遍历2
for (vector<int>iterator it=v.begin(); it != v.end(); it++) {
    cout << *it << endl;
    // 如果遍历的是对象的话,则需要使用(*it)指向类的成员变量 eg. (*it).m_Value, it->m_Value
}

// 遍历3, 包含在头文件:algorithm
void myPrint(int v) {
    cout << v << endl;
}
for_each(v.begin(), v.end(), myPrint);

// 容器嵌套
vector<vector<int> > v;
vector<int> v1;
vector<int> v2;
vector<int> v3;

// 遍历
for (vector<vector<int> >::iterator it=v.begin(); it != v.end(); it++){
    for (vector<int>::iterator vit=(*it).begin(); vit != (*it).end(); vit++) {
        cout << *vit << " ";
    }
    cout << endl;
}

常用容器

string 容器

构造函数

string(); // 创建一个空的字符串, eg: string str;
string(const string& str); // 拷贝构造, eg: string str1(str); string str1 = str;
string(const char* s); // 字符串s构造,eg: string str2("abc"); string str2="abc";
string(int n, char c); // 用n个字符串c来初始化, eg: string str3(10, 'a');

基本赋值操作

string& operator=(const char* s); // str = "abc";
string& operator=(const string& str); // str = str1;
string& operator=(const char c);
string& assign(const char* s);
string& assign(const char* s, int n); // 将s字符串的前n个字符复制给调用对象
string& assign(const string& str);
string& assign(int n, char c);
string& assign(const string& s, int start, int n); // 从s对象中start开始,拷贝n个字符,从0开始索引

存取操作

char& operator[](int n);
char& at(int n);
// [] 和 at的区别: [] 访问越界会直接挂掉,at访问越界会抛出异常
try {
}
catch(...) { // 这里的话捕捉所有的异常,用...来代替
}

// 越界异常的写法
try {
    cout << s.a[100] << endl; // 假定s只有10个元素
    // cout << s[100] << endl; // []的访问方式不会抛出异常
}
catch (out_of_range & e) {
    cout << e.what() << endl;
}

拼接操作

string& operator+=(const string& str);
string& operator+=(const char *str);
string& operator+=(const char c);
string& append(const char* s); // 将字符串s连接到当前字符串结尾
string& append(const char* s, int n); // 将字符串s的前n个字符连接到当前字符串结尾
string& append(const string& s);
string& append(const string& s, int pos, int n); // 从pos开始的n个字符连接到结尾
string& append(int n, char c); // 末尾添加n个字符c

查找和替换

// 查找失败的返回 -1
int find(const string& str, int pos = 0) const; // 从pos开始查找str第一次出现的位置
int find(const char* s, int pos = 0) const;
int find(const char* s, int pos, int n); // 查找s前n个字符
int find(const char c, int pos = 0);
// r stands for right, 从右往左找
int rfind(const string& str, int pos = npos) const;// 查找str最后一次出现的位置,从pos开始
int rfind(const char* s, int pos = npos) const;
int rfind(const char* s, int pos, int n) const;
int rfind(const char c, int pos = 0);
string& replace(int pos, int n, const string& str); // 替换从pos开始的n个字符串为str, 注意!n个字符是指源字符串的n个,全部替换为str,两者没有数量关系
string& replace(int pos, int n, const char* s);

比较

// compare函数>时返回1,<时返回-1,等于是返回0
// 大小按照字母顺序,排前面的越小,大学的A比小写的a小
int compare(const string& s);
int compare(const char* s);

子串

string substr(int pos = 0, int n = npos); // 返回由pos开始的n个字符组成的字符串

// 查找邮件的用户名
string email = "test@qq.com";
int pos = email.find("@");
string name = email.substr(0, pos);

插入和删除

string& insert(int pos, const char* s);
string& insert(int pos, const string& str);
string& insert(int pos, int n, char c);
string& erase(int pos, int n = npos);

string 和 c-style字符串转换

// string 转 char*
string str = "hello"
const char* cstr = str.c_str();
// char* 转 string, char*是可以进行隐式类型转换为string的
char* cstr = "hello"
string str(cstr);

补充

toupper(char c);// 将字符c转换成大写
tolower(char c);// 将字符c转换成小写

vector 容器

和数组array的唯一差别在于空间的灵活性

vector<int> v; // 单端数组 动态数组
v.rbegin() // 最后一个元素
v.rend() // 第一个元素
v.begin()// 第一个元素
v.end() // 最后一个元素
v.push_back() // 尾插
v.pop_back() // 尾删
v.capacity() // v的容量,vector的动态扩展是动态的,不是固定扩展两倍
// vector的动态增长,不是在原有空间上增加,而是找一个更大的空间,然后将原有的数据进行拷贝到新的空间上去,然后释放原有的空间,所以要注意一定空间上发生了重新分配,原有的vector的迭代器就失效了,不能再使用,否则将发生错误。

vector 构造函数

vector<T> v; 
vector<T> v1(v.begin(), v.end()); // 将v对象的内容拷贝过来进行构造
vector<T> v(n, elem); // 将n个elem拷贝给自身
vector<T> v(const vector& vec); // 拷贝构造函数

int arr[] = {1, 2, 3, 4}
vector<int> v1(arr, arr+sizeof(arr)/sizeof(int));
vector<int> v2(v1.begin(), v1.end();

赋值操作

assign(beg, end); // 将[beg, end)区间的数据拷贝赋值给自身
assign(n, elem); // 将n个元素拷贝给自身
vector& operator=(const vector& vec);
swap(vec); // 将vec与自身的元素互换

// 巧用swap收缩空间
vector<int> v;
for (int i = 0; i < 100000; ++i) {
    v.push_back(i);
}

cout << "v's capacity: " << v.capacity() << endl; // 13万多
cout << "v's size: " << v.size() << endl; // 10万

v.resize(3);
cout << "v's capacity: " << v.capacity() << endl; // 13万多
cout << "v's size: " << v.size() << endl; // 3

vector<int>(v).swap(v);// 利用v初始匿名对象,按照v.size()大小开辟匿名对象空间,swap交换匿名对象和v对象的指针,运行结束后,匿名对象销毁,原来的匿名对象空间变成v对象
cout << "v's capacity: " << v.capacity() << endl; // 3
cout << "v's size: " << v.size() << endl; // 3


// 
vector<int> v;
int *p = NULL;
int num = 0;

// 这段代码可以计算在插入100000个数据时总共开辟了多少次空间
// 运行结果得到30次
for (int i = 0; i < 100000; ++i) {
    v.push_back(i);
    if (p != &v[0]) {
        p = &v[0];
        num++;
    }
}

v.reserve(100000); // 则上述代码执行的结果为1,只进行开辟1次空间

大小操作

size(); 
empty(); // 返回true,表示为空,反之返回false
resize(int num); // 重新指定容器长度,容器变长填充默认值,变短超出元素删除
resize(int num, elem); // 变长以elem填充,变短超出的删除
capacity(); // 容器的容量
reserve(int len); // 容器预留len个元素长度,预留位置不初始化,不可访问

数据存取

at(int idx);
operator[];
front(); // 第一个元素 
back(); // 最后一个元素

插入和删除

insert(const_iterator pos, int count, elem);
push_back(elem);
pop_back(elem); // 删除最后一个元素
erase(const_iterator start, const_iterator end);
erase(const_iterator pos); // 删除指定位置元素
clear();

其他

vector<int > v;
for (int i = 0; i < 10; i++) {
    v.push_back(i);
}
// 逆序遍历,注意要使用逆序迭代器
for (vector<int>reverse_iteartor it = v.rbegin(); it != rend(); ++i) {
    cout << *it << endl;
}

// vector的迭代器是随机访问的迭代器,支持跳跃访问
vector<int>::iterator itBegin = v.begin();
itBegin = itBegin + 3; // 这个代码如果编译不出错,表明这个容器支持随机访问,list容器不支持随机访问

deque 容器

deque属于双端数组, vector也可以进行头插,但是效率很低

deque内部用的是中控器(保存缓冲区的地址)+缓冲区的结果

大部分的操作和vector是一样的e

iterator // 普通的迭代器 
reverse_iterator // 逆序迭代器
const_iterator // const迭代器

// deque特有的
push_front(elem) ; // 前插
pop_front(); // 删除第一个元素

Sort算法

deque<int> d;
d.push_back(1);
d.push_back(9);
d.push_back(4);

sort(d.begin(), d.end());// algorithm头文件中, 从小到大排序

// 排序规则的回调函数
bool myCompare(int v1, int v2) {
    return v1 > v2;
}
sort(d.begin(), d.end(), myCompare); // 从大到小

Stack 容器

先进后出的数据结构:First In Last Out(FILO),只能访问栈顶元素

构造

stack<T> s;
stach<T>(const stack &s); // 拷贝构造

存取操作

push(elem); // 栈顶插入元素
pop(); // 栈顶弹出一个元素
top(); // 返回栈顶元素
empty(); 
size();

queue 容器

队列容器,先进先出的数据结构First In First Out(FIFO),只允许由一端入队,一端出队,不提供遍历和迭代器

// 构造函数
queue<T> q;
queue<T>(const queue& q);
// 存取操作
push(elem);
pop(); // 从队头删除一个元素
front(); // 返回第一个元素
back(); // 返回最后一个元素
// 赋值操作
queue& operator=(const queue &q);
// 大小操作
empty();
size();

List 容器

List容器是一个双向循环链表,动态分配存储信息,消耗较大

List容器提供的迭代器是bidirectional Iterators, 不支持随机访问

List有一个重要性质,插入操作和删除操作不会造成原有迭代器的失效。这个在Vector容器中是不成立的,因为vector容器的插入操作可能引起内存的重新分配,导致原有的迭代器失效。

也可以支持逆序遍历,reverse_iterator

// 构造函数
list<T> l;
list(begin, end);
list(n, elem);
list(const list& l);

// 插入删除操作
push_back(elem);
pop_back();
push_front(elem);
pop_front();
insert(pos, elem);
insert(pos, n, elem);
insert(pos, beg, end);
clear();
erase(beg, end);
erase(pos);
remove(elem);

// 大小操作
size();
empty();
resize();
resize(n, elem);

// 复制操作
assign(beg, end); // 将[beg, end)区间的数据拷贝给本身
assign(n, elem); 
list& operator=(const list& l);
swap(l);

// 存取
front(); 
back();

// 反转排序
reverse(); // 链表逆序
sort() // list排序
// 所有不支持随机访问的迭代器,不可以使用系统提供的算法
// 如果不支持系统提供的算法,那么这个类内部会提供
// eg: l.sort(); // 从小打大排序
// 对于自定义类型需要指定回调函数

// !!! 注意!!!
// 删除自定义类型直接使用remove会报错,可以重载双等号来解决

set、multiset容器

set容器的特性,所有的元素都会根据元素的键值自动排序,set的元素不像map那样可以同时拥有实值和键值,set元素既是键值又是实值。set不允许两个元素有相同的键值。

不可以通过set的迭代器来改变set容器的元素值,换句话说,set的迭代器是一种const_iterator

set有和list一样的性质,原有的迭代器在插入删除元素后还是有效的,当然,被删除那个元素的迭代器除外。

multiset特性和set完全相通,唯一的差别在于multiset允许键值重复。set、multiset的话底层是用红黑树实现的。

相关操作和上面提到的操作基本一致,不在说明。

// 插入
insert(elem); // 元素插入只有这个方法,插入后会自动按照从小到大排序
// 删除
erase(pos);
erase(elem);
erase(beg, end);

// 查找操作,key就是实值
find(key); // 查找键key是否存在,若存在,返回该元素的迭代器,不存在返回set.end()
count(key); // 统计键key的个数
lower_bound(keyElem); // 返回第一个key>=keyElem元素的迭代器
upper_bound(keyElem); // key>keyElem
equal_range(keyElem); // 返回相等的上下限两个迭代器

//set 容器不允许插入重复的值
insert(elem); // 返回值是pair<iterator, bool>, bool返回插入成功或失败

// set容器插入后默认是从小打大的,如果从大到小的话可以使用逆序迭代器
// 或者是在插入之前指定从大到小,指定规则, 仿函数
class myCompare {
public:
    // 重载小括号 ()
    bool operator()(int v1, int v2) {
        return v1 > v2;
    }
};
set<T, myCompare> s; 

// !!!!插入自定义数据类型需要一开始指定排序方式

Pair对组

// 创建对组
pair<T1, T2> p(T1, T2);
pair<T1, T2> p = make_pair(T1, T2);

// 取值
p.first
p.second

map、multimap容器

map的特性是会根据元素的键值自动排序,map所有的元素都是pair,同时拥有键值和实值,pair的第一个元素被视为键值,第二个元素被视为实值,同样和set一样,map也是不允许有两个元素有相同的键值(注意这里是键值,key值,value值可以相同)。

同样map也是不能根据迭代器更改元素内容的。

map和list一样,插入删除元素迭代器不失效。

multimap可以支持键值重复。

// 构造函数
map<T1, T2> m;
map(const map& m);
// 赋值操作
map& operator=(const map& m);
swap(map);
// 大小
size();
empty();
// 插入
map.insert(...); // 返回的是pair<iterator, bool>
map<int, string> m;
m.insert(pair<int, string>(3, "Mike");
m.insert(make_pair(1, "Jack"); // 推荐使用
m.insert(map<int, string>::value_type(2, "Jason");
m[3] = "Leon"; // 可以用在访问上
// 删除
clear();
erase(pos);
erase(beg, end);
erase(keyElem); // 删除键值的对组
// 查找和set一致

multimap案例:存储员工信息:姓名 年龄 电话 工资等

STL使用时机

vector使用场景:比如软件的历史操作记录的存储。

deque使用场景:比如排队购票系统

vector与deque的比较:

  1. vector.at()比deque.at()的效率高,因为vector.at(0)是固定的,而deque的开始位置是不固定的

  2. 如果有大量的释放操作的话,vector花费的时间更少

  3. deque支持头部的快速插入和删除,是属于deque的优点

list的使用场景,公交车乘客信息的存储,随时有可能乘客下车,存在频繁的不确定位置元素的插入和删除

set的使用场景:手机游戏的个人得分的存储,要求从高分到低分顺序存储

map使用场景:比如按照ID账号存储十万个账号。

Last updated