前言
目录
- 1. STL简介
- (1)什么是STL
- (2)STL的版本
- (3)STL的六大组件
- 2. string的使用
- 2.1 npos
- 2.2 遍历字符串string的每一个字符
- 2.3 迭代器:
- 2.4 string的内存管理
- 2.5 string模拟实现
- 2.5.1 深拷贝:
- 3 .具体代码实现(string.hpp):
- 3.1 拷贝构造代码实现:
- 3.2 增删查改的代码实现:
- 3.3 String中的成员变量和全局重载
- 3.4 String中的运算符重载
- 3.5 Windows上库里面对String的实现
1. STL简介
(1)什么是STL
STL是(standard template libaray-标准模板库)的首字母缩写,是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
(2)STL的版本
- 原始版本:
由HP实验室完成的原始版本,主要是给Visual studio使用。 - P.J. 版本:
由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。 - RW版本:
- SGR版本:
由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版 本。被GCC(Linux)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。
我们后面学习STL要阅读部分源代码,主要参考的就是这个SGR版本。
(3)STL的六大组件
- 空间配置器–内存池
- 迭代器
- 配接器
- 容器(其实是数据结构)
- 仿函数
- 算法
六大组件之间的关系:
迭代器 主要是用来访问容器的。
Iterators特性: - 迭代器提供了通用的方法来访问容器。
- iterator像指针一样的类型,有可能就是指针(比如vector、String),也有可能不是指针(比如Map、set、 list是类来封装List迭代器),其用法像指针一样的东西。
- **iterator是个类型,**用这个类型可以定义一个对象
2. string的使用
String是类模板,string是被typedef 出来的,比STL产生的早,遵循STL的那一套.
- 使用string的时候,要包含头文件 #include< string >
- typedef basic_string string;
- 由于string这个类中有上百个成员函数的接口,我们要会用其中比较常见的接口,string的学习文档:链接: 传送门
- string是默认带\0 的,和C语言字符串一样。即:string s1;//只有一个\0
Operator at 和Operator [ ]的区别在于,Operator [ ]越界以后会抛异常。
2.1 npos
string 中的npos是静态成员变量,初始值是-1.
2.2 遍历字符串string的每一个字符
遍历字符串有三种方法:
- 第一种方式,下标 + [] – []是C++重载的运算符。
- 第二种方式,迭代器 – 迭代器是用来访问数据结构的。
- 第三种方式,范围for – 前提是:C++11才支持的语法。
void test_string3()
{
//遍历string的每一个字符
string s1("hello");
cout << s1[0] << endl;
s1[0] = 'x';
cout << s1.size() << endl;
//遍历一共有三种方式
//·第一种方式,下标 + [] -- []是C++重载的运算符:
//size就是返回它有多少个字符,是不包含\0的
for (size_t i = 0; i < s1.size(); i++)
{
//s1.operator[](i);相当于调用这个函数
cout << s1[i] << " ";//[]相当于函数调用
}
cout << endl;
//编译器是看类型的,下面的运用和上面的类中调用成员函数完全不同
//const char* s2 = "world";
//s2[i];//*(s2 + i)
//·第二种方式,迭代器 -- 迭代器是用来访问数据结构的:
//sting::iterator是个类型,用这个类型可以定义一个对象
string::iterator it = s1.begin();//或者叫iter
//begin是指向第一个位置
//end不是结束位置,而是最后一个位置的下一个位置
//如果end是最后一个位置的下一个的话就会访问不到最后一个位置
//[ ) -- 左闭右开的结构 -- 方便遍历
//写成小于 < 也是可以的,但是不建议,标准的地方就是写的不等于 !=
//统一用不等于 !=
while (it != s1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
//现阶段理解的迭代器:像指针一样的东西或者就是指针
//·第三种方式,范围for -- 前提是:C++11才支持的语法
//范围for的原理:替换成迭代器
//自动取元素,赋值给ch,自动判断结束,自动++
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
}
现阶段理解的迭代器:像指针一样的东西或者指针。
iterator是个类型,用这个类型可以定义一个对象
- begin是指向第一个位置
- end不是结束位置,而是最后一个数据的下一个位置
- 如果end不是最后一个数据的下一个位置的话,循环条件中就会访问不到最后一个位置
- [ ) – 左闭右开的结构 – 方便遍历
范围for:
- 范围for又叫语法糖,因为它用起来很舒服很好用,省略了大量的代码
- 其实在底层编译器替代成了迭代器,只是上层个看起来厉害
- 大家可以通过看汇编代码来看底层实现的逻辑
- 范围for和迭代器底层并没有太大差异
2.3 迭代器:
四种迭代器分类:
- 第一种,普通的正向迭代器:const_iterator
- 第二种,反向迭代器:reverse_iterator
- 第三种,正向迭代器,能读不能写const_iterator
- 第四种,反向迭代器,能读不能写const_reverse_iterator
//不改变就加const保护
//普通迭代器是可读可写的
void Func(const string& rs)
{
//第三种,正向迭代器,能读不能写
string::const_iterator it = rs.begin();
while (it != rs.end())
{
//(*it) += 1;
cout << *it << " ";
it++;
}
cout << endl;
//第四种,反向迭代器,能读不能写
//string::const_reverse_iterator rit = rs.rbegin();
//auto自动推导:
auto rit = rs.rbegin();
while (rit != rs.rend())
{
//(*rit) -= 1;
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);
}
//iterator是终极方式,[] + 下标,是附带方式
2.4 string的内存管理
string的容量大小:Capacity
string的长度,既可以用length(),也可以用size();
reserve和resize都不会缩容量capacity,但是resize会让size降下来,只留size个。
Reserve只修改capacity大小,不修改size大小.
Resize既修改capacity大小,也修改size大小,会进行初始化
reserve和resize都不会缩容量capacity,但是resize会让size降下来,只留size个。
void test_string6()
{
string s("hello world");
//length产生的比size早
cout << s.length() << endl;
cout << s.size() << endl;
cout << s.max_size() << endl;
//容量要扩容
cout << s.capacity() << endl;
}
2.5 string模拟实现
2.5.1 深拷贝:
浅拷贝的问题: 如果是栈的类,普通的浅拷贝(按字节拷贝),会出现问题,两个栈的str指针指向同一个地方,两个栈相互影响,我们并不希望这样,所以我们要学习一下深拷贝。
浅拷贝的效果:
深拷贝的效果:
3 .具体代码实现(string.hpp):
主要是增删查改和拷贝构造
3.1 拷贝构造代码实现:
#pragma once
#include<iostream>
#include<assert.h>
#include<string>
using namespace std;
//class mystring
//class String
//自己实现的封装在命名空间中防止冲突
namespace Joker
{
//实现一个简单的string,只考虑资源管理深浅拷贝的问题
//暂且不考虑增删查改
//string需要考虑完善的增删查改和使用的string
class string
{
public:
//const对象遍历访问是不可以被修改的,应该实现两种
//两种迭代器的参数不同,函数重载
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';
}*/
//全缺省
//1、"\0"这是有两个\0
//2、""这是有一个\0
//3、'\0'这里是**把\0的assic码值0**给了指针,实际是空指针**(注意:‘\0'的ascII为0 | ’0‘的ASCII值为48)**
//而且如果支持c_str,cout的时候,解引用空指针会报错
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);
}
拷贝构造的现代写法:
- 先构造函数一个string类型的 temp 对象:使用传过来指针实例化一个所需一样的对象
- 再将tmp对象的内容和所要拷贝构造的对象的成员变量进行交换
- 在将这个拷贝函数结束之后,tmp对象的生命周期结束,自动调用其析构函数,释放掉空间
注意:
和temp交换的this 代表的_str指针是随机值,交换给temp之后,析构对随机指针释放是会报错的。
//赋值重载:
//传统写法:
//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类型的对象,调用函数是值传参
- S对象已经是拷贝构造出来的对象,直接将s对象和所需要拷贝的对象交换就好。
~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修饰过的对象调用这个.(const引用中的const只是限定了不能通过此引用去修改变量的值)
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
运算符[ ]的分析:
- 如果是传值返回的话,要是想对其进行修改的话,就不行
- 所以这里的话,必须是传的引用
3.2 增删查改的代码实现:
//s.size(&s)
//size_t size(cosnt string* const this)
size_t size() const
{
return _size;
}
//加上const普通对象可以调用,const对象也可以调用
//不加的话const对象调用会有权限放大的风险
size_t capacity() const
{
return _capacity;
}
//reserve扩容
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
//1、大于capacity
//2、小于capacity,大于size
//3、小于size
//resize的作用
//扩空间 + 初始化
//删除部分数据,保留前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';
}
}
//复用push_back
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
//添加一个字符
void push_back(char ch)
{
//if (_size == _capacity)
//{
// reserve(_capacity == 0 ? 4 : _capacity * 2);
//}
//_str[_size] = ch;
//_size++;
//_str[_size] = '\0';
insert(_size, ch);
}
//复用append
string& operator+=(const char* str)
{
append(str);
return *this;
}
//添加一个字符串
void append(const char* str)
{
/*size_t len = _size + strlen(str);
if (len > _capacity)
{
reserve(len);
}
strcpy(_str + _size, str);
_size = len;*/
insert(_size, 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;
while (end >= pos)
{
_str[end + 1] = _str[end];
end--;
}*/
//最好的一种方式
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = ch;
_size += 1;
return *this;
}
//Insert一个字符串
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) //-- 最好别这样写,怕别人给极端场景,pos,end都是0
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
end--;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
//erase一个数值
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;
}
//求字串的函数
string substr(size_t pos,size_t len=npos)const
{
assert(pos<_size);
size_t reallen=len;
if(len==npos||pos+len >_size)
{
reallen=_size-pos;
}
string sub;
for(size_t i=0;i<reallen;++i)
{
sub+=_str[pos+i];
}
return sub;
}
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);
//KMP,BM只做了解
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;
}
}
全局Swap函数和 string::Swap( )成员函数的区别:
- 上述逻辑和之前C语言实现的逻辑并无二异
- 只是用了C++的语法
//交换 -- 用库里的交换还要调用拷贝构造
void swap(string& s)
{
//调用库里的 -- 库中的(类模板)是全局的,也可以写std
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
3.3 String中的成员变量和全局重载
private:
char* _str;
size_t _size; //有效字符个数 -- 不包含'\0'
size_t _capacity; //存储有效字符的空间
//静态不能给缺省值,强制要求在类外面定义
const static size_t npos;
};
//定义
const size_t string::npos = -1;
//流插入
ostream& operator<<(ostream& out, const string& s)
{
//中间有'\0'就不能将整个字符串打印完
//out << s.c_str() << endl;
//趋向于这样写 -- 这样能保证每个字符都能打印出来
for (auto ch : s)
{
out << ch;
}
return out;
}
//流提取
istream& operator>>(istream& in, string& s)
{
//方法一:
//char ch;
//in >> ch;
//ch = in.get();
//while (ch != ' ' && ch != '\n')
//{
// s += ch;
// //in >> ch;
// ch = in.get();
//}
//要清理掉之前的空间
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;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
3.4 String中的运算符重载
运算符重载不一定是成员函数,可以写成全局
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);
}
}
3.5 Windows上库里面对String的实现
总共有28个字节,成员变量如下:
private:
//好处就是对小字符操作更快,以空间换时间
//<16 字符串存在buff数组中
//>=16 存在_str指向的堆空间上
char _buff[16];
size_t _capacity;
size_t _size;
char* _str;
尾声
看到这里,相信大家对这个C++有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