C++初阶:string类string类 | 浅拷贝和深拷贝(传统写法和现代写法) | string类的模拟实现

Posted 跳动的bit

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++初阶:string类string类 | 浅拷贝和深拷贝(传统写法和现代写法) | string类的模拟实现相关的知识,希望对你有一定的参考价值。

文章目录

【写在前面】

到这里我们就要学会能自己能看文档了,因为就这个 string 类来说就有一百多个接口函数,那肯定记不住呀,我们平时用的大概就二十个,一般我们都是学习它比较常用的,其它的大概熟悉它们有哪些功能,真要用到时再去查文档。可以看到其实 string 就是一个管理字符数组的顺序表,因为字符数组的使用广泛,C++ 就专门给了一个 string 类,由于编码原因,它写的是一个模板。针对 string,一般情况它有三个成员 —— char* _str、size_t _size、size_t _capacity,我们在下面模拟实现 string 时就会看到,其次在学习深浅拷贝时我们只用 char* _str。

C++ 的文档库严格来说有两个:注意前者并不是 C++ 的官网文档,后者才是。这里我们对于官方文档就作为参考,因为它比较乱,平时就用 cplusplus 也够了。

  1. cplusplus
  2. cppreference

此外对于类和对象还有一个深浅拷贝的知识我们将会再这里补充。

一、为什么学习string类

💦 C语言中的字符串

C 语言中,字符串是以 ‘\\0’ 结尾的一些字符的集合,为了操作方便,C 标准库中提供了一些 str 系列的库函数,
但是这些库函数与字符串是分离开的,不太符合 OOP 的思想,而且底层空间需要用户自己管理,稍不留神可
能还会越界访问。

💦 两个面试题(暂不讲解)

字符串转整型数字

字符串相加

在 OJ 中,有关字符串的题目基本以 string 类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用 string 类,很少有人去使用 C 库中的字符串操作函数。

二、标准库中的string类

💦 string类(了解)

string类的文档介绍

  1. 字符串是表示字符序列的类。
  2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
  3. string 类是使用 char(即作为它的字符类型,使用它的默认 char_traits 和分配器类型 (关于模板的更多信息,请参阅 basic_string)。
  4. string 类是 basic_string 模板类的一个实例,它使用 char 来实例化 basic_string 模板类,并用 char_traits 和 allocator 作为 basic_string 的默认参数(根于更多的模板信息请参考 basic_string)。
  5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如 UTF-8) 的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

总结:

  1. string 是表示字符串的字符串类。
  2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作。
  3. string 在底层实际是:basic_string 模板类的别名,typedef basic_string<char, char_traits, allocator> string;。
  4. 不能操作多字节或者变长字符的序列。
  5. 在使用 string 类时,必须包含 string 头文件以及 using namespace std;。

#include<string>
#include<iostream>
using namespace std;
int main()

	cout << sizeof(char) << endl;
	cout << sizeof(wchar_t) << endl;

	char arr1[] = "hello bit";
	char arr2[] = "比特";
	return 0;

📝说明

对于字符,C++ 里提出了两个类型,现阶段接触的是第一个。

可以看到结果一个是 1 和 2,这个东西与编码有关系。

编码 ❓

计算机中存储只有二进制 0 和 1,如何去表示文字呢 ???

这时就建立出对应的编码表:

  1. ASCII > 支持英文
    1Byte = 8Bit,0 - 255 ASCII 编码表就是对 256 个值建立一个对应的表示值

  2. GBK > 我国制定的,常用于 windows 下
    早期 windows 为了打入中国市场,就使用这个编码

  3. utf-8 > 通用,兼容 ASCII,常用于 Linux 下
    世界各国都开始使用计算机了,而早期计算机中只能表示英文,不能表示其它国家的文字,所以需要建立自己的编码表,但各自搞各自的就非常乱,所以就出现了 utf-8,早期是 UniCode

所以据不完全统计汉字大约有十万个左右,所以我们这里用 2 个字节,大概表示 6 万种状态

💦 string类的常用接口说明(只讲最常用的)

1、string类对象的常见构造
(constructor) 函数名称功能说明
string() —— 重点构造空的 string 类对象,即空字符串
string(const char* s) —— 重点用 C-string 来构造 string 类对象
string(size_t n, char c)string 类对象中包含 n 个字符 c
string(const string& s) —— 重点拷贝构造函数
#include<string>
#include<iostream>
using namespace std;   
//大概原理
template<class T>
class basic_string

	T* _arr;
	int _size;
	int _capacity;
