STL简介+string模拟实现
STL简介+string模拟实现
- 1. 什么是STL
- 2. STL的版本
- 3. STL的六大组件
- 4.STL的缺陷
- 5. string
- 5.1 C语言中的字符串
- 5.2 1个OJ题
- 6.标准库中的string类
- 6.1 string类(了解)
- 6.2 string类的常用接口说明
- 1.string类对象的常见构造函数
- 2.析构函数(~string())
- 3.赋值函数 (operator=)
- 6.3 string类对象的容量操作
- 1.size()、length()、capacity()、clear()
- 2.string 在扩容方面是怎么样扩容
- 3.reserve() ->void reserve (size_t n = 0);
- 4.resize()
- 5.缩容
- 6.4 string类对象的访问及遍历操作
- 1. operator[]和迭代器(iterator) 遍历string
- 2.begin() 和 end()
- 3.rbegin() 和 rend()
- 4.at()
- 6.5 string类对象的修改操作
- 1.push_back()、append()、operator+=
- 2.insert()
- 3.erase()
- 4.c_str()
- 5.data()、copy() -- 不常用
- 6.find() - 查找
- 7.substr
- 6.6 string类非成员函数
- 1.operator>>
- 2.operator<<
- 3.getline
- 4.to_string 将int 转换string
- 7. 牛刀小试
- 917.仅仅反转字母
- 387.字符串中的第一个唯一字符
- 125.验证回文串
- 415.字符串相加
- 8.string 模拟实现
- string.h
- string.cpp
- test.cpp
- 9.浅拷贝
- 10.深拷贝
- 11.写时拷贝(了解)
1. 什么是STL
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
2. STL的版本
-
原始版本
Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 HP 版本–所有STL实现版本的始祖。 -
P. J. 版本
由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。 -
RW版本
由Rouge Wage公司开发,继承自HP版本,被C+ + Builder 采用,不能公开或修改,可读性一般。 -
SGI版本
由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版 本。被GCC(Linux)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程 风格上看,阅读性非常高。我们后面学习STL要阅读部分源代码,主要参考的就是这个版本。
3. STL的六大组件
4.STL的缺陷
- STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出来已经相隔了13年,STL才进一步更新。
- STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。
- STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。
- STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码,当然这是模板语
法本身导致的。
5. string
string 本质就是串,它是一个字符数组。只是这个数组可以扩容,可以增删查改。string 本质上是用的非常多的,大家想一想,数据类型本质上就是存各种各样的数据,整形,浮点型是表示数据大小,还有一些更符合的信息都是用string存的,比如说身份证号,它是不能用整形存储的,第一 这是表示大小范围的问题。第二个 有些身份证号码带X,那只能用字符串存。名字、地址 都需要用字符串存。现实生活中有很多东西都用字符串存储,所以string挺重要的。
string 其实是一个类模板,默认string是管理char数组的。
也能管理其他的,比如说还有一个叫wchar_t,它是两个字节。
也有4个字节的,char32_t 就是4个字节的。
当然我们平时这个阶段也接触不到。那C语言有没有串呢?
C语言也有自己的串,它其实是面向过程的实现方式,数据和方法是分离的。数据是你自己管理,空间是你自己管理,方法库里面给你提供了。如下:
5.1 C语言中的字符串
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP(面向对象程序设计)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
就给大家做个最简单的比方,有一个C语言的函数叫strcpy ,strcat 。strcpy 是不是把一块空间拷贝到另一块空间,那这两块空间是不是 你都要自己提供,并且拷贝到那块空间你得保证它俩是一样大的,或者说至少比它大,如果目标空间比源空间小就会存在越界,strcpy 不管这些,是你在copy要注意的,它数据和方法是分离的,就会有很多的问题。用起来就挺烦,既要管空间,又要管方法。strcat是在当前串追加,它有两个很挫的地方, 第一个是它会从前到尾找\0,这就效率很低了,如果前面这个串很长,那它找\0就有消耗,第二个,空间要自己准备好,要有足够的空间。所以C语言这种方法是不好用的。 如果你用string串之后,你就再也不想用C语言这种方法了。
5.2 1个OJ题
字符串相加
在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
6.标准库中的string类
6.1 string类(了解)
string类的文档介绍
- 字符串是表示字符序列的类
- 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
- string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
- string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
- 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
1.string是表示字符串的字符串类
2.该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string;
4. 不能操作多字节或者变长字符的序列。
在使用string类时,必须包含#include< string >以及using namespace std;
6.2 string类的常用接口说明
1.string类对象的常见构造函数
string() ;无参构造 -> string s1
string(const string& s) ;拷贝构造 -> string s2(s1)
string(const string& s,size_t pos,size_t len = npos) ;拷贝s从pos位置len个字符初始化。
string (const char* s) ;c风格字符串构造 ->string(“hello world”);
string(const char* s,size_t n);取c风格字符串前n个字符初始化
string(size_t n,char c); 用n个c字符初始化。
OK,话不多说 ,接下来看下面代码:
string s1(“hello world”) 会调用C风格字符串构造。
string s2 = “hello world”; //单参数函数 -> 隐式类型转换
“hello world”调用string(const char*s) 构造一个string类型的临时对象,再用这个临时对象拷贝构造s2. 构造+拷贝构造->直接构造
const string& s3 = “hello world”;
"hello world"构造一个string的临时对象,又因为临时对象具有常性,s3引用的是临时对象,所以加const.
看完了上述代码 ,这个时候大家就可以理解一个东西了。
假设我们写一个push_back,假设push_back一个string,是指其他数据结构,比如说 链表,顺序表 push_back。我们调用push_back 以前要写有名对象,定义一个有名对象,或者匿名对象,但是还有如下最方便的写法。
2.析构函数(~string())
析构函数底层是把空间给释放掉,要清理资源,它是自动调用的。
3.赋值函数 (operator=)
6.3 string类对象的容量操作
1.size()、length()、capacity()、clear()
测试代码1:
2.string 在扩容方面是怎么样扩容
下面有个问题是:string 在扩容方面是怎么样扩容的。
若s1 小于 15,直接存s1对象_Buf数组中。
大于 15,让s1对象下的指针开空间扩容存,存在堆上。
vs 扩容 1.5倍扩,2倍扩,这些都是不确定的。根据编译器。
其次capacity 比实际空间少一个,有一个多的是预留给\0的(g++,vs)
如何扩容,C++标准并没有规定,取决于编译器实现
下面代码能测试如何扩容
在这地方有个原则是第一次是个2倍扩容,后面是1.5倍,为什么第一次是2倍,后面又是1.5倍,其实跟之前那个结构有关系,string里面小于16的存在它内部的_Buf数组里面,然后数组满了,第一次要扩容至少要开32字节,上面capacity = 15, 严格来说不算扩容,只能算在堆上开空间。下面给大家演示看一下。
s1小于16存在_Buf上,也就是它比较小,它其实没存在堆上,存在对象本身上,对象本身放个数组,当你不断插入数据或者你直接放大于16的string,这个时候它的数据就不存在_Buf上,存在指针_Ptr指向的空间,这个空间其实就是堆空间,所以说第一次严格,来说不算扩容,因为它不是对已有的空间扩容,它是从一个地方存在另一个地方,你可以认为VS的这个设计是一种一空间换时间的方法,效率会高一点点,但是处理复杂程度会麻烦一点点,小于一定程度,它不想去内存中开那么多的小空间,开很多小空间,效率上有一定的影响,其次,就是说,会有一些内存碎片这样的问题。
下面是在Linux环境下的扩容,不同平台下的扩容还是有差异的。
从上图同样的函数插入,不同平台的扩容差异还是很大的。
OK,下面我们来看
3.reserve() ->void reserve (size_t n = 0);
reserve 通常功能是预分配内存空间,以避免频繁的动态内存分配,从而提高性能
1.vector 和 string 在动态增长时,如果当前容量(capacity())不足,会重新分配更大的内存块,并拷贝原有数据到新内存(O(n) 操作)。reserve(n) 提前分配至少能容纳 n 个元素的内存,减少后续 push_back()、emplace_back() 或 insert() 时的扩容次数。
2.reserve(n) 不会改变容器中的元素数量(size() 不变),只是调整底层内存的容量(capacity())。如果 n ≤ 当前 capacity(),reserve() 可能什么都不做(取决于实现)。
3.reserve() 仅分配未初始化的内存,不会调用元素的构造函数
4.reserve() 不会减少内存,如需缩减内存,可用 shrink_to_fit()(C++11)。
5.reserve() 不适用于 std::list、std::map 等非连续内存容器。
6.reserve 开的空间只影响capacity(),不影响size,若
string s;
s.reserve(200); 此时 size = 0; capacity = 207;
s[100] = ‘x’; 会越界, [ ]会调用底层operator[ ] 检查100是不是会小于size 。
resize 可以用[ ] 访问,reserve 不可以
下面是在VS上演示的:
reserve参数小于15,就是把堆上的空间释放,就把它拷贝到_Buf上面,所以说,小于15,它会缩,其他坚决不缩。
linux 上的reserve 给什么就是什么。
所以 reserve 会扩容,但不一定缩容,不同的平台有可能缩也有可能不缩。
reserve有什么意义呢?reserve其实一般我们也不用它缩容,因为缩容也不一定好,其次扩容是有代价的,比如说这有一块空间,我们需要在这个地方开另外一个空间,扩容,比如说,扩2倍,拷贝数据,在释放空间旧的空间,这是标准的扩容动作,原地扩容是很少的,很少能实现原地扩容。那我们看到,我插入200个数据不断不断的扩容,其实是有很大的成本,所以这地方有什么解决方案吗?有,就是我知道我要插入200个数据我可以用reserve提前开空间。
上面reserve完了之后,最容易犯的错误是什么?很多人reserve完了之后,就开始访问空间。
上面操作不能实现,[ ]会调用底层operator[ ] 检查100是不是会小于size,reserve只会改变我们的capacity,不会改变我们的size.空间改了,也就是说不会影响我们的数据。
4.resize()
下面来看我们的resize.
void resize(size_t n,char c) //开 n个位置,每个默认位置给c
假设我想访问这些空间,比如说我加完的结果是76543,我想挨个挨个去放。这个时候就不能用reserve,reserve 只是开了空间,不影响size ,所以就不能用下标访问,这个时候就可以用resize.
如果resize不传第二个默认参数
s1.resize(5) //此时填的是默认字符\0(空字符)
resize其他功能:
插入数据
删除数据
5.缩容
如果想要缩容的话,可以用
这个接口是让它的capacity 与size保持一致。比如说我空间开的很大很大,我插入很多数据,后面又删删,删了半天,我觉的空间浪费的有点多,我想把我的空间给释放下,那就可以调这个接口。但是不要经常调这个东西,这个东西不好。
缩容的本质是以时间换空间。开一块比之前更小的空间,把数据拷贝过来,把原来的空间释放掉。所以不要轻易的缩容,代价挺大的。
注意:
- size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
- clear()只是将string中有效字符清空,不改变底层空间大小。
- resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
- reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
6.4 string类对象的访问及遍历操作
1. operator[]和迭代器(iterator) 遍历string
第一个阶段的普通迭代器(iterator)
注:在基于范围的for循环中,auto 常用于声明循环变量的类型,编译器会根据迭代器的解引用类型来推断循环变量的类型。
迭代器:
iterator 是在类里面typedef的一个类型,属于类域。使用的时候要指定类域
总结: 我们学了三种遍历方式,他们都是可读可写的,从语法层是三种方式,从底层是两种方式,底层只有下标+[]和迭代器
2.begin() 和 end()
第二个阶段的迭代器(const_iterator)
typeid(变量名).name() //打印变量类型
const iterator 迭代器本身不能修改 类似 int* const
const_iterator 迭代器指向的数据不能修改 类似const int*
图示:
还有一种auto的方式(用于让编译器自动推断变量或函数返回类型的类型)
在C++中,
const string s1("hello world");
这行代码声明了一个名为s1
的常量字符串,其值为"hello world"
。这里的const
表示这个字符串是不可修改的,即不能对s1
进行赋值操作。
接下来的auto it1 = s1.begin();
这行代码声明了一个迭代器it1
,其类型由编译器自动推断。s1.begin()
返回一个指向字符串s1
开始位置的迭代器。迭代器是一个可以遍历容器元素的抽象概念,对于字符串来说,迭代器可以遍历字符串中的每个字符。
简单来说,这段代码创建了一个不可修改的字符串,然后获取了一个可以遍历这个字符串的迭代器。使用迭代器,你可以访问字符串中的每个字符,但因为字符串是常量,你不能通过迭代器修改字符串的内容。
总结: 普通迭代器它是给普通的string用的,普通对象用的,const迭代器是给const对象用的,我可以遍历string,但是不能修改数据。
3.rbegin() 和 rend()
反向迭代器 --倒着遍历
下一个问题是:我们说遍历用下标+[ ]就可以了,那其他东西呢,迭代器有没有下标+[ ]替代不了的,有,比如说 让s1按字典序排序。
4.at()
访问pos位置的字符。跟[ ] 功能是一样的。那他们的区别是什么呢?[ ]是暴力检查,一越界就报错。它是抛异常,简单来说就是下面这样的。
6.5 string类对象的修改操作
1.push_back()、append()、operator+=
代码测试:
2.insert()
代码测试:
下标必须是合法的,只有长度可以是非法。
3.erase()
string& replace(size_t pos,size_t len,const char*s);
把pos位置开始的len个字符替换c-string
4.c_str()
与c更好地兼容,C++要用C语言接口就调用c_str()
c_str() 被调用来获取 string 对象 str 的C字符串表示。结果被存储在 const char* 类型的指针 cstr 中。也可以获取底层的指针或者获取首元素的地址。/返回C风格字符串
c_str 可以跟C更好的兼容,有可能我们调的是C语言的接口,C++兼容C,C的库C可以用,C++也可以用。
5.data()、copy() – 不常用
data的功能与c_str 功能类似。但是我们平时用c_str.
我可以把第pos位置len个字符copy到char* s里面.
6.find() - 查找
默认从pos位置查找str/c-string/字符在字符串中的位置。
倒着找,从后往前查找
7.substr
从字符串提取子字符串
string substr(size_t pos = 0,size_t len = npos)const;
从pos位置开始提取len个长度的字符串返回
string file("string.cpp.zip");
size_t pos = file.rfind('.');
//string suffix = file.substr(pos,file.size()-pos);
string suffix = file.substr(pos);
cout<<suffix<<endl; //.zip
左闭右开下标一减就是个数
string url("https://gitee.com/abcdedg");
size_t pos1 = url.find(':');
string url1 = url.substr(0,pos1-0);
cout<<url1<<endl; //httpssize_t pos2 = url.find('/',pos1+3);
string url2 = url.substr(pos1+3,pos2-(pos1+3));
cout<<url2<<endl; //gitee.comstring url3 = url1.substr(pos2+1);
cout<<url1<<endl; //abcdedg
注意:
- 在string尾部追加字符时,s.push_back( c ) / s.append(1, c) / s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
- 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
6.6 string类非成员函数
1.operator>>
2.operator<<
3.getline
输入流对象,存取的字符串,可选的分隔符
cin >> 流提取与scanf一样遇到空格或者换行就结束
默认规定空格或者换行是多个值分割
若要获取一行中包含空格的字符串,不能用>>,要用getline(cin,str);
string str
while(cin>>str)
{cout<<str<<endl;
} //持续的获取流中字符串
用于从标准输入流(cin)读取一行字符串的函数,它会读取输入直到遇到换行符(\n),并将结果存储到字符串str中
4.to_string 将int 转换string
int x = 0,y =0;
cin>>x>>y;
string str = to_string(x+y);
cout<<str<<end;int z = stoi(str); //将str转换整形
string 可以很好的兼容utf-8,gbk;
7. 牛刀小试
917.仅仅反转字母
题目描述:
代码:
387.字符串中的第一个唯一字符
题目描述:
代码:
部分代码解析:
125.验证回文串
题目描述:
代码:
415.字符串相加
题目解析:
代码1:
上面代码时间复杂度是O(N^2),不推荐用上面这种写法。
代码2:
8.string 模拟实现
string.h
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;namespace bit
{class string{public://任何平台里的库都会保证typedef一个iterator,但iterator 的原生类型是char* 还是什么不确定// STL规范了任何容器提供迭代器都typedef 成iterator,他也不会重名,因为每个类都是一个独立的域//迭代器像指针一样的东西//这是一种封装,把迭代器的真实类型通过在类里面typedef以后进行了封装,// 隐藏了底层实现细节(上面进行统一化),提供了一种简单通用访问容器的方式// 不关心底层是什么类型,只要给迭代器就能访问容器,把算法和数据结构隔离开,迭代器是桥梁// 算法通过不同类型的迭代器来访问不同类型的容器来修改数据// 传string 就推演出 string的迭代器//string iterator 不一定是char* ,不同的平台都不同typedef char* iterator;typedef const char* const_iterator;//const 迭代器是指向的内容不能修改,本身可以修改const_iterator begin() const;const_iterator end()const; iterator begin();iterator end();//string(); 无参//string(const char* str); //带参//无参和带参的可以写一个全缺省的+初始化列表//声明和定义分离,缺省参数写在声明//全缺省构造函数string(const char* str = "");//string(const char* str = '\0’); 但'\0'是字符,const char* 接收的是字符串//有同学还写成上面那个,编译能过,'\0'隐式转换成整形,整形隐式转换指针,相当于空指针了,string(const string& s); //拷贝构造//string& operator=(const string& s);string& operator=(string tmp);~string();const char* c_str()const;size_t size() const;char& operator[](size_t pos);const char& operator[](size_t pos) const;void reserve(size_t n);void push_back(char ch);void append(const char* str);string& operator+=(char ch);string& operator+=(const char* str);void insert(size_t pos, char ch);void insert(size_t pos, const char* str);void erase(size_t pos = 0, size_t len = npos);size_t find(char ch, size_t pos = 0);size_t find(const char* str, size_t pos = 0);void swap(string& s);string substr(size_t pos = 0, size_t len = npos);bool operator<(const string& s)const;bool operator<=(const string& s)const;bool operator>(const string& s)const;bool operator>=(const string& s)const;bool operator==(const string& s)const;bool operator!=(const string& s)const;void clear();private :char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;//特例,因为加了const//const static size_t npos = -1;// 不支持//const static double N = 2.2;const static size_t npos;//类里面的静态成员变量就相当于全局变量//声明和定义分离时,.h 放声明,.cpp放定义//静态成员变量不会走初始化列表};istream& operator>> (istream& is, string& str);ostream& operator<< (ostream& os, const string& str);
}
string.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "string.h"
namespace bit
{const size_t string::npos = -1;//迭代器是在类里面typedef 的一个类型 所以iterator 也要指定类域string::iterator string::begin(){return _str;}string::iterator string::end(){return _str + _size;}string::const_iterator string::begin() const{return _str; //_str是char* 可以给const char* 权限缩小}string::const_iterator string::end()const{return _str + _size;}//无参构造/*string::string(){ //_str = nullptr; 不能给空指针,因为调用c_str(),这个函数返回值是const char* 会按字符串打印,会解引用,不能对空指针解引用_str = new char[1] {'\0'};_size = 0;_capacity = 0;}*///错误写法//string(const char* str)// :_str(str) 不能把str给_str,//因为 str 是常量字符串,回头没法给_str 扩容,修改//全缺省构造函数string::string(const char* str):_size(strlen(str)){_str = new char[_size + 1]; //_str 指向申请的动态内存空间_capacity = _size;strcpy(_str, str); //给_str指向的空间拷贝数据}//拷贝构造 如果我们不写,是浅拷贝。1.析构会析构两次。一块空间不能析构两次。// 2.一块空间的内容改变也会影响另一个// 所以要用深拷贝解决。他们应该有各自独立的空间//s2(s1) s1 就是s this 就是s2//传统写法(实在人)/*string::string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}*///现代写法(让别人干活,我们给他交换)//s2(s1)string::string(const string& s){string tmp(s._str); //用s1的值拷贝构造tmp/* std::swap(tmp._str, _str);std::swap(tmp._size, _size);std::swap(tmp._capacity, _capacity);*/swap(tmp); //s2.swap(tmp) s2 与 tmp 交换 此时 s2 不是随机值,因为在成员变量声明我们给缺省值了nullptr//有的编译器s2 给 随机值 ,tmp 与 s2 交换,tmp 出了作用域会调析构,有些编译器会崩。}//方法1 传统写法//s1 = s3//s1 = s1//string& string::operator=(const string& s)//{ // if (this != &s) //判断一下,不能自己给自己赋值// {// char* tmp = new char[s._capacity + 1]; //开一块新空间// strcpy(tmp, s._str); //把s3对象的内容拷贝给新空间里// delete[] _str; //释放s1指向的空间// _str = tmp; // s1 指向新空间// _size = s._size; // _capacity = s._capacity;// }// return *this;//}//方法2//现代写法//s1 = s3 //s1 和 s3 都是已存在的对象//string& string::operator=(const string& s)//{// if (this != &s) // {// string tmp(s._str); //s 就是 s3 ,tmp 开一块跟s3一样大的空间// swap(tmp); // s1.swap(tmp)// }// return *this;//}//方法3 .首先拷贝构造得写好,尽可能的复用//s1 = s3 s1.operator=(s3)(赋值运算符重载)//当执行 s1 = s3 时,tmp 是通过 拷贝构造 从 s3 初始化的(即 string tmp(s3))。传值传参 s3会调用拷贝构造来构造 tmpstring& string::operator=(string tmp) //不能用引用,用引用就是s1与s3交换{swap(tmp); // s1.swap(tmp) , 交换当前对象和 tmp 的资源//tmp 局部对象,函数结束时会调用析构,完成对象中资源的清理工作。return *this; // 返回当前对象的引用}//析构函数string::~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}//返回值为int* 会按指针打印//const char* 会按字符串打印,会解引用,需找到\0才终止//把 C++ 的 string 变成 C 能用的临时字符串const char* string::c_str()const{return _str;}//返回字符串的有效长度(即 _size 成员变量的值)size_t string::size() const{return _size;}//遍历字符串//返回 char&(引用),允许通过 [] 修改字符串内容(如 s1[0] = 'H')char& string::operator[](size_t pos){assert(pos < _size);return _str[pos];}//const operator[] 不能修改字符串内容const char& string::operator[](size_t pos) const{assert(pos < _size);return _str[pos];}//预留内存空间void string::reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}//插入一个字符void string::push_back(char ch){/*if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;_str[_size + 1] = '\0';++_size;*/ insert(_size, ch);}//插入一个字符串void string::append(const char* str){/*size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str + _size, str);_size += len;*/insert(_size, str);}string& string::operator+=(char ch){push_back(ch);return *this;}string& string::operator+=(const char* str){append(str);return *this;}//在pos位置插入一个字符void string::insert(size_t pos, char ch){//插入的位置要<= size //_size 指向\0assert(pos <= _size);if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}//size_t end = _size;// //while(end >= pos) 这种写法如果在pos等于0位置插入就越界了。因为end是size_t end = -1//是 42亿多 。end >= 0 就继续 ,小于0就出来,但是这里end不会小于0。所以可以将size_t -> int//第一种写法 (其中下面是两个问题)/*int end = _size;//在一个运算符,两边的操作数,如果他们的类型不一样,他们会发生隐式类型转换//当有符号遇到无符号,有符号会隐式类型转换成无符号,所以要把pos强转成int在去比较while (end >= (int)pos){_str[end + 1] = _str[end];--end; }*///第二种写法 // 如果 非要把 size_t 写成 无符号整形 那end >= pos 终止条件就是 end<0,然 < 0 就越界//有没有一种办法把 等号去掉 。当然有size_t end = _size+1;while (end > pos){_str[end] = _str[end-1];--end;}_str[pos] = ch;++_size;}//在pos位置插入一个字符串void string::insert(size_t pos, const char* str){assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}/*int end = _size;while (end >= (int)pos){_str[end + len] = _str[end];--end; }memcpy(_str + pos, str, len);_size += len;*/size_t end = _size+len;//while(end >= pos+len)while (end > pos+len-1) //画图{_str[end] = _str[end-len];--end;}memcpy(_str + pos, str, len);_size += len;}//从pos位置删除len个字符void string::erase(size_t pos, size_t len){assert(pos < _size); //pos不可以越界//当len 大于前面字符个数时,有多少就删多少if (len >= _size - pos){_str[pos] = '\0';_size = pos;}else{//strcpy(_str + pos, _str + pos + len);//memcpy(_str+pos,_str+pos+len,len+1);memmove(_str + pos, _str + pos + len, _size - pos - len + 1); // 确保 '\0' 被拷贝_size -= len;}}//从 pos 位置 找 字符 chsize_t string::find(char ch, size_t pos){for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;}}return npos;}//返回子字符串首次出现的位置索引//从字符串pos位置开始找子串sub// const char* strstr(主 const char* str1,const char* str)size_t string::find(const char* sub, size_t pos){char* p = strstr(_str + pos, sub);return p - _str; //指针相减计算的是两个指针之间的元素(char)个数。//当前数据的下标就是前面的数据个数}//s1.swap(s3)void string::swap(string& s){//没有交换string对象//交换char* ,size,capacity ,这是内置类型交换std::swap(_str, s._str);std::swap(_capacity, s._capacity);std::swap(_size, s._size);}string string::substr(size_t pos, size_t len ){//len大于后面剩余字符,有多少取多少if (len > _size - pos){string sub(_str + pos); //用位置_str+pos后面的字符直到取到\0 拿去构造// 构造一个子串返回return sub;}else{string sub;sub.reserve(len);for (size_t i = 0 ; i < len; i++){sub += _str[pos + i];}return sub;}}bool string:: operator<(const string& s)const{return strcmp(_str, s._str) < 0;}bool string::operator<=(const string& s)const{return *this < s || *this == s;}bool string::operator>(const string& s)const{return !(*this <= s);}bool string::operator>=(const string& s)const{return !(*this < s);}bool string::operator==(const string& s)const{return strcmp(_str, s._str) == 0;}bool string::operator!=(const string& s)const{return !(*this == s);}void string::clear(){_str[0] = '\0';_size = 0;}//方法1://流提取 - 要针对之前的空间进行覆盖//get() 是 istream 类的成员函数,用于从输入流(如 cin)中逐个读取字符(包括空格、换行符等空白字符)//istream& operator>> (istream& is, string& str)//{// str.clear(); //清空目标字符串// char ch = is.get(); //is.get() 用于逐个读取字符(包括空白符)// //while(ch != '\n')// while (ch != ' ' && ch != '\n')// {// str += ch; //如果cin提取的内容比较多,str 就不断的+= str就会不断的扩容,大量的扩容也不好// // 有人给了下面这种方法// ch = is.get();// }// return is;//}//方法2:// 问题:直接 str += ch 会导致 string 频繁扩容// 优化:使用 buff[128] 先缓存字符,攒够 127 个字符后再一次性追加到 str,减少扩容次数。//从输入流 istream 读取数据到 string 的功能istream& operator>> (istream& is, string& str){str.clear(); //清空目标字符串:// 使用局部缓冲区 buff[128] 减少字符串的频繁扩容,从而提高性能// 栈上分配的缓冲区,减少动态扩容char buff[128]; //局部数组,出了作用域就销毁了int i = 0; // 记录当前缓冲区位置char ch = is.get(); // 读取第一个字符(相当于初始化ch)while (ch != ' ' && ch != '\n'){buff[i++] = ch; // 存入缓冲区if (i == 127) // 缓冲区即将满(留 1 位给 '\0'){buff[i] = '\0'; // 手动添加字符串结束符// 追加到目标字符串str += buff; //str一次会把空间扩容好,不用频繁的扩容i = 0; // 重置缓冲区索引}//在每次循环结束时读取下一个字符,更新 ch 的值,以便下一次循环条件判断。//如果不写第二次 is.get():ch 的值永远不会更新,循环会无限执行(死循环)//例如,如果第一次读取的是字母 'a',while 条件成立,但 ch 始终是 'a',导致无限循环。ch = is.get();}if (i != 0) // 如果缓冲区还有未处理的数据{buff[i] = '\0'; // 添加结束符str += buff;}return is; // 支持链式调用,如 `cin >> s1 >> s2`}//流插入ostream& operator<< (ostream& os, const string& str){//日期类时写友元是因为要访问私有成员变量//下面这里没有写成友元也可访问私有//一个一个字符去输出,调用公有成员函数 for (size_t i = 0; i < str.size(); i++){os << str[i];}return os;}
}
test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "string.h"
namespace bit
{void test_string1(){bit::string s1("hello world");cout << s1.c_str() << endl; //hello worldfor (size_t i = 0; i < s1.size(); i++){//s1.operator[](i)//s[i]++cout << s1[i] << " "; //h e l l o w o r l d}cout << endl; bit::string::iterator it = s1.begin();while (it != s1.end()){cout << *it << " "; //h e l l o w o r l d++it;}cout << endl;for (auto e : s1){cout << e << " "; //h e l l o w o r l d }cout << endl;bit::string s2;cout << s1.c_str() << endl; //hello worldconst bit::string s3("gwwww");bit::string::const_iterator it3 = s3.begin();while (it3 != s3.end()){//*it3 += 3; 指向的内容不能修改cout << *it3 << " "; //g w w w w++it3;}cout << endl;for (size_t i = 0; i < s3.size(); i++){//s3[i]++; //s3是const对象 要调用const 类型的[],不能修改字符串内容cout << s3[i] << " "; //g w w w w}cout << endl;}void test_string2(){bit::string s1("hello world");cout << s1.c_str() << endl;s1.push_back('x');s1.append("yyyyy");cout << s1.c_str() << endl;s1 += '1';s1 += "中国";cout << s1.c_str() << endl;}void test_string3(){bit::string s1("hello world");s1.insert(5, 'x');cout << s1.c_str() << endl;s1.insert(0, 'y');cout << s1.c_str() << endl;bit::string s2("hello world");s2.insert(3, "qqqq");cout << s2.c_str() << endl;s2.insert(0, "www");cout << s2.c_str() << endl;bit::string s3("hello world");cout << s3.c_str() << endl;s3.erase(7);cout << s3.c_str() << endl;}void test_string4(){bit::string s1("hello world");cout << s1.find('o') << endl;cout << s1.find("wor") << endl;}void test_string5(){bit::string s1("hello world");bit::string s2(s1);s1[0] = 'x';cout << s1.c_str() << endl;cout << s2.c_str() << endl;bit::string s3("yyyyy");s1 = s3;cout << s1.c_str() << endl;cout << s3.c_str() << endl;bit::string s4("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz");s1 = s4;cout << s1.c_str() << endl;cout << s4.c_str() << endl;s1 = s1;cout << s1.c_str() << endl;cout << s3.c_str() << endl;std::swap(s1, s3); cout << s1.c_str() << endl;cout << s3.c_str() << endl;s1.swap(s3);cout << s1.c_str() << endl;cout << s3.c_str() << endl;}void test_string6(){bit::string url("https://chat.deepseek.com/a/chat/s/e0aed764-dd58-42a7-bae3-8ea7d1902beb");size_t pos1 = url.find(":");bit::string url1 = url.substr(0, pos1 + 0);cout << url1.c_str() << endl;size_t pos2 = url.find("/", pos1 + 3);bit::string url2 = url.substr(pos1 + 3, pos2 - (pos1 + 3));cout << url2.c_str()<< endl;bit::string url3 = url.substr(pos2 + 1);cout << url3.c_str() << endl;}void test_string7(){/*bit::string s1("hello world");cout << s1 << endl; */bit::string s1;cin >> s1;cout << s1 << endl;}void test_string8(){//现代写法 拷贝构造 + 赋值运算符重载bit::string s1("hello world");bit::string s2(s1);cout << s1 << endl;cout << s2<< endl;bit::string s3("xxxxx");s1 = s3;cout << s1 << endl;cout << s3 << endl;}void test_string9(){bit::string s1("hello world");bit::string s2(s1);cout << (void*)s1.c_str() << endl; //地址不一样没有用写时拷贝cout << (void*)s2.c_str()<< endl;}
}
int main()
{//bit::test_string1();//bit::test_string2();//bit::test_string3();//bit::test_string4();//bit::test_string5();//bit::test_string6();//bit::test_string7();//bit::test_string8();//bit::test_string8();bit::test_string9();return 0;
}
9.浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。
上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
10.深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
11.写时拷贝(了解)
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。