七、STL与string类

👉 欢迎阅读这篇文章 👇

目录

1、STL

1.1STL是什么

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

1.2STL的六大组件

2、string

2.1 头文件 <string>

头文件string的文档介绍

在头文件 <string> 中,有类模板basic_string。

用该模板实例化出了string类、u16string类、u32string类、wstring类。

实例化出这几个类是由于字符编码的差异。

2.1.1 字符编码

实践中串是一个很重要的承载各种稍复杂信息的结构,如"name"、"名字"。任何数据存入计算机都是二进制,二进制数与字符之间的映射关系叫做编码,我们前面接触的ASCII编码表,就是二进制和英文字符的映射编码表。

计算机为了表示世界各个国家的文字,制定了Unicode(万国码);中国也有一套专门针对中文的GBK编码。

编码标准 本质分类 字节数 收录范围 显著特点
ASCII 字符集 + 编码 固定1字节 仅英文、数字、控制符 计算机编码的基石,所有编码都兼容
GBK 字符集 + 编码 中文固定2字节 2万+简繁汉字 仅在中国大陆Windows遗留系统常用;国际传输极易乱码
Unicode 字符集 看方案 全世界所有字符 只规定字符编号,不规定存储规则;是底层"字典"

Unicode

编码方案 英文字母 中文字符 生僻字 最显著特征
UTF-32 4字节 4字节 4字节 定长,但占用空间最大,几乎不用
UTF-16 2字节 2字节 4字节 Windows、Java内部常用
UTF-8 1字节 3字节 4字节 最常用,兼容ASCII

类与字符编码的匹配

类型 底层字符类型 适用编码
string char UTF-8 / ASCII / GBK
wstring wchar_t 宽字符,平台相关(Windows下UTF-16,Linux下UTF-32)
u16string char16_t UTF-16
u32string char32_t UTF-32
  1. std::string 本身不绑定编码,它只负责保存连续字节序列;里面存UTF-8还是GBK,由业务约定。
  2. u16string、u32string 是专门为Unicode UTF-16/UTF-32设计的字符串实例,等价于 basic_string<char16_t>、basic_string<char32_t>。
  3. 乱码本质:写入时的编码和读取解析使用的编码不一致。

2.2标准库中的string类

使用string类时,必须包含#include头文件以及using namespace std;

string类的文档介绍

2.2.1string类的常用接口

2.2.1.1string类对象的常见构造

(constructor)

函数名称 功能说明
string() 构造空的string类型对象,即空字符串
string(const char*s) 用字符串来构造sting类对象|
string(const string& s) 拷贝构造函数
string(size_t n, char c) string类对象中包含n个字符c

代码演示

cpp 复制代码
void test_string1()
{
	//1.构造空的string类型对象
	string s1;
	//2.用字符串构造string类对象
	string s2("hello world");
	//3.拷贝构造
	string s3(s2);
	string s4 = s2;

	//4.从另一string类对象中的下标(pos)为6的字符开始复制5个(下标从0开始)
	string s5("hello worldxxxxxx");
	string s6(s5, 6, 5);
	cout << s6 << endl;
	//要复制到结束处的方法
	//string s7(s5, 6, s5.size() - 6);
	//string s7(s5, 6, 1000);//当第三个参数大于实际个数的时候,就会取缺省值来去复制(就是复制到最后)
	string s7(s5, 6);//第三个参数不写,会取缺省值来去复制(就是复制到最后)
	cout << s7 << endl;

	//5.从字符串中取7个字符构造
	string s8("hello worldxxxxxxxx", 7);
	cout << s8 << endl;
	//6.用10个字符'*'来构造
	string s9(10, '*');
	cout << s9 << endl;
}
2.2.1.2string类对象中容量操作
函数名称 功能说明
size 返回字符串有效字符长度
length 返回字符串有效字符长度
capacity 返回空间总大小
empty 检测字符串是否释放为空串,是则返回true,否则返回false
clear 清空有效字符
reserve 为字符串预留空间
resize 将有效字符的改数该成n个,多出的空间用字符c填充

注意:

  • size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
  • clear()只是将string中的有效字符清空,不改变底层空间大小。

代码演示

