C++打怪升级(九)- STL之string

C++打怪升级(九)- STL之string_第1张图片

~~~~

  • 前言
  • 1. STL简单介绍
    • 1.1 什么是STL?
    • 1.2 STL的版本
      • 最初的版本
      • P.J版本
      • RW版本
      • SGI版本
    • 1.3 STL的六大组件
    • 1.4 STL的一些缺点
    • 1.5 STL重要吗?
  • 2 编码
      • 2.1 ASCII编码
      • 2.2 Unicode编码
        • UTF-8编码
      • 2.3 GBK编码
  • 3. 类模板basic_string
  • 4. 单字符string类
    • 4.1 什么是string
    • 4.2 string类的核心接口函数初见
      • 构造函数constructor相关
        • 1- 无参构造
        • 2- string类对象做单参数的构造(拷贝构造)
        • 3- C形式的字符串作为单参数的构造
        • 4- n个字符c的构造
        • 5- 了解 - 从string类对象的pos位置开始的长度为len的构造
        • 6- 了解 - 取C形式字符串的前n位构造
        • 7- 了解 - 使用string类对象的迭代器作为参数进行构造
      • 容量capacity操作相关
        • 1- string类对象的大小-size和length
        • 2- string类对象的容量-capacity
        • 3- 判断string类对象是否为空串-empty
        • 4- 清空string类对象的有效字符-clear
        • 5- 请求改变容量 - reserve
        • 6- 重新设置string对象的有效字符个数 - resize
        • 7- 了解 - 字符串的最大大小max_size
        • 8- 了解 - 缩容到与size相同 - shrink_to_fit
      • 访问和遍历相关
        • 1- []运算符重载 - operator[]
        • 2- 迭代器 - begin和end
        • 3- 迭代器 - rbegin和rend
        • 4- 迭代器 - 了解 - cbegin、cend、crbegin、crend
        • 5- 了解 - at函数 - 返回pos位置字符的引用
        • 6- 语法糖 - 范围for遍历字符串的每一个元素
      • 查找和修改相关
        • 1- 在字符串后追加字符串、字符 - operator+=
        • 2- 尾插字符元素 - push_back
        • 3- 在字符串后追加字符串、字符、c_str、迭代器范围元素 - append
        • 4- 为字符串赋一个新的值 - assign
        • 5- 新值替换掉字符串的一部分 - replace
        • 6- 在原字符串内部插入一个新值 - insert
        • 7- 删除string对象的一部分 - erase
        • 8- 了解 - 交换两个string对象的值 成员函数 - swap
        • 9- 返回C形式的字符串 - c_str和data
        • 10- npos
        • 11- 顺序查找指定参数首次出现的位置 - find
        • 12- 倒序查找指定参数首次出现的位置 - rfind
        • 13- 返回string对象的子串的拷贝 - substr
        • 14- 了解 - 其他查找函数
        • 15- 了解 - copy
      • 非成员函数相关
        • 1- operator+
        • 2- 输入输出运算符重载 - operator>>、operator<<
        • 3- 字符串比较大小相关
        • 4- 读取流中的一行 - getline
    • 4.3 Linux(g++)和Windows(Visual Studio)平台下string底层结构的实现差异说明
      • 32位平台 visual studio 2019 下的string的实现结构
      • g++的string的实现结构
        • 引用计数
        • 写时拷贝 copy on write
    • 4.4 string类的模拟实现
      • 1- 私有变量的定义
      • 2- 实现构造函数
        • 常量字符串作为参数构造 - 默认构造
        • 拷贝构造 - string类对象作为参数构造
        • n个字符c构造
        • 迭代器范围作为参数构造
      • 3- 实现赋值运算符重载函数
      • 4- 实现析构函数
      • 5- 实现返回C形式字符串的函数
      • 6- 实现返回string对象大小和容量的函数
      • 7- 实现为string开辟空间的函数 - reserve
      • 8- 实现设置strnig对象有效大小的函数 - resize
      • 9- 实现堆string对象空间的清理函数 - clear
      • 10- 实现判断字符串有效空间是否为空的函数 - empty
      • 11- 与一个string对象交换内容 - swap
      • 12- 实现[]运算符重载
      • 13- 实现尾插push_back函数
      • 14- 实现追加append函数
      • 15- 实现+=运算符重载
        • +=一个字符
        • +=一个常量字符串
        • +=一个string对象
      • 16- 为string对象分配一个新值 - assign
      • 17- 插入一个值 - insert
      • 18- 用新值替换string对象的一部分内容 - replace
      • 19- 删除部分元素 - erase
      • 20- 通过string对象构造子串 - substr
      • 21- 顺序查找值 - find
      • 22- 逆序查找值 - rfind
      • 23- 实现迭代器begin函数
      • 24- 实现迭代器end函数
      • 25- 非成员函数比较大小运算符重载
        • 等于
        • 不等于
        • 大于
        • 大于等于
        • 小于
        • 小于等于
      • 26- 非成员函数 流插入运算符重载<<
      • 27- 非成员函数 流提取运算符重载>>
      • 28- 非成员函数 读取一行 - getline
    • 4.5 深拷贝与浅拷贝
      • 浅拷贝(值拷贝)
      • 深拷贝
        • 拷贝构造函数
        • 赋值运算符重载函数
        • 析构函数
  • 后记

前言

本节开始介绍C++中的STL库中的各种内容,这篇文章主要介绍容器中的string类。


1. STL简单介绍

1.1 什么是STL?

STL即标准模板库(standard template library),是C++标准库的重要组成部分,即是一个可复用的组件库,也是一个包含一系列数据结构与算法的软件框架


1.2 STL的版本

最初的版本

STL最初是由惠普实验室的大佬 Alexander Stepanov、Meng Lee 编写而成。之后Alexander Stepanov、Meng Lee便把STL的源码开源了,允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是需要像原始版本一样开源使用。

P.J版本

P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异

RW版本

Rouge Wage公司开发,继承自HP版本,被C+ + Builder 采用,不能公开或修改,可读性一般。

SGI版本

Silicon Graphics Computer SystemsInc公司开发,继承自HP版 本。被GCC(Linux)采用,可移植性好,
可公开、修改甚至贩卖,从命名风格和编程风格上看,阅读性高。


1.3 STL的六大组件

包括容器、算法、迭代器、仿函数、配接器、空间配置器。
C++打怪升级(九)- STL之string_第2张图片


1.4 STL的一些缺点

  1. STL库更新太慢,STL大的更新版本时间间隔很长,比方说从C++98到C++11STL才有重大的更新,相隔了10多年时间;
  2. STL不支持线程安全,并发情况下需要使用者自己加锁控制,并且锁的粒度比较大;
  3. STL追求极致的效率,导致其内部比较复杂,如类型萃取、迭代器萃取;
  4. STL使用会存在代码膨胀的问题。

1.5 STL重要吗?

重要!
STL作为C++标准库的重要组成部分,是我们学习C++之路上必须掌握的基础和核心知识之一。


2 编码

编码是信息从一种形式或格式转换为另一种形式的过程;

在计算机中数据的储存和运算时都需要使用二进制数进行表示,但我们人类面对二进制数时非常头大,需要把我们所使用的字符使用确定唯一的二进制数表示,这样,计算机仍然使用二进制数进行储存和计算,显示在屏幕上时则显示对应的字符。可以说,把字符和二进制数以一定方式对应起来的过程就是编码。

2.1 ASCII编码

在C语言中我们就已经接触到了ASCII编码这一概念,只不过可能我们印象并不深。
ASCII即美国信息交换标准代码,是基于拉丁字母的编码系统。

ASCII是由美国国家标准协会(American National Standard Institute,ANSI)制定的,使用标准的单字节字符编码方案,用于基于文本的数据。方案起始于50年代后期,在1967年定案。它最初是美国的标准,供不同计算机在相互通信时需共同遵守的西文字符编码标准。现已被国际标准化组织(International Organization for Standardization,ISO)定为国际标准,适用于所有拉丁字母。-- 维基百科

C++打怪升级(九)- STL之string_第3张图片

2.2 Unicode编码

Unicode,全称为Unicode标准(The Unicode Standard),又称为统一码,又译作万国码,是信息技术领域的业界标准,使得电脑能以通用划一的字符集来处理和显示文字,不但减轻在不同编码系统间切换和转换的困扰,更提供了一种跨平台的乱码问题解决方案。
Unicode百科


