STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,它不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
原始版本
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要阅读部分源代码,主要参考的就是这个版本。

容器实际上就是数据结构
(1) STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出来已经相隔了13年,STL才进一步更新。
(2) STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。
(3) STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。
(4) STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码,当然这是模板语法本身导致的。
string属于STL吗?
对此没有真正的答案。" STL"缺乏统一的共识定义,一方面,std::string的开发完全独立于其他容器。另一方面,它已经添加了足够的内容来满足随机访问容器的所有要求。是否选择将其分类为" STL"的一部分完全取决于您。
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
因此C++中就引入了标准库中的string类。
string类的文档介绍
总结:
在使用string类时,必须包含#include头文件( #include < string> )以及using namespace std;
string类的常用接口在cplusplus上面有很清楚的介绍,具体功能直接去cplusplus上阅读即可,下面介绍一些易混淆的功能。
下面就是cplusplus上string类成员函数的截图,

void Teststring()
{
string s1; // 构造空的string类对象s1
string s2("hello bit"); // 用C格式字符串构造string类对象s2
string s3(s2); // 拷贝构造s3
}
注意:
size()与length()方法底层实现原理完全相同,功能完全一样,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
clear()只是将string中有效字符清空,不改变底层空间大小。
resize(size_t n) 与 resize(size_t n, char c)都是改变有效字符个数为n,当n大于当前有效字符个数且容量足够时,就直接在后面追加0或者指定字符c来使得有效字符个数达到n。当n大大于于当前有效字符个数且当前的容量时,resize会先开辟空间(标准库里涉及一个内存对齐,给定20,可能扩的不是20),然后用0或者字符c来添加有效字符个数至n。当n小于当前有效字符个数时,表示要减少有效字符个数,此时容量不变,有效字符个数减少至n(只保留前n个有效字符)。
string s1("hello world");
resize(20,'x');//扩容,在后面追加x直至充满20个字符
resize(14,'x');//无需扩容,直接在后面追加x直至14个字符
resize(5,'x');//容量不变,减少至5个字符
reserve(size_t res_arg=0):直接为string预留空间(扩容),可以避免因为追加字符而扩容带来的消耗(VS下扩容1.5倍,Linux下扩容2倍)。当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。实际上VS下reserve和resize都不会缩容量。
string s;
s.reserve(1000);
在string尾部追加字符时,s.push_back ( c ) , s.append(1, c) ,s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。

我们要注意一下string类域和库定义的swap函数的区别:
这个是string类域的swap函数

这个是库定义的swap函数

s1.swap(s2);//效率高
swap(s1,s2);//效率低
第一个swap交换就直接交换指针即可,效率很高,第二个swap它是一种深拷贝实现,效率较低。
注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
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;
};
指向堆空间的指针,用来存储字符串。
上面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。
在开始模拟实现String类之前,我们先了解一下深拷贝和浅拷贝。
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,这会导致一些问题。
当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规,而且当该对象也进行销毁时,相当于一片空间释放了两次,会导致程序崩溃,最后其中一个对象修改这片空间的内容会影响另一个对象该空间的内容。
大家先看下以下string类的实现是否有问题?
为了和标准库区分,此处使用String

