STL简介
STL(standard template libaray - 标准模板库)是C++标准库的重要组成部分,不仅是一个可复用的组件库,还是一个包罗了数据结构与算法的软件框架。
仿函数 | greater、less…… |
算法 | find、swap、reverse、sort、merge…… |
迭代器 | iterator、const_iterator、reverse_iterator、const_reverse_iterator |
空间配置器 | allocator |
容器 | string、vector、list、deque、map、set、multimap、multiset |
配接器 | stack、queue、priority_queue |
对于STL的学习,第一步是要熟练使用STL,第二步是了解泛型技术的内涵和STL的原理并进行实操,第三步是对STL进行扩展。所以接下来的一段时间,我会将STL的各种容器和重要函数进行讲解,首先从string开始。
string类
在C语言中,字符串是以' \0 '为结尾的字符集合,为了方便操作,C语言标准库中提供了一些 str系列的库函数,但是这些库函数与字符串是分离开的,不符合面向对象的思想,而且底层空间需要用户自己管理,稍不留神可能就会越界访问。在C++中针对这些问题做了一系列工作,即在C++标准库中定义了string类。
标准库中的string类
string类的文档介绍:string - C++ Reference (cplusplus.com)。
需要注意的是,使用string类时,必须包含#include头文件以及using namespace std;
auto 和 范围for
在C++中补充了两个关键字,为了方便后续的学习,我们在此处先了解一下。
auto
- 在早期C/C++中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,后来在C++11中,标准委员会变废为宝赋予了 auto 全新的含义:auto 不再是一个存储类型指示符,而是作为一种全新的类型指示符来指示指示编译器,auto 声明的变量必须有编译器在编译时期推导而得。
- 用auto声明指针类型时,使用 auto 和 auto* 没有任何区别,但用auto声明引用类型时须加&。
- 当在同一行声明多个变量时,这些变量必须是相同类型的,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
- auto 不能作为函数的参数,可以做返回值,但是建议谨慎使用,
- auto 不能直接用来声明数组。
- 换取了遍历,但是牺牲了可读性。
代码讲解:
#include<iostream>
using namespace std;
int func1()
{
return 10;
}
// 不能做参数
void func2(auto a) //X
{}
// 可以做返回值,但是建议谨慎使用
auto func3()
{
return 3;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = func1();
// 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项
auto e; //X
//输出变量的类型名
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
int x = 10;
//使用 auto 和 auto* 没有任何区别
auto y = &x;
auto* z = &x;
//auto声明引用类型时须加&
auto& m = x;
cout << typeid(x).name() << endl;
cout << typeid(y).name() << endl;
cout << typeid(z).name() << endl;
auto aa = 1, bb = 2;
// 编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
auto cc = 3, dd = 4.0; //X
// 编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型
auto array[] = { 4, 5, 6 }; //X
return 0;
}
很多人在接触 auto 的时候不明白它的意义,在我们未来学习的知识越来越复杂时,类型名也会越来越长,为了简化类型名的编写,auto 就有了意义。代码如下(无需看懂,了解即可):
#include<iostream>
#include <string>
#include <map>
using namespace std;
int main()
{
std::map<std::string, std::string> dict = { { "apple", "苹果" },{ "orange",
"橙子" }, {"pear","梨"} };
// auto的用武之地
//std::map<std::string, std::string>::iterator it = dict.begin();
auto it = dict.begin();
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
return 0;
}
范围for
在C语言中,我们想要循环遍历连续的空间,需要先确定空间的范围,但是对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此在C++11中引入了基于范围的 for 循环。范围 for 后跟的括号中由冒号" : "分为两部分,第一部分是范围内用于迭代的变量,第二部分是被迭代的空间,范围 for 自动迭代,自动取数据,自动判断结束。一般我们使用范围 for 可以作用到数组和容器对象上进行遍历,且范围 for 的底层就是迭代器,因此所有容器都支持范围for。
#include<iostream>
#include <string>
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
// C++98的遍历:for循环
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
array[i] *= 2;
}
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
cout << array[i] << endl;
}
// C++11的遍历:范围for,自动赋值,自动迭代,自动判断结束。
// 引用遍历数组:会修改array中的数据。
for (auto& e : array)
e *= 2;
// 拷贝遍历数组:不会修改array中的数据。
for (auto e : array)
cout << e << " " << endl;
// string类的遍历
string str("hello world");
for (auto ch : str)
{
cout << ch << " ";
}
cout << endl;
return 0;
}
string类中的常用接口
1、string类对象中的常见构造函数
函数名称 | 功能说明 |
---|---|
string(); | 构造空的string类对象,即空字符串 |
string(const char* s); | 用C格式的字符串来构造string类对象 |
string(size_t n, char c); | string类对象中包含n个字符l类型的c |
string(const string& s); | 拷贝构造函数 |
2、string类对象中的容量操作函数
函数名称 | 功能说明 |
---|---|
size | 返回字符串有效字符的长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty | 检测字符串释放为空串,是则返回true,不是则返回false |
reserve | 为字符串预留(规定)空间 |
clear | 清空string类对象中的有效字符 |
resize | 将有效字符的个数改为n个,多出的空间用字符c填充 |
注意事项:
- size()与length()函数的方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,并且一般情况下基本都是size()函数。
- clear()函数只是将string中的有效字符清空,并没有改变底层空间的大小。
- resize(size_t n)与resize(sizr_t n, char c)都是将字符串中的有效字符个数修改为n个,不同点在于当字符个数增多时,resize(n)是用 0 来填充多出的元素空间,而resize(n,'c')是用字符c来填充多出来的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
- reserve(size_t res_arg = 0); 为string预留空间,reserve函数不会改变有效元素的个数,但是当reserve传入的参数小于string的底层空间总大小时,在vs编译器环境下,reserve不会改变容量大小,在g++编译器环境下,reserve可能会缩容。因此官方给的回答是发起一个不具约束力的请求,所以只是可能会缩容,真正决定看编译器。
3、string类对象的访问及遍历操作函数
函数名称 | 功能说明 |
---|---|
operator[pos] | 运算符重载,返回pos位置的字符,const string类对象调用 |
begin | 获取string类对象第一个字符的迭代器 |
end | 获取string类对象最后一个字符下一个位置的迭代器 |
rebgin | 获取string类对象最后一个字符的迭代器 |
rend | 获取string类对象第一个字符前一个位置的迭代器 |
范围for | C++11支持的更为简洁的遍历方式 |
#include<iostream>
using namespace std;
void Teststring4()
{
string s("hello zy");
// 3种遍历方式:
// 需要注意的以下三种方式除了遍历string对象,还可以遍历修改string中的字符,
// 另外以下三种方式对于string而言,第一种使用最多,因为可以修改数组。
// 1. for+operator[]
for (size_t i = 0; i < s.size(); ++i)
cout << s[i] << endl;
// 2.迭代器:在string中使用相对较少
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << endl;
++it;
}
// 反向迭代器
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << endl;
rit++;
}
const string s1("hello zy");
// const迭代器
string::const_iterator cit = s1.begin();
while (cit != s1.end())
{
//*cit += 2; cit不能赋值给常量,因为const。
cout << *cit << endl;
cit++;
}
// const反向迭代器
string::const_reverse_iterator rcit = s.rbegin();
while (rcit != s.rend())
{
//*rcit += 2; rcit不能赋值给常量,因为const。
cout << *rcit << endl;
rcit++;
}
// string::reverse_iterator rit = s.rbegin();
// C++11之后,直接使用auto定义迭代器,让编译器自动推导迭代器的类型
//auto rit = s.rbegin();
//while (rit != s.rend())
// cout << *rit << endl;
// 3.范围for:遍历使用较多。
//for (auto ch : s)
// cout << ch << endl;
}
4、string类对象的修改操作
函数名称 | 功能说明 |
---|---|
push_back | 在字符串后尾插一个字符c |
append | 在字符串后追加字符串 |
operator+= | 在字符串后追加字符串str |
c_str | 返回c格式的字符串 |
find | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 在str中从第pos位置起截取n个字符,然后将其返回 |
erase | 删除str中的所有字符,不改变容量大小 |
insert | 插入字符串,位置由参数决定 |
replace | 替换pos位置的字符,可以一换多 |
find_first_of | 返回所给定字符串中任意一个字符的第一次出现的下标 |
find_last_of | 返回所给定字符串中任意一个字符的最后一次出现的下标 |
代码演示:
// 测试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 += 'b'; // 在str后追加一个字符'b'
str += "it"; // 在str后追加一个字符串"it"
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;
}
注意:
- 在string尾部追加字符串时,s.push_back(c) 与 s.append(1,c) 和 s += 'c' 三种实现方式差不多,一般情况下string类的+=重载操作符使用较多,因为+=操作不仅可以连接单个字符,还可以连接字符串。
- 对string操作时,如果能够大概预估到放多少字符,可以通过reserve把空间预留好。
5、string类的重载全局函数
函数名称 | 功能说明 |
---|---|
operator+ | 尽量少用,因为传值返回会导致深拷贝,降低效率 |
operator>> | 输入运算符重载 |
operator<< | 输出运算符重载 |
getline | 获取一行字符串 |
relational operator | 大小比较 |
注意:上面的几个接口大家需要了解,后面的OJ题目中会体现他们的用法。sting类中还有一些其他的操作,这里不一 一列举,大家在需要时自行查看文档即可。
6、vs和g++中string结构的说明
注意:下述结构实在32为平台下进行的验证,该平台下的指针变量占四个字节的空间。
- vs下string的结构:string总共占28个字节,内部结构稍微复杂一些,显示有一个联合体,联合体用来定义string中字符串的存储空间:(1)当字符串的长度小于16时,使用内部固定的字符数组来存放。(2)当字符串的长度大于等于16时,则从堆上开辟空间存放字符串,不再使用数组。
union _Bxty
{
// 用于小缓冲区的存储器或指向大缓冲区的指针
value_type _Buf[_BUF_SIZE]; //_BUF_SIZE = 16
pointer _Ptr;
char _Alias[_BUF_SIZE]; // 允许混叠
} _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那 string 对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
其次,还有一个 size_t 字段保存字符串的长度,一个 size_t 保存从堆上开辟空间的总容量。即 size和 capacity。最后还有一个指针用于做其他事情,这里不作了解。因此在vs下的 string 总共占16+4+4+4=28个字节。
- G++下string是通过写时拷贝来实现的,string对象总共占4个字节,内部只包含一个指针,该指针将来会指向一块堆空间,内部包含了如下字段:1.空间总大小。2.字符串有效长度。3.引用计数。
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
string 练习
1、仅仅翻转字母
给你一个字符串 s
,根据下述规则反转字符串:所有非英文字母保留在原有位置,所有英文字母(小写或大写)位置反转,返回反转后的 s
。
提示:
1 <= s.length <= 100
s
仅由 ASCII 值在范围[33, 122]
的字符组成s
不含'\"'
或'\\'
解析:
这道题比较简单,思路是定义对象的起始点和终止点,通过两端向中间汇聚,当遍历的字符不是字母时继续遍历,为字母时停下,两端进行交换,直到起始变量不再小于终止变量,遍历结束。代码如下:
class Solution {
public:
bool isLetter(char ch)
{
if(ch>='a'&&ch<='z')
return true;
if(ch>='A'&&ch<='Z')
return true;
return false;
}
string reverseOnlyLetters(string s) {
int begin = 0, end = s.size() - 1;
while (begin < end)
{
while (begin < end && !isLetter(s[begin]))
{
begin++;
}
while (begin < end && !isLetter(s[end]))
{
end--;
}
swap(s[begin++],s[end--]);
}
return s;
}
};
给定一个字符串 s
,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1
。
提示:
1 <= s.length <= 10^5
s
只包含小写字母
解析:
因为英文字母只有26个,因此我们开辟一个26字符的数组用来计算每个字符的个数,使用范围for遍历每一个string对象中的字符,在对应位置的数组上++,然后按照string中的字符顺序进行二次遍历,为的是找到数组中为1的字符,并返回下标位置。代码如下:
class Solution {
public:
int firstUniqChar(string s) {
int arr[26] = {0};
for(auto ch : s)
{
arr[ch-'a']+=1;
}
for(int i = 0;i < s.size(); i++)
{
if(1 == arr[s[i]-'a'])
{
return i;
}
}
return -1;
}
};
3、字符串最后一个单词的长度
计算字符串最后一个单词的长度,单词以空格隔开,字符串长度小于5000。(注:字符串末尾不以空格为结尾)。
输入描述:
输入一行,代表要计算的字符串,非空,长度小于5000。
输出描述:
输出一个整数,表示输入字符串最后一个单词的长度。
解析:
本道题的难点在于流输入操作符cin不能接收第一个空格之后的字符,因此我们需要使用string的函数getline,该函数可以保留空白字符继续向后读取,直到遇到换行符 ' \n ' 。思路为使用getline读取字符串后,找到最后一个空格出现的位置pos,使用s.size() - pos - 1就得到答案了。这里要注意的是size()求出来的大小是字符个数,下标需要-1。代码如下:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
while(getline(cin, s))
{
size_t pos = s.rfind(' ');
cout<< s.size() - pos - 1 <<endl;
}
return 0;
}
4、验证回文串
如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 ,字母和数字都属于字母数字字符。
给你一个字符串 s
,如果它是 回文串 ,返回 true
;否则,返回 false
。
提示:
1 <= s.length <= 2 * 10^5
s
仅由可打印的 ASCII 字符组成
解析:
本道题的思路是将所有字母数字字符全部+=到一个新字符串,对新字符串进行逆置拷贝,并进行比较,若相等则为true,反之为false。代码如下:
class Solution {
public:
bool isPalindrome(string s) {
string s1;
for(auto ch : s)
{
// 将小写字母转换成大写字母
if(ch >= 'a' && ch <= 'z')
{
ch -= 32;
}
// 尾插字符
if((ch >= '0' && ch <= '9')
|| (ch >= 'A' && ch <= 'Z'))
{
s1 += ch;
}
}
// 拷贝构造
string s2(s1);
//逆置字符串
reverse(s2.begin(),s2.end());
// 判断是否回文
return s1 == s2;
}
};
5、字符串相加
给定两个字符串形式的非负整数 num1
和num2
,计算它们的和并同样以字符串形式返回。你不能使用任何內建的用于处理大整数的库(比如 BigInteger
), 也不能直接将输入的字符串转换为整数形式。
提示:
1 <= num1.length, num2.length <= 10^4
num1
和num2
都只包含数字0-9
num1
和num2
都不包含任何前导零
解析:
这道题思考起来比较麻烦,因为字符串中存的数字位数可以超过整形最大值,因此stoi函数就不能用了,我们需要对两个字符串中的相同位置的字符相加后得到一个小于10的数,并要判断是否进位,因此我们使用size()函数算出两个字符串的最后一个有效字符的位置(个位),通过遍历的方法将相同位置的字符数字与进位变量 next 以整形的方式相加后+=到新的字符串中,直到两个位置变量都为小于0后结束遍历,翻转字符串就得到了相加后的字符数字。需要注意里面对字符+/- '0' 是为了转换为相应的数据类型。代码如下:
class Solution {
public:
string addStrings(string num1, string num2) {
int end1 = num1.size()-1, end2 = num2.size()-1;
string str;
int next = 0;
while(end1 >= 0 || end2 >= 0)
{
int val1 = end1>=0 ? num1[end1]-'0' : 0;
int val2 = end2>=0 ? num2[end2]-'0' : 0;
end1--;
end2--;
int ret = val1 + val2 + next;
next = ret / 10;
ret %= 10;
str += (ret+'0');
}
if(next == 1)
str += '1';
reverse(str.begin(),str.end());
return str;
}
};
string类的模拟实现
在上面我们已经对string类进行了简单的讲解,各位只需要会正常使用即可。在面试中,面试官总是喜欢让面试者自己来模拟实现string类,最主要的是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。下面我会对以上的函数和一些常用函数进行模拟实现,并对一些常见问题进行说明与补充。
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。
class string
{
public:
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//构造函数
/*string()
:_str(new char[1]{'\0'})
,_size(0)
,_capacity()
{}*/
//String(const char* str = "\0") 错误示范
//String(const char* str = nullptr) 错误示范
// 全缺省的拷贝构造即默认构造
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
// _capacity不包含\0
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 解决深拷贝问题的拷贝构造
string(const string& s)
{
/*_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s.c_str());*/
//现代写法
string tmp(s._str);
swap(tmp);
}
// s2 = s1
/*string& operator=(const string& s1)
{
if (this != &s1)
{
delete[] _str;
_str = new char[s1._capacity + 1];
strcpy(_str, s1._str);
_size = s1._size;
_capacity = s1._capacity;
}
return *this;
}*/
//s2 = s1现代写法
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
}
在上面的代码中,我分别写了普通构造、拷贝构造的字符串传参和类对象传参两种重载形式、赋值重载和析构函数。在字符串传参的拷贝构造中我们如果给了缺省值,那它就可以代替普通构造(这里在类和对象章节讲过,有需要的同学可以翻一下前面的博客)。对比使用类对象传参的拷贝构造的传统写法和现代写法,传统写法的直接复制数据,容易造成浅拷贝问题,导致内存泄漏或对一块空间释放两次,我们需要进行深拷贝来解决此类问题(对申请空间的位置,重新申请空间并将原空间的数据拷贝到新空间);现代写法使用swap函数,先构造了一个临时对象,进行资源交换,这样不仅简化了代码,还避免了手动申请和释放空间,降低了出错风险。在赋值运算符重载中,传统写法需要先检查参数是否等于*this,再手动释放内存;而现代写法中使用接收值参数的方法通过swap进行资源交换,避免了显式释放空间和条件检查,简化了代码,提高了安全性。
// c_str
char* c_str() const
{
return _str;
}
// size
size_t size() const
{
return _size;
}
// capacity
size_t capacity() const
{
return _capacity;
}
// clear
void clear()
{
_str[0] = '\0';
_size = 0;
}
// []重载
char& operator[](size_t pos)
{
return _str[pos];
}
const char& operator[](size_t pos) const
{
return _str[pos];
}
// 迭代器
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
// const迭代器
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
这部分是对一些常用函数和迭代器的简单实现,并不复杂,各位自己看看模仿一下即可。
// reserve
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n+1];
strcpy(tmp, _str);
_capacity = n;
delete[] _str;
_str = tmp;
}
}
// push_back
void string::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
// append
void string::append(const char* str)
{
size_t length = strlen(str);
if (_size + length > _capacity)
{
reserve(_size + length > 2 * _capacity ? _size + length : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += length;
}
// operator+=
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
// insert
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
// 避免size_t将负数转为正整数最大值问题
size_t end = _size+1;
while (end > pos)
{
_str[end] = _str[end-1];
end--;
}
_str[end] = ch;
}
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t length = strlen(str);
if (_size + length > _capacity)
{
char* tmp = new char[_size + length > 2 * _capacity ? _size + length : 2 * _capacity];
}
size_t end = _size + length;
//while (end > pos + length - 1)
while (end >= pos + length)
{
_str[end] = _str[end - length];
end--;
}
for (size_t i = 0; i < length; i++)
{
_str[pos + i] = str[i];
}
_size += length;
}
// erase
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
// find
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos)
{ //从第pos位置开始找第一个匹配的字符串
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
//substr
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len > _size - pos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
在上面的代码中:
-
reserve:用于确保字符串有足够的空间存储
n
个字符。如果n
大于当前容量,就分配新的内存,拷贝现有内容,更新容量,并释放旧内存。 -
push_back:向字符串末尾添加一个字符。如果当前大小等于容量,调用
reserve
扩展容量(初始为 4 或翻倍)。然后将字符添加到末尾并更新大小。 -
append:添加一个 C 风格字符串到末尾,先检查是否需要扩展容量,然后使用
strcpy
将新字符串拷贝到当前字符串末尾,并更新大小。 -
operator+=:重载
+=
运算符以支持字符和字符串的追加,调用push_back
和append
。 -
insert:在指定位置插入一个字符或字符串。如果插入后超出当前容量,则调用
reserve
扩展容量。然后通过移动后续字符来腾出空间。 -
erase:删除指定位置的字符或字符串。根据长度调整大小,并用后续字符覆盖已删除部分。
-
find:查找字符或子字符串,返回首次出现的位置。使用简单循环或
strstr
函数进行查找。 -
substr:返回从指定位置开始的子字符串,创建一个新的
string
对象并分配适当的空间。
// operator
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 strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
// 流插入、提取
ostream& operator<<(ostream& out, const string& s1)
{
for (auto ch : s1)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s1)
{
s1.clear();
const int N = 256;
char buff[N];
int i = 0;
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i] = ch;
i++;
if (i == N - 1)
{
buff[i] = '\0';
s1 += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s1 += buff;
}
return in;
}
上面的代码是重载的全局函数:
-
比较运算符:
operator<
和operator<=
使用strcmp
比较两个字符串,判断它们的字典序关系。operator>
和operator>=
利用operator<=
和operator<
进行逻辑反转,简化代码。operator==
和operator!=
也使用strcmp
,分别判断相等和不相等,提供直观的比较功能。
-
流插入:
operator<<
实现了将自定义字符串输出到流中,使用范围基于的 for 循环逐字符输出,方便与标准输出流的集成。
-
流提取:
operator>>
从输入流中读取字符,直到遇到空格或换行。使用缓冲区来存储读取的字符,确保字符串的拼接,处理了缓冲区满的情况。
以上就是对string类的模拟实现,下面是完整代码:
string.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace zy
{
class string
{
public:
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//构造函数
/*string()
:_str(new char[1]{'\0'})
,_size(0)
,_capacity()
{}*/
// 全缺省的拷贝构造即默认构造
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
// _capacity不包含\0
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 解决深拷贝问题的拷贝构造
string(const string& s)
{
/*_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s.c_str());*/
//现代写法
string tmp(s._str);
swap(tmp);
}
// s2 = s1
/*string& operator=(const string& s1)
{
if (this != &s1)
{
delete[] _str;
_str = new char[s1._capacity + 1];
strcpy(_str, s1._str);
_size = s1._size;
_capacity = s1._capacity;
}
return *this;
}*/
//s2 = s1现代写法
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
// c_str
char* c_str() const
{
return _str;
}
// size
size_t size() const
{
return _size;
}
// capacity
size_t capacity() const
{
return _capacity;
}
// clear
void clear()
{
_str[0] = '\0';
_size = 0;
}
// []重载
char& operator[](size_t pos)
{
return _str[pos];
}
const char& operator[](size_t pos) const
{
return _str[pos];
}
// 迭代器
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
// const迭代器
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
// reserve
void reserve(size_t n);
// push_back
void push_back(char ch);
// append
void append(const char* str);
// operator+=
string& operator+=(char ch);
string& operator+=(const char* str);
// insert
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
// erase
void erase(size_t pos, size_t len = npos);
//find
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
//substr
string substr(size_t pos = 0, size_t len = npos);
private:
char* _str;
size_t _size;
size_t _capacity;
//声明
static const size_t npos;
};
// operator
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
// 声明<<、>>
ostream& operator<<(ostream& out, const string& s1);
istream& operator>>(istream& in, string& s1);
void test_string1();
void test_string2();
}
string.cpp
#include"string.h"
namespace zy
{
// reserve
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n+1];
strcpy(tmp, _str);
_capacity = n;
delete[] _str;
_str = tmp;
}
}
// push_back
void string::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
// append
void string::append(const char* str)
{
size_t length = strlen(str);
if (_size + length > _capacity)
{
reserve(_size + length > 2 * _capacity ? _size + length : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += length;
}
// operator+=
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
// insert
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
// 避免size_t将负数转为正整数最大值问题
size_t end = _size+1;
while (end > pos)
{
_str[end] = _str[end-1];
end--;
}
_str[end] = ch;
}
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t length = strlen(str);
if (_size + length > _capacity)
{
char* tmp = new char[_size + length > 2 * _capacity ? _size + length : 2 * _capacity];
}
size_t end = _size + length;
//while (end > pos + length - 1)
while (end >= pos + length)
{
_str[end] = _str[end - length];
end--;
}
for (size_t i = 0; i < length; i++)
{
_str[pos + i] = str[i];
}
_size += length;
}
// erase
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
// find
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos)
{//从第pos位置开始找第一个匹配的字符串
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
//substr
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len > _size - pos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
// operator
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 strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
// 流插入、提取
ostream& operator<<(ostream& out, const string& s1)
{
for (auto ch : s1)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s1)
{
s1.clear();
const int N = 256;
char buff[N];
int i = 0;
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i] = ch;
i++;
if (i == N - 1)
{
buff[i] = '\0';
s1 += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s1 += buff;
}
return in;
}
//定义分离到.cpp
const size_t string::npos = -1;
}
扩展:写时拷贝
写时拷贝本质上就是一种拖延症,在浅拷贝的基础上增加了引用计数的方式来实现。
引用计数:用来季景路资源使用者的个数。在构造时,将资源的计数记为1,每增加一个对象使用该资源就给计数加一,当某个对象被销毁时,先给该计数减一,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,该资源释放;否则不能释放,否则会导致释放两次甚至更多次。
更多内容请看下面的文章:
C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell
C++的std::string的“读时也拷贝”技术! | 酷 壳 - CoolShell