初阶的C++语法和基本的类和对象我们已经学过了,下面我们会步入一段新的旅程。本章我们将初步了解STL(标准模板库),并且深入探讨其中一个非常重要的容器———string。
目录
(一)STL简介(了解即可)
(1)定义和版本分类
(2)STL六大组件
(3)STL的一些不足
(二)string的认识和使用
(1)string常用接口
1、 string类对象的常见构造
2、string类对象的容量操作
3、string类对象的修改操作
4、string类的非成员函数
5、string类对象的访问及迭代器相关操作*
(三)string的模拟实现
(1)迭代器的实现
(2)构造、拷贝构造和赋值重载的实现
(3)其他操作的实现
(一)STL简介(了解即可)
(1)定义和版本分类
定义:
- 原始版本: 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要阅读部分源代码, 要参考的就是这个版本。
(2)STL六大组件
STL包含着数据结构和算法,经过不断的发展,常用的有下面的六大组件:
(3)STL的一些不足
- 1. STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出 来已经相隔了13年,STL才进一步更新。
- 2. STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。
- 3. STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。
- 4. STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码,当然这是模板语 法本身导致的。
(二)string的认识和使用
经过上面的介绍,我们明白了string是STL库中容器的一个类,我们下面的学习就是逐一地学习STL的各个组件部分以及组件中的数据结构、算法等。
首先,我在这里先介绍一个好用的工具,有利于我们查询库,深入了解分类、实现、用法:
https://cplusplus.com/
下面步入string的学习:
(1)string常用接口
1、 string类对象的常见构造
我们进入上面的cplusplus网站查询string,就可以看到string的常见构造:
这里主要给出了构造函数,析构函数和赋值重载。下面我们一一查看:
构造函数:
这里给出了多种应用方法,构造,缺省构造,拷贝构造等,大家可以参照着使用。
赋值重载:
可以看出,我们可以赋一个字符串,也可以赋一个字符。
举个例子:
2、string类对象的容量操作
我们延续上面的操作,查询知主要的容量操作如下:
最主要的几个操作和用途如下:
#include <iostream>
using namespace std;
#include <string>
// 测试string容量相关的接口
// size/clear/resize
void Teststring1()
{
// 注意:string类对象支持直接用cin和cout进行输入和输出
string s("hello, zc!!!");
cout << s.size() << endl;
cout << s.length() << endl;
cout << s.capacity() << endl;
cout << s << endl;
// 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
s.clear();
cout << s.size() << endl;
cout << s.capacity() << endl;
// 将s中有效字符个数增加到10个,多出位置用'a'进行填充
// “aaaaaaaaaa”
s.resize(10, 'a');
cout << s.size() << endl;
cout << s.capacity() << endl;
// 将s中有效字符个数增加到15个,多出位置用缺省值'\0'进行填充
// "aaaaaaaaaa\0\0\0\0\0"
// 注意此时s中有效字符个数已经增加到15个
s.resize(15);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
// 将s中有效字符个数缩小到5个
s.resize(5);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
}
//====================================================================================
void Teststring2()
{
string s;
// 测试reserve是否会改变string中有效元素个数
s.reserve(100);
cout << s.size() << endl;
cout << s.capacity() << endl;
// 测试reserve参数小于string的底层空间大小时,是否会将空间缩小
s.reserve(50);
cout << s.size() << endl;
cout << s.capacity() << endl;
}
// 利用reserve提高插入数据的效率,避免增容带来的开销
//====================================================================================
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';
}
}
}
// 构建vector时,如果提前已经知道string中大概要放多少个元素,可以提前将string中空间设置好
void TestPushBackReserve()
{
string s;
s.reserve(100);
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';
}
}
}
int main()
{
return 0;
}
3、string类对象的修改操作
样例代码:
// 测试string:
// 1. 插入(拼接)方式:push_back append operator+=
// 2. 正向和反向查找:find() + rfind()
// 3. 截取子串:substr()
// 4. 删除:erase
void Teststring5()
{
string str;
str.push_back(' '); // 在str后插入空格
str.append("hello"); // 在str后追加一个字符"hello"
str += 'z'; // 在str后追加一个字符'z'
str += "cc"; // 在str后追加一个字符串"cc"
cout << str << endl;
cout << str.c_str() << endl; // 以C语言的方式打印字符串
// 获取file的后缀
string file("string.cpp");
size_t pos = file.rfind('.');
string suffix(file.substr(pos, file.size() - pos));
cout << suffix << endl;
// npos是string里面的一个静态成员变量
// static const size_t npos = -1;
// 取出url中的域名
string url("http://www.cplusplus.com/reference/string/string/find/");
cout << url << endl;
size_t start = url.find("://");
if (start == string::npos)
{
cout << "invalid url" << endl;
return;
}
start += 3;
size_t finish = url.find('/', start);
string address = url.substr(start, finish - start);
cout << address << endl;
// 删除url的协议前缀
pos = url.find("://");
url.erase(0, pos + 3);
cout << url << endl;
}
int main()
{
return 0;
}
4、string类的非成员函数
这里就不着重强调了,平时中大家会遇到很多输出输入操作,无需进一步讲解。
5、string类对象的访问及迭代器相关操作*
对对象的访问重点是[ ]的重载,他可以访问string中任意的字符,也可以进行历遍访问操作。
// string的遍历
void Teststring3()
{
string s1("hello zc");
const string s2("Hello zc");
cout << s1 << " " << s2 << endl;
cout << s1[0] << " " << s2[0] << endl;
s1[0] = 'H';
cout << s1 << endl;
// s2[0] = 'h'; 代码编译失败,因为const类型对象不能修改
}
void Teststring4()
{
string s("hello Bit");
// 3种遍历方式:
// 需要注意的以下三种方式除了遍历string对象,还可以遍历是修改string中的字符,
// 另外以下三种方式对于string而言,第一种使用最多
// 1. for+operator[]
for (size_t i = 0; i < s.size(); ++i)
cout << s[i] << endl;
}
我们重点讲解迭代器操作,因为迭代器对于任意一个容器都可以进行历遍访问!!!
比如后面的vector,list等等都可以进行迭代器进行历遍。
迭代器历遍:
首先我们定义一个迭代器对象,然后利用begin()初始化,以及end()来标识范围即可实现历遍。
这里大家可能认为还是上面的[ ]好用,为什么非要用迭代器?
简要回答一下:
迭代器可以实现对任意容器的访问,如后面的list链表也可以同样的操作进行访问,比较具有统一性(而且下面将会介绍范围for,更简便)。
// string的遍历
void Teststring4()
{
// 2.迭代器
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << endl;
++it;
}
// string::reverse_iterator rit = s.rbegin();
// C++11之后,直接使用auto定义迭代器,让编译器推到迭代器的类型
auto rit = s.rbegin();
while (rit != s.rend())
cout << *rit << endl;
}
范围for:
范围for就像一个语法糖,历遍访问可以非常简易的实现,迭代器类型直接用auto代替,直接进行历遍访问(底层还是使用迭代器)。
操作如下:
// string的遍历
// 注意:string遍历时使用最多的还是for+下标 或者 范围for(C++11后才支持)
//
void Teststring4()
{
string s("hello zc");
// 3.范围for
for (auto ch : s)
cout << ch << endl;
}
(三)string的模拟实现
前言,我们模拟实现string首先要构想它是一个什么样的类,成员有什么?
通过上面的学习,我们可以初步定下他的三个类成员就是字符串,大小和容量,如下:
(1)迭代器的实现
对于string类,他的迭代器就是一个char类型的指针,然后记录好begin和end的位置,就可以从头向后历遍访问了。
namespace ZC
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const iterator begin()const
{
return _str;
}
const iterator end()const
{
return _str + _size;
}
private:
char* _str;
size_t _capacity;
size_t _size;
};
(2)构造、拷贝构造和赋值重载的实现
我们上面查询过,构造、拷贝构造函数的版本有很多种:
这里着重要注意的深浅拷贝问题:
浅拷贝:
浅拷贝就是没有对st2另外开空间,然后直接把st1指向的空间赋给st2,这样一来,两者指向同一块空间,增删查改不仅会相互影响,而且对于同一块空间析构函数会调用两次倒置程序崩溃。
深拷贝:
新开辟一段空间,然后一一表示st1的元素拷贝到st2新开的空间上,这样的拷贝不会相互影响。
下面我们分别来看构造函数。拷贝构造和复制重载:
这三类都需要用到深拷贝!!!
//实现一个简单的string,只考虑资源管理深浅拷贝的问题
//暂且不考虑增删查改
//string需要考虑完善的增删查改和使用的string
class string
{
public:
//无参的构造函数
/*string()
:_size(0)
,_capacity(0)
{
_str = new char[1];
_str[0] = '\0';
}*/
//全缺省
//1、"\0"这是有两个\0
//2、""这是有一个\0
//3、'\0'这里是把\0的assic码值0给了指针,实际是空指针
string(const char* str = "") //""是C语言默认常量字符串,后面有\0
:_size(strlen(str)) //strlen()是不会判空指针的
, _capacity(_size)
{
_str = new char[_capacity + 1];//给'\0'多开一个
strcpy(_str, str);
}
//·构造函数:
//·传统的写法:本分,老实,老老实实干活,该开空间开空间,该拷贝数据就自己拷贝数据
//s2(s1); - 深拷贝
//在类里面只要用string对象访问成员都是不受限制的
//私有是限制在类外面使用对象去访问成员
/*string(const string& s)
:_size(strlen(s._str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}*/
//·现代写法:剥削,要完成深拷贝,自己不想干活,安排别人干活,然后窃取劳动成果
//要初始化一下,不然有可能释放野指针
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp);
}
//赋值重载:
//传统写法:
//s1 = s3,如果不传引用返回,用传值返的话会深拷贝,代价太大了
//new失败了之后会抛异常,用try捕获
//string& operator = (const string& s)
//{
// if (this != &s)
// {
// //1、先释放:如果s1开空间失败了,之前的空间也被释放了
// /*delete[] _str;
// _str = new char[strlen(s._str) + 1];
// strcpy(_str, s._str);*/
// //2、先开空间:下面写法可以避免上述问题
// char* tmp = new char[s._capacity + 1];
// strcpy(tmp, s._str);
// delete[] _str;
// _str = tmp;
// _size = s._size;
// _capacity = s._capacity;
// }
// return *this;
//}
//现代写法:
//现代方法一:
/*string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s._str);
swap(tmp);
}
return *this;
}*/
//现代方法二:-- 更简单,一行代码搞定,适用于所有深拷贝
//s 就是 s1的深拷贝,先传参,传参就是拷贝构造
string& operator=(string s)
{
swap(s);
return *this;
}
——构造函数的现代写法:
- 先实例一个string类型的tmp对象:
- 直接根据传来的指针实例化一个所需一样的对象
- 再将tmp对象的内容和所要拷贝构造的对象的成员变量进行交换
- 在将这个拷贝函数结束之后,tmp对象的生命周期结束,自动调用其析构函数,释放掉空间
——赋值重载的现代写法:
- 赋值函数中,形参是string类型的对象,调用函数是值传参
- s对象已经是拷贝的对象了,直接将s对象和所需要拷贝的对象交换就好
注意:
- 要被拷贝构造的对象中的成员变量为随机值,所以里面的str成员指针是随机值
- 这个随机值换给tmp这个对象之后
- tmp对象生命周期结束后,自动调用析构函数,对野指针进行释放,就会出错,程序崩溃
- 所以拷贝构造一开始要在初始化类表中对要被拷贝的对象成员变量进行初始化
(3)其他操作的实现
主要是增删查改,判断大小等操作。
主要有一点大家可能难以理解:
首先为什么要删掉原_str指针再赋值呢?
主要是为了防止_str后面的空间不够开辟的了,如果在原_str处开辟空间不够的话,系统会重新找新的足够大的空间,这样_str的位置就改变了,原来char* _str存放的位置就无法找到新开辟的空间了。
要先开所需要的空间再去拷贝之后再释放
因为如果先释放,当新空间空开失败了的时候
就会出现原来的空间也被释放了,原来的数据也找不到了
const char* c_str()
{
return _str;
}
char& operator[](size_t pos)
{
return _str[pos];
}
const char& operator[](size_t pos) const
{
return _str[pos];
}
const size_t size()const
{
return _size;
}
bool operator>(const string& s)const
{
return strcmp(_str, s._str) >0;
}
bool operator==(const string& s)const
{
return strcmp(_str, s._str) == 0;
}
bool operator<(const string& s)const
{
return strcmp(_str, s._str) < 0;
}
bool operator>=(const string& s)const
{
return !(*this<s);
}
bool operator<=(const string& s)const
{
return *this == s || *this < s;
}
bool operator!=(const string& s)const
{
return !(*this==s);
}
//开空间
void reserve(size_t n)
{
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)
{
// 删除数据--保留前n个
_size = n;
_str[_size] = '\0';
}
else if (n > _size)
{
if (n > _capacity)
{
reserve(n);
}
size_t i = _size;
while (i < n)
{
_str[i] = ch;
++i;
}
_size = n;
_str[_size] = '\0';
}
}
void push_back(char ch)
{
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
void insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size + 1 > _capacity)
{
_capacity *= 2;
}
//区别end=_size while(end>=pos)_str[end -1 ] = _str[end ];这里>=的=没法实现,因为无符号
size_t end = _size+1;
while (end > pos)
{
_str[end ] = _str[end - 1];
end--;
}
_str[pos] = ch;
++_size;
}
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + 1 > _capacity)
{
_capacity = _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;
}
void erase(size_t pos, size_t len = npos)
{
assert(pos <= _size);
if (pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
while (_str[pos+len]!='\0')
{
_str[pos] = _str[pos + len];
pos++;
}
_str[pos] = '\0';
_size -= len;
}
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_capacity, s._capacity);
std::swap(_size, s._size);
}
size_t find(char ch, size_t pos = 0)
{
assert(pos < _size);
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
// kmp
char* p = strstr(_str + pos, str);
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;
}
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
相关链接——>>string的模拟实现
感谢您的阅读,祝您学业有成!