C++标准模板库STL之容器--string

STL简介

        STL(standard template libaray - 标准模板库)是C++标准库的重要组成部分,不仅是一个可复用的组件库,还是一个包罗了数据结构与算法的软件框架。

STL的六大组件及相关函数
仿函数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填充

注意事项:

  1. size()与length()函数的方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,并且一般情况下基本都是size()函数。
  2. clear()函数只是将string中的有效字符清空,并没有改变底层空间的大小。
  3. resize(size_t n)与resize(sizr_t n, char c)都是将字符串中的有效字符个数修改为n个,不同点在于当字符个数增多时,resize(n)是用 0 来填充多出的元素空间,而resize(n,'c')是用字符c来填充多出来的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
  4. 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类对象第一个字符前一个位置的迭代器
范围forC++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;
}

注意:

  1. 在string尾部追加字符串时,s.push_back(c) 与 s.append(1,c) 和 s += 'c' 三种实现方式差不多,一般情况下string类的+=重载操作符使用较多,因为+=操作不仅可以连接单个字符,还可以连接字符串。
  2. 对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_backappend

  • 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博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1558972.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

2020年华为杯数学建模竞赛D题论文和代码

无人机集群协同对抗 摘 要&#xff1a; 本文针对非线性约束条件下红蓝双方无人机集群协同对抗的最优规划问题&#xff0c;结合贪婪队形、非线性规划、内点法、蒙特卡洛方法和全联立正交配置有限元法&#xff0c;构建了无人机集群协同对抗推演模型。 针对问题一&#…

OBOO鸥柏全户外液晶广告机室外触控一体机技术标参数公布

整机参数技术公布&#xff1a; OBOO鸥柏智能43寸/49寸/55寸/65寸/75寸/86寸/98寸/110寸全户外LCD液晶安卓系统网络广告屏/室外触摸屏查询一体机投标参数投标标准款。 1、液晶屏具工业级液晶面板&#xff0c;亮度为2000cd/㎡&#xff0c;并且具备自动感光亮度调节&#xff0c;…

工业网关的主要功能-天拓四方

引言&#xff1a; 在当今工业自动化的浪潮中&#xff0c;工业网关作为一种重要的网络连接设备&#xff0c;扮演着不可或缺的角色。其强大的功能使得工业设备能够无缝接入网络&#xff0c;实现远程监控、控制、数据采集和协议转换等&#xff0c;极大地提升了工业生产的效率和智…

算法专题五: 位运算

目录 常见位运算总结1. 位1的个数2. 比特位计数3. 汉明距离4. 只出现一次的数字5. 只出现一次的数字Ⅲ6. 判定字符是否唯一7. 丢失的数字8. 两正数之和9. 只出现一次的数字Ⅲ10. 消失的两个数字 常见位运算总结 重点 : 1. 位1的个数 算法思路: 这道题就用到了我们总结的那个第…

图解 微信开发者工具 小程序源码 调试、断点标记方法 , 微信小程序调试器,真机调试断点调试方法,小程序网络API请求调试方法 总结

在我们使用微信开发者工具进行微信小程序开发的时候&#xff0c;在这个微信开发者工具的代码编辑框里面我们是无法像使用vscode, idea等IDE工具时那样直接对代码打断点进行调试&#xff0c; 原因是小程序实际上他就是一个web浏览器应用的包装, 在其内部使用的还是类似chrome的…

C/C++程序员为什么要了解汇编?了解汇编有哪些好处?如何学习汇编?

目录 1、概述 2、从汇编的角度去理解问题的若干实例说明 2.1、使用空指针去访问类的数据成员或调用类的虚函数为什么会引发崩溃&#xff1f; 2.2、从汇编代码的角度去理解多线程的执行细节&#xff0c;去理解多线程在访问共享资源时为什么要加锁 2.3、使用Windbg静态分析d…

Canal 扩展篇(阿里开源用于数据同步备份,监控表和表字段(日志))

1.Canal介绍 Canal把自己伪装成从数据库&#xff0c;获取mysql主数据库的日志&#xff08;binlog&#xff09;信息&#xff0c;所以要想使用canal就得先开启数据库日志 https://github.com/alibaba/canal Canal 主要用途是基于 MySQL 数据库增量日志解析&#xff0c;提供增量…

Spring Boot2.x教程:(五)日志分割

日志分割 1、概述2、为什么选择Logback2.1、创建配置文件2.2、配置说明2.3、修改应用程序配置2.4、启动应用程序 3、总结 大家好&#xff0c;我是欧阳方超&#xff0c;可以扫描下方二维码关注我的公众号“欧阳方超”&#xff0c;后续内容将在公众号首发。 1、概述 在现代应用程…

