标准库中的string类
string不是STL中的,而是C++标准库中的,但是从功能上来说,归到STL也行,string就是来管理字符串的
string类的常用接口说明
下面只介绍常用的:
(constructor)函数名称 | 功能说明 |
---|---|
string() | 构造空的string类对象,即空字符串 |
string(const char* s) | 用C-string来构造string类对象 |
string(size_t n, char c) | string类对象中包含n个字符c |
string(const string&s) | 拷贝构造函数 |
string s1;
string s2("1111111");
string s3(s2);
//string重载了流插入和流提取
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cin >> s1;
cout << s1;
展示了常用的,我们来看看文档中剩下的几个:
string s1("hello world");
string s2(s1, 6, 5);
cout << s2 << endl;
//拷贝长度不够的情况:
string s3(s1, 6, 15);
cout << s3 << endl;//不会报错,有多少拷多少
//npos缺省值的应用:
string s4(s1, 6);
cout << s4 << endl;
//取字符串的前n个:
string s1("hello world", 5);
cout << s1 << endl;
string s(5, 'x');
cout << s << endl;
string成员函数
string会自动调用析构函数
这里我们着重介绍一下operator =
string元素访问
这里着重介绍operator[],我们就可以像数组那样,用[]来访问和修改.
//string的底层大概可以这样理解:(动态增长就顺序表那套)
class string
{
public:
char& operator[](size_t i)//还可以直接搞成内联函数
{
assert(i < _size);
return _str[i];//引用返回方便修改
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
string s("hello world");
s[0] = 'x';//直接修改
cout << s << endl;
//还有一个好处就是检查数组越界
string的遍历方式
下标+[]
//1.下标+[]
string s1("hello world");
cout << s1[2] << endl;
迭代器
所有容器都可以用这种方式访问
//2.迭代器:
string s2("hello world");
string::iterator it = s2.begin();
while (it != s2.end())
{
cout << *it << " ";
++it;
}
end()返回最后一个位置的下一个位置,iterator的使用就像指针一样,但不是指针
所有的容器都可以用迭代器访问
反向迭代器:
reverse_iterator ,rbegin()指向最后一个位置,rend()指向第一个位置的前一个位置,和正向迭代器一样,是左闭右开的。
string s("hello world");
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << " ";
++rit;//注意这里还是使用++而不是--
}
cout << endl;
正向写反向迭代器也可以,但是……没必要。而且很麻烦,其实一般情况下,反向迭代器用的也不多
const迭代器
只读不能修改指向的内容,自己可以修改,类似于const int*
const string s("hello world");
//不能用普通迭代器,否则会放大权限:
string::const_iterator cit = s.begin();
string::const_reverse_iterator rcit = s.rbegin();
while (cit != s.end())
{
//*cit *= 2就不行,因为这里的*cit是常量
cout << *cit << " ";
++cit;
}
cout << endl;
while (rcit != s.rend())
{
//*cit *= 2就不行
cout << *rcit << " ";
++rcit;
}
这里用auto会更爽:
auto cit = s.begin();//之前的老版本有专门的cbegin()和crend(),用这个也可以,因为C++是向前兼容的。
auto rcit = s.rbegin();
注意,用const修饰的反向迭代器是本身不能修改,不要混了
const string::iterator cit = s.begin();//这里cit本身不能修改
范围for(C++11及以上支持)
//3.范围for
string s3("hello world");
//字符赋值,自动迭代,自动判断结束
for (auto ch : s3)//auto代表自动推导类型,这里自动推导为char
{
cout << ch << " ";
}
cout << endl;
看着NB,但是底层和迭代器是一样的,被编译后会替换成迭代器,底层就是迭代器
如果我想修改,该怎么办?-- 换成引用,auto&
for (auto& ch : s3)//auto代表自动推导类型,这里自动推导为char
{
ch += 2;
cout << ch << " ";
}
对于string而言,这三种方式在性能上没有区别,你喜欢哪种就用哪种就行
auto的用法说明:
int main()
{
int a = 10;
auto b = a;
cout << typeid(b).name() << endl;//typeid可以查看类型
//用auto可以简化:
map<string, string> dict;
auto mit = dict.begin();//这里直接用auto,就不用敲那么一大堆了
return 0;
}
//auto不能做函数的参数,但可以做返回值:
//void func(auto a){}
auto func()
{
return 3;
}
谨慎使用auto作为返回值,当你套很多层的时候,看着会很不方便
auto func1()
{
return 1;
}
auto func2()
{
return func1();
}
auto func3()
{
return func2();
}
这种你看着就会很不方便
int array[] = { 1,2,3,4,5,6,8,7,8 };
for (auto& e : array)//语法上不开空间,实际还是会消耗,但是如果容器或者数组里面的数据比较大,还是建议把引用加上,日常这两个你随便用就行
e *= 2;
for (auto e : array)
cout << e << " ";
cout << endl;
注意这里auto的替换工作不是在预处理阶段完成的,因为预处理阶段完成的是比较简单的,而auto的替换比较复杂
auto还可以加*,表示指针
int x = 10;
auto* y = &x;
不加*,右边随便是什么,加了 *,右边必须是指针
string容量相关函数
size和length
length是早期的叫法,专门用于string,后来STL出来以后,就改成size了,因为length不具有通用性,但是length是不能取小的,因为要向前兼容
string s("hello world");
cout << s.size() << endl;
cout << s.length() << endl;
max_size
也是早期的设计,表示string最大能开多长,但是没啥意义
cout << s.max_size() << endl;
capacity
string s;
size_t sz = s.capacity();
cout << "initial capacity:" << sz << endl;
cout << "making s grow:" << endl;
for (int i = 0; i < 100; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed:" << sz << endl;
}
}
第一次是2倍扩容,注意每次的capacity要加上1,因为有一位是留给\0的
这是在windows上,那么在linux上会怎么扩容呢?
string s1("hello world");
string s2("xxxxxxxxxxxxxxxxxxxxx");
//大小是一样的,因为你存到buffer上,你的_str指针也会占一定的空间,你存到指针指向的空间,Buffer就废弃了,你不用空间,不代表空间不存在
cout << sizeof(s1) << endl;
cout << sizeof(s2) << endl;
为啥是28,16 + 4对齐,20 + 2 * 4 = 28
但是linux下,这玩意只有8字节(图片显示中的最后一行):
具体是怎么实现8字节的这里就不详细介绍了,后续会介绍的。
扩容这东西,多了浪费,少了不够,而且频繁的扩容是很消耗的,那么有没有什么办法可以减少扩容呢?那就看看下面的两个接口:reserve 和 resize
reserve和resize
reserve就是提前开好空间
string s;
s.reserve(100);//必须开100,但是VS会开得比100大,因为有整数倍对齐
size_t sz = s.capacity();
cout << "initial capacity:" << sz << endl;
记住,我们这里reserve的100是不包含\0的,我们平时谈论的string的容量,也是不包含\0的
reserve会扩容,那它会不会缩呢?一般不会,但是不同的平台会有所不同,我们看看标准文档里面咋写的
具体咋样,我们写代码看看不就行了:
string s("hello worldxxxxxxxxxxxx");
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
s.reserve(20);
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
s.reserve(28);
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
s.reserve(40);
cout << s.size() << endl;
cout << s.capacity() << endl;
windows下,VS是不会缩的,大了才扩,小了不动
我们再看看linux下:
会缩。
所以不同的平台实现是有所不同的
clear
清理所有数据,严格来说一般是不清容量的,但是有些地方也会清除容量,这个看具体的,比如我现在不想要这些数据了,想要一些新的数据,就可以用clear
string s("hello worldxxxxxxxxxxxx");
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
s.reserve(20);
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
s.reserve(28);
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
s.reserve(40);
cout << s.size() << endl;
cout << s.capacity() << endl << endl;
s.clear();
cout << s.size() << endl;
cout << s.capacity() << endl;
再来看看linux下的clear
也是不会清除容量的。
empty
判空,就是看有没有字符,不包括\0
shrink_to_fit
缩容,但是这玩意有时候也不一定会缩
string插入相关接口
push_back
尾插一个字符
append
尾插一个字符串
string s("hello world");
s.push_back('x');
s.append("yyyyyy");
cout << s << endl;
+=(最常用的接口)
s += ' ';
s += "333333333";//空间不够就扩容
cout << s << endl;
assign
又是一个冗余接口,覆盖当前的值,类似于拷贝
insert
//头插
s.insert(0, "99999 ");
cout << s << endl;
注意我们头插要谨慎使用,因为头插需要挪动数据,效率很低
string s("hello world");
//插入一个字符:
char ch = 't';
s.insert(0, 1, ch);
// s.insert(0, ch);//不能这样写,没提供相关接口
s.insert(s.begin(), ch);
cout << s << endl;
erase
string s("hello world");
s.erase(6, 1);//第6个位置删除一个字符
//头删:
s.erase(0, 1);
s.erase(s.begin());
//尾删:
s.erase(--s.end());
s.erase(s.size() - 1, 1);
erase也是一样,当你给的lenth比字符串长的时候,删除到字符串末尾就不再删除
一般只尾插,不删除,如果你需要不断的头插,那你可能需要重新看看自己的代码逻辑
replace
//string s("hello world");
少替换多,需要扩容:
//s.replace(5, 1, "%%");
多替换少,需要往前挪:
//s.replace(5, 4, "%%");
//把所有的空格替换成%%
string ss("hello world hello bite ");
size_t pos = ss.find(' ');
while (pos != string::npos)
{
ss.replace(pos, 1, "%%");
pos = ss.find(' ', pos + 2);
}
cout << ss << endl;
但当你需要插入大量字符时,这种方法就会很低效,这时候我们可以采用时间换空间的办法:
string s("hello world hello bite ");
string tmp;
//还可以提前预留空间,这样会更高效:
tmp.reserve(s.size());
for (auto ch : s)
{
if (ch == ' ')
tmp += "%%";
else
tmp += ch;
}
cout << tmp;
replace只有在平替的情况下才最有用,就是一换一,二换二这种情况,但是,这种情况我们用[]不是更香吗?
swap
交换两个string对象,把我构造出来的空间直接给你
string tmp("hello world");
string sss;
sss.swap(tmp);
string的其他相关操作
c_str
返回底层字符串的指针,它的作用是兼容C语言,因为我们有时候设计某些库提供相关接口的时候会设计成C语言的,比如Mysql数据库,Mysql提供的相关接口就是C写的
return C的字符串,也就是const char*,同时为了兼容C,会加上\0作为结束
string str;
cin >> str;
// FILE* fout = fopen(str, 'w');//我们这里传str就不行,因为它是string类型的,而这里是C的接口,需要的是const char*类型的
FILE* fout = fopen(str.c_str(), "r");//这样就可以了
data
和c_str类似,也是为了解决兼容问题,上面的代码我们也可以返回data
get_allocator
不需要管,这是空间配置器,又叫内存池
copy
copy一个字串过来,就是一个拷贝,用得很少
char buffer[20];
std::string str ("Test string...");
std::size_t length = str.copy(buffer,6,5);
buffer[length]='\0';
std::cout << "buffer contains: " << buffer << endl;
find和rfind
正向和反向查找
substr
获取字串,这是用得比较多的,上面的copy我们不咋用,一般用这个
取出string中从pos开始的len个位置,并构造成string返回,第二个参数有缺省值,如果不传,默认从pos位置开始取到字符串结束位置
string s("test.cpp");
size_t pos = s.find('.');
string suffix = s.substr(pos);
cout << suffix << endl;
find_first_of、 find_last_of、 find_first_not_of 、 find_last_not_of
这名字起得很搓,它的意思是找到所有出现的位置,应该是find_any_of
string s("Returns a newly constructed string object with its value initialized to a copy of a substring of this object.");
size_t found = s.find_first_of("abcd");//找到字符串中所有有a或b或c或d的位置
while (found != string::npos)
{
s[found] = '*';
found = s.find_first_of("abcd", found + 1);
}
cout << s << endl;
这接口名真是……稀碎……
find_last_of就是倒着找,比如我们划分路径和文件名的时候,就可以用这个,比如windows下一个完整的路径:C:\Users\admin\AppData\Roaming\Typora
划分出来,路径就是C:\Users\admin\AppData\Roaming\,文件名就是Typora,那么这时候我们就可以借助这个接口来实现
string s1("C:\\Users\\admin\\AppData\\Roaming\\Typora");//windows文件路径
string s2("/usr/bin/hello");//linux文件路径
size_t pos = s1.find_last_of("/\\");
cout << "路径:" << s1.substr(0, pos + 1) << endl;
cout << "文件名:" << s1.substr(pos + 1) << endl;
compare
废物接口,无需多说,直接用operator就可以
string中非成员函数重载
operator +
为什么+不重载成成员函数呢?因为它想支持下面的玩法:
void test_string1()
{
string s1("hello");
//玩法一:string+字符串
string s2 = s1 + " world";
//玩法二:字符串+string
string s3 = "world" + s1;
//如果重载为成员函数,第一个参数就绝对是string,那就不能进行玩法二了
}
relational operators
字符串大小比较,它重载了一堆,其实有第一个就够了,因为常量字符串可以隐式类型转换成string类型
swap
注意这个和前面的swap不一样,前面的是成员函数,这里是全局的,但是它底层会去调x.swap(y),是为了避免深拷贝
getline
这个接口我们结合一个题来看
字符串最后一个单词的长度
注:
cin和scanf一样,它们拿不到空格,哪怕你再写一个string去缓冲区里面拿,也拿不到空格,因为cin是把空格作为分隔符来看待的
这时候,我们就需要用到getline了
下面是正确代码:
#include <iostream>
using namespace std;
int main() {
string str;
getline(cin, str);
size_t pos = str.rfind(' ');
cout << str.size() - pos - 1 << endl;
}
getline还可以自定义终止符,比如
getline(cin, str, '*');
string相关题目练习
仅仅反转字母
这个地方用迭代器反而麻烦,我们就用下标:
class Solution {
public:
bool isLetter(char c)
{
if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
return true;
return false;
}
string reverseOnlyLetters(string s) {
int left = 0, right = s.size() - 1;
while(left < right)
{
while(left < right && !isLetter(s[left]))//加上left < right,防止越界
{
++left;
}
while(left < right && !isLetter(s[right]))
{
--right;
}
swap(s[left++], s[right--]);
}
return s;
}
};
这个思路就和快排的horea方法很像。
字符串中的第一个唯一字符
这题,就用之前计数排序的思路就会很简单:
class Solution {
public:
int firstUniqChar(string s) {
int arr[26] = {0};
for(auto ch : s)
{
++arr[ch - 'a'];
}
for(size_t i = 0;i < s.size();++i)
{
if(arr[s[i] - 'a'] == 1)
return i;
}
return -1;
}
};
验证回文串
class Solution {
public:
bool isLetterOrNum(char ch)
{
return ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'));
}
bool isPalindrome(string s) {
for(auto& ch : s)
{
if(ch >= 'a' && ch <= 'z')
ch -= 32;
}
int left = 0, right = s.size() - 1;
while(left < right)
{
while(left < right && !isLetterOrNum(s[left]))
{
++left;
}
while(left < right && !isLetterOrNum(s[right]))
{
--right;
}
if(s[left++] != s[right--])
return false;
}
return true;
}
};
字符串相加
这就类似于大整数相加
stoi:可以把字符串转成整型
stoll:把字符串转成long long整型
to_string:可以把任何类型转成字符串
思路:
STL中的reverse可以实现任何容器的逆置,但是要求双向索引,虽然string里面没有逆置函数,我们可以用这个,reverse要求传容器的左右区间,且是左闭右开区间
代码实现:
class Solution {
public:
string addStrings(string num1, string num2) {
int end1 = num1.size() - 1, end2 = num2.size() - 1;
int next = 0;//记录进位
string str;
while(end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
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;
}
};
字符编码
人类的文字信息由各种符号组成,但是内存和磁盘只有0101,为了存储这些符号,牢美发明了编码,就是值和符号映射编码关系,一个值对应一个符号,最开始就是ascii编码表
比如char buf[] = “apple sort”;内存中实际存储的是对应的值:
打印的时候就回去访问内存,然后拿到编码去对应的表里面查,打印的过程就是一个查表的过程
但是ascii编码只适合牢美,为了推广至世界,就提出了unicode,又叫统一码,万国码,把全世界所有国家的文字都编进去
常见的汉字就是两个字节编码一个汉字
char str[] = "牛马";
cout << strlen(str) << endl;//4
而且同音字会相邻存储
比如有的字符只需要一个字节就可以存储,那么UTF-8就可以选择,但是UTF-16和UUTF-32起步就是2个字节和4个字节,没得选,会浪费空间,所以UTF-8用得最多
再看看,为什么我们的string要设计为模板?
utf-16前加u,utf-32前加U
除了上面的编码,还有一个GBK编码,GB是国标的意思,是我们自己搞的一套自己的编码,特殊需求的时候,比如windows中文版默认的就是用的GBK,主要收录中日韩的汉字
说白了,GBK就是为了防止访问不全的,就是专门搞中文的。
string模拟实现
大框架:
namespace cbn
{
class string
{
public:
//各类接口
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
默认构造函数
我们初始化_str的时候,最后不要直接让 _str = nullptr,要给\0预留一个字节大小的空间
带参构造函数
用初始化列表初始化不太好,因为我们之前说过,初始化列表初始化的顺序是按照声明的顺序来的,所以这里_str会先初始化,这时候的 _size就是个随机值,很明显不对
//string.h文件
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
namespace cbn
{
class string
{
public:
string()
:_str(new char[1])
,_size(0)
,_capacity(0)
{}
string(const char* str)
{
_size = strlen(str);
_capacity = _size;//这里不能加1,capacity里面是不包含\0的
_str = new char[_capacity + 1];
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
void test_string1()
{
string s1;
string s2("hello world");
}
}
//string.cpp文件
#include "string.h"
//多个相同的namespace最终会合到一起
namespace cbn
{
}
//Test.cpp文件
#include "string.h"
int main()
{
cbn::test_string1();
return 0;
}
我们来测试一下代码:
这种问题就是我们经常遇见的,因为类内是默认内联的,但是类外不是,这里的test_string1()函数,因为string.cpp和Test.cpp两个文件都包含了头文件string.h,所以这个函数会在这两个文件生成的目标文件中各自有一份,最后链接合到一起的时候因为有两份就会发生冲突
解决方案1:把test_string1()函数变成静态的,因为静态的只在当前文件可见,改成inline也可以,因为内联也是只在当前文件展开
最标准的解决方案:声明和定义分离,在头文件中定义,在.cpp文件中实现即可
默认构造和带参构造我们可以合并实现,写成全缺省的
//短小,频繁调用的函数,直接放在类内即可,默认内联
string(const char* str = "")//C语言规定,常量字符串后面必有\0,这里啥都没有,但是有一个\0
{
_size = strlen(str);
_capacity = _size;//这里不能加1,capacity里面是不包含\0的
_str = new char[_capacity + 1];
strcpy(_str, str);//strcpy会拷贝\0
}
析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
size
size_t size() const
{
return _size;
}
operator[]
char& operator[](size_t p)
{
assert(p < _size);//防止越界
return _str[p];
}
//const版本
const char& operator[](size_t p) const
{
assert(p < _size);//防止越界
return _str[p];
}
iterator
这里我们只实现简单版本的,复杂版本的后续链表部分再介绍
typedef char* iterator;
对,就这么简单,我们可以把迭代器简单看作指针
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
namespace cbn
{
void test_string1()
{
string s("hello world");
string::iterator it = s.begin();
while (it != s.end())
{
*it += 2;
cout << *it << " ";
++it;
}
cout << endl;
}
}
实现了迭代器,范围for也就可以用了,但是范围for要求必须原封不动的跟着库里面的命名方式走,因为它就是一个傻瓜式的替换,并不是看你实现了什么,比如你这里把begin()改成Begin(),范围for就跑不动了
string s("hello world");
for (auto ch : s)
{
cout << ch << " ";
}
cout << endl;
记住,迭代器不一定就是指针,这里我们实现的是最简单的版本,迭代器还可能是自定义类型,比如STL源代码中的迭代器的实现就是自定义类型:
迭代器的设计是一种封装的体现,因为底层可能是数组、链表、树,不管底层是什么,我们都可以用使用begin()访问第一个,end()访问最后一个的下一个,它屏蔽了底层代码的实现细节,提供了统一的接口,对不同的数据类型可以使用类似的访问方式,不需要关心底层实现细节。
在insert之后,迭代器会失效,因为指向原来位置的值会因为Insert的原因被挪动或者被修改。
const_iterator
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
push_back
ps::
//static const size_t npos = -1;//本来是不准这么写的,但是这就可以,因为开发者可能那啥有点不正常吧……,而且只有static const修饰的整型可以,double这些都不行
void string::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
//别忘了\0这小老弟
_str[_size] = '\0';
}
append
void string::append(const char* str)
{
//按需扩容:
size_t len = strlen(str);
if (_size + len > _capacity)
{
//可以对齐一下:
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);//不够2倍,按2倍对齐,避免反复扩容
}
strcpy(_str + _size, str);//用strcat还需要找\0,不如直接copy
_size += len;
}
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);//=_size相当于尾插了
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
}
循环那里,当end < pos的时候循环才会结束,end = 0 时,循环继续,end = -1,但是,end是size_t类型的,他就不可能等于-1,-1就是整型最大值了,就会陷入死循环了,那我把end改成int类型可以吗?不可以,这是C语言留下的一个大坑。
当操作符两边操作数的类型不一样的时候,编译器会悄悄做类型提升或截断,类似于这里的比较和+这些,它会让范围小的向范围大的去做提升
因为while中的判断条件是end >= pos,=的存在,会使两边的类型保持一致,也就是因为pos是size_t类型的,所以end会被提升为size_t类型的,这就又回到问题的开始了,所以这方法不行
解决方法1:把pos强转为int类型
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
--end;
}
解决方法2:想办法把>=换成>
让end指向\0的下一个
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
除了插入单个字符,我还想插入字符串怎么办?
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);//=_size相当于尾插了
size_t len = strlen(str);
if (_size + len == _capacity)
{
reserve(_size + len > _capacity * 2 ? _size + len : 2 * _capacity);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end-len];
--end;
}
//插入字符串:
for(size_t i = 0;i < len;++i)
{
_str[pos+i] = str[i];
}
+size += len;
}
erase
//声明
//删:
void erase(size_t pos, size_t len = npos);//缺省参数只能在声明中给,不能在定义中给
//定义
void string::erase(size_t pos, size_t len)
{
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
这里我们就用strstr函数就行,注意这个函数的返回值是一个指针
//声明:
//找:
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
//定义:
size_t string::find(char ch, size_t pos = 0)
{
for (size_t i = 0; i < _size; ++i)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos = 0)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (str == nullptr)
{
return npos;
}
else
{
return ptr - _str;//指针-指针,返回的就是当前位置的下标
}
}
substr(从pos位置开始,取len个字符,构造一个字串)
//声明:
//取子串:
string substr(size_t pos = 0, size_t len = npos);
下面是错误演示:
//定义:
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
//len大于剩余字符长度,更新一下len,就直接后面有多少取多少
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;
}
sub出了作用域会销毁,suffix就成了野指针了,所以我们这里要自己实现深拷贝
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
加上这个,上面的代码就没毛病了
既然实现了拷贝构造,我们就来实现一下赋值,我们不写赋值的时候,默认生成的赋值也是浅拷贝的,而且会把之前的空间覆盖掉,造成内存泄漏,所以我们这里要自己实现
=(赋值)
//赋值:s2 = s1
string& operator = (const string& s)
{
if (this != &s)//同一个位置就别来了
{
//把s2原本的空间释放,因为s1和s2的空间大小可能不一样
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
return *this;
}
}
relational operators(比较大小)
按字符取比,直接调strcmp
//比较大小:(复用,只需要实现两个即可,< 和 =)
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);
}
测试:
void test_string5()
{
string s1("test.cpp.zip");
string s2("hello world");
string s3("hello world");
cout << (s1 < s2) << endl;//流插入和流提取的优先级比比较符号高,记得加括号
cout << (s1 >= s2) << endl;
cout << (s2 == s3) << endl;
//const char* 可以隐式类型转换成string,所以我们不单独实现也可以调用:
cout << ("hello world" == s2) << endl;
cout << (s1 == "hello world") << endl;
}
单参数构造函数支持隐式类型转换,所以我们这里不用单独实现字符串和string的比较,因为我们支持用一个const字符串去进行构造。
clear
清除所有数据,不删除空间
//清除:
void clear()
{
_str[0] = '\0';
_size = 0;
}
operator << 和 >>(重载流提取和流插入)
//声明:
//输入输出:
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& const in, string& s);
//定义:
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& const in, string& s)
{
先清除string中原本有的字符
//s.clear();
//char ch;
in >> ch;
//ch = in.get();//cin是读不到空格和换行符的,它会读,但是会直接忽略
//while (ch != ' ' && ch != '\n')
//{
// s += ch;
// ch = in.get();//用get每次读一个字符进来
//}
//优化版本:避免频繁扩容
const int N = 256;
char buff[N];//用buffer作为缓冲区
int i = 0;
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)//必须是255,因为有\0
{
buff[i] = '\0';
s += buff;//缓冲区满了,就进s
i = 0;
}
ch = in.get();
}
if (i > 0)//说明有剩余字符
{
buff[i] = '\0';
s += buff;
}
return in;
}
buffer是在栈上开的,开辟速度很快,比我们提前去reserve优化很多,至于N给多少,取决于你,一般给个256比较适中
getline
遇到空格不会停,遇到换行才停,相当于就是把上面的代码中的判断条件改改就可以了
void getline(istream& in, string& s)
{
const int N = 256;
char buff[N];//用buffer作为缓冲区
int i = 0;
char ch;
ch = in.get();
while (ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)//必须是255,因为有\0
{
buff[i] = '\0';
s += buff;//缓冲区满了,就进s
i = 0;
}
ch = in.get();
}
if (i > 0)//说明有剩余字符
{
buff[i] = '\0';
s += buff;
}
}