UTF-8编码

UTF-88-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用一至四个字节对Unicode字符集中的所有有效编码点进行编码,属于Unicode标准的一部分。
UTF-8百科
UTF-8是变长的多字节编码方式,使用非常广泛。最少使用一个字节,最多使用6个字节表示一个字符,基本 兼容ASCII编码。
C++打怪升级(九)- STL之string_第4张图片


2.3 GBK编码

汉字内码扩展规范,简称GBK,全名为**《汉字内码扩展规范(GBK)》1.0版**,由中华人民共和国全国信息技术标准化技术委员会1995年12月1日制订,国家技术监督局标准化司和电子工业部科技与质量监督司1995年12月15日联合以《技术标函[1995]229号》文件的形式公布。 GBK共收录21886个汉字和图形符号,其中汉字(包括部首和构件)21003个,图形符号883个。–维基百科

GBK编码
GBK采用两个字节对汉字进行编码,理论上有可以编码2^16(即65536)个汉字。
C++打怪升级(九)- STL之string_第5张图片


3. 类模板basic_string

前面已经稍微了解了编码的概念,C++中的string为了应对不同的编码格式而提供了单字符的string和多字符的string如wstring、u16string、u32string。它们都基于类模板basic_string而来

basic_string类模板

C++打怪升级(九)- STL之string_第6张图片

单字符string

C++打怪升级(九)- STL之string_第7张图片

宽字符wstring

C++打怪升级(九)- STL之string_第8张图片

双字符u16string

C++打怪升级(九)- STL之string_第9张图片

四字符u32string

C++打怪升级(九)- STL之string_第10张图片


4. 单字符string类

4.1 什么是string

typedef basic_string<char> string;

string是表示字符序列的类;
string类是basic_string模板类的一个实例,使用char来实例化basic_string模板类,并用char_traits
和allocator作为basic_string的默认参数;
string不属于STL,早在STL被C++引入之前string就已经存在;在STL被引入之后,string在保留原来功能的基础上又新增了STL容器的通用功能,所以string可以当做STL容器的一部分;
注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个
类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
string类不能操作多字节或者变长字符的序列;

4.2 string类的核心接口函数初见

string类的接口函数多达100多个,我们不可能直接记住这100多个接口函数,我们需要做到的是熟练掌握使用频繁的核心接口函数,而对于不太重要的部分有个印象就行,将来使用到时我们查找文档即可。

C++打怪升级(九)- STL之string_第11张图片

构造函数constructor相关

1- 无参构造

函数声明

string();

例子

#include 
#include 

int main(){
	string str;//无参构造
    return 0;
}
2- string类对象做单参数的构造(拷贝构造)

函数声明

string (const string& str);

例子

string str1;
string str2(str1);//str1对象作为参数构造str2
3- C形式的字符串作为单参数的构造

C形式的字符串特点是以'\0'结尾;

函数声明

string (const char* s);

例子

string str("hello world");//常量字符串"hello world"作为参数构造str对象
4- n个字符c的构造

函数声明

string (size_t n, char c);

例子

string str(5, 'x');// str的值为 "xxxxx"
5- 了解 - 从string类对象的pos位置开始的长度为len的构造

函数声明
其中len大于string类对象的总大小时,那么就只到string类对象结束为止,超过的长度不考虑;
且len是缺省参数,不给定len时默认为npos,而npos是size_t的-1,即(2^32)-1,即默认到string类对象结束为止;

string (const string& str, size_t pos, size_t len = npos);

例子

string str1("0123456789");
string str2(str1, 3, 5);// str2的值为"34567"
6- 了解 - 取C形式字符串的前n位构造

函数声明
当n大于C形式字符串的长度时,实际上取得是整个C形式字符串作为参数进行的构造;

string (const char* s, size_t n);

例子

string str("0123456789", 5); // str的值为"01234"
7- 了解 - 使用string类对象的迭代器作为参数进行构造

函数声明
两个迭代器参数,分别表示迭代器起始范围和迭代器结束范围;

template <class InputIterator>
  string  (InputIterator first, InputIterator last);

例子

string str1("hello world");
string str2(str1.begin(), str1.begin() + 5);// str2的值为"hello"

容量capacity操作相关

1- string类对象的大小-size和length

size()函数声明

size_t size() const;

例子

string str("012345");
str.size();//结果为6

length()函数声明

size_t length() const;

例子

string str("012345");
str.length();//结果为6

关于size和length函数的说明:

  • size和length函数返回的都是string类对象的大小,即有效元素的数量;
  • string出现在STL之前,length正是STL加入C++语言中前string返回其大小的函数。但是在引入了STL之后,为了与STL中其他容器的一致性又新增加了size函数表示string的大小,并且由于C++语言是向前兼容的所以最早的length函数依然不能删掉,以防止先前的一些程序因为使用length函数而出现问题。
  • 二者的确是冗余的,但是这是历史造成的。
2- string类对象的容量-capacity

函数声明

size_t capacity() const;

例子

string str("0123456789");
str.capacity();//结果大于等于15
3- 判断string类对象是否为空串-empty

也即判断string类对象大小是否为0;

函数声明

bool empty() const;

例子

string str1();
string str2("hello world");
str1.empty();//结果为false
str2.empty();//结果为true
4- 清空string类对象的有效字符-clear

clear只清空对象的有效字符,即只改变size的大小,不会改变空间capacity的大小;

函数声明

void clear();

例子

string str("0123456");
str.clear();// capacity为15,size等于0
5- 请求改变容量 - reserve

reverse扩容不改变size,改变capacity;

请求容量小于等于capacity时,reverse函数不起作用,即size和capacity均不变;
请求容量大于capacity时,reverse函数起作用;
reverse函数一般不会导致容量变小;

用途
在string对象有效字符size不断增长中,将会不断达到容量上限,在每次达到容量上限时均需要进行扩容处理为该对象分配更大的空间用于存放字符。这将涉及到频繁的扩容操作,而扩容操作是有一定的资源消耗的,频繁的扩容将会更有可能导致程序运行效率的降低,增加资源的消耗。为此我们需要有目的的减少可能进行的扩容操作,于是我们需要请求扩容函数reverse。在程序一开始我们就使用reverse函数先为string对象开辟预期的适当的空间,以便减少扩容次数。

函数声明

void reserve (size_t n = 0);

例子

string str;
str.reserve(1000);//请求为str开辟不少于1000个字符的空间,即capacity大于等于1000
6- 重新设置string对象的有效字符个数 - resize

resize小于size时,删除有效字符,size将会减小;
resize大于等于size,小于等于capacity,resize不起作用;
resize大于capacity时,扩容,size不变,capacity增大;

函数声明

void resize (size_t n);
void resize (size_t n, char c);

例子

string str1("hello world");// size=12,capacity=15
str1.resize(5); // 结果为 "hello",   size=5,capacity=15
str1.resize(20);// 结果为 "hello               ",size=20,capacity>=20

string str2("hello world");// size=12,capacity=15
str2.resize(5, 'x'); // 结果为 "hello",   size=5,capacity=15
str2.resize(20, 'x');// 结果为 "helloxxxxxxxxxxxxxxx",size=20,capacity>=20

C++打怪升级(九)- STL之string_第12张图片

7- 了解 - 字符串的最大大小max_size

统一返回一个固定的很大的值,且该值无实际意义,这个值与具体的编译器有关;

函数声明

size_t max_size() const;

例子

string str1;
string str2("hello world");

str1.max_size();//一个固定的很大的值
str2.max_size();//一个固定的很大的值
8- 了解 - 缩容到与size相同 - shrink_to_fit

shrink_to_fit函数期望capacity与size相等,但是结果可能是capacity>=size

函数声明

void shrink_to_fit();

例子

string str("hello world");
str.resize(5);
str.shrink_to_fit();// 结果size=5,capacity=15 --visual Studio2019

访问和遍历相关

1- []运算符重载 - operator[]

string的底层是一个字符指针指向的动态开辟的字符数组,每次[]调用找找到并返回的是字符数组下标为pos的字符的引用。

函数声明

char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;

例子