ubuntu18.04系统中图形化界面

一、Ubuntu 18.04 中&#xff0c;使用 GDM 作为默认的图形用户界面&#xff08;GUI&#xff09;管理器。GDM 是 GNOME Display Manager 的缩写&#xff0c;它是用于 Ubuntu 的显示管理器&#xff0c;负责处理登录和会话管理。 通过命令行重启 Ubuntu 18.04 上的图形界面服务&am…

技术路线图用什么画?用这个在线工具轻松完成绘制!

在当今快速发展的技术世界中&#xff0c;技术路线图已成为企业和团队不可或缺的战略规划工具。它不仅能够清晰地展示技术发展方向&#xff0c;还能帮助团队成员、利益相关者和投资者更好地理解和参与技术战略的制定过程。但不可否认的是&#xff0c;创建一个有效的技术路线图并…

付费计量系统实体和接口(6)

13.7.3 Sub-classification of the Metering functions计量功能的子分级 The Metering function primarily deals with the measurement of the quantity of delivered electrical energy to the consumer. These measurements are made available for use by other functions …

qt 3D编程

Qt 3D是一个用于构建交互式3D图形应用的库&#xff0c;它是Qt库的一 部分。Qt 3D提供了一组C和QMLAPI&#xff0c;帮助开发者快速构 建3D应用程序。 一、核心模块 Qt3DCore 功能&#xff1a;提供3D场景中的基本概念&#xff0c;如实体&#xff08;Entity&#xff09;、组件&…

R语言运行地理探测器模型

地理探测器&#xff08;GeoDetector&#xff09;是一种用于空间分析的统计模型&#xff0c;它能够探测空间分异性以及揭示其背后驱动力的一组方法。它的核心思想是基于这样的假设&#xff1a;如果某个自变量对某个因变量有重要影响&#xff0c;那么自变量和因变量的空间分布应该…

java的LinkedList

java的LinkedList 什么是LinkedListLinkedList的模拟实现LinkedList的使用ArrayList和LinkedList的区别 什么是LinkedList LinkedList的官方文档 LinkedList的底层是双向链表结构&#xff0c;由于链表没有将元素存储在连续的空间中&#xff0c;元素存储在单独的结点中&#xf…

【Redis】Set类型的常用命令与应用场景

目录 1.命令小结 2.命令解析 3.编码方式与应用场景 1.命令小结 &#xff08;1&#xff09;set的特点 1&#xff09;set中存放的数据也都是String类型 2&#xff09;set集合中的元素是无须的 3&#xff09;set集合中的元素是唯一的&#xff0c;不可重复 &#xff08;2&a…

MySql 之 Binglog 复制

复制是一种将数据从一个 MySQL 数据库服务器异步复制到另一个的技术。使用 MySQL 复制选项&#xff0c;您可以复制所有数据库、选定的数据库甚至选定的表&#xff0c;具体取决于您的使用情况。 前提条件 确保在源服务器上启用了二进制日志记录。确保复制配置中的所有服务器都有…

uniapp——h5的控制台调试、h5调试

介绍 小程序在调试的时候可以打开调试模式&#xff0c;可以看到console.log的打印情况。 但是H5运行到手机上没有默认的调试的模式&#xff0c;但是可以人为手动加一个。 如何实现 1、main.js文件 import Vconsole from ‘vconsole’ /** 关闭正式环境打印日志&#xff…

Centos7.5 安装和配置jdk17

目录 一、下载JDK17包 二、将安装包放入服务器 三、解压jdk包到/usr/lib/jvm 四、修改JDK环境配置 1、打开配置文件 2、最后一行插入 3、立即生效 4、检查版本 一、下载JDK17包 访问网址:Java Downloads | Oraclehttps://www.oracle.com/java/technologies/downloads…

音频功放工作原理:【B类】

上一节我们讲了A类音频功放的工作原理&#xff0c;也知道了它的优缺点&#xff1a; A类功放优点&#xff1a;高增益&#xff0c;低失真&#xff0c;音质好 A类功放缺点&#xff1a;热量高&#xff0c;效率低&#xff0c;功率小 为了解决A类功放的缺点&#xff0c;业界又引入…

重学SpringBoot3-集成Redis(十)之实时统计和分析

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-集成Redis&#xff08;十&#xff09;之实时统计和分析 1. 实时统计和分析的常见场景2. 使用 Redis 数据结构进行实时统计3. 使用Redis String实现计数器…