cpp 复制代码
void test_string2()
{
	string s1("12345");
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;

	//演示capacity的变化
	s1 += "+++++++++++++++";
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;

	//演示clear
	s1.clear();
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;

	cout << s1.max_size() << endl;

	//演示reserve
	string s2("hello world hello world hello world hello world ");
	cout << s2 << endl;

	//将所有的空格替换为%%
	//在下面的循环过程中会有空间不够需要扩容的情况,我们可以使用reserve为s3预留空间
	string s3;
	s3.reserve(s2.size());
	for (auto ch : s2)
	{
		if (ch != ' ')
			s3 += ch;
		else
			s3 += "%%";
	}
	s2 = s3;
	cout << s2 << endl;

	string s4("12345xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
	s4.reserve(10);//一般不会将capacity改小,不会将capacity改成10
	s4.reserve(100); // 扩容,会改变capacity

	//演示resize

	//会改变size
	
	//1.给定的大小大于size(原来是空)------扩容+插入数据
	string s5;
	s5.resize(100, 'x');
	cout << s5 << endl;

	//2.给定的大小小于size------删除数据
	s5.resize(10);
	cout << s5 << endl;

	//3.给定的大小大于size(原来不空)------部分插入(原来存在的数据不改)
	s5.resize(50, 'y');
	cout << s5 << endl;
}
2.2.1.3关键字auto和for

auto

  • auto:让编译器自动推导变量的类型,auto不是类型,是类型推导占位符,编译器根据初始化表达式算出真实类型。
  • 用auto声明指针类型的时候,可以用auto和auto*,但是auto*只能声明指针类型。
  • 用auto声明引用的时候,只能用auto&。
  • 在同一行声明多个变量的时,这些变量必须是相同的类型,否则编译器就会报错。
  • auto不能作为函数的参数(c++11可以),但可以作为函数的返回值,但是要谨慎使用。
  • auto不能直接用来声明数组。
cpp 复制代码
int func1()
{
	return 10;
}

//不能作参数,c++11可以作参数
void func2(auto a)
{ }

//可以作返回值,但要谨慎使用
auto func3()
{
	return 10;
}

void test_string4()
{
	auto x1 = 1;
	auto x2 = 0.1;

	//简化类型的书写
	std::map<std::string, std::string> m;
	//std::map<std::string, std::string>::iterator mit = m.begin();
	auto mit = m.begin();

	//指针的声明可以用auto和auto*
	int a = 0;
	auto p1 = &a;
	auto* p2 = &a;
	//auto* p3 = a;//auto*只能声明指针

	//引用的声明只能用auto&
	auto& p4 = a;

	//同一行声明多个变量,必须是相同类型
	auto b = 3, c = 5;
	//auto b = 4, c = 2.5;//编译器会报错

	//作参数和返回值演示
	func2(b);

	auto d = func3();

	//auto不能声明数组
	//auto arr[10] = { 0 };
}

范围for

  • 对于一个有范围的集合而言,由程序员来说名循环的范围是多余的,因此,c++11中引入了基于范围的for循环。
  • for循环后的括号是由冒号" :"分为两部分:第一部分是范围内用于迭代的变量 ,第二部分是被迭代的范围。
  • for循环会自动迭代,自动取数据,自动判断开始和结束。
  • 范围for可以作用在数组和容器对象上进行遍历。
  • 范围for的底层其实就是替换为迭代器进行遍历。
cpp 复制代码
void test_string5()
{
	//范围for可以作用在数组的遍历
	int array[] = { 1, 2, 3, 4, 5 };
	//迭代变量只读不写
	for (const auto& e : array)
		cout << e <<" ";
	cout << endl;
	//迭代变量可读可写
	for (auto& e : array)
	{
		e *= 2;
		cout << e << " ";
	}
	cout << endl;
	//范围for对容器的对象进行遍历
	string s1("12345");
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;
}
2.2.1.4string类对象的迭代器

迭代器可以理解为:指向string中字符的"指针封装",不是指针,但是用法类似于指针。

常用迭代器成员

函数名称 功能说明
begin 正向迭代器,指向第一个字符
end 正向迭代器,指向最后一个字符的下一个位置
rbegin 反向迭代器,指向最后一个字符
rend 反向迭代器,指向第一个字符的前一个位置

类型写法

cpp 复制代码
string::iterators it;//可读可写的迭代器
string::const_iterators cit;//只读不写的迭代器
string::reverse_iterators rit;//可读可写的反向迭代器
string::const_reverse_iterators crit;//只读不写的反向迭代器
cpp 复制代码
void test_string6()
{
	string s1("12345");

	//遍历容器时,可读可写------正向迭代器
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		(*it)++;
		cout << *it << " ";
		it++;
	}
	cout << endl;
	//遍历容器时,只读不写------const正向迭代器
	string::const_iterator cit = s1.begin();
	while (cit != s1.end())
	{
		//(*cit)++;
		cout << *cit << " ";
		cit++;
	}
	cout << endl;
	//反向遍历,可读可写------反向迭代器
	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		(*rit)++;
		cout << *rit << " ";
		rit++;
	}
	cout << endl;
	//反向遍历,只读不写------const反向迭代器
	//string::const_reverse_iterator crit = s1.rbegin();
	auto crit = s1.rbegin();
	while (crit != s1.rend())
	{
		//(*crit)++;
		cout << *crit << " ";
		crit++;
	}
}