;
//typedef basic_string<char> string;
int main()

	string s1;
	string s2("hello");  
	string s3("比特");
	string s4(10, 'a');
	string s5(s2);
	//对于string它重载了流提取和流插入等,这里就可以直接用
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	cout << s4 << endl;
	cout << s5 << endl;
	
	//赋值 ———— 深拷贝
	s1 = s5;
	cout << s1 << endl;

📝说明

basic_string ❗

string s1; || string s2(“hello”); ❗

通过调试我们发现 s1 虽然是无参的,但并不是啥都没有,且会在第一个位置放一个 \\0。

2、string类对象的容量操作
函数名称功能说明
size —— 重点返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty —— 重点检测字符串释放为空串,是返回 true,否则返回 false
clear —— 重点清空有效字符
reserve —— 重点为字符串预留空间
resize —— 重点将有效字符的个数改成 n 个,多出的空间用字符 c 填充

#include<string>
#include<iostream>
using namespace std;
void test_string1()

	//1、size | length
	string s1("hello world");
	cout << s1.size() << endl;
	cout << s1.length() << endl;
	cout << "----------cut----------" << endl;
	//2、max_size
	string s2;
	cout << s1.max_size() << endl;
	cout << s2.max_size() << endl;	
	cout << "----------cut----------" << endl;
	//3、capacity
	cout << s1.capacity() << endl;
	cout << "----------cut----------" << endl;
	//4、resize
	string s3("hello world");
	cout << s3.size() << endl;
	cout << s3 << endl;
	//s3.resize(20);//n大于当前的字符串的长度且没有指定c,所以hello world\\0\\0\\0\\0...   
	//s3.resize(5);//n小于当前的字符串的长度, 它会删除掉从n开始的这些字符
	s3.resize(20, 'x');//n大于当前的字符串的长度且指定c,所以hello worldxxxx...
	cout << s3.size() << endl;
	cout << s3 << endl;
	cout << "----------cut----------" << endl;
	//5、reserve
	string s4("hello world");
	s4.reserve(20);
	cout << s4 << endl;
	cout << s4.size() << endl;
	cout << s4.capacity() << endl;
	s4.reserve(10);
	cout << s4 << endl;
	cout << s4.size() << endl;
	cout << s4.capacity() << endl;
	cout << "----------cut----------" << endl;
	//6、clear | empty
	string s5("hello world");
	cout << s5 << endl;
	cout << s5.empty() << endl;;
	s5.clear();
	cout << s5 << endl;
	cout << s5.empty() << endl;
	cout << "----------cut----------" << endl;
	//7、shrink_to_fit 暂且不演示
   
void test_string2()

	string s;
	size_t sz = s.capacity();
	cout << "making s grow:\\n" << sz << endl;
	for(int i = 0; i < 500; ++i)
	
		s.push_back('c');
		if(sz != s.capacity())
		
			sz = s.capacity();
			cout << "capacity changed:" << sz << '\\n';
		
	
	cout << "----------cut----------" << endl;

int main()

	test_string1();
	test_string2();

	return 0;

📝说明

size || length ❗

两者的功能相同,一般我们比较常用的是 size。

对于 string 是在 STL 这个规范前被设计出来的,因此在 Containers 下并没有 string:

早期说要算字符串字符的长度,所以早期提供的接口就叫 length,至于后面要加 size 的原因是后面增加了 map、set 这样的树,所以用 length 去表示它的数据个数就不合适了。

max_size ❗

它也是早期设计比较早的,属于一个没用的接口 —— 从操作系统中获取最大的长度。本意是想告诉你这个字符串最大你能定义多长, 这个接口在设计的时候其实不好实现,它没有办法标准的去定义这个接口,因为它有很多不确定的因素。所以它这个地方是直接给你 232 ,也就是 4G。所以没什么价值,以后再遇到就直接跳过了。

capacity ❗

对于 string 对象而言,如果 capacity 是 15,意味着它有 16 个字节的空间,因为有一个位置是 \\0 的。对于 capacity 它会随着字符串的大小而增容,这里默认是 15。

resize ❗

resize 的前者版本可以让字符串的长度变成 n;后者可以让字符串的 n 个长度变成 c。

如果 n 是小于当前的字符串的长度, 它就会缩减到 n 个字符,删除掉从 n 开始的这些字符。

如果 n 是大于当前的字符串的长度,通过在末尾插入尽可能多的内容来扩展当前内容,倘若指定了 c,则新元素被初始化为 c 的副本,否则它们就是值初始化字符 (空字符 \\0)。