string str("12345");
for(int i = 0; i < str.size(); ++i){
	cout << str[i] << " ";
}
cout << endl;
// 依次输出 1 2 3 4 5
2- 迭代器 - begin和end

迭代器是像指针一样的类型,底层不一定是指针实现的,但是不妨碍我们像使用指针一样使用迭代器。

begin函数返回一个迭代器,该迭代器指向对象的第一个字符元素;
end函数返回一个迭代器,该迭代器指向对象的最后一个字符元素的下一个位置;

函数声明

iterator begin();
const_iterator begin() const;
//----------------------------------
iterator end();
const_iterator end() const;

例子 - 非const迭代器 - 可读可写

string str("123456");
string::iterator it = str.begin();
while(it != str.end()){
	(*it)++;
    cout << *it << " ";
    it++;
}
cout << endl;
//结果为2 3 4 5 6 7

例子 - const迭代器 - 只读

const string str("123456");
string::const_iterator cit = str.begin();
while(cit != str.end()){
	//(*it)++;
    cout << *cit << " ";
    cit++;
}
cout << endl;
//结果为1 2 3 4 5 6
3- 迭代器 - rbegin和rend

rbegin返回一个反向迭代器,该反向迭代器指向第一个字符前的理论字符,该理论字符认为是字符串的反向结束;
rend返回一个反向迭代器,该反向迭代器指向最后一个字符;

函数声明

reverse_iterator rbegin();
const_reverse_iterator rbegin() const;
//------------------------------------------
reverse_iterator rend();
const_reverse_iterator rend() const;

例子

string str("123456");
string::reverse_iterator rit = str.rbegin();
while(rit != str.rend()){
	(*rit) ++;
    cout << *rit << " ";
    rit++;
}
cout << endl;
//结果为 7 6 5 4 3 2
const string str("123456");
string::const_reverse_iterator crit = str.rbegin();
while(crit != str.rend()){
	cout << *crit << " ";
    crit++;
}
cout << endl;
//结果为 6 5 4 3 2 1
4- 迭代器 - 了解 - cbegin、cend、crbegin、crend

前面所讲的迭代器其实包含了本小节的四种迭代器类型,是通过函数重载实现的;
但是有人提出函数重载导致返回的迭代器类型到底是const的还是非const的并不清晰,所以C++11新增了四个明确返回const修饰的迭代器的函数来满足需求,实际上并不需要这四个函数就是了。

cbegin返回一个const迭代器,该迭代器指向字符串的第一个字符元素;
cend返回一个const迭代器,该迭代器指向字符串的最后一个有效字符的下一个位置;
crbegin返回一个const反向迭代器,该反向迭代器指向字符串最后一个有效字符的位置;
crend返回一个const反向迭代器,该反向迭代器指向字符串的第一个字符前理论字符的位置,该理论字符认为是字符串的反向结束;

函数声明

const_iterator cbegin() const;
const_iterator cend() const;
const_reverse_iterator crbegin() const;
const_reverse_iterator crend() const;
5- 了解 - at函数 - 返回pos位置字符的引用

函数声明

char& at (size_t pos);

const char& at (size_t pos) const;

例子

string str("hello world");

str.at(4); // 结果为 '0'
6- 语法糖 - 范围for遍历字符串的每一个元素
string str("hello world");

for(auto& e : str){
	e++;
    cout << e << " ";
}
cout << endl;
//结果为i f m m p ! x p s m e

查找和修改相关

1- 在字符串后追加字符串、字符 - operator+=

+=运算符重载

函数声明

string& operator+= (const string& str); // string (1)

string& operator+= (const char* s); // c-string (2)

string& operator+= (char c);  // character (3)

例子

string str1("base");
string str2("1234");
str1 += str2;	// "base1234"
str1 += "weihe";// "base1234weihe"
str1 += '@'; // "base1234weihe@"
2- 尾插字符元素 - push_back

函数声明

void push_back (char c);

例子

string str("hello");

str.push_back('#'); // "hello#"
3- 在字符串后追加字符串、字符、c_str、迭代器范围元素 - append

与string的构造函数形参格式相似,用法相似,其中比较实用的是追加一个string、c_str、n个c字符;

函数声明

string& append (const string& str); // string (1)	
//尾插一个strin对象str

string& append (const string& str, size_t subpos, size_t sublen); //substring (2)
//尾插一个string对象str的子串substr,该子串包含从str的subpos位置开始的sublen长度的字符

string& append (const char* s); //c-string (3)
//尾插一个常量字符串

string& append (const char* s, size_t n); //buffer (4)
//尾插一个常量字符串的前n个字符

string& append (size_t n, char c); // fill (5)
//尾插n个字符c

template <class InputIterator> // range (6)
   string& append (InputIterator first, InputIterator last);
//尾插迭代器范围[first, last)内的字符元素

例子

string str("base");
string s("1234");

str.append(s);					// "base1234"
str.append("weihe");			// "base1234weihe"
str.append(5, '@');				// "base1234weihe@@@@@"
str.append(s, 1, 2);			// "base1234weihe@@@@@23"
str.append("hello world", 5);	// "base1234weihe@@@@@23hello"
str.append(s.begin(), s.end());	// "base1234weihe@@@@@23hello1234"
4- 为字符串赋一个新的值 - assign

assign操作将会为原字符串赋一个新值,这可能涉及到原字符串size和capacity的变化,具体是否改变既取决于新值的大小是否大于原字符串的大小,也取决于不同平台下assign函数的底层实现;

可能的情况是:

  • 新值的大小小于等于原字符串时 assign操作之后字符串size和capacity可能不变,也可能改变;
  • 新值的大小大于原字符串时 assign操作之后字符串size和capacity一定改变:string底层字符指针释放开辟的旧空间,再开辟合适的新空间,再把新值的内容依次复制到新空间中;

函数声明

string& assign (const string& str); // string (1)
//string对象str赋值给原字符串

string& assign (const string& str, size_t subpos, size_t sublen); // substring (2)
//string对象str的子串substr赋值给原字符串,该子串包含str从subpos位置开始的sublen个字符

string& assign (const char* s); // c-string (3)
//常量字符串赋值给原字符串

string& assign (const char* s, size_t n); // buffer (4)
//常量字符串的前n个字符赋值给原字符串

string& assign (size_t n, char c); // fill (5)
//n个字符c赋值给原字符串

template <class InputIterator> // range (6)
   string& assign (InputIterator first, InputIterator last);
//迭代器范围[first, last)内包含的字符元素赋值给原字符串

例子

string str("base");
string s("hello world");

str.assign(s); 						// 结果为 "hello world"
str.assign(s, 0, 5); 				// 结果为 "hello"
str.assign("weihe"); 				// 结果为 "weihe"
str.assign("weihe", 3); 			// 结果为 "wei"
    str.assign(5, 'x'); 			//结果为 xxxxx
str.assign(s.begin() + 6, s.end()); //结果为 "world"
5- 新值替换掉字符串的一部分 - replace

replace结果是新值替换原字符串的部分内容,这不可避免的涉及到原字符串被替换位置之后的元素整体向后移动的操作,时间复杂度是O(n)。可以想到的是多次进行replace操作时,会涉及到频繁的不可忽视的元素移动导致的时间浪费,所以replace操作使用不多;
更多采用的思路是空间换时间,比如创建一个新的字符串,遍历原字符串,遇到非替换字符就尾插到新字符串,遇到替换字符就把要替换的新值尾插到新字符串末尾,直到遍历完原字符串为止,最后再把新字符串的内容整体赋值给原字符串即可。

函数定义

string& replace (size_t pos,  size_t len,  const string& str); // string (1)
//原字符串pos位置开始的len个字符替换为str
string& replace (iterator i1, iterator i2, const string& str);
//原字符串迭代器范围[i1,i2)替换为str

string& replace (size_t pos,  size_t len,  const string& str, 
                 size_t subpos, size_t sublen);               // substring (2)
//原字符串pos位置开始的len个字符替换为str的subpos开始的sublen个字符
	
string& replace (size_t pos,  size_t len,  const char* s);    // c-string (3)
//原字符串pos位置开始的len个字符替换为C形式字符串
string& replace (iterator i1, iterator i2, const char* s);
//原字符串迭代器范围[i1,i2)替换为C形式字符串