迭代器还可以用在泛型算法上,脱离具体的数据存储结构,通过迭代器来访问数据。

cpp 复制代码
//迭代器在泛型算法上的应用
string s1("12345);
reverse(s1.begin(), s1.end());
2.2.1.5string类对象的访问及遍历操作
函数名称 功能说明
operator\[\] 返回pos位置的字符
begin+end begin获取第一个字符的迭代器,end获取最后一个字符下一个位置的迭代器
rbegin+rend rbegin获取第一个字符的反向迭代器,end获取最后一个字符下一个位置的反向迭代器
范围for C++11支持更简洁的范围for的新遍历方式

遍历操作代码演示

cpp 复制代码
void test_string3()
{
	//遍历1:下标+[]方式
	string s1("12345");
	for (size_t i = 0;i < s1.size();i++)
	{
		cout << s1[i] << "|";
	}
	cout << endl;
	//遍历2:迭代器------正向遍历
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << "%";
		++it;
	}
	cout << endl;
	//迭代器------反向遍历
	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		cout << *rit << "%";
		++rit;
	}
	cout << endl;
	//遍历3:范围for
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;
}
2.2.1.6练习

1.仅仅反转字母------力扣(LeetCode)

cpp 复制代码
class Solution {
public:
    bool isLetter(char s){
        if(s>='a'&&s<='z')
        return true;
        if(s>='A'&&s<='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;
            }
            while(left<right&&!isLetter(s[right]))
            {
                --right;
            }

            swap(s[left],s[right]);
            ++left;
            --right;
        }
        return s;
    }
};

2.验证回文串------力扣(LeetCode)

cpp 复制代码
class Solution {
public:
	//判断是否是字母字符或者数字字符
    bool isLetterOrNumber(char c)
    {
        if(c>='a'&&c<='z')
        return true;
        if(c>='A'&&c<='Z')
        return true;
        if(c>='0'&&c<='9')
        return true;

        return false;
    }
    //将大写字母字符改为小写字母字符
    void ChangeLetter(char& c){
        if(c>='A'&&c<='Z')
        c+=32;
    }
    bool isPalindrome(string s) {
        int left = 0,right = s.size()-1;
        for(auto& ch:s){
            ChangeLetter(ch);
        }
        while(left<right)
        {
            while(left<right&&!isLetterOrNumber(s[left]))
            {
                left++;
            }
            while(left<right&&!isLetterOrNumber(s[right]))
            {
                right--;
            }
            if(s[left]!=s[right])
            return false;
            left++;
            right--;
        }
        return true;
    }
};

3.字符串中的第一个唯一字符------力扣(LeetCode)

