C++入门第六篇—STL模板---string【下】string模板的实现

前言:

在上一篇文章中,我们详细介绍了string模板库的一系列函数,为了进一步加深我们的理解以及我们的代码能力,我们接下来来实现一下我们自己的string模板库,模拟实现的过程中我们要对很多细节进一步的把控理解,包括很多新的概念和方法,不仅仅是要熟练掌握string库,同时也要对C++的知识点的细节运用更加精确。这便是我们模拟实现的目的。

string模拟实现:

1.第一部分:实现string库的基础功能:

既然是string库,我们首先就要先能构建一个字符串为主体的类,由前面的知识点我们知道,string的模板本质上是一种类,故在这里我们模拟实现的时候,我们也将其按照一种类处理。
首先先让我们考虑一个类的成员变量应该都有什么,字符串的本质是什么?是数组,那么对于数组,我们就可以让其按照顺序表的方式去处理,在顺序表中,我们首先定义了一个指针来指向一个字符串数组,同时创建了两个整型size和capacity,其中size用来统计数组中元素的个数,capacity用来统计我们为这个数组开辟的内存的大小,故我们这样构建类的成员:

1.类成员:

class my_string
{
private:
	char* _str;
	size_t _size;
	size_t _capacity;
	const static size_t npos;
}

注意,由于我们的string类的参数经常给一个缺省值npos,实际上它代表-1,由于存储的是补码的原因,本质上它是一个全1的很大的数,这样保证了我们字符串的后续的处理是全面的,而不会出现遗漏的现象。
对于static修饰的变量,我们要让其在类外全局修饰如下:

const size_t string::npos = -1;//全局变量在类外定义,作为static使用

2.构造函数:

在有了成员之后,我们就要为类创建它的构造函数,由于我们需要在堆区动态开辟数组内存,故我们的构造函数是需要我们手动去书写动态开辟。但是,在写之前让我们先想好我们构造字符串时可能会出现的情况,我们可能会构造一个空的字符串或者默认给值的字符串,所以,我们就要针对这两种情况去构建我们的构造函数,首当其冲的便是初始化的问题,我们在这里可以使用初始化列表来实现如下:

string(const char* str="")//构造函数,我们默认构造函数最好是给全缺省,我们让其缺省值为空字符串,这样倘若什么也不传也不会是空指针,而是直接空字符串返回
	:_size(strlen(str)),//这里要注意细节,注意变量定义的顺序,
	_capacity(_size)
{
	_str = new char[_capacity + 1];//永远要留一个给\0
	strcpy(_str, str);
}

由之前的知识可以知道,在书写构造函数时,我们一般都给构造函数赋全缺省值,而在这里我们只需要传入的便是一个const的字符串作为参数,由于前面我们已经考虑到我们会有创建空字符串的情况,故我们在这里直接给其缺省值为” “,这样,倘若我们不传字符,则默认创建的字符串是空字符串,倘若传,则直接以我们传入的字符串作为次字符串的元素。在初始化列表这里,我们同样有一个细节,那就是我们的初始化顺序是怎样的?
由前面学到的知识可以知道,初始化列表的顺序并不是从上到下依次排列的,而是按照我们成员变量的顺序从上到下依次初始化,所以,我们在这里一定要注意数据流的传输顺序,比如,我们的指针变量是第一位,倘若我们初始化的时候先动态开辟,由于capacity此时还没有被初始化,编译器默认给一个随机值,这样我们的动态开辟的空间就炸了,同理,我们的capacity为其初始化的size的数据,但是size在第二位,故倘若capacity先被创建,此时的size也是随机值,这样我们后续的扩容就会出现很大的问题,故在这里我们有两种解决方案
1.第一种是调整成员变量的顺序,按照初始化列表的顺序调整
2.第二种是根据成员变量的顺序调整初始化列表和构造函数体