string& replace (size_t pos,  size_t len,  const char* s, size_t n); // buffer (4)
//原字符串pos位置开始的len个字符替换为C形式字符串的前n个字符
string& replace (iterator i1, iterator i2, const char* s, size_t n);
//原字符串迭代器范围[i1,i2)替换为C形式字符串的前n个字符

string& replace (size_t pos,  size_t len,  size_t n, char c); // fill (5)
//原字符串pos位置开始的len个字符替换为n个字符c
string& replace (iterator i1, iterator i2, size_t n, char c);
//原字符串迭代器范围[i1,i2)替换为n个字符c


template <class InputIterator>                               // range (6)
  string& replace (iterator i1, iterator i2,InputIterator first, InputIterator last);
//原字符串迭代器范围[i1,i2)的元素替换为另一个迭代器范围[first,last)内元素

例子

string str("0123456789");
string s("weihe");

str.replace(1, 4, s); 						// 结果为 "0weihe56789"
str.replace(str.begin(), str.end() - 1, s); // 结果为 "weihe9"
string str("0123456789");
string s("weihe");

str.replace(1, 4, s, 0, 3); //结果为 "0wei56789"
string str("0123456789");

str.replace(1, 4, "hello"); // 结果为 "0hello56789"
str.replace(str.begin() + 1, str.begin() + 6, "4321"); // 结果为 "0432156789"
string str("0123456789");

str.replace(1, 4, "hello", 3); 							// 结果为 "0hel56789"
str.replace(str.begin() + 1, str.begin() + 4, "4321"); 	// 结果为 "0432156789"
string str("0123456789")

str.replace(1, 4, 5, 'x'); 								// 结果为 "0xxxxx56789"
str.replace(str.begin() + 1, str.begin() + 6, 5, '#'); 	// 结果为 "0#####56789"
string str("0123456789");
string s("weihe");

str.replace(str.begin() + 1, str.begin() + 5, s.begin() + 3, s.end());// 结果为 "0he56789"
6- 在原字符串内部插入一个新值 - insert

函数声明

string& insert (size_t pos, const string& str); 		// string (1)
//在原字符串的pos位置之前插入string对象str

string& insert (size_t pos, const string& str, size_t subpos, size_t sublen);// substring (2)
//在原字符串的pos位置之前插入string对象str的子串,该子串是从subpos位置开始的sublen长度的string对象str的拷贝

string& insert (size_t pos, const char* s); 			// c-string (3)
//在原字符串的pos位置之前插入常量字符串s

string& insert (size_t pos, const char* s, size_t n); 	// buffer (4)
//在原字符串的pos位置之前插入常量字符串s的前n个字符

string& insert (size_t pos, size_t n, char c);		 	// fill (5)
//在原字符串的pos之前位置插入n个字符c
void insert (iterator p, size_t n, char c);
//在原字符串的迭代器p位置插入n个字符c

iterator insert (iterator p, char c); 					// single character (6)
//在原字符串的迭代器p位置插入1个字符c

template <class InputIterator> 							// range (7)
   void insert (iterator p, InputIterator first, InputIterator last);
//在原字符串的迭代器p位置插入迭代器范围[first, last)内包含的字符元素

例子

string str("012345");
string s("qwert");

str.insert(1, s);		// 结果为 "0qwert12345"
str.insert(1, s, 2, 3);	// 结果为 "0ertqwert12345"
string str("012345");

str.insert(1, "weihe");		// 结果为 "0weihe12345"
str.insert(1, "weihe", 3);	// 结果为 "0weiweihe12345"
string str("012345");

str.insert(1, 5, 'x'); // 结果为 "0xxxxx12345"
str.insert(str.begin() + 1, 3, '#'); // "0###xxxxx12345"
string str("012345");
string s("weihe");

str.insert(str.begin() + 1, s.begin(), s.begin() + 3); // 结果为 "0wei12345"
7- 删除string对象的一部分 - erase

对应string对象的有效大小也会随之减小;

函数声明

string& erase (size_t pos = 0, size_t len = npos); //sequence (1)
//顺序删除字符串的从pos开始的len个字符

iterator erase (iterator p); //character (2)
//删除迭代器位置为p的字符元素

iterator erase (iterator first, iterator last); //range (3)
//删除迭代器范围[first, last)内包含的所有字符元素

例子

string str("0123456789");

str.erase(6, 4); // 结果为 "012345"
str.erase(str.begin() + 3); // 结果为 "01245"
str.erase(str.begin() + 1, str.end()); // 结果为 "0"
8- 了解 - 交换两个string对象的值 成员函数 - swap

swap交换之后,两个string对象的值,size,capacity是完全交换;

函数声明

void swap (string& str);

例子

string str1("hello world hello world");
string str2("weihe");

str1.swap(str2);

C++打怪升级(九)- STL之string_第13张图片

9- 返回C形式的字符串 - c_str和data

C形式字符串的特点是以'\0'结尾。

c_str和data均返回字符指针常量,返回类型是const cahr*;
c_str保证返回指针常量指向的内容是以'\0'结尾的,而data并不确保指针常量指向的内容以'\0'结尾;

c_str函数声明

const char* c_str() const;

例子

string str("hello world");

str.c_str(); // 结果为 "hello world"

data函数声明

const char* data() const;

例子

string str("hello world");

str.data(); // 结果为 "hello world"
10- npos

npos是size_t类型的值为-1的成员变量;
npos实际的值为(2^32)-1;
也可能npos的值不是-1,实际值也不是(2^32)-1,而是其他值,与具体的实现有关。

11- 顺序查找指定参数首次出现的位置 - find

find的返回值类型是size_t,不存在负数;

函数声明

size_t find (const string& str, size_t pos = 0) const; // string (1)

size_t find (const char* s, size_t pos = 0) const; // c-string (2)

size_t find (const char* s, size_t pos, size_t n) const; // buffer (3)
//查找字符串s的前n个字符

size_t find (char c, size_t pos = 0) const; // character (4)

例子

string str("012345hello world1234");
string s("world");

str.find(s, 1); // 结果为 12
str.find("2345"); // 结果为 2
str.find("1234", 5, 3); // 结果为 17
str.find('x', 1); // 结果为 npos
12- 倒序查找指定参数首次出现的位置 - rfind

除了是倒序查找之外,与find函数基本相同;

函数声明
这里的pos是父字符串中参与查找的最后一个有效字符的位置,也就是说,只查找到pos位置;
n是子串参与查找的前n个字符,之后的字符即使存在也不考虑;

size_t rfind (const string& str, size_t pos = npos) const; // string (1)
	
size_t rfind (const char* s, size_t pos = npos) const; // c-string (2)
	
size_t rfind (const char* s, size_t pos, size_t n) const; // buffer (3)
	
size_t rfind (char c, size_t pos = npos) const; //character (4)

例子

string str("01234512345");
string s("45");

str.rfind(s); //结果为 9
str.rfind("45", 6); //结果为 4
str.rfind("12345", 7, 3); // 结果为 6
str.rfind('x'); // 结果为 npos
13- 返回string对象的子串的拷贝 - substr

函数声明

string substr (size_t pos = 0, size_t len = npos) const;
//原字符串从pos位置开始的len个字符构造新的string对象并传值返回

例子

string str("hello world");
string s;

s = str.substr(6, 5); // 结果为 "world"
14- 了解 - 其他查找函数

find_first_of 查找与给定参数中的任意单个字符匹配的第一个字符的位置;
find_last_of 查找与给定参数中的任意单个字符匹配的最后一个字符的位置;
find_first_not_of 查找与给定参数中所有字符都不匹配的第一个字符的位置;
find_last_not_of 查找与给定参数中所有字符都不匹配的最后一个字符的位置;

15- 了解 - copy

把从pos位置开始的len个字符拷贝到指针s指向的空间中;
注意,copy不会在拷贝完成后的最后一个字符后加上’\0’,所以s指向的字符序列末尾不一定是’\0’,为了防止输出到屏幕上时因为找不到’\0’而越界,我们需要手动在最后一个字符后添加上一个’\0’字符。

函数声明

size_t copy (char* s, size_t len, size_t pos = 0) const;

例子

string str("hello world");

char arr[20];
str.copy(arr, 5, 6); // 结果为 "world烫烫烫...
arr[6] = '\0'; // arr为 "world"

非成员函数相关