上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造,而默认的拷贝构造函数是浅拷贝。最终导致的问题是,s1、s2共用同一块内存空间,在释放时,同一块空间会被释放多次而引起程序崩溃,而且一个对象对于该片空间进行修改会影响另一个对象。
可以采用深拷贝解决浅拷贝问题,即每个对象都有一份独立的资源,不要和其他对象共享。
我去开一片和你有一样大的空间,在把你的值复制过来,这样就不会存在多个对象共享同一份资源导致的若干问题。

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。如果不显示给出,那么编译器会生成默认的拷贝构造函数和=运算符重载函数,那么用一个对象初始化一个对象时,就会调用默认拷贝构造函数,此时会发生浅拷贝,会把资源空间的地址也按位拷贝过去,此时两个对象就会共享一片空间,进而导致上述的一系列问题,而且用一个对象给另一个对象赋值时,会调用默认生成的=运算符重载函数,也一样会发生浅拷贝,导致两个对象共享一片空间。
写时拷贝就是一种拖延症,如果我只是想简单的做个深拷贝的空间拷贝,其他什么事情都不做,那么这个代价就有点大,此时我们就考虑共享一片空间,于是写时拷贝在浅拷贝的基础之上增加了引用计数的方式来实现。引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放(即最后一个用该资源的人释放资源);否则就不能释放,因为还有其他对象在使用该资源。当然这样一个对象修改会影响其他对象,所以如果谁需要写谁再去拷贝,即不写不拷贝,谁写谁拷贝,核心理念就是延迟拷贝。,如果没人写就不需要拷贝就赚了。
namespace bit
{
// string需要考虑完善的增删查改和使用的string
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
/*string()
:_size(0)
,_capacity(0)
{
_str = new char[1];
_str[0] = '\0';
}*/
//默认构造函数是将_str指向一个空字符串,用全缺省的默认构造函数更灵活
//String(const char* str = "\0") 错误示范,这表示字符串有0,并不是空字符串
//String(const char* str = nullptr) 错误示范,nullptr并不表示空字符串
string(const char* str = "")
: _size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//传统写法的拷贝构造和赋值运算符重载,老老实实干活,该开空间自己开空间,该拷贝数据就自己拷贝数据
string(const string& s)
:_size(strlen(s._str))
, _capacity(_size)
{
// 拷贝构造函数,对_str的初始化一步完成不了,我们在函数体中进行初始化
//这是深拷贝
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
//此处返回的是引用,返回引用的好处是:1.减少拷贝 2.可以对返回对象进行修改
}
//现代写法的拷贝构造和赋值运算符重载
//一样要完成深拷贝,但是自己不想干活,安排别人干活,然后窃取劳动成果去,资本行为吗,剥削行为!
//先写一个swap函数
void swap(string& s)
{
std::swap(_str,s._str);
std::swap(_size,s._size);
std::swap(_capacity,s._capacity);
}
string(const string& s)
//一定要记得初始化,不初始化,换了个随机值,析构的时候会报错
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);//调用默认构造函数来完成深拷贝
//这里不要用库里的swap函数,库里的swap函数是一种深拷贝,效率太低,用String类域的swap函数,直接换底层即可
swap(tmp);
}
/*string& operator=(const string& s)
{
if(this != &s)
{
string tmp(s._str);
swap(tmp);
}
return *this;
}*/
//更简洁的赋值运算符现代版写法
string& operator=(string s)
{
//传参的时候就进行了拷贝构造,然后直接进行交换即可
//在传参的时候已经完成了深拷贝了,所以这里判不判段是否是给自己赋值也无所谓
swap(s);
return *this;
}
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
const char* c_str() const
{
return _str;
}
/*普通对象可以掉用const成员函数,因为这是权限的缩小,但是const对象不能调用普通成员函数,因为这是权限的放大,对于[]运算符我们需
要些const和非const版本,因为对于普通对象而言它会调用非const的运算符重载函数获取可以修改的返回值(会调用更加匹配的),对于const对象,会调用const的运算符重载函数获取不可修改的返回值(会调用更加匹配的)。*/
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//一般const成员函数对应的返回值类型也是const的,错误实现导致const对象也可以被修改
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
/*对于size函数,我们只需要写一个const函数即可,因为const对象和非const对象都可以调用该函数。*/
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
if (n > _capacity)
{
reserve(n);
}
for (size_t i = _size; i < n; ++i)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
/*size_t end = _size;
while(end >= pos)
{
_sttr[end+1] = _str[end];
--end;
这段代码是有问题的,因为end是一个无符号数,当它减为-1时相当于达到了整形的最大值,此时访问就越界了,这段代码无法进行头插*/
/* int end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}
这样做也是不行的,因为end是int,pos是unsigned int,二者发生
关系运算的时候会发生整形提升,那么int类型的end又提升为了 unsigned int的end,那么又会发生上面一样的问题
*/
/*int end = _size;
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
--end;
}
我们可以把pos又强转为int,这样头插就进去了,但是库里面就是unsigned int,模拟实现要和库里的参数类型一致*/
size_t end = _size+1;
//这样我等于0的时候就终止了,不会像以前一样小于0才停止,也就没有了以前的问题
while (end > pos)
{
_str[end] = _str[end-1];
--end;
}
_str[pos] = ch;
_size++;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
/*if (len == 0)
{
return *this;
}*/
if (_size + len > _capacity)
{
reserve(_size + len);
}
// 往后挪动len个位置
size_t end = _size + len;
while (end > pos+len-1)
//while (end >= pos + len)怕极端情况pos和len都为0,但是可以通过上面的if进行一个提前判断进行解决
{
_str[end] = _str[end -len];
--end;
}
strncpy(_str + pos, str, len);//strcpy遇到0才停止
_size += len;
return *this;
}
void push_back(char ch)
{
insert(_size, ch);
}
void append(const char* str)
{
insert(_size, str);
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& earse(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t begin = pos + len;
while (begin <= _size)
{
_str[begin - len] = _str[begin];
++begin;
}
_size -= len;
}
return *this;
}
size_t find(char ch, size_t pos = 0)
{
for (; pos < _size; ++pos)
{
if (_str[pos] == ch)
{
return pos;
}
}
return npos;
}
size_t find(const char* str, size_t pos = 0)
{
const char* p = strstr(_str + pos, str);
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;//返回下标
}
}
private:
char* _str;
size_t _size; // 有效字符个数
size_t _capacity; // 实际存储有效字符的空间
//const static size_t npos = -1;
const static size_t npos;
};
const size_t string::npos = -1;
ostream& operator<<(ostream& out, const string& s)
{
/*out<
for (auto ch : s)
{
out << ch;
}
return out;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
istream& operator>>(istream& in, string& s)
{
//方案1
//char ch;
in >> ch;
//cin是识别不了空格或者换行的,你输入多个字符时,它会认为空格或者换行为多个字符之间的间隔,所以它永远不会识别空格或换行
//while (ch != ' ' && ch != '\n')
//{
// s += ch;
// //in >> ch;
//}
//方案2
//char ch;
//ch = in.get();
//while (ch != ' ' && ch != '\n')
//{
// s += ch;
// ch = in.get();
//}
//return in;
//方案3
//读满我一次性加到string后面,比多次调用+=效率更高。
s.clear();//先把已经存在的数据给清掉
char ch;
ch = in.get();
char buff[128] = {'\0'};
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
s += buff;
memset(buff, '\0', 128);
i = 0;
}
ch = in.get();
}
s += buff;//避免string频繁扩容
return in;
}
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 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 !(s1 == s2);
}
}
思考一个问题:我们该如何遍历一个字符串呢?
方法一:
string s1("hello");
for(size_t i = 0;i<s1.size();i++)
{
cout<<s1[i]<<" ";
//[]是一个重载运算符,实际上调用了s1.operator[](i);
}
cout<<endl;
重载[]运算符如下:
char& operator[](size_t pos)
{
return _str[pos];
}
还有另一个const版本的重载函数如下:
const char& operator[](size_t pos) const
{
return _str[pos];
}
如果使用该运算符的string是const的那么就需要使用这个const版本的运算符重载。值得注意的是,原来返回引用,是因为函数结束后,变量还存在,那么我们直接返回它本身,可以减少拷贝,这里返回引用的确也可以减少拷贝,但主要的是为了使返回的变量能够被修改,如果返回的不是引用,那么返回的就是拷贝的临时变量,临时变量具有常性,是不可以被修改的。
这里我们还需要注意一个问题,[]与at的功能是一样的。
s[3]与s.at(3)的功能都是一样的
但是它们处理错误的方式不同。
s[100]越界,那么[]会直接报错
s.at(100)越界,at函数会抛出一个异常
方法二:利用迭代器
迭代器是一个像指针一样访问数据结构的东西
我们首先使用的是正向迭代器,它通常用于正向遍历
//迭代器是用于访问数据结构的
string s1("hello");
string::iterator it = s1.begin();//属于string的迭代器
while(it != s1.end)//s1.begin永远表示第一个位置的地址,s1.end永远表示最后一个位置'\0'的地址
{
cout<<*it<<" "
++it;
}
cout<<endl;
那如果我想倒着遍历字符串该怎么办呢?
这个时候就可以利用反向迭代器,它可以用于反向遍历
string::reverse_iterator rit = s.rbegin();
while(rit != s.rend())
{
cout<<*rit<<" ";
++rit;
}
cout<<endl;
反向迭代器的工作原理如下:

我们已经知道迭代器相当于一个指针,既然相当于一个指针,那么如果其指向一个const对象,那么我们就有可能可以通过该迭代器修改该const对象,那么编译器就会报错!比如下面这段代码s.begin返回一个iterator,iterator保存该字符串首字符的地址,那么我们就可以根据这个iterator修改这个const字符串,所以编译器会报错。

于是就引入了const iterator,const iterator指向const对象,受const限制,我们无法通过iterator来修改这个const对象,此时就是合理的,下面代码修改如下。

根据官网的这份文档我们可以得知,普通对象会调用begin()/end(),返回的是iterator,const对象调用const begin()/const end(),返回一个const iterator。(对于反向迭代器同理)


有时我们觉得用迭代器写一列代码太麻烦了
string::const_reverse_iterator rit = rs.rbegin();
此时我们就可以用auto来帮我们自动推导:
auto rit = rs.rbegin();
方法三:利用范围for
for(auto ch : s1)
{
cout << ch<<" ";
}
cout << endl;
范围for的底层实际上就是利用了迭代器