String函数

一、string类
1、(1)字符串:字符的集合,以‘\0'结尾。
     (2)字符数组:存放字符的数组。
2、封装一个字符串,即封装string类:
(1)
 
# define _CRT_SECURE_NO_WARNINGS
# include<iostream>
# include<string.h>
using namespace std;
//string类管理字符串,空间一般为一个字符数组
class String
{
public:
 /*String()//构造函数
  :_pStr(new char[1])//该字符串中只有'\0',没有其他内容,把'\0'给到引用空间内
 {
    *_pStr = '\0';//正确
  //*_pStr = 0;//此方法可取
  //_pStr = 0;//把指针置空,存在内存泄漏
  //_pStr = '\0';//把指针置空
  //*_pStr = '0';//'\0'和字符0的含义不同
  }
 String(const char *pStr)//给出空间的首地址(该字符串除了'\0',还有其他的内容
 {
  //对指针进行判空
  if (NULL == pStr)
  {
   //把空指针当成空串进行处理,空串里面有一个'\0'
   _pStr = new char('\0');
  }
  //把字符串里面的内容存到空间里面
  //1、开辟空间
  _pStr = new char[strlen(pStr) + 1];
  //2、把内容拷贝到新空间里面
  strcpy(_pStr, pStr);
 }*/
 //将上述两中方式合为一个,给定空字符串为一个缺省值
 String(const char *pStr=" ")
 {   
   if (NULL == pStr)
  {
   _pStr = new char('\0');
  }
  else{
   //1、开辟空间
   _pStr = new char[strlen(pStr) + 1];
   //2、把内容拷贝到新空间里面
   strcpy(_pStr, pStr);
  }
  }
 String(const String& s)
  :_pStr(s._pStr)
 {}
 //赋值运算符重载
 String& operator=(const String& s)
 {
  if (this != &s)//若不是自己给自己赋值,字符指针和s中的内容相同
  {
   _pStr = s._pStr;
  }
  return *this;
 }
 ~String()
 {
  if (_pStr)
  {
   //空间存在,就释放空间
   delete[] _pStr;
   _pStr = NULL;
  }
 }
private:
 char* _pStr;//给成动态数组,外部用户给与几个字符,即管理几个字符
};
void FuncTest()
{ 
 String s1("haha");
 String s2;
}
int main()
{
 FuncTest();
 return 0;
}

此方式会导致程序崩溃,因为s1与s2会使用同一块空间

当s2调用析构函数,释放空间后,s1并不知情,s1成为一个野指针;当s1会尝试把s2已经释放的空间再释放一次,就出现问题。
(2)使用测试代码:如下,也会出现问题,程序会崩溃:
 
void FuncTest()
{
                String s1("haha");
 String s3;
                s3 = s1;
}

出现此问题是因为赋值运算符里面出现问题:

以上(1),(2)即为浅拷贝,(只把对象里面的东西原样放到另一个对象里面,导致多个对象共用同一块空间,导致一块空间被释放多次,引起程序崩溃或内存泄漏。
(3)上述问题的解决方式之一:深拷贝
A、不要公用同一块空间,给每一个对象给一个空间
a、此种方式会出现错误:
 
String(const String& s)
  { 
  if (_pStr)
   delete[] _pStr;
  _pStr = new char[strlen(s._pStr) + 1];
  strcpy(_pStr, s._pStr);
 }

错误原因:此时当前对象还不存在,不用考虑是否为空,若是释放,会导致程序崩溃。
改正:先初始化为空,此时就不用判空和释放
 
String(const String& s)
  :_pStr(NULL)
 { 
  _pStr = new char[strlen(s._pStr) + 1];
  strcpy(_pStr, s._pStr);
 }

或者:
 
String(const String& s)
  :_pStr(new char[strlen(s._pStr)+1])
 { 
   strcpy(_pStr, s._pStr);
 }

B、对于
void FuncTest()
{
                String s1("haha");
 String s3;
                s3 = s1;
}

a、方式1,可以采用:申请新空间、拷贝元素、释放旧空间、更改指向(指向新空间)

 
String& operator=(const String& s)
 {
  if (this != &s) 
  {
   char *pTmp = new char[strlen(s._pStr) + 1];
   strcpy(pTmp, s._pStr);
   delete[] _pStr;
   _pStr = pTmp;//让当前对象指向新空间
   }
  return *this;
 }

b、方式2
 
String& operator=(const String& s)
 {
  delete[] _pStr;//释放旧空间
  _pStr = new char[strlen(s._pStr) + 1];//开辟和s大小相同新空间
  strcpy(_pStr, s._pStr);//把s中的内容拷贝过来
}

若是开辟空间失败,使用方式1不会对原来的空间造成影响;但是对于方式2开辟空间失败,会使原空间的内容不存在。

深拷贝:让每一个对象都拥有自己的空间,让处理的更深。

C、检测一下浅拷贝的方式有什么错误(实现一个简洁版本的浅拷贝):

class String
{
public:
	String(const char *pStr = " ")
	{
		if (NULL == pStr)
		{
			_pStr = new char('\0');
		}
		else{
			//1、开辟空间
			_pStr = new char[strlen(pStr) + 1];
			//2、把内容拷贝到新空间里面
			strcpy(_pStr, pStr);
		}
	}
	String(const String& s)
 	{ //当前对象,每个对象得String类里面都有一个_pStr指针,当前对象得指针没有赋初始值,所以是一个野指针,空间中最后存的得什么值,就指向以什么为地址得指针。
		String strTmp(s._pStr);//创建临时对象,临时对象也有一块空间、有一个指针_pStr,通过s里面得字符串构造出来的,指针调用的是构造函数而不是拷贝构造函数。临时变量的指针指向tmp空间,该空间与s里面放的内容一模一样。
		Swap(_pStr, strTmp._pStr);//交换两个对象中的内容,临时对象的指针变成一个野指针。出了函数的作用域要销毁,但是该空间不是从堆上创建的空间,释放会出错。
	}
	 //赋值运算符重载
	/* 
	String& operator=(const String& s)
	{
		if (this != &s)
		{
			String strTmp(s);
			Swap(_pStr, strTmp._pStr);
		}
		return *this;
	}
	*/

	/* //不用自己给自己赋值,直接交换
	String& operator=(const String& s)
	{ 
			String strTmp(s);
			Swap(_pStr, strTmp._pStr);
		return *this;
	}
	*/
	/*//不传引用,直接用s里面的值进行交换
	String& operator=(String s)
	{
 		Swap(_pStr, strTmp._pStr);
		return *this;
	}
	*/
	~String()
	{
		if (_pStr)
		{
 			delete[] _pStr;
			_pStr = NULL;
		}
	}
private:
	char* _pStr; 

};
void FuncTest()
{
	String s1("haha");
	String s2(s1);
}
int main()
{
	FuncTest();
	return 0;
}

此时程序崩溃,说明拷贝构造函数有问题。

改正拷贝构造函数:

//拷贝构造函数的解决方案:给定一个初始值,赋为空
  String(const String& s)
  :_pStr(NULL)
 {  
  String strTmp(s._pStr); 
  Swap(_pStr, strTmp._pStr); 
 }

b、使用测试函数:

void FuncTest()
{
	String s1("haha");
	String s2;
	s2 = s1;
}

说明赋值运算符的方式出现问题,创建临时变量,也会调用拷贝构造函数。此时不能判断是哪个函数出现问题(拷贝构造函数还是赋值运算符)。

判断下列赋值运算符的函数是否出现问题:

A、

String& operator=(const String& s)
	{
		if (this != &s)
		{
			String strTmp(s);
			Swap(_pStr, strTmp._pStr);
		}
		return *this;
	}

没有问题:直接拷贝拷贝构造函数的内容,用临时对象与当前对象交换当前对象和参数都已经存在,指针指向的空间一定是合法的,不用赋空,直接交换。出了if判断句,即释放this返回临时对象里面的空间。

B、

String& operator=(const String& s)
	{ 
			String strTmp(s);
			Swap(_pStr, strTmp._pStr);
		return *this;
	}

没有问题:只是少了一步检测是否为自己给自己赋值的操作。如果不是自己给自己赋值,没有问题;如果是自己给自己赋值,出了函数的作用域,销毁原来的空间,留下的是临时空间,自己给自己赋值时,相当于临时空间没有含义,但是不会出现问题。但是不如A的方式好。创建临时对象是在函数体里面由用户自己创建。

C、

String& operator=(String s)
	{
 		Swap(_pStr, strTmp._pStr);
		return *this;
	}
不会出现问题。传值,传参时相当于生成了一个临时变量,此时相当于当前对象和临时变量交换空间,出了作用域临时变量的空间会销毁,临时变量销毁的空间即为当前对象的旧空间。不论是不是自己给自己赋值都要创建临时对象(传参期间由编译器创建),原理与B类似。此种方式也不如A好。

(3)为了解决浅拷贝的问题,还可以采用:同一块空间进行计数,销毁一次即减一,如果计数为1则s2不能销毁,当s1减一后为0,s1必须进行释放。就不会存在一个空间被释放两次的问题。

a、把计数给成普通的类成员:不行

class String
{
public:
	String(const char *pStr = " ")
	{
		if (NULL == pStr)
		{
			_pStr = new char('\0');
		}
		else{
			//1、开辟空间
			_pStr = new char[strlen(pStr) + 1];
			//2、把内容拷贝到新空间里面
			strcpy(_pStr, pStr);
		}
		_count = 1;//构造函数标记被几个函数在使用
	}
	//拷贝构造函数
	String(const String& s)
		:_pStr(s._pStr)
		, _count(++s._count)
	{

	}
	~String()
	{
		if (_pStr && 0 == --_count)
		{
			delete[] _pStr;
		}
	}
private:
	char* _pStr;
	mutable int _count;//给定一块整型的计数空间
};
void FunTest()
{
	String s1("haha");
	String s2(s1);
}
int main()
{
	FunTest();
 	return 0;
}

但此时也会出现问题,每个对象都包含一份s2把自己计数减1,变为1;但是s1并不知道,此时也用2减,也为1,都不会释放,导致内存空间不能释放。

b、让计数的变量共享,给成static。解决方式:s2释放后让s1知道s2释放了一个。

class String
{
public:
	String(const char *pStr = " ")
	{
		if (NULL == pStr)
		{
			_pStr = new char('\0');
		}
		else{
			//1、开辟空间
			_pStr = new char[strlen(pStr) + 1];
			//2、把内容拷贝到新空间里面
			strcpy(_pStr, pStr);
		}
		_count = 1;//构造函数标记被几个函数在使用
	}
	//拷贝构造函数
	String(const String& s)
		:_pStr(s._pStr)
 	{
		_count++;//静态成员变量不能出现在函数体里
	}
	~String()
	{
		if (_pStr && 0 == --_count)
		{
			delete[] _pStr;
		}
	}
private:
	char* _pStr;
	static int _count;//给定一块整型的计数空间
};
int String::_count = 0;//刚开始没有空间,将初始值给为0
void FunTest()
{
	String s1("haha");
	String s2(s1);
}
int main()
{
	FunTest();
 	return 0;
}

但是若是此时测试代码:

void FunTest()
{
	String s1("haha");
	String s2(s1);
	String s3;
}

对于s3出了作用域需要销毁,计数变量变为0。但是此时s1和s2的计数变量也是0,再销毁就会变成负数,出现错误。

c、有几个空间,计数给几个;独立的用独立的空间,给定独立的计数,共享的给出一个计数空间。销毁空间时,若是一个计数已经用了,则减一,用另一个变量继续判定。直到计数为0,则释放。释放字符串空间和存放引用计数的空间。

class String
{
public:
	String(const char *pStr = " ")
		:_pcount(new int(1))//引用计数初始值给为1
	{
		//存放字符串的空间
		if (NULL == pStr)
		{
			_pStr = new char('\0');
		}
		else{
			//1、开辟空间
			_pStr = new char[strlen(pStr) + 1];
			//2、把内容拷贝到新空间里面
			strcpy(_pStr, pStr);
		}
 	}
	//拷贝构造函数
	String(const String& s)
		:_pStr(s._pStr)
		, _pcount(s._pcount)//共享计数空间
	{
		++(*_pcount);
 	}
	~String()
	{
		if (_pStr && 0 == --*_pcount)
		{
			delete[] _pStr;//销毁字符串的空间
			_pStr = NULL;
			delete _pcount;//销毁引用变量的空间
			_pcount = NULL;
		}
	}
private:
	char* _pStr;
	int* _pcount;//几个空间就有几个计数
};
 void FunTest()
{
	String s1("haha");
	String s2(s1);
	String s3;
}
int main()
{
	FunTest();
	return 0;
}

(4)

class String
{
public:
	String(const char *pStr = " ")
		:_pcount(new int(1))//引用计数初始值给为1
	{
		//存放字符串的空间
		if (NULL == pStr)
		{
                           _pStr = new char[1];
			_pStr = new char('\0');
		}
		else{
			//1、开辟空间
			_pStr = new char[strlen(pStr) + 1];
			//2、把内容拷贝到新空间里面
			strcpy(_pStr, pStr);
		}
 	}
	//拷贝构造函数
	String(const String& s)
		:_pStr(s._pStr)
		, _pcount(s._pcount)//共享计数空间
	{
		++(*_pcount);
 	}
	 
	//赋值运算符的重载
	String& operator=(const String&s)
	{
 			//考虑是否为当前对象,若是共用同一块空间,改变引用计数的值;
			//没有共用一块空间,则将引用计数减1,减1后若是为0,则释放原空间。,指向新空间,将新空间里面的计数加1
			//如果两个对象共用一块空间,可以认为是自己给自己赋值。
			//如果地址不相同,则当前对象和s没有共用同一块空间
			if (_pStr != s._pStr)
			{
				if (_pStr && 0 == --*_pcount)
				{
					delete[] _pStr;
					delete _pcount;
				}
				//共用同一块空间、共用同一个引用计数
				_pStr = s._pStr;
				_pcount = s._pcount;
				++*_pcount;//引用计数增加
			}
		return *this;
	}
	~String()
	{
		if (_pStr && 0 == --*_pcount)
		{
			delete[] _pStr;//销毁字符串的空间
			_pStr = NULL;
			delete _pcount;//销毁引用变量的空间
			_pcount = NULL;
		}
	}
private:
	char* _pStr;
	int* _pcount;//几个空间就有几个计数
};
 void FunTest()
{
	String s1("haha");
	String s2(s1);
	String s3;
}
int main()
{
	FunTest();
	return 0;
}

若是少释放一个空间会导致错误,而且会存在空间的浪费。解决方案:把两个空间给成一个连续的空间,上面保存引用计数,下面保存字符串。指针向前倒4个字节即可以释放。代码如下:

class String
{
public:
	String(const char *pStr = " ")
 	{
		//存放字符串的空间
		if (NULL == pStr)
		{
			_pStr = new char[1+4];
			_pStr += 4;//引用计数占前四个字节,真正开始,得从加上四个字节之后。
			_pStr = new char('\0');
		}
		else{
			//1、开辟空间
			_pStr = new char[strlen(pStr) + 1+4];
                        _pStr += 4;
			//2、把内容拷贝到新空间里面
			strcpy(_pStr, pStr);
		}
		//放引用计数,空间首地址得位置,_pStr向前偏移四个字节
		GetRef() = 1;
	}
	//拷贝构造函数
	String(const String& s)
		:_pStr(s._pStr)
	{
		GetRef()++;
	}

	//赋值运算符的重载
	String& operator=(const String&s)
	{
		//考虑是否为当前对象,若是共用同一块空间,改变引用计数的值;
		//没有共用一块空间,则将引用计数减1,减1后若是为0,则释放原空间。,指向新空间,将新空间里面的计数加1
		//如果两个对象共用一块空间,可以认为是自己给自己赋值。
		//如果地址不相同,则当前对象和s没有共用同一块空间
		if (_pStr != s._pStr)
		{
			Release();
			//共用同一块空间、共用同一个引用计数
			_pStr = s._pStr;
			++GetRef();//引用计数增加
		}
		return *this;
	}
	~String()
	{
		Release();
	}
private:
	//获取引用计数
	int &GetRef()
	{
		return *((int *)_pStr - 4);
	}
	//释放得函数
	void Release()
	{
		if (_pStr && 0 == --GetRef())
		{
			_pStr -= 4;
			delete[] (_pStr-4);//销毁字符串的空间
			_pStr = NULL;
		}
	}
private:
	char* _pStr;
 };
void FunTest()
{
	String s1("haha");
	String s2(s1);
	String s3;
	s3 = s2;
}
int main()
{
	FunTest();
	return 0;
}

(6)写时拷贝(COW):

class String
{
public:
	String(const char *pStr = " ")
 	{
		//存放字符串的空间
		if (NULL == pStr)
		{
			_pStr = new char[1+4];
			_pStr += 4;//引用计数占前四个字节,真正开始,得从加上四个字节之后。
			_pStr = new char('\0');
		}
		else{
			//1、开辟空间
			_pStr = new char[strlen(pStr) + 1+4];
			_pStr += 4;
			//2、把内容拷贝到新空间里面
			strcpy(_pStr, pStr);
		}
		//放引用计数,空间首地址得位置,_pStr向前偏移四个字节
		GetRef() = 1;
	}
	//拷贝构造函数
	String(const String& s)
		:_pStr(s._pStr)
	{
		GetRef()++;
	}
	//普通成员函数,不能调用const修饰得
	char &operator[](size_t index)
	{
		return _pStr[index];
	}
	const char &operator[](size_t index)const
	{
		return _pStr[index];
	}
	//赋值运算符的重载
	String& operator=(const String&s)
	{
		//考虑是否为当前对象,若是共用同一块空间,改变引用计数的值;
		//没有共用一块空间,则将引用计数减1,减1后若是为0,则释放原空间。,指向新空间,将新空间里面的计数加1
		//如果两个对象共用一块空间,可以认为是自己给自己赋值。
		//如果地址不相同,则当前对象和s没有共用同一块空间
		if (_pStr != s._pStr)
		{
			Release();
			//共用同一块空间、共用同一个引用计数
			_pStr = s._pStr;
			++GetRef();//引用计数增加
		}
		return *this;
	}
	~String()
	{
		Release();
	}
private:
	//获取引用计数
	int &GetRef()
	{
		return *((int *)_pStr - 4);
	}
	//释放得函数
	void Release()
	{
		if (_pStr && 0 == --GetRef())
		{
			_pStr -= 4;
			delete[] _pStr;//销毁字符串的空间
			_pStr = NULL;
		}
	}
private:
	char* _pStr;
 };
void FunTest()
{
	String s1("haha");
	String s2(s1);
	String s3;
	s3 = s2;
	const String s4("world");
	s4[0];
}
int main()
{
	FunTest();
	return 0;
}

但是此程序存在问题,如果s1和s2和S3共用同一个空间,修改一个三个都会发生改变,仍然为一个浅拷贝。如下测试代码,会导致s1和s2,S3同时发生改变。

void FunTest()
{
	String s1("haha");
	String s2(s1);
        String s3;
        String s4(s3);
        s3=s1;
	s1[0] = 'w';
}

为了优化上述代码,我们要改变哪一个,则将该成员分离出来。我们改变重载下标函数中的内容如下:

char &operator[](size_t index)
	{
		//引用计数大于1,当前对象一定和别人共用同一块引用空间,
		if (GetRef()>1)
		{
			//分离当前对象,即引用计数减1
			--GetRef();
			//给当前对象重新开辟空间,用当前对象构造
			String strTmp(_pStr);
			//把当前对象的字符串赋值为空
			_pStr = NULL;
			Swap(_pStr, strTmp._pStr);
		}
		return _pStr[index];
	}

采用测试代码如下,会发生错误:

void FunTest()
{
	String s1("haha");
	String s2(s1);
	String s3;
	String s4(s3);
	s3 = s1;
	s1[0] = 'w';
	const String s5(s4);
	s5[0];
}

这是因为const类型的函数不能调用普通类型的成员函数。解决方法,再重载一个const类型的下标运算符

	char &operator[](size_t index)
	{
		//引用计数大于1,当前对象一定和别人共用同一块引用空间,
		if (GetRef()>1)
		{
			//分离当前对象,即引用计数减1
			--GetRef();
			//给当前对象重新开辟空间,用当前对象构造
			String strTmp(_pStr);
			//把当前对象的字符串赋值为空
			_pStr = NULL;
			Swap(_pStr, strTmp._pStr);
		}
		return _pStr[index];
	}
//const类型的引用的返回值一般都为const,因为当前对象不能改变
	const char &operator[](size_t index)const
	{
		return _pStr[index];
	}
此String类不适用于多线程

总结:

1、浅拷贝:位拷贝、拷贝构造函数、赋值运算符重载

         多个对象共用同一块资源,当对象被销毁时,同一块资源会被释放 

         多次引起程序崩溃,或者内存泄漏

解决方式:

(1)深拷贝---每个对象共同拥有自己的资源,必须程序员显示提供拷贝构造函数以及赋值运算符提供:A、普通版本深拷贝 B、简洁版深拷贝---拷贝(调用构造函数创建临时变量,用临时变量和当前对象交换成员变量)。这种方式不易出错,建议使用。

                   ---赋值

(2)引用计数---普通成员变量(每个对象中各有一份,改变自己的,不能改变其他人的 不行)

                      ----静态成员变量(所有的对象所共享,只要调用构造函数,计数置为1;被当前对象一个人使用,不行)

                      ----整型的指针

2、一些常见的String类的功能:

(1)构造空的字符串

string ( );

(2)拷贝构造函数(类类型对象的引用)

string ( const string& str );

(3)构造函数(不是用字符串中所有的内容构造当前对象,而是pos表示从pos位置开始向后取几个字符;npos取得字符得个数。

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

(4)构造函数(用几个字符来构造,n代表有效字符得个数)

string ( const char * s, size_t n );

(5)用字符串直接构造当前对象

string ( const char * s );

(6)字符串构造好了,里面放了n个字符c

string ( size_t n, char c );

(7)用一段区间构造,

template<class InputIterator> string (InputIterator begin, InputIterator end);

(8)获取字符串得长度:size或者length

(9)resize:把当前字符串中有效元素得个数改变成n个字符,char c说明把n个字符中多出来得用c补齐。

void resize ( size_t n, char c );
void resize ( size_t n );相当于调用上一个函数,少传递了第二个参数

(10)capacty:底层空间得实际大小

(11)reserve:把当前字符串底层空间得容量改变,容量既可以增大,又可以减小

(12)clear:清空;调用后成为空字符串

(13)下标运算符得重载:传递要获取地方得字符

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

(14)at:返回这个位置得字符

(15)append:拼接

(16)push_back:尾插

(17)assign:给字符串重新赋值(可以用一个字符串给另一个字符串赋值哦)【还可以用c格式得字符串进行赋值,相当于重写了一遍】

string& assign ( size_t n, char c );把一个字符串赋值为c

(18)insert:任意位置得插入字符或者字符串

string& insert ( size_t pos1, const string& str );

从当前位置pos1开始插入一个字符串;

 string& insert ( size_t pos1, size_t n, char c );

从当前位置开始插入n个字符c

(19)erase:删除


 string& erase ( size_t pos = 0, size_t n = npos );

从当前位置开始删除多少字符

如果两个参数都没有传,相当于清空,把所有得元素都删除

(20)replace:替换某一个位置得字符

(21)swap:对两个字符串进行交换

(22)c_str:C语言格式得字符串

const char* c_str ( ) const;

(23)copy:拷贝

find:查找

rfind:反响查找:从后往前查找

find_first_of:查找第一次出现得

find_last_of:查找最后一次出现得

substr:子串

strcmp:比较

此后就不一一重复介绍了,详细可以查看资料得意


猜你喜欢

转载自blog.csdn.net/xuruhua/article/details/80790050
今日推荐