1- operator+

非成员函数operaor+把两个对象相加,传值返回相加之后的对象,注意参与相加的两个对象的值始终不变;

函数定义

string operator+ (const string& lhs, const string& rhs); // string (1)
//两个常量string对象相加

string operator+ (const string& lhs, const char*   rhs); // c-string (2)
string operator+ (const char*   lhs, const string& rhs);
//一个常量string对象和一个常量字符串相加,前且二者后顺序可以颠倒

string operator+ (const string& lhs, char          rhs); // character (3)
string operator+ (char          lhs, const string& rhs);
//一个常量sring对象和一个字符相加,且二者前后顺序可以颠倒

例子

string str1("hello");
string str2(" world");

str1 + str2; // // 结果为 "hello world"
str2 + str1; // 结果为 " worldhello"
string str("hello");
const char* p = " world";

str + p; // 结果均为 "hello world"
p + str; // 结果均为 " worldhello"
string str("hello ");
char ch = 'x';
str + ch; // 结果均为 "hello x"
ch + str; // 结果均为 "xhello "
2- 输入输出运算符重载 - operator>>、operator<<

函数声明

istream& operator>> (istream& is, string& str);

ostream& operator<< (ostream& os, const string& str);
3- 字符串比较大小相关

函数声明

// (1)	等于
bool operator== (const string& lhs, const string& rhs);
bool operator== (const char*   lhs, const string& rhs);
bool operator== (const string& lhs, const char*   rhs);
// (2)	不等于
bool operator!= (const string& lhs, const string& rhs);
bool operator!= (const char*   lhs, const string& rhs);
bool operator!= (const string& lhs, const char*   rhs);
// (3)	小于
bool operator<  (const string& lhs, const string& rhs);
bool operator<  (const char*   lhs, const string& rhs);
bool operator<  (const string& lhs, const char*   rhs);
// (4)	小于等于
bool operator<= (const string& lhs, const string& rhs);
bool operator<= (const char*   lhs, const string& rhs);
bool operator<= (const string& lhs, const char*   rhs);
// (5)	大于
bool operator>  (const string& lhs, const string& rhs);
bool operator>  (const char*   lhs, const string& rhs);
bool operator>  (const string& lhs, const char*   rhs);
// (6)	大于等于
bool operator>= (const string& lhs, const string& rhs);
bool operator>= (const char*   lhs, const string& rhs);
bool operator>= (const string& lhs, const char*   rhs);

例子

string str1("abcd");
string str2("abc");

str1 == str2;
str1 > str2;
str1 >= str2;
str1 < str2;
str1 <= str2;
str1 != str2;
4- 读取流中的一行 - getline

getline 产生的背景分析

C语言中的scanf、C++中的流提取运算符>> 在读取字符时默认的规则是读取到空格或换行符就停止,这样就导致如果用户输入的一行待读取字符中包含空格字符时,将会导致只读取到空格字符之前就结束了,空格之后的字符被丢弃了,这是scanf和>>不能解决的。于是string有了getline函数用于从输入流中读取一行字符,遇到空格不会停下,直到遇到结束字符或换行符时才停止读取字符。

函数声明

istream& getline (istream& is, string& str, char delim);
//从is中提取字符追加到str中,直到遇到结束字符delim

istream& getline (istream& is, string& str);
从is中提取字符追加到str中,直到遇到结束字符'\n',即默认的结束字符是换行符

例子

// 控制台中待读取的一系列字符(包含空格字符,为了突出空格字符,本例空格字符用'_'字符替代)
// abcdefg_hijk_lmn\n
//-----------------------------------------
string str;

cin >> str; // 结果为 "abcedfg"
// 控制台中待读取的一系列字符(包含空格字符,为了突出空格字符,本例空格字符用'_'字符替代)
// abcdefg_hijk_lmn\n
//-----------------------------------------
string str;
getline(cin, str); //结果为 "abcdefg_hijk_lmn"

4.3 Linux(g++)和Windows(Visual Studio)平台下string底层结构的实现差异说明

32位平台 visual studio 2019 下的string的实现结构

32位系统下,指针占4个字节;
string包括一个联合体union,这个联合体有三个成员:

union _Bxty
{ // storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;

一个大小为16的字符数组_Buf,一个指针_ptr,另一个大小为16的字符数组;
该联合体的大小是16byte;

string还包括:
一个变量size记录字符串的长度,该变量大小为4;
一个变量capacity记录从对上开辟的总空间大小,该变量大小为4;
一个指针变量做其它的一些事,该变量大小为4;

所以,一个空的string对象的大小为16+4+4+4 = 28byte
C++打怪升级(九)- STL之string_第14张图片
这种实现思路在保存的字符串长度小于16时,不需要开辟新空间,内部的空间可以满足需要;
字符串长度大于16时,在堆上开辟空间存放字符串,原字符数组的内容也移动到新开辟的空间中,之后字符数组不存放内容;
为什么是字符数组大小是16?
太小时遇到稍微长一点的储存需要就需要开辟新空间,字符数组的优势没有很好的体现;
太大时则会导致空间的过度浪费,因为很多情况字符串的长度并不是很长,默认空间也够存放;

g++的string的实现结构

g++下的string通过写时拷贝实现,string内部只有一个指针变量,占4个字节;
这个指针指向了一块堆上的空间,该空间包含了
ptr指针,指向上储存字符串的堆空间;
size字符串长度;
capacity堆空间总大小;
refCount引用计数;

struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};

C++打怪升级(九)- STL之string_第15张图片

引用计数

在进行涉及到资源管理的拷贝操作时,拷贝对象不再申请资源,而是直接使用原对象的资源,为了防止对象销毁时导致资源被多次释放便引入引用计数变量,用于记录申请的唯一一份资源正在被几个对象使用,这样在对象销毁调用析构函数会先进行判断,只有当引用计数为1时才进行资源的释放。

引用计数的好处
减少了资源的消耗,提高了效率,因为新对象不需要再额外申请资源;
缺点是多个对象共同管理同一份资源,如果是只读资源没有影响;如果是可写资源,对象每次读取相同位置的内容时结果不受该对象控制,因为其他对象可能会修改内容;

写时拷贝 copy on write

在拷贝时会先使引用计数+1,拷贝对象和原对象共用同一块空间;
只有当向拷贝对象内写入内容时,拷贝对象再开辟新的堆空间,内部指针随之由指向原对象空间变为指向自己开辟的空间,先把原对象指向空间的内容复制到新空间中,然后在拷贝对象指向的空间上进行写入操作,引用计数-1;

通过引用或迭代器得到原对象某一位置的内容时,会导致写时拷贝失效。因为编译器不知道你对该位置进行写操作还是读操作。
写时拷贝技术存在很多问题,为了安全和稳定,如今多数的STL去掉了写时拷贝技术。


4.4 string类的模拟实现

string类的底层实现不止一种,本文也仅是其中一种实现方式。

1- 私有变量的定义

  • 定义字符指针变量_str,其指向堆上开辟的连续空间;
  • 定义size_t类型的变量_size,表示当前string类对象的字符个数;
  • 定义size_t类型的变量_capacity,表示有效空间的大小,不包含末尾的'\0'字符,故开辟的总空间大小为_capacity + 1;

为了防止自己定义的string类与标准库中的string冲突,可以修改类名如改为my_string,也可以把自定义的string类放在命名空间weihe中;
注意,在命名空间外使用命名空间的变量或函数等需要使用域作用限定符指定命名空间域;

namespace weihe {
    class string {
    public:
        
    private:
        char* _str;		//字符指针指向堆上的连续空间
        size_t _size;	//string对象的有效字符的个数
        size_t _capacity;//string对象的有效空间大小,不包含末尾的'\0'字符
    };
}

2- 实现构造函数

功能:初始化string对象;

常量字符串作为参数构造 - 默认构造

注意默认构造提供的缺省值"",实质上一个只包含一个'\0'字符的空串;
"\0"作为缺省值,"\0"包含了两个'\0'字符,是有效字符只有一个的字符串,不是一个空串;
nullptr作为缺省值,在函数体内我们需要使用C语言库函数中的strlen函数计算字符串的大小,该函数不会对传入的参数进行判断是否合法即是否是空指针、野指针等,而是直接访问指针指向空间元素,但缺省值nullptr赋值给形参时在strlen这一步程序将会报错;
'\0'作为缺省值,'\0'是一个字符,而形参类型是const cahr*,实际上是把字符'\0'的ASCII十进制码值0赋值给了指针变量,也就是说指针形参指向了0地址处的空间,而该空间并未分配给本程序使用,之后通过该指针形参访问指向的空间时即非法访问,是野指针的问题;

