📖前言
🎬STL简介:
(1)什么是STL:
STL是(standard template libaray-标准模板库)的首字母缩写,是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
(2)STL的版本:
我们后面学习STL要阅读部分源代码,主要参考的就是这个版本。
虽然每个版本整体大致相同,但是有些细节处理还是不尽相同的,所以会出现同一段代码在不同平台上跑出来的结果是不同的,有可能在Linux平台上能跑过,在Windows平台上会崩溃的情况。
(3)STL的六大组件:
后期我们会慢慢介绍这六个组件。
我们主要的任务是:学会使用 + 了解框架 + 模拟实现。
1. string的使用
string是类模板,string是被typedef出来的,比STL产生的早,遵循STL的那一套。
- 使用string的时候,要包含头文件 #include< string >
- 由于string这个类中有上百个成员函数的接口,我们要会用其中比较常见的接口,其余不熟悉的我们要学会自己查文档,自主学习的能力很重要,一般看了成员函数参数和下面的介绍就差不多了
- string的学习文档:👉传送门
1.1 初始化子字符串:
string是管理动态增长字符数组,这个字符串以\0结尾。
void test_string1()
{
char str[10];
string s1;
string s2("hello world");
s2 += "!!!";
string s3(s2);
string s4 = s2;
string s5("");
string s6("https://cplusplus.com/reference/string/string/string/", 4);
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
cout << s5 << endl;
cout << s6 << endl;
string s7(10, 'x');
cout << s7 << endl;
string s8(s2, 6, 3);
cout << s8 << endl;
string s9(s2, 6, 100);
cout << s9 << endl;
string s10(s2, 6);
cout << s10 << endl;
}
- string是属于std这个标准库的命名空间
- std::string s
直接见代码:
void test_string2()
{
string s1("hello");
string s2("xxx");
s1 = s2;
s1 = "yyy";
s1 = 'y';
}
1.2 遍历字符串string的每一个字符:
遍历字符串有三种方法:
- 第一种方式,下标 + [] – []是C++重载的运算符。
- 第二种方式,迭代器 – 迭代器是用来访问数据结构的。
- 第三种方式,范围for – 前提是:C++11才支持的语法。
void test_string3()
{
string s1("hello");
cout << s1[0] << endl;
s1[0] = 'x';
cout << s1.size() << endl;
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
cout << endl;
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
}
- 用下标是类中封装了运算符重载这个成员函数,直接调用即可
- sting::iterator是个类型,用这个类型可以定义一个对象
现阶段理解的迭代器:像指针一样的东西或者就是指针。
- begin是指向第一个位置
- end不是结束位置,而是最后一个数据的下一个位置
- 如果end不是最后一个数据的下一个位置的话,循环条件中就会访问不到最后一个位置
- [ ) – 左闭右开的结构 – 方便遍历
范围for:
- 范围for又叫语法糖,因为它用起来很舒服很好用,省略了大量的代码
- 其实在底层编译器替代成了迭代器,只是上层个看起来厉害
- 大家可以通过看汇编代码来看底层实现的逻辑
- 范围for和迭代器底层并没有太大差异
1.3 迭代器:
四种迭代器分类:
- 第一种,普通的正向迭代器:
- 第二种,反向迭代器:
- 第三种,正向迭代器,能读不能写
- 第四种,反向迭代器,能读不能写
void Func(const string& rs)
{
string::const_iterator it = rs.begin();
while (it != rs.end())
{
cout << *it << " ";
it++;
}
cout << endl;
auto rit = rs.rbegin();
while (rit != rs.rend())
{
cout << *rit << " ";
rit++;
}
cout << endl;
}
void test_string5()
{
string s("hello world");
string::iterator it = s.begin();
while (it != s.end())
{
(*it) += 1;
cout << *it << " ";
it++;
}
cout << endl;
cout << s << endl;
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
(*rit) -= 1;
cout << *rit << " ";
rit++;
}
cout << endl;
cout << s << endl;
Func(s);
}
以我们目前的理解方式,迭代器是个指针。
正向迭代器:
反向迭代器(rbegin):
- 普通的迭代器是可读可写的
- 要是不想string的内容不被修改
- 可以用const_iterator 或者 const_reverse_iterator类型的反向迭代
如果迭代器类型太长的话,可以用之前学过的auto直接根据对象类型直接推导出来。
- string::const_reverse_iterator rit = rs.rbegin();
- auto自动推导:auto rit = rs.rbegin();
1.4 string的内存管理:
string的容量大小: string的长度,既可以用length(),也可以用size();
void test_string6()
{
string s("hello world");
cout << s.length() << endl;
cout << s.size() << endl;
cout << s.max_size() << endl;
cout << s.capacity() << endl;
}
因为length产生的比size早,string产生的比STL早,所以兼容STL中的size()。
扩容的规律:
void TestPushBack()
{
string s;
size_t sz = s.capacity();
cout << "making s grow:\n";
for (int i = 0; i < 100; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
由此可见VS平台下是1.5倍扩容。
补充:Linux下是2倍的扩容。
reserve/resize/clear的使用:
void test_string7()
{
string s("hello world!!!");
cout << s.size() << endl;
cout << s.length() << endl;
cout << s.capacity() << endl;
cout << s << endl;
s.clear();
cout << s.size() << endl;
cout << s.capacity() << endl;
s.resize(10, 'a');
cout << s.size() << endl;
cout << s.capacity() << endl;
s.resize(15);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
s.resize(5);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
}
- resever只是开空间,而resize是开空间 + 初始化
- s.resize(10, ‘a’);将s中有效字符个数增加到10个,多出位置用’a’进行填充
- s.resize(15);将s中有效字符个数增加到15个,多出位置用缺省值’\0’进行填充
- 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
reserve和resize都不会缩容量capacity,但是resize会让size降下来,只留size个。
push_back 和 erase和之前数据结构一样,使用起来直接调用该成员函数即可: insert的其他用法:
直接见代码:
void test_string9()
{
string s("hello");
s.insert(3, 1, 'x');
s.insert(s.begin() + 3, 'y');
cout << s << endl;
s.insert(0, "sort");
cout << s << endl;
}
在指定位置前面插入字符或字符串。
string获取子字符串,解析网址为例,直接见下述代码:
void test_string4()
{
string url1("http://www.cplusplus.com/reference/string/string/find/");
string url2("https://blog.csdn.net/m0_63059866?spm=1000.2115.3001.5343");
string& url = url2;
string protocol;
size_t pos1 = url.find("://");
if (pos1 != string::npos)
{
protocol = url.substr(0, pos1);
cout << "protocol:" << protocol << endl;
}
else
{
cout << "非法的url" << endl;
}
string domain;
size_t pos2 = url1.find('/', pos1 + 3);
if (pos2 != string::npos)
{
domain = url.substr(pos1 + 3, pos2 - (pos1 + 3));
cout << "domain:" << domain << endl;
}
else
{
cout << "非法url" << endl;
}
string uri = url.substr(pos2 + 1);
cout << "uri:" << uri << endl;
}
2. string模拟实现
2.1 深拷贝:
浅拷贝的问题:
如果是栈的类,普通的浅拷贝(按字节拷贝),会出现问题,两个栈的str指针指向同一个地方,两个栈相互影响,我们并不希望这样,所以我们要学习一下深拷贝。
浅拷贝的效果:
深拷贝的效果:
2.2 具体代码实现(string.h):
#pragma once
#include<iostream>
#include<assert.h>
#include<string>
using namespace std;
namespace YY
{
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(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(tmp);
}
string& operator=(string s)
{
swap(s);
return *this;
}
构造函数的现代写法:
- 先实例一个string类型的tmp对象:直接根据传来的指针实例化一个所需一样的对象
- 再将tmp对象的内容和所要拷贝构造的对象的成员变量进行交换
- 在将这个拷贝函数结束之后,tmp对象的生命周期结束,自动调用其析构函数,释放掉空间
负值重载的现代写法:
- 赋值函数中,形参是string类型的对象,调用函数是值传参
- s对象已经是拷贝的对象了,直接将s对象和所需要拷贝的对象交换就好
注意:
- 要被拷贝构造的对象中的成员变量为随机值,所以里面的str成员指针是随机值
- 这个随机值换给tmp这个对象之后
- tmp对象生命周期结束后,自动调用析构函数,对野指针进行释放,就会出错,程序崩溃
- 所以拷贝构造一开始要在初始化类表中对要被拷贝的对象成员变量进行初始化
~string()
{
if (_str != nullptr)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
const char* c_str() const
{
return _str;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
这里用引用返回的原因:
- 如果是传值返回的话,返回的是临时对象,具有常性,要是相对其进行修改的话没救不行
- 传的是引用的话就没有这种问题
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& operator+=(char ch)
{
push_back(ch);
return *this;
}
void push_back(char ch)
{
insert(_size, ch);
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
void append(const char* str)
{
insert(_size, str);
}
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 += 1;
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);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
end--;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
string& erase(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;
}
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
上述代码就是之前学过的数据结构 — 顺序表了
- 上述逻辑和之前C语言实现的逻辑并无二异
- 只使用C++这门语言写的,用了C++的语法
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
上述交换函数在现代写法的构造函数和赋值重载中用的到。
private:
char* _str;
size_t _size;
size_t _capacity;
const static size_t npos;
};
const size_t string::npos = -1;
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
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, sizeof(char) * 128);
i = 0;
}
ch = in.get();
}
s += buff;
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);
}
}
总结:
成员函数中层层调用、相互复用,封装在一个类当中,极大地考察了我们之前学的类和对象和C++各种语法,需要大家细心 + 谨慎。
但行好事莫问前程,欲戴皇冠必承其重,我们在路上,努力且不放弃……
|