cpp 复制代码
class Solution {
public:
    int firstUniqChar(string s) {
        int count[26]={0};
        //遍历字符串,统计各个字符出现的次数
        size_t i = 0;
        for(i=0;i<s.size();i++)
        {
            count[s[i]-'a']++;
        }
        //遍历字符串,找到第一个出现的次数是1的
        for(i=0;i<s.size();i++)
        {
            if(count[s[i]-'a']==1){
                return i;
            }
        }
        //没找到
        return -1;
    }
};
2.2.1.7string类对象的修改操作
函数名称 功能说明
push_back 在字符串后尾插字符c
append 在字符串后追加一个字符串
operator+= 在字符串后追加字符串str
assign 给字符串后分配一个新值,替换当前内容
insert 在pos所指向的字符的前面,向字符中插入额外字符
erase 删除字符串中的一部分字符
replace 替换字符串中从pos位置开始,长度为len的内容
swap 交换两个string对象里保存的字符串内容
c_str 返回c格式的字符串
find 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
substr 在str中从pos位置开始,截取n个字符,返回
rfind 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
npos size_t 的最大值
cpp 复制代码
void test_string7()
{
	//追加-push_back\append\+=
	string s1("hello");
	s1.push_back('w');
	s1.push_back('o');
	s1.push_back('r');
	cout << s1 << endl;

	s1.append("ld");
	cout << s1 << endl;

	s1.append(5, '!');
	cout << s1 << endl;

	string s2("12345");
	s1.append(s2.begin(), s2.end());
	cout << s1 << endl;

	s1.append(++s2.begin(), --s2.end());
	cout << s1 << endl;

	string s3 = "hello";
	s3 += ' ';
	cout << s3 << endl;

	s3 += "world";
	cout << s3 << endl;

	s3 += s3;
	cout << s3 << endl;

	s3.assign("xxxxxxxx");
	//与s3="xxxxxxxx";等价
	cout << s3 << endl;

	//插入-insert-慎重使用,因为挪动数据会有效率损失
	string s4("12345");
	s4.insert(0, 1, '0');//头插一个字符,需要写三个参数(需要在第二个参数写明插入1个)
	s4.insert(0, "xxxxx");//头插字符串
	s4.insert(s4.begin(), '1');//迭代器的方式头插字符,只需写两个参数
	cout << s4 << endl;

	s4.insert(5, 1, 'y');
	cout << s4 << endl;
	//使用迭代器插入一个字符
	s4.insert(s4.begin() + 5, 'y');
	cout << s4 << endl;

	//删除-erase
	string s5("123456789");
	cout << s5 << endl;

	s5.erase(0, 1);//头删
	cout << s5 << endl;

	s5.erase(4, 2);//中间删
	cout << s5 << endl;

	s5.erase(4);//从指定位置开始删完(第二个参数不写,有缺省值)
	cout << s5 << endl;

	//替换-replace
	string s6("123456789");
	s6.replace(3, 3, "abc");
	cout << s6 << endl;

	//多替换少,或者少替换多(删除的和插入的不一样),都涉及挪动数据,谨慎使用
	s6.replace(3, 5, "xxx");//要替换5个长度,只给了3个字符,删5插3
	cout << s6 << endl;

	//交换-swap
	string s7("12345");
	s6.swap(s7);
	cout << "s6:" << s6 << endl;
	cout << "s7:" << s7 << endl;

	//全局交换函数
	swap(s6, s7);
	cout << "s6:" << s6 << endl;
	cout << "s7:" << s7 << endl;
}

//将一个链接拆分为协议、域名、路径
void split(const string& s)
{
	size_t i1 = s.find(':');//第二个参数从下标为0位置开始可以省略(有缺省值0)
	string protocol = s.substr(0,i1);//协议
	cout << protocol << endl;

	size_t i2 = s.find('/', i1 + 3);
	string domainname = s.substr(i1 + 3, i2 - (i1 + 3));
	cout << domainname << endl;

	string path = s.substr(i2 + 1);//长度不写就可以取到s的最后
	cout << path << endl;
}

//string类对象的修改操作2
void test_string8()
{
	//c_str
	//使用fopen进行文件操作
	string filename("string.cpp");
	FILE* fout = fopen(filename.c_str(), "r");
	if (fout == NULL)
	{
		perror("fopen fail");
		return;
	}
	/*char ch = fgetc(fout);
	while (ch != EOF)
	{
		cout << ch;
		ch = fgetc(fout);
	}*/

	//find+substr

	//把一个网址分别提取出协议、域名、路径
	string url1("https://legacy.cplusplus.com/reference/string/string/find/");
	split(url1);
}
2.2.1.8练习

字符串相加------力扣(LeetCode)