string(const char* str = "") {
    // 这里的形参是缺省参数,默认是空字符串"",而不是"\0"、nullptr
    _size = strlen(str);
    _capacity = _size;
    _str = new char[_capacity + 1];// 总空间大小包含末尾的'\0'字符,开辟的空间需要 + 1
    strcpy(_str, str);
}

拷贝构造 - string类对象作为参数构造

注意参数只有一个且是string类类型的引用

传统写法

  1. 直接开辟**new**需要的空间
  2. 把待拷贝对象的值依次复制到自己的空间中
// 传统写法
string(const string& str) {
    //
    _str = new char[str._capacity + 1];
    _size = str._size;
    _capacity = str._capacity;

    strcpy(_str, str._str);
}

现代写法

  1. 把自己的指针由随机值指向nullptr
  2. 创建一个新的string对象tmp,并用待拷贝对象str内的指针指向的内容(是char*类型)进行初始化
  3. 此时tmp对象中的内容(重点是指针指向的内容)就是我们需要的,所以直接交换tmp和自己内部指针的指向、大小、容量即可
  4. tmp作为局部对象,拷贝构造函数返回前会销毁tmp,调用其析构函数,析构nullptr时底层的free什么也不做,不会导致程序问题

注意,现代写法的出现不是为了提高程序运行效率,而是为了书写方便简洁,很省事;

C++打怪升级(九)- STL之string_第16张图片

// 现代写法 - 躺着享福法
string(const string& str) 
    : _str(nullptr)
    , _size(0)
    , _capacity(0) {

    string tmp(str._str);
    swap(tmp);
    /*swap(_str, tmp._str);
    swap(_size, tmp._size);
    swap(_capacity, tmp._capacity);*/
}
n个字符c构造
string(size_t n, char ch) {
    _size = _capacity = n;
    _str = new char[_capacity + 1];
    for (int i = 0; i < _size; ++i) {
        _str[i] = ch;
    }
    _str[_size] = '\0';
}
迭代器范围作为参数构造

模板类型InputIterator一般是已定义的存在于类内的各种迭代器iterator类型;
传入的类型是整型时会发生强制类型转换,引起截断(即由int->char,int的32个bit位只保留低8位);
类型不匹配时,解引用后的赋值操作可能会出现二者类型不同且无法强转的错误情况;

//
template <class InputIterator>
string(InputIterator first, InputIterator last) {
    _capacity = _size = last - first;
    _str = new char[_capacity + 1];
    int i = 0;
    while (first != last) {
        _str[i] = *first;
        i++;
        first++;
    }
    
    _str[_size] = '\0';
}

3- 实现赋值运算符重载函数

避免自己给自己赋值,进行特殊判断函数直接返回
传统写法

  1. 开辟一段新空间使额外指针指向其起始空间;
  2. 把待赋值对象str的内容依次复制到新空间中;
  3. 释放delete[]旧空间,更新字符指针,使其指向新空间起始地址,同时更新大小_size和容量_Capacity

注意,现代写法的出现不是为了提高程序运行效率,而是为了书写方便简洁,很省事;

// 传统写法
string& operator= (const string& str) {
    if (*this != str) {
        //
        char* tmp = new char[str._capacity + 1];
        strcpy(tmp, str._str);
        //
        delete[] _str;
        _size = _capacity = 0;
        //
        _str = tmp;
        _size = str._size;
        _capacity = str._capacity;
    }

    return *this;
}

现代写法

  1. 创建新的string对象**tmp**,并让待赋值**string**对象的C形式字符串作为参数进行初始化;
  2. **tmp**的内容是我们需要的,分别交换自己和tmp内成员变量的值:指针成员**_str**、大小**_size**、容量**_capacity**;
  3. tmp作为局部对象,函数返回前会销毁,调用析构函数时由于指针**_str**指向的空间是有效的,不会出错;

C++打怪升级(九)- STL之string_第17张图片

// 现代写法
string& operator= (const string& str) {
    if (*this != str) {
        string tmp(str._str);
        swap(tmp);
    }

    return *this;
}

4- 实现析构函数

完成资源的清理和释放

delete[]释放的对象是空指针时,在函数内部多次跳转后最终由free处理;
对于free函数,会对参数进行判断,是空指针nullptr时不会进行释放操作,直接返回;

~string() {
    delete[] _str;
    _str = nullptr;
    _size = _capacity = 0;
}

5- 实现返回C形式字符串的函数

const char* c_str() const {
    return _str;
}

6- 实现返回string对象大小和容量的函数

size_t size() const {
    return _size;
}
size_t capacity() const {
    return _capacity;
}

7- 实现为string开辟空间的函数 - reserve

reserve函数一般用于扩容处理,对于缩容请求比如传入的参数小于等于string对象原有效空间的大小则不进行处理直接,函数直接返回;

void reserve(size_t n) {
    if (n <= _capacity) return;
    char* tmp = new char[n + 1];
    strcpy(tmp, _str);
    delete[] _str;
    _str = tmp;
    _capacity = n;
}

8- 实现设置strnig对象有效大小的函数 - resize

注意,末尾的'\0'需要我们手动设置;
C++打怪升级(九)- STL之string_第18张图片

void resize(size_t n, char c = '\0') {
    if (n <= _size) {
        _size = n;
        _str[_size] = '\0';
    }
    else if (n <= _capacity) {
        while (_size < n) {
            _str[_size++] = c;
        }
        _str[_size] = '\0';
    }
    else {
        reserve(n);
        _capacity = n;
        while (_size < n) {
            _str[_size++] = c;
        }
        _str[_size] = '\0';
    }
}

9- 实现堆string对象空间的清理函数 - clear

void clear() {
    _size = 0;
    _str[0] = '\0';
}

10- 实现判断字符串有效空间是否为空的函数 - empty

bool empty() const {
    return _size == 0;
}

11- 与一个string对象交换内容 - swap

C++algorithm头文件中有一个swap函数模板,达到交换的目的需要创建一个额外的对象并完成3次赋值,对于内置类型和大小比较小的类来说,3次赋值影响并不大;
但是对于对于大小比较大或涉及资源需要管理导致拷贝时需要深拷贝时的类来说,3次赋值产生的开销不可忽略,会影响程序的效率;

algorithm头文件中的swap函数模板:

// std::swap
template <class T> void swap ( T& a, T& b )
{
  T c(a); a=b; b=c;
}

string类内部需要管理从堆上申请的空间,大小可能会较大,所以在string类内部需要实现简洁的交换函数;
对于申请的空间,只需交换两个对象内部的指针指向,就可以完成内容的交换,这样就优化掉了3次深拷贝,只进行内置类型的交换;

void swap(string& str) {
    std::swap(_str, str._str);
    std::swap(_size, str._size);
    std::swap(_capacity, str._capacity);
}

12- 实现[]运算符重载

包括非const和const版本

operator返回pos位置的引用,同时会判断传入的pos是否符合字符串string的有效范围,如果超过范围就会报断言assert错误。
由于[]可读可写,所以这里需要提供非const和const两个版本:
因为可写,所以一定需要非const版本的函数;而又存在const修饰的strin对象做参数的情况,此时非const函数形参接受const string对象将会导致权限的放大只读->可读可写,所以还需要const版本的函数。

char& operator[](size_t pos) {
    assert(pos < _size);
    return _str[pos];
}

const char& operator[](size_t pos) const {
    assert(pos < _size);
    return _str[pos];
}

13- 实现尾插push_back函数

增加元素往往伴随着扩容操作的出现,所以需要提前判断容量大小;
每次只新增1个字符元素,所以_size每次加1,遇到扩容情况时,基本上选择2倍扩容,当然1.5倍也行;
需要稍微注意的是别忘记手动加上末尾的'\0'

void push_back(char c){
    if (_size == _capacity) {
        int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
        reserve(newCapacity);
        _capacity = newCapacity;
    }
    _str[_size] = c;
    _size++;
    _str[_size] = '\0';
}