对于 s3.resize(20); s3[19] 是有效位置,因为对于 operator[] 里它会 assert(pos < _size)。

reserve ❗

请求 capacity。

注意这里不是你要多少 capacity 它就给多少 capacity,它在增容时还要对照不同编译器下自己的增容规则,最终容量不一定等于字符串长度,它可能是相等的,也可能是更大的。

如果 n 大于当前字符串的 capacity,那么它会去扩容。

如果 n 小于当前字符串的 capacity,这里跟不同的平台有关系。文档里是这样说的:其他情况下 (小于或等于),有可能它会缩容(开一块新空间,将数据拷贝,释放原有空间),也有可能不对当前空间进行影响,只是变换 capacity 的值。已证,VS 和 Linux 下不会缩容,STL 的标准也是这样规定的,这是实现 STL 的人决定的。

对于 s4.reserve(20); s4[19] 是无效位置,因为对于 operator[] 里它会 assert(pos < _size)。

string 增容是怎么增的 ❓

test_string2():

可以看到 Windows VS 下初始容量是 15 ,除了第一次,其余的大概是以 1.5 倍增容。

g++ ???

可以看到在不同的编译器下增容规则也不同,Linux g++ 下初始容量是 0,其余是以 2 倍增容的。

clear | empty ❗

清理字符串 | 判空字符串


resize 和 reserve 有什么价值 ❓

对于 resize,既要开空间,还要对这些空间初始化,就可以用 resize —— s.resize(20, ‘x’);

对于 reserve,明确知道需要多大空间的情况,可以提前把空间开好,以减少增容所带来的代价 —— s.reserve(500);

3、string类对象的访问及遍历操作
函数名称功能说明
operator[] —— 重点返回 pos 位置的字符,const string 类对象调用
begin + endbegin 获取一个字符的迭代器,end 获取最后一个字符下一个位置的迭代器
rbegin + rendbegin 获取一个字符的迭代器,end 获取最后一个字符下一个位置的迭代器
范围 forC++11 支持更简洁的范围 for 的新遍历方式

#include<assert.h>
#include<string>
#include<iostream>
using namespace std;   
//大概原理
template<class T>
class basic_string

public:
	char& operator[](size_t pos)
	
		assert(pos < _size);//检查越界:s2[20] -> err
		return _arr[pos];	
	
private:
	T* _arr;
	int _size;
	int _capacity;
;
//typedef basic_string<char> string;
int main()

	//逐一遍历字符串
		//1、operator[]
	string s1("hello world");
	for(size_t i = 0; i < s1.size(); ++i)
	
		s1[i] += 1;//写
	
	for(size_t i = 0; i < s1.size(); ++i)
	
		cout << s1[i] << " ";//读
	
	cout << endl;
	
		//2、范围for
	string s2("hello world");
	for(auto& e : s2)
	
		e += 1;//写	
	
	for(auto e : s2)
	
		cout << e << " ";//读
	
	cout << endl;
	
		//3、迭代器
	string s3("hello world");
	string::iterator it = s3.begin();
	while(it != s3.end())
	
		*it += 1;//写
		++it;
	
	it = s3.begin();
	while(it != s3.end())
	
		cout << *it << " ";//读
		++it;
	
	cout << endl;
	
	return 0;

📝说明

operator [] | at ❗

operator[] 和 at 的价值是一样的,但是不一样的地方是对于越界:operator[] 直接使用断言报错,比较粗暴;而 at 则会抛异常。

范围 for ❗

这是 C++11 中提出的新语法,比较抽象,对于它的原理,后面会再模拟实现它。

注意 auto e : s3 只能读,不能写;auto& e : s3 可读可写。

迭代器 ❗

它是 STL 中六大组件中的核心组件,这里先见个面。

begin() 返回第一个有效数据位置的迭代器;

end() 返回最后一个有效数据的下一个位置的迭代器;

目前可以认为它类似于指针,—— 有可能是指针,有可能不是指针,因为不仅仅 string 有迭代器,其它容器里也有迭代器。

while(it != s3.end()) 也可以 while(it < s3.end()),但不建议。这里是数组,换成小于它依然没问题,但是如果是链表,小于就不行了。所以这里统一就用不等于。

迭代器的好处是可以用统一类似的方式去访问修改容器 (也就意味着会了 string 的迭代器就等于会了其它容器的迭代器):

为什么这里要提供三种遍历方式 ❓

其实只提供了两种,因为范围 for 本质是迭代器,也就是说一个容器支持迭代器才会支持范围 for,后面会演示。

