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 }; //Xreturn 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 = 16pointer _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;}
};
2、字符串中的第一个唯一字符
给定一个字符串 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_strchar* c_str() const{return _str;}// sizesize_t size() const{return _size;}// capacitysize_t capacity() const{return _capacity;}// clearvoid 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_strchar* c_str() const{return _str;}// sizesize_t size() const{return _size;}// capacitysize_t capacity() const{return _capacity;}// clearvoid 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;}// reservevoid reserve(size_t n);// push_backvoid push_back(char ch);// appendvoid append(const char* str);// operator+=string& operator+=(char ch);string& operator+=(const char* str);// insertvoid insert(size_t pos, char ch);void insert(size_t pos, const char* str);// erasevoid erase(size_t pos, size_t len = npos);//findsize_t find(char ch, size_t pos = 0);size_t find(const char* str, size_t pos = 0);//substrstring substr(size_t pos = 0, size_t len = npos);private:char* _str;size_t _size;size_t _capacity;//声明static const size_t npos;};// operatorbool 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
{// reservevoid string::reserve(size_t n){if (n > _capacity){ char* tmp = new char[n+1];strcpy(tmp, _str);_capacity = n;delete[] _str;_str = tmp;}}// push_backvoid string::push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}// appendvoid 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;}// insertvoid 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;}// erasevoid 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;}}// findsize_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;}}//substrstring 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;}// operatorbool 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;}//定义分离到.cppconst size_t string::npos = -1;
}
扩展:写时拷贝
写时拷贝本质上就是一种拖延症,在浅拷贝的基础上增加了引用计数的方式来实现。
引用计数:用来季景路资源使用者的个数。在构造时,将资源的计数记为1,每增加一个对象使用该资源就给计数加一,当某个对象被销毁时,先给该计数减一,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,该资源释放;否则不能释放,否则会导致释放两次甚至更多次。
更多内容请看下面的文章:
C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell
C++的std::string的“读时也拷贝”技术! | 酷 壳 - CoolShell
C++面试中string类的一种正确写法 | 酷 壳 - CoolShell
STL 的string类怎么啦?_stl to string-CSDN博客