14- 实现追加append函数

append追加的是长度为len的字符串,原string对象的有效字符大小为_size,故追加后的实际有效大小是len+_size;
需要比较len+_size与原string对象有效空间大小_capacity的大小:
如果len+_size <= _capacity,不进行扩容,直接进行后续操作;
否则进行扩容:
扩容有两种选择:1.直接开辟len+_size大小的有效空间;
2.开辟2 * _capacity大小的有效空间;
如果len+_size <= _capacity,进行2倍扩容;
否则直接开辟len+_size大小的有效空间
//实际开辟的空间将会多1个,用来存放末尾的'\0';
之后依次把待追加字符串的所有元素依次追加到原string后,最后在_size + len处补上'\0'作为结尾。

string& append(const char* str) {
    int len = strlen(str);
    if (len + _size > _capacity) {
        int newCapacity;
        if (len + _size <= 2 * _capacity) {
            reserve(2 * _capacity);
            newCapacity = 2 * _capacity;
        }
        else {
            reserve(len + _size);
            newCapacity = len + _size;
        }
        _capacity = newCapacity;
    }

    for (int i = _size, j = 0; j < len; ++i, ++j) {
        _str[i] = str[j];
    }
    _str[_size + len] = '\0';
    _size += len;

    return *this;
}

15- 实现+=运算符重载

+=一个字符

尾插一个字符,复用函数push_back()

string& operator+=(char c) {
    push_back(c);
    return *this;
}
+=一个常量字符串

追加一个常量字符串,复用函数append()

string& operator+=(const char* str) {
    append(str);
    return *this;
}
+=一个string对象

16- 为string对象分配一个新值 - assign

string& assign(const string& str)

string& assign(const string& str) {
    // 不能自己给自己分配
    if (*this != str) {
        // 动态申请与str容量大小相等的空间,暂时由指针tmp指向
        // 开辟的空间包括有效空间_capacity和结尾的'\0'字符所需的空间,是_capacity + 1
        char* tmp = new char[str._capacity + 1];
        // 把str的全部字符依次复制到tmp指向的空间中
        strcpy(tmp, str._str);
    	// 释放_str指向的旧空间,再让_str指向新空间的起始地址
        delete[] _str;
        _str = tmp;
        _size = str._size;
        _capacity = str._capacity;
    }
    return *this;
}

string& assign(const string& str, size_t subpos, size_t sublen)


string& assign(const string& str, size_t subpos, size_t sublen) {
    // 判断下标是否在有效范围内
    assert(subpos < str._size);
    // 不能自己给自己分配
    if (*this != str) {
        // 把sublen作为有效字符的个数,那么sublen不会超过从subpos开始到结尾的字符个数,
        // 如果sublen超过了则改为subpos开始到结尾的字符个数
        if (sublen > str._size - subpos) sublen = str._size - subpos;
        // 
        char* tmp = new char[sublen + 1];
        strncpy(tmp, str._str + subpos, sublen);
        tmp[sublen] = '\0';
        //
        delete[] _str;
        _str = tmp;
        _capacity = _size = sublen;
    }
    return *this;
}

17- 插入一个值 - insert

在pos右侧插入新值

对于涉及到边界的判断需要谨慎;
对于本例的移动字符的第一种方法,while条件判断pos如果不强转为int那么在特殊情况下如pos==0时,由于possize_t类型的,所以当end减到-1时会整型提升为极大值(2^32) -1,从而陷入死循环;

string& insert(size_t pos, size_t len, char c)

string& insert(size_t pos, size_t len, char c) {
    // 1. 判断pos和subpos是否在有效范围
    assert(pos <= _size);
    if (_size == _capacity) {
        int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
        reserve(newCapacity);
        _capacity = newCapacity;
    }
	// 2. 向后移动pos及其之后的字符,每个字符均向后移动len个长度
    // 法1 - 强制类型转换
    /*int end = _size;
    while (end >= (int)pos) {
        _str[end + len] = _str[end];
        end--;
    }*/
    // 法2 - 错位
    int end = _size + len;
    while (end > pos + len - 1) {
        _str[end] = _str[end - len];
        end--;
    }
	// 3. 为pos及其之后的len个位置依次赋值
    for (size_t i = pos; i < pos + len; ++i) {
        _str[i] = c;
    }
    _size += len;

    return *this;
}

string& insert(size_t pos, const string& str, size_t subpos = 0, size_t sublen = npos)

string& insert(size_t pos, const string& str, size_t subpos = 0, size_t sublen = npos) {
    // 1. 判断pos和subpos是否在有效范围
    assert(pos <= _size && subpos <= str._size);
    // 2. 判断是否扩容
    if (_size == _capacity) {
        int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
        reserve(newCapacity);
        _capacity = newCapacity;
    }
    // 3. 后移
    // 注意sublen不能超过从subpos开始的有效字符个数str._size - subpos
    if (sublen > str._size - subpos) 
        sublen = str._size - subpos;
    // 法1
    /*int end = _size;
    while (end >= (int)pos) {
        _str[end + sublen] = _str[end];
        end--;
    }*/
    // 法2
    int end = _size + sublen;
    while (end > pos + sublen - 1) {
        _str[end] = _str[end - sublen];
        end--;
    }

    // 4. 赋值
    for (size_t i = pos, j = subpos; i < pos + sublen; ++i, ++j) {
        _str[i] = str[j];
    }
    _size += sublen;

    return *this;
}

18- 用新值替换string对象的一部分内容 - replace

string& replace(size_t pos, size_t len, const string& str, size_t subpos = 0, size_t sublen = npos) {
    // 1. 判断下标是否合法
    assert(pos <= _size && subpos <= str._size);
    if (len > _size - pos)
        len = _size - pos;
    if (sublen > str._size - subpos)
        sublen = str._size - subpos;
    // 2. 判断是否扩容
    if (_size - len + sublen > _capacity) {
        reserve(_size - len + sublen);
        _capacity = _size - len + sublen;
    }
    // 3. 移动pos位置开始的len个字符之后的所有元素 ,别忘记最后的'\0'
    size_t blankLen = max(len, sublen);
    memmove(_str + pos + blankLen, _str + pos + len, _size - pos - len + 1);
    // 4. 拷贝
    memmove(_str + pos, str._str + subpos, sublen);

    _size = _size - len + sublen;

    return *this;
}

对于第3步移动元素来说,由于替换位置的元素个数与待替换元素个数关系未知,所以存在3种情况:

  1. len
  2. len>sublen 需要整体向前移动
  3. len==sublen 不移动
    并且移动前后的位置可能存在部分重叠,所以不能使用strcpymemcpy,应该使用memmove;

19- 删除部分元素 - erase

string& erase(size_t pos = 0, size_t len = npos)

思路1

string& erase(size_t pos = 0, size_t len = npos) {
    // 1. 判断p下标是否越界
    assert(pos < _size);
    // 2. pos及其之后的所有元素都删除,此时不需要移动,
    //	  只需在pos处加上结尾字符'\0'即可
    if (len == npos || pos + len > _size) {
        _str[pos] = '\0';
    }
    // 3. 删除的是中间部分的元素,后面未删除的元素需要向前移动使整体连续
    strcpy(_str + pos, _str + pos + len);

    _size -= len;

    return *this;
}

思路2

// 删除pos位置开始的len个的元素 - erase
string& erase(size_t pos = 0, size_t len = npos) {
    // 1. 判断p下标是否越界
    assert(pos <= _size);
    // 2. 处理len,把len作为待删除的有效字符元素个数
    if (len > _size - pos) len = _size - pos;
    // 3. 移动未删除元素 - 包括有效字符和'\0'字符
    strcpy(_str + pos, _str + pos + len);
    //memmove(_str + pos, _str + pos + len, _size - pos - len + 1);

    _size -= len;

    return *this;
}

string& erase(iterator p)

string& erase(iterator p) {
    while (p != _str + _size) {
        *p = *(p + 1);
        p++;
    }
    _size--;
    return *this;
}

20- 通过string对象构造子串 - substr