cpp 复制代码
class Solution {
public:
    string addStrings(string num1, string num2) {
        string retstr;
        int end1 = num1.size()-1,end2 = num2.size()-1;
        int next = 0;//进位
        while(end1>=0||end2>=0)
        {
            int n1 = end1>=0?num1[end1--]:'0';
            int n2 = end2>=0?num2[end2--]:'0';
            int ret = n1-'0'+n2-'0'+next;
            next = ret/10;
            ret = ret%10;

            ret+='0';
            //尾插
            retstr+=ret;
        }
        //尾插
        if(next)
        {
            next+='0';
            retstr+=next;
        }
        //逆置
        reverse(retstr.begin(),retstr.end());
        return retstr;
    }
};
2.2.1.9string类对象的非成员函数
函数名称 功能说明
ralational operators 大小比较
operator+ 字符串拼接(效率不高)
swap 全局字符对象交换(是把全局模板swap的类型规定成string而成的函数)
getline 获取一行字符串
operator>> 输入运算符重载
operator<< 输出运算符重载
cpp 复制代码
void test_string9()
{
	string s1("string.cpp");
	string s2("aaa");
	const char* p1 = "bbb";

	cout << (s1 < s2) << endl;
	cout << (s1 < p1) << endl;
	cout << (p1 < s2) << endl;//写成非成员函数是为了这种,常量字符串在前的比较情况(其实就算是成员函数也可以,p1指向的bbb会隐式转换成类对象)

	cout << s1 + s2 << endl;
	cout << s1 + p1 << endl;
	cout << p1 + s1 + s2 << endl;

	//输入hello world
	string s3;
	cin >> s3;
	//cout << s3 << endl;
	//读取到空格的时候会停止,(空格、换行符分割成两个值)

	//这样会读取hello world但无法读取到一个对象中
	string s4;
	cin >> s4;
	cout << s3 << endl;
	cout << s4 << endl;

	cin.ignore();

	//使用getline读取一行字符串
	string s5;
	getline(cin, s5);
	cout << s5 << endl;
}

字符串与数值转换函数

2.2.1.10练习

3、string类的模拟实现

我们选择使用sting.h、sting.cpp来模拟实现string类,在string.h中,我们创造一个命名域,就可以使用string来命名类了,

cpp 复制代码
//string.h
#include<string.h>

namespace chen
{
	class string
	{
	public:
		//.....
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

我们知道,项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。

所以我们在.cpp文件中实现函数功能的时候,在外面也写一个命名域,这样就可以不用在每次都写域名::string::函数名()了

cpp 复制代码
#include "string.h"

namespace chen
{
	//空字符对象初始化
	string::string(){}
	
	//字符串初始化字符对象
	string::string(const char* s){}

	//析构
	string::~string(){}

	//.......
}

3.1构造函数的模拟实现

3.1.1空字符串构造字符对象的模拟实现

使用空字符串构造字符类对象需要注意的是空字符串不是没有空间,是只有一个结束符'\0',对于_str的初始化不能直接给空指针,调用 c_str()、遍历、strcpy 会空指针崩溃。

cpp 复制代码
string::string()
	/*:_str(nullptr)
	,_size(0)
	,_capacity(0)*/ //不能这样进行初始化空字符(空 string 对象也要有`\0`,不能直接给空指针)
	:_str(new char[1])
	,_size(0)
	,_capacity(0)
{
	_str[0] = '\0';
}

3.1.2字符串构造字符对象的模拟实现

  1. 初始化列表只初始化_size,只调用一次strlen,效率高
  2. 进入函数体时_size已经是正确长度,再new char[_size+1],多开 1 字节存\0
  3. _capacity等于有效字符长度
  4. strcpy自动把末尾\0一起拷贝进去

不要在初始化列表写 _str(new char[_size+1]),成员初始化顺序看类内声明顺序,会先初始化_str,此时_size是随机值,内存分配出错、越界、析构崩溃。

cpp 复制代码
string::string(const char* s)
/*:_str(new char[strlen(s) + 1])
, _size(strlen(s))
, _capacity(strlen(s))*/ //这样写会出想效率偏低,调用三次strlen函数

//尝试先定义_size,然后利用_size去定义其他的两个
/*:_size(strlen(s))
,_str(new char[_size+1])
,_capacity(_size)*/ //但是这样也会出问题,初始化列表的初始化顺序是看声明的顺序,那么就算是把_size写到前面
					//也会先初始化_str,这样就是初始化的_size的随机值,后续在析构的时候就会出问题
					//问题:开的内存太小,但是字符串更长,strcpy 硬往里写,写出去了,越界,堆会分配的内存前后藏着管理标记,越界写会把这些标记弄坏。等到 delete[] 的时候,系统发现堆信息不对,直接崩溃。

//我们选择,只在初始化列表中初始化_size,其他的都在函数体内写,这样无论什么顺序都不会出错
	:_size(strlen(s))
{
	_str = new char[_size + 1];
	_capacity = _size;
	strcpy(_str, s);
}

3.2析构函数的模拟实现

cpp 复制代码
string::~string()
{
	delete[] _str;
	_size = _capacity = 0;
}

3.3string类对象中容量操作size、\[\]的实现

cpp 复制代码
//求大小
size_t size() const { return _size; }

//下标访问
char& operator[](size_t pos) {
	assert(pos < _size);
	return _str[pos];
}
const char& operator[](size_t pos)const {
	assert(pos < _size);
	return _str[pos];
}

3.4string类对象的迭代器的模拟实现

string 的迭代器本质就是 char*,用 typedef 重命名。

const_iterator 是 const char*,限制的是解引用后的内容;不是 char* const(限制指针本身)。

cpp 复制代码
//迭代器
typedef char* iterator;
typedef const char* const_iterator;	//目的是:这里的const对象是为了不改变字符类对象的字符,
									//如果是const iterator,那就是不能改变指针的指向,不符合要求,所以typedef一个
									// const修饰目的是不改变指向的内容才行
iterator begin() { return _str; }
iterator end() { return _str + _size; }

const_iterator begin()const { return _str; }
const_iterator end()const { return _str + _size; }

3.5string类对象的修改操作的模拟实现

push_back、append、reserve、operator+=、insert、erase、find、substr的模拟实现

push_back、append、reserve

要注意push_back和append的区别