在这里我比较倾向于第二种,因为我们在构建一个类成员的时候是不会去思考其顺序的,没法做到精确的调整,不如随意调整顺序,然后根据顺序去调整初始化的顺序,**在这里,我思考到str为第一个成员,故比不可能让其首先初始化,故我们将动态开辟的过程写在构造函数体内,构造函数一定是先完成初始化列表后才能进入构造函数的函数体,然后,我们首先将字符串的长度传给size,然后再初始化capacity,这样,我们初始化的过程就没有随机值的问题了。**如上面的程序,还需要强调的一点,我们动态开辟我们的数组的时候,由于我们capacity是没有考虑到\0的,故我们要capacity+1,这一位是为\0开辟的,千万别忘了,否则字符串没法判断结束。

3.返回字符串长度和内存大小:

程序如下,由于我们之前学到的知识点,类内私有的成员是不能在类外直接访问的,所以我们只能通过类成员函数来访问类的的私有数据,如下:

//返回字符串的长度
void size()
{
  return _size;
}
//返回对象的内存大小
void capacity()
{
  return _capacity;
}

4.原始版本的打印字符串:

由于有了我们的size()函数,我们可以写一个最为简化的字符串遍历如下:

void c_str()
{
   int i=0;
   for(i=0;i<size();i++)
   {
      cout<<_str[i];
   }
}

但后续我们会通过迭代器去实现一个标准的字符串打印函数。

5.标准流输出赋值运算符重载:

首先,让我们先考虑到一个问题,我们的cout<<变量这种写法,要求我们的ostream流作为参数是一定要在我们的字符串前面的,但是在成员函数中,由于我们的this指针的默认性,它是作为第一个参数进入函数的,这样我们的参数顺序就和正好相反了,故我们的输出流函数就要作为全局变量写在类的外面,且参数要求第一个参数为osteam流,第二个参数是我们的string类的参数引用,如下:

ostream& operator<<(ostream& out, const string& s)//流输出要写成全员函数,写在里面自己带一个this了直接
{
	for (auto ch : s)//这里传迭代器必须要在前面的函数上加const,否则迭代器不匹配
	{
		out << ch;
	}
	return out;
}

在上一篇文章中,我们详解过迭代器的知识点,迭代器的本质可以理解为一种指针,再使用迭代器变量的时候我们也确实涉及到指针的移动以及指针的解引用问题,我们使用范围for来遍历字符串,这就需要我们有一个迭代器,也就是一个匹配上我们传入参数字符串的迭代器,在这里,我们的字符串是const 类型的,故我们对应的迭代器也就是通过char*指针包装起来的,故我们使用类型重定义将其包装起来

typedef char* iterator;//模拟实现迭代器访问容器,迭代器本质上就是指针实现的
typedef const char* const_iterator;//迭代器的类型要对应,本质上就是指针要对应,const和非const要分开

在这里我们提前准备好分别对应读写的迭代器,防止后续的传参出现问题。
由此我们就能使用范围for来打印字符串了,如上面的程序。

6.[ ]赋值运算符重载:

const char& operator[](size_t pos)const //重载[](只可读不可写),用了const修饰
{
	assert(pos < _size);//断言输入的下标合法性
	return _str[pos];
}

我们采取的思路,就是将指针对应的字符串的下标对应返回,我们这里使用了const修饰,故证明了这里只可读不可写也不可更改。

7.字符串最开头位置begin,字符串的结尾位置end的获取:

在这里,为了模仿string库中的beign cbegin end cend,我们采取了读写分离的函数书写方式:

 const_iterator begin()const//是可以修改的,故不要加const
{
	return _str;
}
 const_iterator end()const
{
	return _str + _size;
}
 iterator begin()//这种情况下是可以修改的,对应的类会调用对应的迭代器,注意迭代器是很智能的,它会去匹配对应的指针
 {
	 return _str;
 }
 iterator end()
 {
	 return _str + _size;
 }

用对应的迭代器去接受相应的返回值,从而实现对字符传头尾位置的获取,然后通过操作迭代器,我们就能实现字符串的遍历,我们的范围for本质上就是在调用这个利用迭代器遍历的过程,其实现方式是相同的,但是是由编译器自己实现的,我们只需要为其准备好对应的迭代器和函数封装即可,编译器会自己封装。

