👉 欢迎阅读这篇文章 👇
目录
- 1、STL
- 2、string
-
- [2.1 头文件 `<string>`](#2.1 头文件
<string>) -
-
- [2.1.1 字符编码](#2.1.1 字符编码)
-
- 2.2标准库中的string类
- [2.1 头文件 `<string>`](#2.1 头文件
- 3、string类的模拟实现
1、STL
1.1STL是什么
STL(standard template library)-标准模板库,是C++标准库的重要组成部分 ,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
1.2STL的六大组件

2、string
2.1 头文件 <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 |
std::string本身不绑定编码,它只负责保存连续字节序列;里面存UTF-8还是GBK,由业务约定。u16string、u32string是专门为Unicode UTF-16/UTF-32设计的字符串实例,等价于basic_string<char16_t>、basic_string<char32_t>。- 乱码本质:写入时的编码和读取解析使用的编码不一致。
2.2标准库中的string类
使用string类时,必须包含#include头文件以及using namespace std;
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练习
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;
}
};
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练习
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字符串构造字符对象的模拟实现
- 初始化列表只初始化
_size,只调用一次strlen,效率高 - 进入函数体时
_size已经是正确长度,再new char[_size+1],多开 1 字节存\0 _capacity等于有效字符长度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():从输入流拿一个原始字符(无格式输入)
- 不会跳过空格、
\n、tab,空白字符也能读出来 - 读到
EOF返回EOF - 只是取出这个字符,不会自动丢弃换行
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;
}