  • push_back是尾插字符,由于最后需要补'\0',判断是否需要扩容的时候,要_size 严格大于等于 _capacity就扩容,还要注意当_size为0的时候,需要给reserve传一个具体的大小,不然还是0。
  • append是追加字符串,判断是否需要扩容的时候,要将要插入的字符串的长度加上再和capacity去比较,扩容操作也要将要插入的字符串的长度加上。
  • 扩容需要用到reserve操作,注意reserve一般不会往小的改变,而且只能异地扩容,开辟新的空间的时候,要多开一个,因为capacity不包含'\0',后续操作有可能会单独追加一个'\0'。
  • operator+=复用push_back和append即可。
cpp 复制代码
void string::reserve(size_t n)
{
	//一般不会往小的改变
	if (n > _capacity)
	{
		//只能异地扩容
		//开辟空间,每次都多开一个,为'\0'预留空间(capacity不包含'\0')
		char* tmp = new char[n + 1];
		//挪动数据
		strcpy(tmp, _str);
		//释放原空间
		delete[]_str;
		_str = tmp;
		_capacity = n;
	}
}

void string::push_back(char ch)
{
	//扩容
	if (_size >= _capacity)
	{
		string::reserve(_capacity * 2);
	}
	//插入
	_str[_size] = ch;
	_size++;
	_str[_size] = '\0';
}

void string::append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		string::reserve(_size + len);
	}
	strcpy(_str + _size, str);
	_size += len;
}

string& string::operator+=(char ch)
{
	string::push_back(ch);
	return *this;
}

string& string::operator+=(const char* str)
{
	string::append(str);
	return *this;
}

insert

  • 使用insert分为插入字符和字符串,两者扩容的时候逻辑不一样,可以参考push_back和append的区别。
  • 使用insert需要挪动数据,在实现挪动数据的时候要注意,判断结束的时候会出现size_t类型永远不会小于0而出现无限循环,具体可以看下面的代码注释。
cpp 复制代码
void string::insert(size_t pos, char ch)
{
	//扩容
	if (_size == _capacity)
	{
		//reserve(_capacity * 2);//问题:如果初始给的空字符对象,那么就算扩容后也是0
		reserve(_size == 0 ? 4 : _capacity * 2);
	}
	//挪动数据

	//原始思路
	//size_t end = _size;
	//while (end>=pos)//问题:这里的end是size_t类型的,假如初始给的空字符对象,pos就是0
	//				//那么理想情况下就是end走到-1,结束循环,但是end是size_t类型,end到-1就会变成整形最大值
	//				//end永远大于0,就会无限循环,永远不会结束
	//{
	//	_str[end + 1] = _str[end];
	//	--end;
	//}

	////方法一:
	////解决方案------将end改为int类型,又面临一个问题,
	////当某个操作符的各个操作数属于不同类型时,其中一个操作数就要转换成另一个操作数的类型,算术转换
	////pos是size_t类型的,end是int类型的,所以,end就会转换成pos的类型,即end还是以size_t类型进行的运算
	////还和之前的问题一样
	////解决方法------将pos强制类型转换为int类型
	//int end = _size;
	//while (end >= (int)pos)
	//{
	//	_str[end + 1] = _str[end];
	//	--end;
	//}

	//方法二:我们知道,上面所讲述的错误就是因为和0去比较无法结束循环导致的,那么就考虑,不和0去比较
	//我们让end代表要挪动的数据的下一个位置,这样当要挪动0位置的时候,end=1就是最后一次了,下面--end后就变成了0,结束
	//这样就不会出现永远>pos的情况
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		--end;
	}

	_str[pos] = ch;
	++_size;

}