// 得到string对象的pos位置开始,长度为len的字符序列构造的子串
string substr(size_t pos = 0, size_t len = npos) const {
    //
    assert(pos < _size);
    //
    if (len > _size - pos) len = _size - pos;
    //
    string ret;
    ret.reserve(len);
    ret.resize(len);
    for (size_t i = pos, j = 0; i < pos + len; ++i, ++j) {
        ret._str[j] = _str[i];
    }
    ret._str[len] = '\0';
    return ret;
}

21- 顺序查找值 - find

涉及到字符串查找算法

  • 暴力匹配 strstr
  • KMP算法
  • BF算法
  • BM算法

注意strstr函数底层是暴力匹配,返回的是匹配成功位置的指针,失败返回空指针

size_t find(const string& str, size_t pos = 0) const {
    assert(pos < _size);
    char* p = strstr(_str + pos, str._str);

    return p == nullptr ? npos : p - _str;
}

size_t find(char c, size_t pos = 0) const;

size_t find(char c, size_t pos = 0) const {
    assert(pos < _size);
    while (pos < _size) {
        if (_str[pos] == c) {
            return pos;
        }
        pos++;
    }
    return npos;
}

22- 逆序查找值 - rfind

size_t rfind(const string& str, size_t pos = npos) const;
这里的pos是父字符串中参与查找的最后一个有效字符的位置,也就是说,只查找到pos位置;

size_t rfind(const string& str, size_t pos = npos) const {
    // 调整pos范围
    if (pos >= _size) pos = _size - 1;
    // pos作为理论查找的最后一个字符位置,pos之后的全部字符需要省略,
    /* 根据C形式字符串特性,以'\0'结尾,所以我们需要人为替换pos的
        下一个位置的字符为'\0',这样对于strstr函数字符串长度就提前结束
        了,当然保证最后string对象内容不变,需要提前记录替换字符然后恢复; 
    */
    char flag = _str[pos + 1];
    _str[pos + 1] = '\0';
    // 查找
    // 法1
    // char* p = nullptr;
    // while ((int)pos >= 0) {
    //     p = strstr(_str + pos, str._str);
    //     if (p != nullptr) {
    //         _str[pos + 1] = flag;
    //         return p - _str;
    //     }
    //     pos--;
    // }
    // 法2
    int end = pos + 1;
    char* p = nullptr;
    while (end > 0) {
        p = strstr(_str + end - 1, str._str);
        if (p != nullptr) {
            _str[pos + 1] = flag;
            return p - _str;
        }
        end--;
    }

    _str[pos + 1] = flag;

    return npos;
}

size_t rfind(char c, size_t pos = npos) const;
n是子串参与查找的前n个字符,之后的字符即使存在也不考虑;

size_t rfind(char c, size_t pos = npos) const {
    //
    if (pos >= _size) pos = _size - 1;
    //
    for (size_t i = pos + 1; i > 0; --i) {
        if (_str[i - 1] == c) return i - 1;
    }

    return npos;
}

23- 实现迭代器begin函数

包括非const迭代器和const迭代器

typedef char* iterator
typedef const char* const_iterator

iterator begin() {
    return _str;
}
const_iterator begin() const {
    return _str;
}

24- 实现迭代器end函数

包括非const迭代器和const迭代器

typedef char* iterator
typedef const char* const_iterator

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

25- 非成员函数比较大小运算符重载

等于

作为string类的友元函数声明

friend bool operator==(const string& ls, const string& rs);

bool operator==(const string& ls, const string& rs) {
    return strcmp(ls._str, rs._str) == 0;
}
不等于

作为string类的友元函数声明

friend bool operator!=(const string& ls, const string& rs);

bool operator!=(const string& ls, const string& rs) {
    return !(ls == rs);
}
大于

作为string类的友元函数声明

friend bool operator>(const string& ls, const string& rs);

bool operator>(const string& ls, const string& rs) {
    return strcmp(ls._str, rs._str) > 0;
}
大于等于

作为string类的友元函数声明

friend bool operator>=(const string& ls, const string& rs);

bool operator>=(const string& ls, const string& rs) {
    return ls == rs || ls > rs;
}
小于

作为string类的友元函数声明

friend bool operator<=(const string& ls, const string& rs);

bool operator<(const string& ls, const string& rs) {
    return strcmp(ls._str, rs._str) < 0;
}
小于等于

作为string类的友元函数声明

friend bool operator<=(const string& ls, const string& rs);

bool operator<=(const string& ls, const string& rs) {
    return ls == rs || ls < rs;
}

26- 非成员函数 流插入运算符重载<<

ostream& operator<<(ostream& out, const string& str) {
    out << str._str;
    return out;
}

27- 非成员函数 流提取运算符重载>>

C语言scanf和流提取运算符>>都以字符空格' '或换行符\n作为读取内容的默认间隔,所以使用二者无法从流中读取到字符空格' '或换行符\n;
如果想要读取到字符空格' '或换行符\n,需要使用getline函数;

istream& operator>>(istream& in, string& str) {
    str.clear();
    char c = in.get();
    // 首次从输入流读取字符,如果是空格或换行字符,就丢弃继续读取下一个字符;
    while (c == ' ' || c == '\n') {
        c = in.get();
    }
    // 法1
    // 从首个非空白和换行字符开始,到下一次出现空白或换行字符为止的所有内容是作为一个整体被提取;
    //while (c != ' ' && c != '\n') {
    //	str += c;
    //	c = in.get();
    //}

    // 法2
    /* 为了避免每读取一个字符就尾插导致频繁扩容,但是我们又
    	不知道从流中读取多少字符,使用reserve预先开辟空间也
        不好确定具体开多少;
       一种折中的思路是 创建一个大小适当的buffer缓冲数组,减少
        可能得扩容次数,同时也避免过多浪费空间;每次从流中提取都顺
        序存放在缓冲数组buffer中,当缓冲数组满了就一次性把整个
        缓冲数组的内容尾插到string对象后,最后需要额外处理缓冲数组
        不满但提取结束的情况。*/
    char buffer[128];
    buffer[127] = '\0';
    size_t size = 0;
    
    while (c != ' ' && c != '\n') {
        if (size == 127) {
            str += buffer;
            size = 0;
        }
        buffer[size++] = c;
        c = in.get();
    }
    //
    if (size > 0) {
        buffer[size] = '\0';
        str += buffer;
    }
    return in;
}

28- 非成员函数 读取一行 - getline

istream& getline(istream& in, string& str, char delim = '\n') {
    // 读取一行,遇到终止字符delim或换行符'\n'时结束
    char c = in.get();
    while (c != delim && c != '\n') {
        str += c;
        c = in.get();
    }

    return in;
}

4.5 深拷贝与浅拷贝

浅拷贝(值拷贝)

浅拷贝又称为值拷贝,我们接触最多的拷贝情况就是浅拷贝。

浅拷贝是一个对象的内容原封不动的拷贝到另一个对象中的,所以浅拷贝完成后原对象与拷贝对象的内容完全相同,但是如果原对象包含申请的资源时就会出现问题:
原对象与拷贝对象将会同时包含同一份申请的资源,比如各自的指针变量指向相同的堆上申请的一段连续空间。对于类来说,在程序结束前拷贝对象和原对象先后销毁并释放申请的资源,而资源实际上只申请了1份,但将会释放2次,在第二次释放资源时程序出错,释放了不属于该程序的资源空间。

C++打怪升级(九)- STL之string_第19张图片

深拷贝

对于浅拷贝遇到的问题,需要深拷贝救场:
对于非资源管理部分是值拷贝,而对于资源管理部分则是拷贝对象去申请新的资源,然后把原对象的资源上的内容拷贝到拷贝对象新申请的资源上;
深拷贝的实现一般涉及到类中的拷贝构造函数赋值运算符重载函数析构函数;

拷贝构造函数

拷贝构造函数的实现-传统写法和现代写法

赋值运算符重载函数

赋值运算符重载函数的实现-传统写法和现代写法

析构函数

析构函数的实现-传统写法和现代写法
C++打怪升级(九)- STL之string_第20张图片


后记

本节主要介绍了C++中string类的常见接口函数,然后简单实现了一个自己的string类,结构简单但是基本功能具备,比如默认构造、拷贝构造、复制运算符重载、析构、尾插push_back、+=运算符重载、流插入流提取运算符重载、迭代器等。


E N D END END

你可能感兴趣的:(C++之打怪升级,c++,开发语言,学习)