什么是STL?
STL(standard template libaray - 标准模板库) :是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
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)采用, 可移植性好,可公开、修改甚至贩卖,从命名风格和编程风格上看, 阅读性非常高。
string类
C语言中的字符串。C语言中,字符串是以\0结尾的一些字符的集合,为了操作方便,C标准库中提供的str系类的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要自己管理,稍不留神还可能越界访问。
cpp
void test_string()
{
string s1;//默认构造
string s2("hello world");//用const char* 的带参构造
string s3(s2);//拷贝构造
string s4(s2, 6, 5);//从s2的第6位开始向后拷贝5个字符
//如果拷贝的长度超过了字符串的实际长度,则拷贝到字符串结尾为止
//如果第三个参数省略,默认拷贝到字符串结尾
string s5("hello world", 5);
//取字符串前五个字符初始化
string s6(10, 'x');
//取连续n个字符x进行初始化
s2[0] = 'x';//s2[0]的返回值是传引用返回,因此可以用来修改内容
int x = s2.size();//返回字符串长度
}
上面我们结合具体代码简要看了一些string的相关用法,结合之前写过的内容,我们可以发现string的底层逻辑大概是以下这样:
cpp
class String
{
private:
char* _str;
size_t _size;
size_t _capacity;
};
在遍历string容器的整个过程中,我们提供了下面三种方式:
- 下标 +
[] - 迭代器
- 范围
for
第一种遍历方式我们可以类比于数组,数组怎么访问,我们这里也可以怎么访问,第三种范围for适用于容器或数组的遍历,使用简单。
这里我们着重介绍一下最重要的------迭代器。迭代器提供了一种通用的访问容器的方式,可以通过它访问所有容器。它类似于指针但又不一定是指针。
正向迭代器:
cpp
string::iterator it = s.begin();
这里我们用迭代器定义了严格对象it,返回开始位置的迭代器。也可以写成auto it = s2.begin();。相当于指针,可以改变容器的元素。
但值得注意的是:当我们使用s.end()时,end返回的时最后一个位置的下一个位置。
反向迭代器:
cpp
string::reverse_iterator rit = s.rbegin();
这里返回的是最后一个元素的迭代器,相反的是:s.rend();返回的就变成了第一个元素的前一个位置,当我们使用反向迭代器的++时,反向迭代器是向前移动的,即反向迭代器是倒着走的。
const迭代器:
当我们遇到这样的字符串的定义时:
cpp
const string s("hello world");
此时,字符串被const修饰时,我们不能使用普通迭代器,需要使用const迭代器,相比于普通迭代器,只需要在iterator前面加上const_即可。
cpp
string::const_iterator cit = s.begin();
和其他被const修饰的成员语言,cit这个迭代器只能读,不能写。
同样的,const迭代器和普通迭代器一样有着正向和反向之分。
cpp
string::const_reverse_iterator dit = s.rbegin();
综上所述,迭代器有四种:分别为iterator、reverse_iterator、const_iterator、const_reverse_iterator。这里我们只是简单介绍一下迭代器,在后面我们可以更加深刻的领悟迭代器的魅力。
cpp
void test_string()
{
string s("hello world");
//遍历容器的方式
//1.下标+[]
for (size_t i = 0; i < s.size(); i++)
cout << s[i] << " ";
//2.迭代器
//正向迭代器
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
//反向迭代器
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << " ";
rit++;
}
//const迭代器
const string s1("hello world");
string::const_iterator cit = s1.begin();
while (cit != s1.end())
{
cout << *cit << " ";
cit++;
}
string::const_reverse_iterator dit = s1.rbegin();
while (dit != s1.rend())
{
cout << *dit << " ";
dit++;
}
//3.范围for
//范围for使用于容器或数组的遍历,使用简单
for (auto ch : s)
{
cout << ch << " ";
}
}
补充:auto关键字
在这里补充2个C++11的小语法。
在早期C / C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,auto有了全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
用auto声明指针类型时,用auto和auto* 没有任何区别, 但用auto声明引用类型时则必须加&。当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
注:auto不能作为函数的参数, 可以做返回值, 但是建议谨慎使用。auto不能直接用来声明数组
cpp
void test()
{
int a = 1;
auto b = a;
auto c = 'd';
auto d = 3.12;
//不能写成auto e;编译器无法推导出类型,无法分配空间
//typeid可以查看变量的类型
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto不能定义数组
//void func(auto a)错误,不能用作参数,但是可以用作返回值
//auto func()正确,可以用作返回值,但建议谨慎使用
}
下面我们用代码中讲解的方式大致了解一下使用相关的内容:
cpp
void test_string()
{
string s("hello world");
//length和size的区别:
//length不具有通用性,只有string类有length方法,而size具有通用性,几乎所有的容器都有size方法。
//length和size的功能是一样的,都是返回字符串的长度。但最好用size。
cout << s.length() << endl;
cout << s.size() << endl;
//capacity:返回字符串容量
cout << s.capacity() << endl;
//reserve:提前开空间,避免扩容,提高效率。当空间小于当前字符串长度是,不会缩小容量,但当当前空间大于字符串且缩小后的空间大于字符串长度时,会缩小容量。不会影响字符串!
// 每个编译器不同,看具体情况
string s;
s.reserve(100);//提前开100的空间,当push的时候不再扩容(没满)
//resize:改变字符串的大小,可以变大也可以变小。变大时,默认用'\0'填充,也可以指定填充字符
s.resize(10);//变成10个字符,默认用'\0'填充
//clear:清空数据,但一般不清理容量
s.clear();
//empty:判断是否为空
if (s.empty());
}
关于insert:
尽量减少insert的使用,过度使用会产生效率问题
cpp
void test_string()
{
string s = "1234567";
s.push_back('8');//在字符串末尾尾插一个字符
s.append("000");//在字符末尾尾插一个字符串
s += "000";
//appned作用和 s += 一样,建议使用 += 运算符更加简洁
s.insert(3, "xxx");//在下标3的位置之前插入字符串"xxx"
s.insert(0, "hello");//可以这样实现头插,但是实现效率较低
}
关于erase:
erase的使用同样需要注意,也可能导致效率问题
cpp
void test_string()
{
string s = "1234567";
s.erase(6, 1);//从第六个位置开始,删掉一个元素
s.erase(0, 1);
s.erase(s.begin());//这两种操作可以使用头删
s.erase(--s.end());
s.erase(s.size() - 1, 1);//这两种操作可以实现尾删
s.erase(3);//默认删除下标为3之后的所有元素
}
关于replace:
替换字符串中的内容,也容易导致效率问题
cpp
void test_string7()
{
string s = "hello world hello china";
s.replace(5, 1, "%%");//将下标为5开始往后一个字符替换为%%
string::npos;//表示字符串的最大下标值,实际上是一个无符号整型的最大值
size_t pos = s.find("hello");//查找字符串hello第一次出现的位置,默认是从头开始查找
//将空格替换为%%
//方法一:
size_t ss = s.find(' ');
while (ss != string::npos)
{
s.replace(ss, 1, "%%");
ss = s.find(' ', ss + 2);//从当前位置的下两个位置查找
}
//方法二:
string tmp; tmp.reserve(s.size());//提前开空间,避免扩容
for (auto ch : s)
{
if (ch == ' ')
tmp += "%%";
else
tmp += ch;
}
swap(tmp, s);
size_t pos1 = s.find("he", 5);//从下标5开始查找hello
size_t pos2 = s.rfind("he");//从后往前查找he第一次出现的位置
string sf = s.substr(6, 5);//从下标6开始往后取5个字符,返回一个新的字符串,如果没有第二个参数或第二个参数超出范围,则取到字符串结尾
//find_first_of:查找字符串中第一个出现的指定字符的位置
string str("Please, replace the vowels in this sentence by asterisks.");
size_t found = str.find_first_of("abcd");//找到字符串中a、b、c、d中第一个出现的位置,第一个找到的是abcd中任意一个都可以
while (found != string::npos)
{
str[found] = '*';
found = str.find_first_of("abcd", found + 1);//向后找其中的任意一个元素
}
//find_last_of:和find_first_of类似,不过是从后往前找
}
string的底层模拟实现:
这里我们简要提及一下封装后可以进行string的模拟实现,通过模拟实现,我们可以更清晰的了解string的运行原理。
关于封装:
迭代器的设计也是封装的一种体现
屏蔽了底层实现细节,提供了统一的类似访问容器的方式
不需要关心容器底层结构和实现细节
达到的遍历互通的效果,不考虑底层(底层已经根据不同的容器封装好),可以直接使用
cpp
namespace yyyy
{
class string
{
public:
typedef char* iterator;//自己封装出一个迭代器
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}//实现迭代器:迭代器本质上模拟的是指针的行为
string()
:_str(new char[1]{'\0'})//不能直接给空指针,至少给一个字符
,_size(0)
,_capacity(0)
{}
//短小频繁调用的函数,可以直接定义到类里面,默认是inline
string(const char* str = "")//这里全缺省不能给nuplltr空指针
{
_size = strlen(str);
//_capacity不包含\0
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);//拷贝字符串,连\0也会拷贝
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
char& operator[](size_t pos)
{
assert(pos < _size);//断言,防止发生越界
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);//断言,防止发生越界
return _str[pos];
}
private:
char* _str;//指向字符串空间的首地址
size_t _size;//字符串的长度
size_t _capacity;//字符串的容量
};
}
这样我们最简单的string的模拟实现就完成了。
希望对你有所帮助,本文完。