C++中的STL标准模板库和string
一.STL简介
1.什么是STL
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
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
:双向链表(插入/删除高效) -
deque
:双端队列 -
forward_list
(C++11):单向链表 -
array
(C++11):固定大小数组
-
-
关联容器:基于键(Key)排序的元素集合
-
set
/multiset
:唯一/重复键集合(红黑树实现) -
map
/multimap
:键值对映射
-
-
无序容器(C++11):哈希表实现
-
unordered_set
/unordered_multiset
-
unordered_map
/unordered_multimap
-
-
2. 算法(Algorithms)
-
作用:操作容器中元素的通用函数模板(独立于容器类型)。
-
分类:
-
非修改序列操作:
find()
,count()
,for_each()
-
修改序列操作:
copy()
,replace()
,shuffle()
-
排序/二分搜索:
sort()
,binary_search()
-
数值运算:
accumulate()
(在<numeric>
中)
-
-
关键特性:通过迭代器访问容器,实现容器与算法的解耦。
3. 迭代器(Iterators)
-
作用:容器与算法之间的"粘合剂",提供统一的元素访问接口(类似指针)。
-
分类(按功能递增):
4. 仿函数(Functors)/ 函数对象
-
作用:行为类似函数的对象(重载了
operator()
的类)。 -
优势:可携带状态(成员变量),比函数指针更灵活。
-
STL内置仿函数(在
<functional>
中):-
算术运算:
plus<T>
,minus<T>
-
比较运算:
less<T>
,greater<T>
-
逻辑运算:
logical_and<T>
-
5. 适配器(Adapters)
-
作用:修改其他组件的接口以提供新功能。
-
常见类型:
-
容器适配器:
-
stack
:基于deque
/list
实现后进先出(LIFO) -
queue
:先进先出(FIFO) -
priority_queue
:优先队列(基于vector
+堆算法)
-
-
迭代器适配器:
-
reverse_iterator
:逆向遍历 -
insert_iterator
:插入元素而非覆盖
-
-
函数适配器:
-
bind()
(C++11):绑定参数 -
过时的
bind1st
/bind2nd
(C++17移除)
-
-
6. 分配器(Allocators)
-
作用:封装容器内存管理的底层细节(内存分配/释放)。
-
默认行为:使用
std::allocator
(调用new
和delete
)。 -
自定义场景:
-
内存池优化
-
共享内存管理
-
调试内存分配
-
7.六大组件之间的协作关系
二.string
1.介绍
string类严格来说并非STL中的部分,但是字符串在现实生活中十分常见,而C语言中的一些字符操作函数难以满足人的需求,C++中的string类方便快捷,十分具有学习价值
2.叙述模式
之后所有的STL库容器都会以这样的方式进行介绍:基本操作的使用,原理,迭代器,模拟实现
3.标准库中的string
1.string类
http://www.cplusplus.com/reference/string/string/?kw=string
2.auto和范围for
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
auto不能直接用来声明数组
#include<iostream>
#include <string>
#include <map>
using namespace std;
int main()
{
std::map<std::string, std::string> dict = { { "apple", "苹果" },{ "orange",
"橙子" }, {"pear","梨"} };
// auto的使用,极大简化
//std::map<std::string, std::string>::iterator it = dict.begin();
auto it = dict.begin();
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
return 0;
}
范围for
范围for本质上还是利用迭代器进行容器的遍历,对于一个有范围的容器而言,使用范围for更加方便也不容易犯错。值得注意的是,范围for是否会改变被遍历容器的值,在于是否加引用符号&
#include<iostream>
#include <string>
#include <map>
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;
} f
or (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
cout << array[i] << endl;
}
// C++11的遍历
for (auto& e : array)
e *= 2;
for (auto e : array)
cout << e << " " << endl;
string str("hello world");
for (auto ch : str)
{
cout << ch << " ";
}
cout << endl;
return 0;
}
3.string类中常用接口和方法
1.常用构造
(constructor)函数名称 | 功能说明 |
string() (重点) | 构造空的string类对象,即空字符串 |
string(const char* s) (重点) | 用C-string来构造string类对象 |
string(size_t n, char c) | string类对象中包含n个字符c |
string(const string&s) (重点) | 拷贝构造函数 |
void Teststring()
{
string s1; // 构造空的string类对象s1
string s2("hello bit"); // 用C格式字符串构造string类对象s2
string s3(s2); // 拷贝构造s3
}
2.string类对象的容器操作
函数名称 | 功能说明 |
size(重点) | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty (重点) | 检测字符串释放为空串,是返回true,否则返回false |
clear (重点) | 清空有效字符 |
reserve (重点) | 为字符串预留空间 |
resize (重点) | 将有效字符的个数改成n个,多出的空间用字符c填充 |
注意:
1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接
口保持一致,一般情况下基本都是用size()。
2. clear()只是将string中有效字符清空,不改变底层空间大小。
3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不
同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char
c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数
增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参
数小于string的底层空间总大小时,reserver不会改变容量大小。
3.string的遍历
函数名称 | 功能说明 |
operator[] (重点) | 返回pos位置的字符,const string类对象调用 |
begin+ end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
rbegin + rend | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
4,string类对象的修改操作
函数名称 | 功能说明 |
push_back | 在字符串后尾插字符c |
append | 在字符串后追加一个字符串 |
operator+= (重点) | 在字符串后追加字符串str |
c_str(重点) | 返回C格式字符串 |
find + npos(重点) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
5.string类的非成员函数
函数 | 功能说明 |
operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> (重点) | 输入运算符重载 |
operator<< (重点) | 输出运算符重载 |
getline (重点) | 获取一行字符串 |
relational operators (重点) | 大小比较 |
4.vs和g++下的string(32位环境)
vs下string的结构
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:
当字符串长度小于16时,使用内部固定的字符数组来存放当字符串长度大于等于16时,从堆上开辟空间.
union _Bxty
{ // storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
最后:还有一个指针做一些其他事情,一共16+4+4+4=28字节
g++下string的结构
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:
空间总大小字符串有效长度引用计数
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
两种编译器扩容策略对比
vs第一次扩容是两倍正是在vs中有一个长度为16字节的buff数组,超出这个范围将会在堆上开空间,因此第一次扩容较为特殊,之后均为扩1.5倍。而在g++中,则是稳定按照2倍进行扩容。
三.模拟实现string
1.string.h
用于定义string类的基本框架和一些简短的函数
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once#include<iostream>
#include<string>
#include<assert.h>
using namespace std;namespace wjh {class string {private:char* _str;size_t _size;size_t _capacity;//这里的npos定义为-1,而类型为size_t//原因是他认为字符串长度不会超过非负数的全1(约42亿字节)static const size_t npos;public://迭代器typedef char* iterator;typedef const char* const_iterator;// 构造函数string(const char* str = ""){_size = strlen(str);// _capacity不包含\0_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);}string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}// 析构函数~string(){delete[] _str;_str = nullptr;_size = _capacity=0;}// 赋值运算符重载//这里要做深拷贝string& operator=(const string& s){ if (this != &s){//首先要释放当前对象资源delete[] _str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}return *this;}iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}//取到字符串的首地址const char* c_str() const{return _str;}//上面提到的string各种各样的操作void clear() {_str[0]='\0';_size = 0;}size_t size() const{return _size;}size_t capacity() const{return _capacity;}//重载operator []//传char&的原因是为了修改//这样可以通过operator[]对s[i]处的值进行修改//类似于使用数组,很方便//并且加入assert可以顺便实现检查越界char &operator[](size_t index) {assert(index<_size);return _str[index];}const char &operator[](size_t index) const{assert(index<_size)return _str[index];}//需要在类外实现void reserve(size_t n);void push_back(char ch);void append(const char* str);string& operator+=(char ch);string& operator+=(const char* str);void insert(size_t pos, char ch);void insert(size_t pos, const char* str);void erase(size_t pos, size_t len = npos);size_t find(char ch, size_t pos = 0);size_t find(const char* str, size_t pos = 0);string substr(size_t pos = 0, size_t len = npos);};//非类成员函数bool operator<(const string& s1, const string& s2);bool operator<=(const string& s1, const string& s2);bool operator>(const string& s1, const string& s2);bool operator>=(const string& s1, const string& s2);bool operator==(const string& s1, const string& s2);bool operator!=(const string& s1, const string& s2);ostream& operator<<(ostream& out, const string& s);istream& operator>>(istream& in, string& s);
}
2.string.cpp
#include"string.h"
namespace wjh {const size_t string::npos = -1;//为字符串预留空间void string::reserve(size_t n) {if (n > _capacity) {char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}//尾插字符void string::push_back(char ch) {//判断越界if (_size == _capacity) {reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch;_str[_size] = '\0';}string& string::operator+=(char ch) {push_back(ch);return *this;}//追加字符串void string::append(const char* str) {int len = strlen(str);if (_size + len >= _capacity) {reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);}strcpy(_str + _size, str);_size += len;}string& string::operator+=(const char* str) {append(str);return *this;}//插入字符void string::insert(size_t pos, char ch) { //pos是否越界assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}// 挪动数据size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];--end;}_str[pos] = ch;++_size;}//插入字符串void string::insert(size_t pos, const char* str) { assert(pos <= _size);//判断越界int len = strlen(str);if (_size+len>= _capacity){reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);}//挪动数据int end = _size + len;while (end > pos + len - 1) {_str[end] = _str[end - len];}//插入数据for (int i = 0; i < len; ++i){_str[pos + i] = str[i];}_size += len;}void string::erase(size_t pos, size_t len){ assert(pos < _size);//如果是pos之后的所有数据if (len >= _size - pos){_str[pos] = '\0';_size = pos;}else{for (size_t i = pos + len; i <= _size; i++){_str[i - len] = _str[i];}_size -= len;}}size_t string::find(char ch, size_t pos) {assert(pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;}}return npos;}//找字符串size_t string::find(const char* str, size_t pos) {assert(pos < _size);//从pos开始找const char* p = strstr(_str + pos, str);if (p==nullptr){return npos;}else {return p - _str;}}//返回一个子串string string::substr(size_t pos, size_t len) { assert(pos < _size);if (len > _size - pos) {len=_size - pos;}string sub;sub.reserve(len);for (size_t i = pos; i < pos + len; i++){sub+=_str[pos+i];}return sub;}bool operator<(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) < 0;}bool operator<=(const string& s1, const string& s2){return s1 < s2 || s1 == s2;}bool operator>(const string& s1, const string& s2){return !(s1 <= s2);}bool operator>=(const string& s1, const string& s2){return !(s1 < s2);}bool operator==(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) == 0;}bool operator!=(const string& s1, const string& s2){return !(s1 == s2);}ostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}istream& operator>>(istream& in, string& s){s.clear();const int N = 256;char buff[N];int i = 0;char ch;//in >> ch;ch = in.get();while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == N - 1){buff[i] = '\0';s += buff;i = 0;}//in >> ch;ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;}
}
3.知识补充
1.对于迭代器,它从用法上有四种:正向,反向,const和普通两两组合。其中对于const迭代器,它用于访问那些仅可读的容器或对象,而对于普通的迭代器,不仅可读而且可写。
const string s3("hello world");
string::const_iterator cit = s3.begin();
while (cit != s3.end())
{//*cit += 2;显然这里已经不能修改容器的元素,因为使用了const迭代器cout << *cit << " ";++cit;
}
cout << endl;//string::const_reverse_iterator rcit = s3.rbegin();
auto rcit = s3.rbegin();
while (rcit != s3.rend())
{// *rcit += 2;cout << *rcit << " ";++rcit;
}
cout << endl;