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的缺陷

  1. STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出来已经相隔了13年,STL才进一步更新。
  2. STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。
  3. STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。
  4. 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类的文档介绍

  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. 不能操作多字节或者变长字符的序列。
在使用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保持一致。比如说我空间开的很大很大,我插入很多数据,后面又删删,删了半天,我觉的空间浪费的有点多,我想把我的空间给释放下,那就可以调这个接口。但是不要经常调这个东西,这个东西不好。
缩容的本质是以时间换空间。开一块比之前更小的空间,把数据拷贝过来,把原来的空间释放掉。所以不要轻易的缩容,代价挺大的。
注意:

  1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
  2. clear()只是将string中有效字符清空,不改变底层空间大小。
  3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
  4. 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

注意:

  1. 在string尾部追加字符时,s.push_back( c ) / s.append(1, c) / s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
  2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。

6.6 string类非成员函数

在这里插入图片描述

1.operator>>

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

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,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/86977.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/86977.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

golang实现一个mysql中随机获取cookies的API

之前用FASTAPI写了一个随机cookies请求的接口,现在尝试用golang实现同样的效果 1.编写go代码 package mainimport ("database/sql""encoding/json""fmt"_ "github.com/go-sql-driver/mysql""log""net/http"&quo…

[Vue2组件]三角形角标

[Vue2组件]三角形角标 <template><div class"ys-subscript" :style"svgStyle"><svg width"200" height"200" viewBox"0 0 200 200" xmlns"http://www.w3.org/2000/svg"><!-- 三角形背景 - 右…

洛谷刷题4

B4354 [GESP202506 一级] 假期阅读 题目传送门 B4354 难度&#xff1a;入门 很简单的题&#xff0c;如果小A看的页数≤这本书的页数&#xff0c;输出他看的页数 否则&#xff0c;输出这本书的页数 AC代码&#xff1a; #include <iostream> using namespace std; in…

【基于Echarts的地图可视化】

<!DOCTYPE html> <html> <head><meta charset"utf-8"><title>中国牛只分布可视化</title><script src"https://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js"></script><script src"h…

系统架构设计师备考之架构设计基础

1.计算机系统基础知识 1.1.计算机系统概述 计算机系统的定义与组成 计算机系统是指用于数据管理的计算机硬件、软件及网络组成的系统。 计算机系统可划分为硬件和软件两部分。硬件由机械、电子元器件、磁介质和光介质等物理实体构成&#xff1b; 软件是一系列按照特定顺序组织…

针对华为云服务器使用率过大

从这两张监控图可以看出&#xff0c;服务器在大约上午 10:30 前后经历了一次明显的负载变化&#xff1a; 1. 图表解读 CPU 使用率 从凌晨到上午约 10:00 前&#xff0c;CPU 基本处于 0–2% 的闲置状态。10:00–14:00 之间&#xff0c;CPU 利用率逐步攀升&#xff0c;多次冲击 3…

记dwz(JUI)前端框架使用之--服务端响应提示框

目录 前言 一、DWZ服务器端响应种类 二、如何增加info级别的消息提示 1.打开项目的BaseController.java类 2.打开项目的dwz.min.js文件 3.最后在前端DWZ的主加载页面或者js文件中添加如下代码&#xff1a; 前言 本篇文章没有讲太多东西&#xff0c;主要是个人工作记录保…

leetcode 295. 数据流的中位数

时间复杂度分析&#xff1a;为什么你的中位数查找方案会超时&#xff1f; 分析你提供的MedianFinder实现&#xff0c;其时间复杂度较高的原因主要在于findMedian函数的实现方式。让我详细解释&#xff1a; 代码时间复杂度分析 你的代码中两个主要函数的时间复杂度如下&#…

大语言模型介绍

随着2022年底 ChatGPT 再一次刷新 NLP 的能力上限&#xff0c;大语言模型&#xff08;Large Language Model&#xff0c;LLM&#xff09;开始接替传统的预训练语言模型&#xff08;Pre-trained Language Model&#xff0c;PLM&#xff09; 成为 NLP 的主流方向&#xff0c;基于…

STM32 CCR寄存器

​1. CCR寄存器在输入捕获模式下的特性​ ​只读属性​&#xff1a; 当定时器通道配置为输入捕获模式&#xff08;如捕获上升沿/下降沿&#xff09;时&#xff0c;CCR寄存器硬件自动变为只读​。软件写入操作无效&#xff0c;只能在捕获事件发生时由硬件自动更新为当前CNT值。…

【JS-6-ES6中的let和const】深入理解ES6中的let和const:块级作用域与变量声明的新范式

在ES6(ECMAScript 2015)之前&#xff0c;JavaScript中只有var一种变量声明方式&#xff0c;这导致了许多作用域相关的问题。ES6引入了let和const两种新的变量声明方式&#xff0c;彻底改变了JavaScript的作用域规则。本文将深入探讨let和const的特性、优势以及它们与var的区别。…

[C语言]数据类型关键字详解

基本数据类型 关键字说明存储大小(通常)取值范围(通常)示例int声明整型变量4字节(32位系统)-2,147,483,648 到 2,147,483,647int count 100;char声明字符型变量1字节-128 到 127 或 0 到 255char grade ‘A’;float声明单精度浮点数4字节1.2e-38 到 3.4e38 (约6-7位有效数字…

黑马python(二十二)

目录&#xff1a; 1.Python操作Mysql基础使用 2.Python操作Mysql数据插入 3.综合案例 1.Python操作Mysql基础使用 2.Python操作Mysql数据插入 3.综合案例 代码复用 黑马python&#xff08;二十一&#xff09;章节的的代码&#xff0c;读取文件内容

课堂笔记:吴恩达的AI课(AI FOR EVERYONE)-W1 深度学习的非技术性解释

深度学习的非技术性解释 &#xff08;1&#xff09;示例1&#xff1a;以商场为主买T恤为例&#xff0c;价格和需求的关系怎么样&#xff1f; 一般来说&#xff0c;价格越高&#xff0c;需求越少 这里输入A是 价格&#xff0c;输出B是需求&#xff0c;其中的映射关系是神经元&a…

dlib检测视频中的人脸并裁剪为图片保存

环境要求 找个带有基本cv配置的虚拟环境安装上dlib依赖的人脸检测的基础环境即可&#xff0c;主要是&#xff1a; pip install boost dlib opencv-python缺的按提示安装。 demo 设置好视频路径和图像保存路径&#xff0c;裁剪尺寸&#xff08;默认256&#xff09;以及裁剪帧…

真的!ToDesk远程控制已上线原生鸿蒙系统!

2025年5月&#xff0c;ToDesk远程控制正式宣布完成对PC鸿蒙系统的适配&#xff0c;成为业界首批原生支持HarmonyOS OS的跨端远控工具。 作为国内支持上亿设备的远程控制软件&#xff0c;ToDesk以无缝互联、快速响应、安全无界为核心&#xff0c;重新定义了跨设备远程协作的界限…

Java-58 深入浅出 分布式服务 ACID 三阶段提交3PC 对比2PC

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月16日更新到&#xff1a; AI炼丹日志-29 - 字节…

matplotlib 绘制饼图

1、功能介绍&#xff1a; 使用 python 的 matplotlib 库来创建一个简单的饼图。 2、代码部分&#xff1a; import matplotlib.pyplot as plt# 示例数据 labels [A, B, C, D, E] # 类别标签 sizes [15, 30, 45, 5, 5] # 每个类别对应的数值&#xff08;百分比&#xff09…

用Rust写平衡三进制除法器

1、除法的本质 除法的本质是减法&#xff0c;也就是一个大的数减去一个小的数&#xff0c;比如:10/2&#xff0c;也就是10-2-2-2-2-20&#xff0c;所以商5余0&#xff0c;10/3&#xff0c;也就是10-3-3-31&#xff0c;所以商3余1&#xff0c;这也是很常见的方法&#xff0c;但如…

深入探索WordPress Multisite:构建与管理多站点网络

随着互联网的快速发展&#xff0c;越来越多的企业和个人开始使用内容管理系统来搭建和维护自己的网站。WordPress作为全球最受欢迎的CMS之一&#xff0c;因其强大的功能和灵活性&#xff0c;成为了许多网站管理员的首选平台。而在一些特定需求的场景下&#xff0c;WordPress Mu…