●🧑个人主页:你帅你先说. ●📃欢迎点赞👍关注💡收藏💖 ●📖既选择了远方,便只顾风雨兼程。 ●🤟欢迎大家有问题随时私信我! ●🧐版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。
1.STL简介
1.1什么是STL
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
1.2STL的版本
- 原始版本
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.3STL的六大组件
1.4STL的缺陷
1.STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出来已经相隔了13年,STL才进一步更新。 2.STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。 3.STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。 4.STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码,当然这是模板语法本身导致的。
2.为什么学习string类?
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
3.标准库中的string类
讲到string类就必须科普一些知识
我们知道,数据是以二进制的形式在内存中存储的,因为计算机只认识0 和1 ,比如我们存一个数字10 ,在计算中就会存入1010 ,除了数字,我们还可能会存入字母、符号等,计算机不会直接就把这些符号存进去,而是制定了一个规则,这些符号和字母对应一个值,这就是我们所熟知的ASCII码,比如存字符'A' ,对应65 ,计算机中就会存入1000001 ,符号也是类似的原理,早期计算机是欧美那些国家发明出来的,但随着国家之间加强合作,最终每个国家都会使用计算机编程,所以每个国家都会制定一套各自语言的存储规则,所以就有人制定了一个表示全世界的编码表 ,叫做utf (Unicode Transformation Format),它有很多编码方式,例如utf-8、utf-16、utf-32。现在用的最多的是utf-8 ,所以编码实际上就是值和符号建立映射关系。当然,我们国家也有自己一套中文量身定制的编码表,叫gbk
3.1string类常用接口
3.1.1string类对象的常见构造
函数名称 | 功能说明 |
---|
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 s2("hello bit");
string s3(s2);
}
3.1.2string类对象的容量操作
函数名称 | 功能说明 |
---|
size | 返回字符串有效字符长度 | empty | 检测字符串释放为空串,是返回true,否则返回false | clear | 清空有效字符 | reserve | 为字符串预留空间 | resize | 将有效字符的个数该成n个,多出的空间用字符c填充 | capacity | 返回空间总大小 |
size&&clear reserve resize resize和reserve的区别在于resize不仅仅是开空间,还会对这些空间进行初始化(默认为0)。换句话说,reserve中_size的值是不变的,变的只是_capacity,而resize则会改变_size的值,扩容多少_size的值就是多少。
- size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一 致,一般情况下基本都是用size()。
- clear()只是将string中有效字符清空,不改变底层空间大小。
- resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字 符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的 元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
- reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于 string的底层空间总大小时,reserver不会改变容量大小。
3.1.3string类对象的访问及遍历操作
函数名称 | 功能说明 |
---|
operator[] | 返回pos位置的字符,const string类对象调用 | at | 获取字符串中的字符 | back | 访问最后一个字符 | front | 访问第一个字符 | begin/end | begin获取第一个字符的迭代器/end获取最后一个字符下一个位置的迭代器 | rbegin/rend | rend获取第一个字符前一个位置的迭代器/rbegin获取最后一个字符下一个位置的迭代器 |
operator[] at 迭代器+begin/end 我们发现,迭代器的访问方式有点像指针,那迭代器有什么意义呢? 对于string,下标就足够好用了,确实可以不用迭代器,但是对于其它容器呢?迭代器的意义就在于所有的容器都可以使用迭代器这种方式去访问修改。
3.1.4string类对象的修改操作
函数名称 | 功能说明 |
---|
push_back | 在字符串后尾插字符c | append | 在字符串后追加一个字符串 | operator+= | 在字符串后追加字符串str | c_str | 返回C格式字符串 | find/rfind + npos | 从字符串pos位置开始往后/前找字符c,返回该字符在字符串中的位置 | substr | 在str中从pos位置开始,截取n个字符,然后将其返回 | getline | 读入一行字符串,遇到’\n’结束 |
push_back && append
- 在string尾部追加字符时,s.push_back? / s.append(1, c) / s += 'c’三种的实现方式差不多,一般 情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
- 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
c_str 我们发现fopen函数只支持const char* 类型(即C语言格式的字符串)的数据,此时就可以用c_str来转换。 find/rfind && npos && substr 现在我们有一个需求,要求取出文件的后缀名,就需要用上这些函数了。 npos 是一个常数,用来表示不存在的位置,一般是取-1,转换成size_t 类型后也是一个很大的数,可以认为不存在。 接下来我们还有个需求,要把http://www.cplusplus.com/reference/string/string/ 这条网址分成协议 、域名 、虚拟目录 getline 第一个参数需要传入输入流 ,第二个参数需要传一个字符串。
3.1.5字符串和其它数据类型的转换
4.string类的模拟实现
为了避免与库里面的stirng类冲突,我们可以自己定义个命名空间。
#pragma once
#include<string>
namespace ljt
{
class string
{
public:
string(const char* str) :_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
};
}
这里说几个小细节,构造函数中不能写成_str(str) ,因为这样传过来的字符串是无法更改的,所以必须在堆上申请空间进行存储。strlen(str)+1 这里面加1的原因是还有一个'\0' 也占空间大小。 此时我们测试一下程序,发现程序奔溃了。 我们通过调试来看看是什么问题。
这个时候我们就发现问题了,s1和s2在类里面定义的指针都指向了同一块空间,当程序结束时,s2析构函数释放空间,s1空间又释放了一次空间,一块空间被释放了两次,造成了程序崩溃。 这里就涉及到了深浅拷贝问题,浅拷贝就是完全复制粘贴,就是上面这个例子。在这里用浅拷贝显然不行,所以我们可以使用深拷贝,刚刚我们发现程序崩溃的原因是一个空间释放多次,为了解决这个问题,我们可以自己写一个拷贝构造函数,且每次构造都开一个空间,这样就能避免重复释放相同空间。
#pragma once
#include<string>
namespace ljt
{
class string
{
public:
string(const char* str) :_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
string(const string& s):_str(new char[strlen(s._str)+1])
{
strcpy(_str, s._str);
}
~string()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
};
}
此时通过调试发现就不是同一块空间了。 刚刚我们写的深拷贝是传统写法,深拷贝还有一种现代写法。
string(const string& s):_str(nullptr)
{
string tmp(s._str);
swap(_str,tmp._str);
}
这种写法是这样的,首先让tmp开一块和s._str一样的空间,然后交换_str和tmp._str所指向的空间,然后出了作用域tmp会调用析构函数,释放空间。 当然,深浅拷贝不仅仅这么简单。 我们发现,这种情况下又崩溃了。 图解原因 此时s1指向了s3开的空间,而s1开的空间又没有释放,可我们刚刚不是已经解决这个问题了吗?在这里需要区分一下不同写法所调用的函数是哪些
int main()
{
String s1("hello");
String s2 = "world";
String s3(s1);
String s4 = s1;
String s5;
s5 = s1;
return 0;
}
尤其要注意string s4 = s1 和s5 = s1 ,一个是初始化,一个是赋值。 所以要想解决刚刚那个问题,我们还得写一个拷贝赋值运算符,也就是重载=运算符。
string& operator=(const string& s)
{
if (this != &s)
{
delete[] _str;
char* tmp = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this;
}
这段代码一般来说大多数情况下是没有问题了,但有一种特殊情况,如果new失败呢?前面我们说过new失败会抛异常,那你失败就失败吧,但空间已经被你释放了,所以这段代码还要再优化一下。
string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[strlen(s._str) + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
}
return *this;
}
同样地,这段代码也是传统写法,它也有现代写法。
string& operator=(const string& s)
{
if(this != &s)
{
string tmp(s);
swap(_str,tmp._str);
}
return *this;
}
思想和上面深拷贝的类似。 这段代码还可以写的更简洁
string& operator=(string s)
{
swap(_str,s._str);
return *this;
}
接下来我们开始写string类 的增删查改 我们增加_size 和_capacity 两个变量,然后对构造函数进行完善
namespace ljt
{
class string
{
public:
string(const char* str = "") :_size(strlen(str)),_capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
string(const string& s):_str(nullptr) :_size(0), _capacity(0)
{
string tmp(s._str);
swap(_str,tmp._str);
swap(_size,tmp._size);
swap(_capacity,tmp._capacity);
}
string& operator=(string s)
{
swap(_str,s._str);
swap(_size,s._size);
swap(_capacity,tmp._capacity);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
实现c_str()函数
const char* c_str()const
{
return _str;
}
实现size()函数
size_t size()
{
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];
}
实现[]重载后,不仅可以得到某个位置的值,还可以修改某个位置的值,因为返回的是引用。 如果把引用去掉,就只能访问,不能修改。 迭代器
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str+_size;
}
reserve()函数
void reserve(size_t n)
{
if(n > _capacity)
{
char* tmp = new char[n+1];
strcpy(tmp,_str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
resize()函数
void resize(size_t n, char ch = '\0')
{
if (n <= _size)
{
_size = n;
}
else
{
if (n > _capacity)
{
reserve(n);
}
memset(_str + _size,ch,n - _size);
_size = n;
}
_str[n] = '\0';
}
push_back()函数
void push_back(char ch)
{
if(_size == capacity)
{
reserve(_capacity == 0 ? 4:capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
append()函数
void append(const char* str)
{
size_t len = strlen(str);
if(_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size,str);
_size += len;
}
string类里的swap() C++里除了标准库里有swap()函数,string类里面也有swap()函数,那为什么string类还要单独写一个swap()函数?相信大家可以猜到,可能string类里的swap()函数是专门针对string类写的,所以效率可能会更高,确实是这样的。 这是C++标准库里的swap()函数 我们看到,标准库里的方法要进行三次拷贝构造,且都是深拷贝。 而string类里的swap()函数只是简单进行值的交换,所以效率更高。 string类+=运算符重载
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string类比大小运算符重载
bool operator<(const string& s1,const string& s2)
{
retrun strcmp(s1.c_str(),s2.c_str()) < 0;
}
bool operator==(const string& s1,const string& s2)
{
retrun strcmp(s1.c_str(),s2.c_str()) == 0;
}
bool operator<=(const string& s1,const string& s2)
{
retrun s1 < s2 || s1 == s2;
}
bool operator>(const string& s1,const string& s2)
{
retrun strcmp(s1.c_str(),s2.c_str()) > 0;
}
bool operator>=(const string& s1,const string& s2)
{
retrun !(s1 < s2);
}
bool operator!=(const string& s1,const string& s2)
{
retrun !(s1 == s2);
}
find()函数
size_t find(char ch)
{
for(size_t i = 0;i < _size;i++)
{
if(ch == _str[i])
{
return i;
}
}
return npos;
}
size_t find(const char* s,size_t pos = 0)
{
const char* ptr = strstr(_str + pos,s);
if(ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
insert()函数
string& insert(size_t pos,char ch)
{
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++;
return *this;
}
string& insert(size_t pos,const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if(_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + len;
while(end > pos + len)
{
_str[end] = _str[end-1en];
end--;
}
strncpy(_str + pos,s,len);
_size += len;
return *this;
}
erase()函数
string& erase(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos+len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
<<运算符重载
ostream& operator<<(ostream& out,const string& s)
{
for(int i = 0;i < s.size();i++)
{
out<<s[i];
}
return out;
}
>>运算符重载
istream& operator>>(istream& in,string& s)
{
char ch = in.get();
while(ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get()
}
return in;
}
喜欢这篇文章的可以给个一键三连 点赞👍关注💡收藏💖
|