void string::insert(size_t pos, const char* s)
{
	//扩容
	size_t len = strlen(s);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}
	//挪动数据
	size_t end = _size + len;
	//while (end - len > pos - 1)//错误:假设空字符对象,那么end-len是0,pos-1是整形最大值
	//								end-len永远小于pos-1,循环永远也不会进去
	while(end>pos+len-1)
	{
		_str[end] = _str[end - len];
		--end;
	}
	//插入数据
	for (size_t i = 0;i < len;i++)
	{
		_str[pos++] = s[i];
	}
	_size += len;
}

erase

  • 使用erase,需要声明和定义一个静态成员变量npos作为len的缺省值,静态成员变量要在类里面声明,类外面初始化,而且不能在.h文件中初始化,声明的时候不可以给缺省值。
  • erase的实现分为len大于剩余的字符串长度和len小于剩余的字符串长度两种情况,第一种情况就是直接删除完,第二种情况需要挪动数据。
cpp 复制代码
//初始化静态成员变量npos
size_t string::npos = -1;

void string::erase(size_t pos, size_t len)
{
	//len大于后面剩余的字符串个数
	if (len == string::npos || len >= _size - pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	//len小于后面剩余的字符个数
	else
	{
		for (size_t i = pos+len;i <= _size;i++)
		{
			_str[pos++] = _str[i];
		}
	}
}

find

  • 查找分为查找字符和查找字符串,查找字符只需要遍历字符串查找即可;查找字符串需要用strstr函数确认子串在主串第一次出现的位置,在用指针-指针运算获得位置即可。
  • find函数是const成员函数(const类型对象也可以使用)
cpp 复制代码
size_t string::find(char ch, size_t pos)const
{
	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* s, size_t pos)const
{
	assert(pos < _size);
	char* ptr = strstr(_str+pos, s);
	if (ptr == nullptr)
	{
		return npos;
	}
	return ptr - _str;
}

substr

  • 取子串的逻辑类似于erase,分为len大于后面的字符串个数和len小于等于后面的字符串个数两种情况。
  • 取子串需要创建临时对象sub,只能传值返回,不能传引用返回。
  • reserve(len) 提前开好容量,避免多次扩容。
  • 由于 VS 的 RVO 返回值优化,return sub 不再调用拷贝构造,直接在外部接收对象的内存上构造 sub;但优化不是万能的,关闭优化后依旧依赖深拷贝拷贝构造,否则浅拷贝会崩溃。
cpp 复制代码
string string::substr(size_t pos, size_t len)const
{
	//len大于剩余的字符串长度
	if (len == npos || len > (_size - pos))
	{
		len = _size - pos;
	}
	//len小于等于剩余的字符串长度
	string sub;
	sub.reserve(len);
	while (len--)
	{
		sub += _str[pos++];
	}
	return sub;
}

3.6string类对象的拷贝构造和赋值运算符重载的模拟实现

不管是返回值需要拷贝构造还是用拷贝构造创建新的对象,对于string类,都需要进行深拷贝,不能不写调用编译器自动生成的浅拷贝的拷贝构造。

拷贝构造

  • 需要显式写深拷贝的拷贝构造
  • 初始化新空间的时候要记得比原来的多开一个空间,因为_capacity不包含'\0'
cpp 复制代码
string::string(const string& s)
	:_str(new char[s._capacity+1])//多开一个空间,capacity不包含'\0'
	,_size(s._size)
	,_capacity(s._capacity)
{
	strcpy(_str, s._str);
}

赋值运算符重载

  • 拷贝构造是创建一个新对象并初始化;赋值运算符重载是修改已经存在的对象。
  • 赋值运算符重载步骤(s2 = s1)
    • 释放 s2 原来的旧堆空间
    • 开辟新空间,容量和 s1 保持一致(多开 1 字节存\0)
    • 将 s1 的字符串数据拷贝到 s2
  • 赋值用引用返回,避免值返回带来额外拷贝构造,提升效率。
  • 必须判断自赋值(this != &s),防止自己给自己赋值时,提前把源数据释放掉。
cpp 复制代码
string& string::operator=(const string& s)
{
	if (this != &s) {
		//释放旧空间
		delete[]_str;
		//开辟新空间,与s的大小保持一致
		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}

	return *this;
}

3.7string类对象的非成员函数的模拟实现

3.7.1operator+

  • 非成员operator+,返回新 string 对象,不会修改 lhs、rhs 本身
  • 重载三个版本:string + string、string + 常量字符串、常量字符串 + string
  • 复用已经写好的operator+=,减少重复代码
  • 必须做成非成员函数,才能支持"abc" + s(左边是 const char*)
  • 局部对象 ret 返回,会触发拷贝构造(RVO 可以优化掉拷贝,但深拷贝代码依然要实现)
cpp 复制代码
string operator+(const string& lhs, const string& rhs)
{
	//拷贝构造
	string ret = lhs;
	//复用+=,+=的参数是字符串
	ret += rhs.c_str();
	return ret;
}
string operator+(const string& lhs, const char* rhs)
{
	string ret = lhs;
	ret += rhs;
	return ret;
}
string operator+(const char* lhs, const string& rhs)
{
	string ret = lhs;
	ret += rhs.c_str();
	return ret;
}

3.7.2operator<<

  • 非成员operator<<,为了支持cout << s这种写法
  • 第一个参数是输出流引用,返回流引用,支持连续输出 cout << s1 << s2
  • 用范围 for 遍历打印每个字符,不能直接 out << _str (会把\0后面垃圾内容也打出)
cpp 复制代码
ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}

