C++String容器详解

在这里插入图片描述

标准库中的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;
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值