前言
在C语言中,字符串是以’\0’结尾的一些字符的集合。为了操作方便,C语言中还提供了一些控制字符串的函数例如strcpy,strcmp,strcat等等。但是这些函数与字符串是分离开的,并不符合C++封装的特性。于是C++中由单独产生了一个string类。
📒博客主页:要早起的杨同学的博客
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文所属专栏: 【C++拒绝从入门到跑路】
✉️坚持和努力一定能换来诗与远方!
💬参考在线编程网站:🌐牛客网🌐力扣
🙏作者水平有限,如果发现错误,敬请指正!感谢感谢!
本文相关练习题及讲解
了解标准库中的string类
文档介绍:string - C++ Reference (cplusplus.com)
string是一个类,string实例化的对象实际是一个字符数组。
string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string
在string类使用时,必须包含#include<string>
头文件和展开命名空间using namespace std
。
下面介绍string类的一些常用接口,大部分都是string类的成员函数。如果由什么下面没有提到的接口或者不明白的可以查文档http://www.cplusplus.com/reference/string/
看文档主要看三个板块:
- 接口函数的声明
- 接口函数的功能及参数和返回值说明
- 使用样例的代码
补充:
编码:计算机中只存储二进制数(0 / 1),那如何去表示文字呢,需要制定对应的编码表,规定用哪些二进制数字表示(映射)哪个符号,当然每个人都可以约定自己的一套,而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,比如美国有关的标准化组织就出台了 ASCII 码表(基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言)
ASCII 码使用指定的 7 位或 8 位二进制数组合来表示 128 或 256 种可能的字符,到目前为止共定义了 128 个字符。
所以早期的计算机中只能表示英文,不能表示其它国家的文字,当全世界各个国家都开始使用计算机了,就需要建立出对应语言的编码表。
UTF - 8 是对不同范围的字符使用不同长度的编码,这样能够适应不同的语言,比如有些语言单字节编码就够了,有些语言需要多字节编码才够。
还有其它编码表,GBK码(对多达2万多的简繁汉字进行了编码,简体版的Win95和Win98都是使用GBK作系统内码)等等。
注意:使用 string 类需要包含头文件
一. string类的常用接口说明
1.1 string类对象的构造函数
函数名称 | 功能说明 |
---|---|
string() | 无参默认构造函数,构造空string类对象,即空字符串 |
string(const char * s) | 用c字符串构造string类对象 |
string(size_t n,char c) | 实例化的类对象中包含n个字符c |
string(const string& s) | 拷贝构造函数,将对象s拷贝构造一个新对象 |
void Teststring()
{
string s1; // 构造空的string类对象s1
string s2("hello world"); // 用C格式字符串构造string类对象s2
string s3(s2); // 拷贝构造s3
}
👉思考:空串是什么都没有吗,存储空间为空吗?
1.2 string对象的容量函数
函数名称 | 功能说明 |
---|---|
size⭐ | 返回字符串中有效字符长度,对应字符串结尾的‘\0’的下标 |
length | 返回字符串中有效字符长度 |
capacity | 返回空间的总长度,字符长度size超过capacity会自动扩容 |
empty⭐ | 检测字符串是否为空串,是返回true,不是返回false |
clear⭐ | 清空有效字符串,空间不释放,内容没改变,只是size变为0 |
reserve⭐ | 为字符串预留空间,就是提前开辟一段你想要大小的空间,如果不用reserve,编译器会自己开辟一段空间。 |
resize⭐ | 将有效字符串个数改成n个,n小于size,缩小size到n。n大于size,增大size到n,多出来的空间用字符c填充,不给c,用’\0’填充 |
注意:length和size底层实现是一样的,引入size的原因是为了与其它容器保持一致,一般情况下用size。
clear()函数只是将对象的size置0,不改变底层空间大小。
👉resize 函数的两种重载形式:
void resize (size_t n);
void resize (size_t n, char c);
resize(size_t n,char c),当n小于对象size时,直接改变对象的size到n。当n大于对象size时,直接增大对象size到n,多出来的空间用给的字符c填充,没给字符c默认用\0填充。
👉reserve 函数介绍
void reserve (size_t n = 0);
reserve是提前将对象capacity空间直接开辟到某一值。
如果reserve里的值开辟空间小于capacity值,不会改变capacity。
size始终都不会改变。
为什么上面代码reserve(50),会扩容到63。因为字符保存时存在一定的内存对齐,一般是某一值的正数倍。
这样看下面一个代码:
如果提前知道套插入多少字符,提前开辟空间,防止扩容效率损失。
思考:resize 和 reserve 的意义在哪里呢?
reserve 的作用:如果知道需要多大的空间,可以利用 reserve 提前一次性把空间开好,避免增容带来的开销。
resize 的作用:既要开好空间,还要对这些空间初始化,就可以使用 resize
1.3 string类对象的访问及遍历操作
函数名称 | 功能说明 |
---|---|
operator[]⭐ | []操作符重载函数,返回对字符串中 pos 位置处的字符的引用(string 类对象支持随机访问)(一般物理地址是连续的才支持) |
begin() | 获取第一个字符位置的迭代器 |
end() | 获取最后一个字符下一个位置的迭代器 |
rbegin() | 反向迭代器(可以反向遍历对象),和end一样 |
rend() | 反向迭代器,和begin一样 |
范围for | C++11支持的一种更简洁的范围for的新遍历方式 |
说明:
- 因为重载了[]操作符,可以直接用**对象+[],**就像数组的形式访问字符。
.at(i)
越界抛异常, [] 越界断言报错。 - 迭代器现在的理解:string类里含有一个迭代器,所以定义一个迭代器要声明是string类里的,begin和end返回的是迭代器,可以用迭代器接收或者作比较。
- 关于迭代器:
[begin(),end() )
end()返回的不是最后一个数据位置的迭代器,返回的是最后一个位置下一个位置。也要注意的是,C++中凡是给迭代器一般都是给的 [ ) 左闭右开的区间,迭代器是类似指针一样东西,循环判断就要用 不等于 !=。 - 迭代器的意义:像string、vector支持[]遍历,但是list、map等容器不支持[],我们就要用迭代器遍历,所以迭代器是一种统一使用的方式。
对于const类型的对象有对应的const类型的迭代器
iterator 遍历 可读可写
const_iterator const对象遍历 只读
反向迭代器:
也是使用++
操作符,如果按照指针来理解的话就是从字符串的末尾依次--
往前走。
总结一下,一共四种迭代器
- iterator
- const_iterator
- reverse_iterator
- const_reverse_iterator
前两个用的多
1.4 string类修改插入操作
函数名称 | 功能 |
---|---|
push_back(char c) | 在字符串尾插字符 |
append(const char * s) | 常用的参数是一个字符串类型的参数,也可以使用迭代器 |
insert(size_t pos, char* s) | 第一个参数是位置,第二个参数是字符或字符串。在某一个位置开始插入字符串,给的位置要合法,不合法会报错。 |
erase(char* s, size_t len = npos) | 第一个参数是开始地址,第二个参数是删除的字符数len,从起始位置开始删除len个字符,不给参数值就是默认从给的地址开始删完。 |
operator+=(常用) | 在字符串尾插字符或字符串 |
c_str | 返回c格式字符串 |
find(char c,size_t pos) | 在字符串pos位置开始往后找字符或字符串,返回该字符在字符串中第一次出现位置,不输入位置默认从开始往后找 |
rfind | 在字符串pos位置开始往前找字符或字符串,返回该字符在字符串中第一次出现位置,不输入位置默认从后往前找 |
substr(size_t pos, szie_t len) | 在字符串中从pos位置开始,截取len个字符,然后返回 |
sort(s.begin(), s.end()) | 排序,s是string类对象,参数是迭代器 |
npos | string的静态成员变量,static const size_t npos=-1;全1,size_t为无符号整形,实际40几亿 |
👉append 用法
push_back在字符串后尾插字符,append在对象后尾插字符串,+=重载即可尾插字符又可尾插字符串,所以常用+=。
注意:
- 在string尾部追加字符时,s.push_back© / s.append(1, c) / s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
- 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
这样看不出什么不同,但是看下面这样。
原生打印是全部字符都打印出来(包括‘\0’),c_str是遇到‘\0’截止。
由于C语言字符串以\0结尾所以ret只打印了hello,但是打印对象也没有将\0打印出来,这是因为有的字符在输出框是不可见的\0就属于不可见的。
👉substr()接口:
取出url中的域名
以上图代码为例,
i1等于4,指向冒号:。
i2从i1+3(指向第一个w)位置找第一个出现的 /
并指向这个/
补充:
函数名称 | 功能 |
---|---|
insert(pos,char * s) | 在pos位置插入字符或者有效字符串,给的位置要合法,不合法会报错 |
erase(pos,n) | 从pos位置开始删除n个字符,不给参数n传值就是默认从pos位置开始删完 |
- 尽量少用insert,因为底层实现是数组,头部或者中间插入需要挪动数据
- erase要删除的字符数多于字符串本身长度不会报错,有多少删多少
- erase如果第一第二个参数都不给就是默认删除整个字符串
1.5 string 非成员函数
函数 | 功能 |
---|---|
operator+ | 一对象加上字符或者字符串,返回一对象,对象自身不改变。少用,返回不是引用。效率低 |
operator<< | 输出操作符重载函数 |
operator>> | 输入操作符重载函数 |
getline(cin,对象) | 获取一行字符,包括空格。 |
relational operators (string) | 比较大小 |
cin输入遇到空格,换行结束。getline遇到换行结束。
例题
class Solution {
public:
string reverseOnlyLetters(string S)
{
char* pLeft = (char*)S.c_str();
char* pRight = pLeft + (S.size()-1);
while(pLeft < pRight)
{
// 从前往后找,找到一个字母
while(pLeft < pRight)
{
// 找到有效字母后停下来
if(isalpha(*pLeft))
break;
++pLeft;
}
// 从后往前找,找一个字母
while(pLeft < pRight)
{
// 找到有效字母后停下来
if(isalpha(*pRight))
break;
--pRight;
}
if(pLeft < pRight)
{
swap(*pLeft, *pRight);
++pLeft;
--pRight;
}
}
return S;
}
};
1.6 补充一些接口
C 语言库文件 <ctype.h>
中的处理 C 字符的接口
字符处理函数:
函数名称 功能说明 int isalpha(int c) 检查字符是否为字母,是返回非零(true),不是则返回0(false) int isdigit(int c) 检查字符是否为十进制数字,是返回非零(true),不是则返回0(false) int isalnum ( int c ) 检查字符是否为字母或者数字,是返回非零(true),不是则返回0(false) 字符转换函数:
函数名称 功能说明 int tolower(int c) 把字母转换成小写字母,返回转换之后的字符 int toupper(int c) 把字母转换成大写字母,返回转换之后的字符
头文件 <string>
中:
- 函数 std::to_string(C++11):将数值转换为字符串,返回 string 类对象。
- 函数 std::stoi(C++11):将字符串转换为整数,返回 int 整数。
头文件 <algorithm>
中:
函数 std::reverse:反转范围 [first,last) 中元素的顺序。
// 传一段迭代器区间 [first, last) template <class BidirectionalIterator> // 双向迭代器 void reverse (BidirectionalIterator first, BidirectionalIterator last); 123
函数 std::sort:将 [first,last) 范围内的元素按升序排序。
// 传一段迭代器区间 [first, last),默认排升序,若要排降序,需要传仿函数 template <class RandomAccessIterator, class Compare> // 随机访问迭代器 void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp); 123
👉Example:
// 也可传数组,因为指向数组空间的指针是天然的迭代器 int arr[] = { 1, 5, 4, 2, 3 }; sort(arr, arr + 5);
二. string类的模拟实现
2.1 深拷贝和浅拷贝
上面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。string类如果采用浅拷贝的方式,会造成一些问题:
说明:上述string类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
① 浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规。要解决浅拷贝问题,C++中引入了深拷贝。
② 深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
2.2 模拟实现string类
string是一个管理字符数组的类,要求这个字符数组结尾用’\0’标识,我们需要实现以下接口:
1、拷贝构造和赋值重载实现深拷贝
2、增删查改的相关接口(跟顺序表类似)
3、重载一些常见运算符。如: >、<、<<、>>、[]…
4、迭代器
在上文中我们熟知了C++string类的一些常用接口的使用,下文我们来简单实现一下这些常用的接口,带你了解string类的底层原理,让你能轻而易举的掌握string类的使用。
说明注意点:在我们自己实现string类时,如果你的类名用的与标准库类名相同时,需要一个命名空间将其与标准库里的string类分开。
下面是代码的分开实现,完整代码在最下面给出。
构造,析构函数和赋值操作符的重载
//要在堆上开辟空间将字符串拷贝过去,不能直接在初始化列表里初始化,否则不可修改
//默认构造函数
string(const char* str = "")//空串里面有一个\0
:_str(new char[strlen(str)+1]) //空间大小等于有效字符串个数+\0
{
strcpy(_str, str);
_size = strlen(_str);
_capacity = _size;
}
//传统写法
//拷贝构造
//string(const string& s)
// :_str(new char[strlen(s._str) + 1])
//{
// strcpy(_str, s._str);
// _size = strlen(_str);
// _capacity = _size;
//}
赋值运算符重载
//string& operator=(const string& s)
//{
// if (this != &s)
// {
// delete[] _str;
// _str = new char[strlen(s._str) + 1];
// strcpy(_str, s._str);
// _size = s._size;
// _capacity = s._capacity;
// }
// return *this;
//}
//现代写法 多写一个交换函数
//创建一个临时对象,然后将临时对象的值交换过去,函数结束后,临时对象自动销毁
//_str会交换指向空间的地址
//构造一个和S1一样的tmp,然后交换S2和tmp成员变量里面保存的地址,这样S2的成员变量就指向了开辟的空间。
//S2的成员变量必须要被初始化为nullptr,否则交换值后调用tmp的析构函数会报错。
string(const string& s)
:_str(nullptr)//要赋空指针,保证tmp析构无误
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp); // 等价于this->swap(tmp);即(*this).swap(tmp);调用的是我们实现的
}
string& operator=(string ps)
{
swap(ps);
return *this;
}
void swap(string& s)//我们自己实现的swap
{
::swap(_str, s._str);//调用全局域的swap函数(库里的函数)
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
注意:拷贝构造和赋值操作符重载函数,这里蕴含着深浅拷贝问题。具体细节见上文的讲解。 如果不定义着两个函数,编译器会自动生成,但是仅仅只是按字节进行拷贝,也就是值拷贝/浅拷贝。在上面函数中,如果只是进行浅拷贝,两字符指针变量指向堆上的同一块空间。当对象出作用域时,调用析构函数,但是同一块空间不能释放两次,会导致程序奔溃。
解决方法就是使用深拷贝:
在堆上再申请一块空间,将拷贝值或者赋值值copy过去。将需要被赋值或者被拷贝的对象的字符指针指向新申请的空间。
容量函数,operator[]重载函数和c_str、clear函数
//capacity 容量相关接口
size_t size()const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
bool empty()const
{
return _size == 0;
}
// []
char& operator[](size_t index)
{
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
assert(index < _size);
return _str[index];
}
void clear()
{
_size = 0;
_str[0] = '\0';
}
const char* c_str() const
{
return _str;
}
resize和reserve函数
void resize(size_t n, char c = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
//n比容量大先扩容
if (n > _capacity)
{
reserve(n);
}
for (size_t i = _size; i < n; i++)
{
_str[i] = c;
}
_str[n] = '\0';
_size = n;
}
}
void reserve(size_t n)
{
if (n > _capacity)
{
_capacity = n;
char* tmp = new char[_capacity + 1];
strncpy(tmp, _str, _size+1);
delete[] _str;
_str = tmp;
}
}
插入和删除函数
void push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = c;
_str[_size + 1] = '\0';
++_size;
}
string& operator+=(char c)
{
push_back(c);
return *this;
}
void append(const char* str)
{
size_t len = _size + strlen(str);
if (len > _capacity)
{
reserve(len);
}
strcpy(_str + _size, str);
_size = len;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
查找函数和比较函数
bool operator<(const string& s)
{
char* end1 = _str;
char* end2 = s._str;
while (*end1 == *end2)
{
end1++;
end2++;
}
if (*end1 < *end2)
{
return true;
}
else
{
return false;
}
}
bool operator<=(const string& s)
{
return (*this < s || *this == s);
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return (*this > s || *this == s);
}
bool operator==(const string& s)
{
char* end1 = _str;
char* end2 = s._str;
while (*end1 == *end2)
{
if (*end1 == '\0')
{
return true;
}
end1++;
end2++;
}
return false;
}
bool operator!=(const string& s)
{
return !(*this == s);
}
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
// 返回子串s在string中第一次出现的位置
size_t find(const char* str, size_t pos = 0) const
{
assert(pos < _size);
const char* ret = strstr(_str + pos, str);
if (ret)
{
return ret - _str;
}
else
{
return npos;
}
}
插入和删除函数
// 在pos位置上插入字符c/字符串str,并返回该字符串
string& insert(size_t pos, const char c)
{
assert(pos < _size + 1);
// xxxxxxxxc\0
*this += c; // 能实现扩容
//循环也可以用指针实现
for (int i = _size - 1; i > pos; i--)
{
_str[i] = _str[i - 1];
}
_str[pos] = c;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);//等于_size相当于尾插
int len = strlen(str);
*this += str;
//i-len等于pos的时候把pos位置的数据挪走
for (int i = _size - 1; i - len != pos - 1; i--)
{
_str[i] = _str[i - len];
}
strncpy(_str + pos, str, len);
return *this;
}
//或者
string& insert2(size_t pos, const char* str)
{
assert(pos <= _size);//等于_size相当于尾插
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
char* end = _str + _size;
while (end >= _str + pos)
{
*(end + len) = *end;
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
// 删除pos位置上的元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len = npos)//npos默认删完
{
assert(pos < _size);
//要删的元素个数比剩余的元素多
//不用额外判断npos,-1传给size_t会类型转换
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
//剩余字符多
else
{
循环写法
+1 保存\0
//for (int i = pos; i < _size - len + 1; i++)
//{
// _str[i] = _str[i + len];
//}
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
operator<<和operator>>操作符重载及getline函数
这三个函数在全局域(我们定义的命名空间)中实现,我们在前面的博客中提到过,因为会有争夺第一参数位置的问题。
ostream& operator<<(ostream& out, const string& s)
{
for (auto e : s)
{
out << e;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
istream& getline(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
完整代码
#pragma once
#include<iostream>
#include<assert.h>
using std::cout;
using std::cin;
using std::endl;
using std::ostream;
using std::istream;
using std::swap;
namespace yzy
{
class string
{
public:
typedef char* iterator;
const iterator begin() const
{
return _str;
}
const iterator end() const
{
return _str + _size;
}
///
//默认构造函数
string(const char* str = "")//空串里面有一个\0
:_str(new char[strlen(str)+1]) //空间大小等于有效字符串个数+\0
{
strcpy(_str, str);
_size = strlen(_str);
_capacity = _size;
}
//传统写法
//拷贝构造
//string(const string& s)
// :_str(new char[strlen(s._str) + 1])
//{
// strcpy(_str, s._str);
// _size = strlen(_str);
// _capacity = _size;
//}
赋值运算符重载
//string& operator=(const string& s)
//{
// if (this != &s)
// {
// delete[] _str;
// _str = new char[strlen(s._str) + 1];
// strcpy(_str, s._str);
// _size = s._size;
// _capacity = s._capacity;
// }
// return *this;
//}
//现代写法
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp); // 等价于this->swap(tmp);即(*this).swap(tmp);
}
string& operator=(string ps)
{
swap(ps);
return *this;
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
//capacity 容量相关接口
size_t size()const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
bool empty()const
{
return _size == 0;
}
//不完整的resize
//void resize(size_t n, char c = '\0')
//{
// reserve(n);
// if (n < _size)
// {
// _size = _capacity;
// _str[_size] = '\0';
// }
// else
// {
// for (size_t i = _size; i < _capacity; i++)
// {
// _str[i] = c;
// }
// _str[n] = '\0';
// _size = _capacity;
// }
//}
void resize(size_t n, char c = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
//n比容量大先扩容
if (n > _capacity)
{
reserve(n);
}
for (size_t i = _size; i < n; i++)
{
_str[i] = c;
}
_str[n] = '\0';
_size = n;
}
}
void reserve(size_t n)
{
if (n > _capacity)
{
_capacity = n;
char* tmp = new char[_capacity + 1];
strncpy(tmp, _str, _size+1);
delete[] _str;
_str = tmp;
}
}
//modify
void push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = c;
_str[_size + 1] = '\0';
++_size;
}
string& operator+=(char c)
{
push_back(c);
return *this;
}
void append(const char* str)
{
size_t len = _size + strlen(str);
if (len > _capacity)
{
reserve(len);
}
strcpy(_str + _size, str);
_size = len;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
void clear()
{
_size = 0;
_str[0] = '\0';
}
const char* c_str() const
{
return _str;
}
//
// []
char& operator[](size_t index)
{
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
assert(index < _size);
return _str[index];
}
//
//relational operators
bool operator<(const string& s)
{
char* end1 = _str;
char* end2 = s._str;
while (*end1 == *end2)
{
end1++;
end2++;
}
if (*end1 < *end2)
{
return true;
}
else
{
return false;
}
}
bool operator<=(const string& s)
{
return (*this < s || *this == s);
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return (*this > s || *this == s);
}
bool operator==(const string& s)
{
char* end1 = _str;
char* end2 = s._str;
while (*end1 == *end2)
{
if (*end1 == '\0')
{
return true;
}
end1++;
end2++;
}
return false;
}
bool operator!=(const string& s)
{
return !(*this == s);
}
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
// 返回子串s在string中第一次出现的位置
size_t find(const char* str, size_t pos = 0) const
{
assert(pos < _size);
const char* ret = strstr(_str + pos, str);
if (ret)
{
return ret - _str;
}
else
{
return npos;
}
}
// 在pos位置上插入字符c/字符串str,并返回该字符串
string& insert(size_t pos, const char c)
{
assert(pos < _size + 1);
// xxxxxxxxc\0
*this += c; // 能实现扩容
//循环也可以用指针实现
for (int i = _size - 1; i > pos; i--)
{
_str[i] = _str[i - 1];
}
_str[pos] = c;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);//等于_size相当于尾插
int len = strlen(str);
*this += str;
//i-len等于pos的时候把pos位置的数据挪走
for (int i = _size - 1; i - len != pos - 1; i--)
{
_str[i] = _str[i - len];
}
strncpy(_str + pos, str, len);
return *this;
}
//或者
string& insert2(size_t pos, const char* str)
{
assert(pos <= _size);//等于_size相当于尾插
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
char* end = _str + _size;
while (end >= _str + pos)
{
*(end + len) = *end;
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
// 删除pos位置上的元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len = npos)//npos默认删完
{
assert(pos < _size);
//要删的元素个数比剩余的元素多
//不用额外判断npos,-1传给size_t会类型转换
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
//剩余字符多
else
{
循环写法
+1 保存\0
//for (int i = pos; i < _size - len + 1; i++)
//{
// _str[i] = _str[i + len];
//}
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
};
const size_t string::npos = -1;
ostream& operator<<(ostream& out, const string& s)
{
for (auto e : s)
{
out << e;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
istream& getline(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
//测试函数
void Test_String1()
{
string s1("good morning");
cout << s1.c_str() << endl;
string s2(s1);
cout << s2.c_str() << endl;
string s3 = "good";
cout << s3.c_str() << endl;
s3 = s1;
cout << s3.c_str() << endl;
}
void Test_String2()
{
string s1("good morning");//12
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1.empty() << endl;
s1.reserve(20);
cout << s1.c_str() << endl;
cout << s1.capacity() << endl;
s1.resize(10, 'x');
cout << s1.c_str() << endl;
cout << s1.capacity() << endl;
}
void Test_String3()
{
string s1("good morning");//12
s1 += " afternoon";//10
cout << s1.c_str() << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1 += '!';//1
cout << s1.c_str() << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
string::iterator it = s1.begin();
s1[0] = 's';
while (it != s1.end())
{
cout << *it << " ";
it++;
}
}
void Test_String4()
{
string s1 = "abcdf";
string s2 = "abcde";
cout << (s1 < s2) << endl;
cout << (s1 <= s2) << endl;
cout << (s1 > s2) << endl;
cout << (s1 >= s2) << endl;
cout << (s1 == s2) << endl;
cout << (s1 != s2) << endl;
}
void Test_String5()
{
string s1 = "abcde";
cout << s1.find('b', 1) << endl;
s1.insert(0, 'x');
cout << s1.c_str() << endl;
s1.insert2(0, "qqq");
cout << s1.c_str() << endl;
s1.erase(2,6);
cout << s1.c_str() << endl;
}
void Test_String6()
{
string s1("hello world");
s1.resize(20, 'x');
s1 += "!!!";
cout << s1 << endl;
cout << s1.c_str() << endl;
string s2("hello world");
cin >> s2;
cout <<"s2:" << s2 << endl;
}
}
不同的编译器实现的方式不同,结构不同,但是增删查改的逻辑和用法都是一样的。如果同样的代码在另一个编译器上不通过,这是正常的。
三. 写时拷贝(了解)
首先回顾一下浅拷贝引发的问题:
- 同一块空间会被析构(释放)多次
- 一个对象修改会影响另外一个对象
为了解决这两个问题:
为了应对同一块空间会被析构多次这个问题,提出了引用计数。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成 1,每增加一个对象使用该资源,就给计数增加 1,当某个对象被销毁时,先给该计数减 1,然后再检查是否需要释放资源,如果计数为 1,说明该对象是资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
为了应对一个对象修改会影响另外一个对象这个问题,提出了写时拷贝计数。
写时拷贝就是一种拖延症,是在「浅拷贝」的基础之上增加了引用计数的方式来实现的。
多个对象共用同一块内存空间,哪个对象去写数据,哪个对象就再进行深拷贝,本质是一种延迟深拷贝。当然,如果不进行写数据,那就不用进行深拷贝,提高了效率。
但这种方案也是有副作用的,现在基本上也被放弃了。
推荐文章:
- 写时拷贝技术:C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell
- 写时拷贝在读取时的缺陷:C++的std::string的“读时也拷贝”技术! | 酷 壳 - CoolShell
👉我们来验证一下 STL string 是否用的是写时拷贝技术
#include<iostream>
#include<string>
int main()
{
std::string s1("hello world");
std::string s2(s1); // 拷贝构造
printf("%p\n", s1.c_str()); // c_str()函数返回其指向字符数组的地址
printf("%p\n", s2.c_str());
// 修改
s2[0] = 'x';
printf("%p\n", s1.c_str());
printf("%p\n", s2.c_str());
return 0;
}
运行结果(VS2019 下 PJ 版本的 STL):没有用写时拷贝技术,直接深拷贝。
运行结果(Linux 下 SGI 版本的 STL):这个早期15年的版本用了写时拷贝技术,加上了引用计数。
最新版本的 gcc 编译器