8.strcmp字符串比较的运算符重载一系列函数:

**由于比较的逻辑性是可以互通的,故我们可以通过逻辑的复用使这一类的函数书写的非常快捷和准确,在进行date日期类函数的书写时,我们就已经使用了这种思路来进行比较运算符重载的书写,同时我们的比较的底层依旧使用C语言的strcmp函数来实现,**如下:

 //对于运算符问题就是直接复用
 bool operator>(const string& d2)const//运算符>重载
 {
	 return strcmp(_str, d2._str) > 0;
 }
 bool operator==(const string& d2)const//运算符==重载
 {
	 return strcmp(_str, d2._str) == 0;
 }
 bool operator>=(const string& d2)const//运算符>=重载
 {
	 return *this==d2||*this>d2;
 }
 bool operator<(const string& d2)const//运算符<重载
 {
	 return !(*this >= d2);
 }
 bool operator<=(const string& d2)const//运算符<=重载
 {
	 return !(*this > d2);
 }
 bool operator!=(const string& d2)const//运算符!=重载
 {
	 return !(*this == d2);
 }

如上面,我们只需要写出== >,剩下的< != >= >=就都可以直接通过逻辑操作直接复用出来,学会这种思路,真的很关键,会让我们的代码非常简便。

9.尾插字符:

尾插数据,和顺序表一样,我们首先最需要关注的问题依旧是扩容的问题,我们在这里已经先进行一个判断,即我们size是否等于capacity,倘若等于,我们就进入扩容函数进行扩容,扩容的函数如下:

 void reserve(size_t n)//调整容量
 {
	 if (n > _capacity)
	 {
		 char* tmp = new char[n + 1];//注意要多开一个空间给\0
		 strcpy(tmp, _str);//注意,C++没有类似C语言那样的realloc函数,C++只能手动开空间然后拷贝给过去后,释放掉原空间后,再把指针转移到新空间的位置
		 delete[] _str;
		 _str = tmp;
		 _capacity = n;//别忘了扩容要改变_capacity
	 }
 }

对于扩容的问题,我们可能首选到C语言的realloc函数,但是C++并没有提供这样的函数,如果用realloc就没法对数据进行初始化,所以我们使用new来开辟一块新的空间作为我们的新的字符串空间,然后将数据拷贝过去,delete掉之前的空间即可,然后调整capacity即可,如上面的程序。
由此,我们就可以进行我们的尾插字符的函数:

void push_back(char ch)//尾插字符
{
 if (_size == _capacity)
 {
	 reserve(_capacity==0 ? 4:_capacity*2);//注意这里是有bug,倘若capacity为0的话即使扩大2倍依旧是0,故这里的问题很大,所以和当初一样,我们在开辟顺序表的时候使用了三目操作符,我们这里也使用三目操作符
 }
 _str[_size++] = ch;
 _str[_size] = '\0';//对于单个字符的尾插,别忘了\0
}

10.尾插字符串:

其大致的思路和尾插字符差不多,只不过调整为字符串罢了:
不过,我们首先要考虑一下加上字符串长度后能否超过我们的capacity,倘若超过就要扩容

 void append(const char* str)//尾插一个字符串
 {
	 size_t len = strlen(str);
	 if (_size + len > _capacity)
	 {
		 reserve(_size + len);
	 }
	 strcpy(_str+_size, str);//注意开始拷贝的位置
	 _size += len;
 }

11.+=赋值运算符重载:

 string& operator+=(char c)//+=运算符重载,单字符
 {
	 push_back(c);
	 return *this;
 }
 string& operator+=(const char* arr1)//+=运算符重载,字符串
 {
	 append(arr1);
	 return *this;
 }

在这里包括字符和字符串两种,本质上他们就是复用了尾插的函数,但是+=赋值运算符是最常用的,故这个很关键。