3.7.3比较函数

以==、< 作为基准,其余的都只依靠这俩

cpp 复制代码
bool operator== (const string& lhs, const string& rhs)
{
	return strcmp(lhs.c_str(), rhs.c_str()) == 0;
}
bool operator< (const string& lhs, const string& rhs)
{
	return strcmp(lhs.c_str(), rhs.c_str()) < 0;
}
//依赖
bool operator!= (const string& lhs, const string& rhs)
{
	return !(lhs == rhs);
}
bool operator> (const string& lhs, const string& rhs)
{
	return !(lhs<=rhs);
}
bool operator>= (const string& lhs, const string& rhs)
{
	return !(lhs<rhs);
}
bool operator<= (const string& lhs, const string& rhs)
{
	return (lhs < rhs) || (lhs == rhs);
}

3.7.4swap

  • 成员 swap:交换 _str、_size、_capacity,只交换指针,不拷贝字符,O (1) 高效 ,参数不能加const(要修改对象)
  • 非成员 swap:调用成员 swap
  • 全局模板swap函数。
cpp 复制代码
void string::swap(string& s)
{
	//使用全局函数swap(模板函数)依次交换_str、_size、_capacity
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);

}
void swap(chen::string& s1, chen::string& s2)
{
	//复用string类成员函数即可
	s1.swap(s2);
}

3.7.5operator>>

in.get():从输入流拿一个原始字符(无格式输入)

  1. 不会跳过空格、\n、tab,空白字符也能读出来
  2. 读到 EOF 返回 EOF
  3. 只是取出这个字符,不会自动丢弃换行
cpp 复制代码
istream& operator>>(istream& in, string& s)
{
	char ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}
	return in;
}

3.7.6getline

cpp 复制代码
istream& getline(istream& cin, string& s)
{
	char ch = cin.get();
	while (ch != '\n')
	{
		s += ch;
		ch = cin.get();
	}
	return cin;
}
相关推荐
沐晓时光26 分钟前
C语言入门,字符串和字符串函数知识讲解
c语言·开发语言
xxxiugou1231 小时前
C++命名空间与引用全解析
开发语言·c++
用户7624117567052 小时前
第十五章 人工智能基础
c++
yangmu32032 小时前
编程语言发展简史:从机器指令到智能协作
开发语言
爱写Bug的浮游4 小时前
unique_ptr 零开销的真相与边界
c++·facebook
Pniubi5 小时前
Java为什么要分堆和栈
java·开发语言
xiaoye-duck5 小时前
《C++ 项目:手写大模型接入 SDK》ChatGPT接口设计(下):流式响应实现和测试
网络·c++·语言模型·chatgpt
小小晓.5 小时前
Qt 对话框、QLineEdit 与布局管理实战详解
开发语言·qt
炸薯条!6 小时前
博客系统测试报告
开发语言·lua