operator [] 可以像数组一样去访问,这种方式对于 vector 连续的数据结构是支持的,但是不支持 list。

迭代器这种方式是任何容器都支持的方式,所以迭代器才是容器通用的访问方式。

再简单了解迭代器 ❗

#include<string>
#include<iostream>
using namespace std;
void Print1(const string& s)//不需要写

	string::iterator it = s.begin();//err,普通迭代器
	//string::const_iterator it = s.begin();//ok,const迭代器
	//string::const_iterator it = s.cbegin();//ok,C++11提供的cbegin
	while(it != s.end())
	
		cout << *it << " ";
		++it;	
	
	cout << endl;

void Print2(const string& s)

	string::reverse_iterator rit = s.rbegin();//err,普通反向迭代器
	//string::const_reverse_iterator rit = s.rbegin();//ok,const反向迭代器
	//auto rit = s.rbegin();//ok,auto的体现
	while(rit != s.rend())
	
		cout << *rit << " ";
		++rit;//注意这里不是--
	
	cout << endl;

void test_string()

	string s1("hello");
	Print1(s1);

	string s2("hello");
	Print2(s2);	

int main()

	test_string();
	return 0;

📝说明

这里利用迭代器打印 s1:

查文档:begin 有两个版本

所以报错的原因是:s 是 const 对象,返回的是 const 迭代器,而这里使用普通迭代器来接收。解决方法就是用 const 迭代器接收。

在文档中看到 C++11 中又提供了一个 cbegin ???

为什么 C++11 把 const 迭代器单独拎出来,大概原因应该是 begin 里有普通 / const 迭代器不太明确,容易误导。不知道大家是怎么想的,我是觉得有点画蛇添足了。

rbegin:

rbegin 就是反向迭代器,顾名思义就是反向遍历。


auto 的价值 ❗

到了迭代器这里我们就可以用 auto 来替代比较长的类型,auto 的价值得以体现。

4、string类对象的修改操作
函数名称功能说明
push_back在字符串后尾插字符 c
append在字符串后追加一个字符串
operator+= —— 重点在字符串后追加字符串 str
c_str —— 重点返回 c 格式字符串
find + npos —— 重点从字符串 pos 位置开始往后找字符 c,返回该字符在字符串中的位置
rfind从字符串 pos 位置开始往前找字符 c,返回该字符在字符串中的位置
substr在 str 中从 pos 位置开始,截取 n 个字符,然后将其返回

#include<string>
#include<iostream>
using namespace std;
void test_string1()

	//1、push_back | append | operator +=
	string s1("hello");
	string s2("world");
	s1.push_back('+');
	s1.append("world");
	cout << s1 << endl;
	s1 += '+';
	s1 += "bit+";
	s1 += s2;
	cout << s1 << endl;
	cout << "----------cut----------" << endl;
	//2、insert | erase
	string s3("hello");
	s3.insert(0, "bit ");
	cout << s3 << endl;
	//s3.erase(5);//从5删到nops
	s3.erase(5, 2);//从5往后删2个
	cout << s3 << endl;
	cout << "----------cut----------" << endl;

void test_string2()

	//要求取出文件的后缀
	string file1("test.txt");
	string file2("test.c");
	string file3("test.txt.zip");//对test.txt文件打了个包
	size_t pos1 = file1.find('.');
	if(pos1 != string::npos)//当find的返回值不等于npos就找到了
	
		//1、substr
		string sub1 = file1.substr(pos1);//同substr(pos1, file1.size()-pos1);,但没必要,因为这里是取后缀,而substr有缺省参数npos
		cout << sub1 << endl;
	
	size_t pos2 = file2.find('.');
	if(pos2 != string::npos)
	
		string sub2 = file2.substr(pos2);
		cout << sub2 << endl;
	
	//由此可见对于file3,以上代码不适用
	size_t pos3 = file3.find('.');
	if(pos3 != string::npos)
	
		string sub3 = file3.substr(pos3);
		cout << sub3 << endl;
	
	//更适用的来说取文件的后缀应当使用另一个接口rfind
	size_t pos4 = file3.rfind('.');
	if(pos4 != string::npos)
	
		string sub4 = file3.substr(pos4);
		cout << sub4 C++初阶:STL —— stringstring类 | 浅拷贝和深拷贝(传统写法和现代写法) | string类的模拟实现

C++初阶第九篇——string类(string类中一些常见接口的用法与介绍+string类的模拟实现)

C++初阶---STL入门+(string)

C++初阶string(上)

C++初阶string(下)

C++初阶---string类的模拟实现