12.任意插入字符/字符串:

 void insert(size_t pos,char c)//任意插单字符
 {
	 assert(pos <= _size);
	 if (_size == _capacity)
	 {
		 reserve(_capacity == 0 ? 4 : 2 * _capacity);
	 }
	 int end = _size;//现在的_size对应的就是下标,别搞错了
	 while ((int)pos <= end)//注意,对于双目操作符会存在一个隐式类型转换的问题,即有符号整型会被强转为无符号整型,这样就导致-1会大于0,后续再访问就出错了,故为了让其不错,我们将size_t类型的数据强转为int整型去比较即可
	 {
		 _str[end + 1] = _str[end];
		 end--;
	 }
	 _str[pos] = c;
	 _size++;
 }

类似插入排序的一种思想,任意插入字符最需要关w注的地方是我们的pos是无符号整型,而我们的end是int,在使用操作符号时,有符号整型会被转换为无符号整型,这就导致我们的end到了0之后变成-1依旧符合循环的条件,但是数组是没有-1下标的,这就导致了越界,所以,在这里我采取了将无符号强转为有符号去比较,就解决了这样的问题。
后面的插入字符串如下:

 void insert(size_t pos,const char*str)//任意插字符串
 {
	 assert(pos <= _size);
	 int len = strlen(str);
	 if (_size + len > _capacity)
	 {
		 reserve(_size + len);
	 }
	 int end = _size;
	 while ((int)pos <= end)//由于pos有等于0的可能性,故我们依旧需要强转,这个别忘了
	 {
		 _str[end + len] = _str[end];
		 end--;
	 }
	 strncpy(_str + pos, str, len);//限制长度的拷贝,在后面加上一个要拷贝的个数,注意能使用函数就用函数
	 _size += len;
 }

在这里,我们需要处理的问题和插入单个字符大差不差,但是我们在这里使用了strncpy部分拷贝字符串的一部分进入到我们的字符串中

13.任意删除字符串:

 void erase(size_t pos, size_t len = npos)//任意删除一段
 {
	assert(pos <= _size);
	if (len = npos || pos + len >=_size)
	{
		_str[pos] = '\0';
		_size = pos;//别忘了处理_size,这个容易忽略
	}
	else
	{
		int begin = pos + len;
		while (begin <= _size)
		{
			_str[begin -len] = _str[begin];//完全不用控制两个变量,用begin++减去len1,就相当于pos每次++
			begin++;
		}
		_size -= len;
	}
 }

在这里,我们需要注意的问题就是,一旦我们传入的删除的字符串的位数过大,或者我们根本没给字符串的长度,默认使用缺省值npos即无限长,这就导致我们pos位置之后的都要删除,在C C++都是遇到\0判定字符串结束,故我们直接在pos位置给一个\0,然后调整size长度即可,而对于有限的长度,我们只需要利用一个begin去控制数组的前后两个位置赋值即可。

14.从指定位置去寻找字符:

 size_t find(char ch,size_t pos=0)//从指定位置查找指定字符
 {
	 for (size_t i = pos; i < _size; i++)
	 {
		 if (_str[i] == ch)
		 {
			 return i;
		 }
	 }
	 return npos;//找不到就返回极大值
 }

没什么细节需要过多注意,遍历返回即可

15.从指定位置去寻找字符串:

size_t find(const char* str,size_t pos=0 )//从指定位置找指定字符串
{
 const char* p = strstr(_str+pos, str);//这样就满足从某个位置开始找了
 if (p)
 {
	 return p - _str;//在同一个连续的数据结构内,可以进行指针的相减,得到的是两个指针之间的元素个数,在这里要返回size_t,故我们选择指针相减法
 }
 else
 {
	 return npos;
 }
}

在这里,我们利用的C语言的strstr函数去寻找对应目标字符串的第一个位置的指针,并且接收,在前面指针的知识中我们知道,在同一个顺序结构中,指针的相减是有意义的,它可以求出两个指针之间的元素的个数,故这里我们让返回的指针和字符串的头指针相减,即可得到从头到我们找到的字符串之间的元素个数,即对应的字符串的下标(正好差一个,即为对应下标)

16.取得一个子字符串:

程序如下:

	 string substr(size_t pos,size_t len=npos)//取子串
	 {
		 string s;
		 size_t end = pos + len;
		 if (len == npos ||end >=_size)
		 {
			 len = _size - pos;
			 end = _size;
		 }
		 s.reserve(len);
		 for (size_t i = pos; i < end; i++)
		 {
			 s += _str[i];
		 }
		 return s;//注意,在这里由于涉及到动态开辟内存的原因,故我们要显式给拷贝构造函数,要不然会出现反复调用析构而多次释放从而报错的问题
	  }

我们这里要注意的细节就是,和我们删除一段字符串一样,我们仍需要对len的长度一旦超过后续的长度,就需要对我们传入的字符串的长度进行限制,即拷贝允许拷贝的一部分取得,而不是全部的,故首先我们需要先判断,一旦超过我们就要将后续的全部都拷贝给我们构建的s的string类,在这里采取了一个一个字符给的方式,而没有使用strncpy,因为这里不涉及到拷贝,所以赋值即可。
最后说说返回值的问题,由于我们的返回值并不是引用,而是返回一个不存在的string类型,故我们需要拷贝一份临时变量才能返回,同时,由于我们在这里对s进行了动态开辟,故我们的拷贝构造就不能默认构造了,而是要显式去写拷贝构造函数,要不然就会出现多次调用析构多次释放而报错的问题。

17.拷贝构造:(利用现代写法去处理,建议反复琢磨)

我们使用一种采取中间值tmp交换的现代写法来写:

void swap(string& s)//交换数据
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}
string(const string& s)//拷贝构造现代写法,利用tmp交换处理
	:_str(nullptr),
	_size(0),
	_capacity(0)//由于有个时候内置类型默认的拷贝构造不会处理,故编译器一旦给随机值的话,tmp出了作用域就会被销毁,调用析构函数,此时交换数据tmp的_str是随机的,长度未知,很容易造成无限次的析构递归,直接炸了,故为了保险起见,我们在拷贝构造这里给*this赋初值,保证后续交换时不会出现随机的问题
{
	string tmp(s._str);
	swap(tmp);
}

在这里,我们利用s._str构建了一个tmp的空间,然后将其与this交换,注意,由于我们的拷贝时我们的this里面的成员变量是随机的,这导致tmp一旦调用析构就会有可能调用无限次的析构从而导致崩溃,所以我们在拷贝构造的前面最好先给我们的this先初始化一下,导致发生这样的错误

18.赋值运算符重载(非常简便的写法,建议反复思考研究)

string& operator=(string tmp)//更加极致的赋值运算符重载现代写法:直接传值传参要拷贝构造,因此在这里tmp就是传入参数的拷贝构造,把tmp直接跟this交换即可,出了作用域tmp直接就销毁了,然后重复上面的写法,非常巧妙,完全利用了类的特点,这种方式要多积累
{
	swap(tmp);
	return *this;
}

我在这里采取更为精妙的现代写法,在这里我不传引用,而是直接传值,这样,tmp就直接根据s对象调用了拷贝构造,甚至不需要单独写出来,然后让其swap与this交换,返回*this即可。

19.改变字符串的长度:

程序如下:

void resize(size_t n, char ch='\0')//重置字符串的长度并且多余的部分给指定的字符,分三种情况考虑,_size小于等于之前的_size或者比原来大,比原来大又分为扩容或者不扩容,但这个不影响,扩容函数会自己判断
{
 if (n <= _size)
 {
	 _str[n] = '\0';
	 _size = n;
 }
 else
 {
	 reserve(n);//先考虑扩容的问题
	 while (_size < n)//然后再把指定字符插入进去
	 {
		 _str[_size] = ch;
		 _size++;
	 }
	 _str[_size] = '\0';//最后别忘了补\0作为字符串的结尾
 }
}

在这里我们分为两种情况,第一种是我们往小了缩,只需要直接给尾部改成\0即可,这样字符串就会在\0处判定结尾从而达到了缩减长度的效果,倘若是往大了缩,我们首先就需要考虑到扩容的问题,然后从_size位置出发,依次放入我们想要放入的字符,最后别忘了补上\0。
!!!!!我在这里必须要强调的是:字符串的结尾必须以\0结尾,故我们千万别忘了补上\0!!!!!

20.赋值运算符重载>>流输入:(重点!!!有很重要的处理字符串输入的方法以及一种新的输入扩容的思路)

istream& operator>>(istream& in, string& s)//流输入全员函数,这样把第二个参数带到第二个位置上,倘若在类里面io流是抢不过隐含的this指针的
{//流提取默认遇到\0或者空格就停止,且流提取从缓冲区得到字符,故我们要创建一个变量用来提取我们输入的字符并且将其形成循环
	s.clear();//直接输入字符串,防止出现尾插的情况
	char ch = 0;
	char extrabook[128+1];//辅助空间:extrabook     别忘了多开一个给\0,利用辅助空间的方法去减少扩容的过程,提高效率,这种方法很好,要反复琢磨理解
	size_t i = 0;
	ch = in.get();//故这种方式是拿不到空格或者换行的,故我们引用一个接收一切in流的函数in.get()来接收包括空格换行在内的一切字符,即可解决这个问题,就像C语言的getchar()一样
	while (ch != ' '&& ch != '\n')//注意换行是\n,不是\0,别搞错了,\0是字符串结尾的标志,但是我们本身的打不出来的
	{
		extrabook[i++] = ch;
		if (i == 128)//当辅助空间蓄满了,就将数据传给s,然后重新给辅助空间蓄数据,直到遇到空格或者\0停止
		{
			extrabook[i] = '\0';
			s += extrabook;
			i = 0;
		}
		ch = in.get();
	}
	if (i != 0)//如果i等于0说明上一次就结束了,不用再补\0了
	{
		extrabook[i] = '\0';
		s += extrabook;
	}
	return in;
}
//注意,cin和scanf一样,他们都不识别空格和换行符,他们都不会将其存入到变量中,故我们必须找到一个方法让其可以接收空格或者换行符


//在这里考虑一下如何扩容的问题
//对于扩容问题,我们可以采取辅助空间的方法,创建一个辅助的数组一部分一部分向字符串中插入数据,这样扩容也是一部分一部分开辟的,不会频繁的去扩容,甚至扩容一次就可以,极大的节省了效率

如同scanf一样,我们的cin也是无法识别到空格和\n的,故我们利用in.get()将其调整为可以识别空格和\n在内的一切字符,然后按照我们常规的,倘若不是空格和\n就一直输入,保证字符串的完整性,同时,为了防止我们出现的尾插的情况,我们要先清空我们原先的字符串,本质上就是在第一个位置放一个\0,直接让字符串识别到第一个字符是\0就结束。,然后将我们得到的字符一个一个放入我们字符串即可。
但之后我们思考这样一个问题,倘若我们根据情况,输入一个很长的字符串,我们就要反复多次调用扩容函数,这样严重影响了效率,故在这里我们采取了一种更为高效的方法:缓存字符串输入法。
如上面的程序,我们创建了一个extrabook字符串,这一次我们先向这个字符串传入字符,直到这个字符串满了,我们再让s+=extrabook,然后刷新extrabook,重新向里面输入字符,直到我们的输入空格或者\n,此时,如果我们的extrabook依旧有字符直接+=即可,然后这样我们的扩容次数就被极大的减少了,这种方法即为有效,我们应当反复琢磨并且掌握缓存字符串输入法,你可以这样理解,在这里,我们的extrabook如同一个水坝,水一旦涨的过多就将其输送给农田灌溉,然后继续蓄水,反复这个过程,这样极大提高了效率
在这里还是要强调一下我们的in.get()这个很有用,它可以识别所有字符,有点类似getchar(),再处理一些题目时非常有效,最好记下来!!!
我们的string库自身也采取了这种方法,让我们看VS是如何处理的:
程序如下:

int main()
{
 string d1;
 string d2("hello world");
 cout<<sizeof(d1)<<endl;
 cout << sizeof(d2) << endl;
 return 0;
}

结果如下:
在这里插入图片描述
我们发现,不管是空字符串还是传入字符串,初始的对象的大小都是40个字节,除了我们的三个固定的成员占12个字节外,还存在着一个类似我们的extrabook的数组存在,如下:
在这里插入图片描述
在这里,我们的allocator便是一个有着28个元素的字符数组,加上12个字节正好是40个字节,为了证明它的作用,哪怕是:

string d2("hello world11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111");

这样去构建一个字符串,对于allocator来说,他依旧是缓存足够的长度,不会改变自己的长度,如下:
在这里插入图片描述
故现在我们知道,string类就使用了这种缓存字符串的方式,来减少扩容的次数。(在LINUX甚至更加极端,仅仅4字节,只有一个指针作为成员变量,利用延时拷贝和引用计数来进行处理)

总结:

由此,我们的模拟string类基本就实现了,但还是有很多功能需要我们去探索,我们实现的这些都是我们常用的string库的函数,而且我们模拟实现string类的本质依旧是方便我们去使用string类以及学到一些方法,这是最关键的,要清楚自己的学习目的!!!!

补充知识点:

在实现我们的string类中,我想最让我们头疼的就是深拷贝和浅拷贝的问题,故在这里我们好好的分析一下这个问题:
我们常说的浅拷贝,正如我们前面提到的那样,它的危害在于它可能会析构多次,同时一个改变会导致另一个也发生改变,因为它的指针可能同时指向一个内存空间,但浅拷贝也不是一无是处,对于不涉及指针或者不涉及内存的成员类时,使用浅拷贝的效果更好,故我们这样总结了一下浅拷贝和深拷贝的用法:
!!!!对于数据本身就存在对象里面的,我们适合浅拷贝,当倘若数据不是存在对象中,而是存在由成员的指针变量指向的对象外的空间比如堆区动态开辟的空间时,我们就必须要使用深拷贝!!!!

但是,难道就没有使用浅拷贝同时可以解决多次释放的影响的问题的方法么?
在这里,我们可以采取这样的一种方法:
我们可以使用引用计数法,利用一个count来记录实时有几个对象的指针变量同时指向一块动态开辟的空间,拷贝构造时count++,调用析构时–,这样当计数减到0时,说明执行到这一析构的指针正是最后指向这块空间的指针,此时便可以由这个指针来释放这块空间,这样就不会发生多次释放的问题了,同时使用浅拷贝就可以做到,从始至终都是一块空间。如下:
在这里插入图片描述
这种方法确实解决了我们的问题,但是它依旧没法解决我们修改一个数据时另一个数据也同时被修改的问题,就比如在这里,我们对A2指向的字符串,实际上就是在修改A1 A3 A4,这不是我们想看见的,所以同样的思路,我们在这里可以使用延时拷贝的方式。延时拷贝,顾名思义,它并不是无脑的使用拷贝构造,依旧需要我们的size统计指向一块空间的对象的指针个数,倘若是1证明只有一个指针指向这块空间,故我们就可以直接对这块空间进行修改,倘若size>1,说明此时有多个指针指向这一块空间,故这种情况下我们就只能拷贝一份空间在新空间修改并赋给我们对象的指针变量,这样能在一定程度上解决问题,但是倘若我们要修改数据,则必定要拷贝数据,但这也是写的时候拷贝而不是无脑的直接拷贝。
这种方案的意义:如果拷贝了,没有修改数据就极大的提高的效率,但是倘若修改了数据,就只能另外开辟一块空间去去修改而不能在原空间上修改,除非只有一个指针指向一块空间!!!

总结:

以上便是我们string类的全部内容,通过模拟和讲解,我们要掌握的时如何熟练的使用string库,从而为我们做题和运用的时候提供方便,更加快捷的开发程序,同时在模拟的过程中学习一些思路和方法,扩展我们的程序思路,这便是主要的目的,希望大家认真去体会和领悟!!!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值