1. 程式人生 > >String(深淺拷貝、字串增刪查改、寫時拷貝)

String(深淺拷貝、字串增刪查改、寫時拷貝)

淺拷貝與深拷貝
所謂淺拷貝是指將物件的每一個值拷貝給另一個物件那麼就存在兩個問題:
如果是動態申請的空間值拷貝後會對該空間析構兩次,程式崩潰;
一個物件值的改變會用影響另一個物件。

class String  //管理字串的類
{
public :
char *c_str()
	{
		return _str;
	}
	~String() //解構函式
	{
		if (_str)
		{
             delete[] _str;
			 _str = NULL;
		}	
	}
private :
	char *_str;
};
String(char *str)//不帶參建構函式
	 :_str(str)  //淺拷貝
	{

	}

str2是動態開闢,可修改並且不會影響物件s1。
深拷貝

String()//物件裡是空串
		:_str(new char[1])  //為了與delete[]匹配
	{
		_str[0] = '\0';
	}
	String(char *str = "")//預設建構函式,隱含有'\0';不能給'\0'.型別不匹配 
		:_str(new char[strlen(str)+1])//深拷貝,先開一樣大的空間,+1為了存'\0'
	{
		strcpy(_str, str);//再拷貝
	}
	//上述兩個建構函式只能存在一個,否則對於無參函式,無法識別呼叫哪一個建構函式

插入圖片
總結:深拷貝即先開一樣大空間再拷貝。
拷貝構造、賦值和operator[ ]

   String(const String& s)//拷貝構造
		:_str(new char[strlen(s._str)+1])//先開一樣大空間
	{
		strcpy(_str, s._str);//拷貝
	}
	String& operator=(const String &s)//賦值
	{ //可以用void返回,但用string& 返回是為了連續賦值
		if (this != &s)//防止自己給自己賦值,如果自己給自己賦值,delete[]後,將變為隨機值
		{
			delete[]_str;//先將物件原空間釋放
			_str = new char[strlen(s._str) + 1];
			strcpy(_str, s._str);
		}
		return *this;
	}
	size_t Size()//計算串大小
	{
		return strlen(_str);
	}
	char& operator[](size_t pos)//operator[]可以像陣列一樣訪問元素
	{
		assert(pos < Size()); //要呼叫Size()函式,可以考慮用空間換時間
		return _str[pos];//出了作用域還存在,用&返回
	}

插入圖片
現代寫法(拷貝、賦值)
現代寫法即剝削,剝削其他變數。

class String
{
public:
	String(char *str = "")//預設建構函式,隱含有'\0';不能給'\0'.型別不匹配 
		:_str(new char[strlen(str) + 1])//深拷貝,先開一樣大的空間
	{
		strcpy(_str, str);//再拷貝
	}

	String(const String& s)//拷貝構造
		:_str(NULL)//初始化為空,保證在交換後tmp._str不為隨機值,可以析構
	{
		String tmp(s._str);//構造,使tmp.str=s._str
		swap(_str, tmp._str);//庫裡有swap函式,this->_str剝削tmp._str	
	}
	String& operator=(const String &s)//賦值
	{
		//不用判斷this != &s,因為析構tmp對this沒有影響
		String tmp(s._str);//構造 一樣大的空間但不指向同一段空間
		swap(_str, tmp._str); //剝削
		return *this;
	}

	//還可寫為:
	String& operator =( String s)//物件s是臨時變數,相當於上例的tmp
	{
		swap(_str, s._str);//剝削
		return *this;
	}

	~String() //解構函式
	{
		if (_str)
		{
			delete[] _str;
			_str = NULL;
		}

	}
	char *c_str()
	{
		return _str;
	}
private:
	char *_str;
};

字串類增刪查改

class String1
{
public:
	String1(char *str = "") //構造
	{
		_capacity = strlen(str);//_capacity不包括'\0'
		_size = _capacity;
		_str = new char[_capacity + 1];//+1存'\0'
		strcpy(_str, str);
	}
	//拷貝構造現代寫法
	String1(const String1& s)//拷貝構造 剝削
		:_str(NULL) //需要初始化為空,交換後,tmp._str為空
		,_size(0)
		,_capacity(0)
	{
		String1 tmp(s._str);//構造tmp
		swap(_str, tmp._str);//剝削
		swap(_size, tmp._size);
		swap(_capacity, tmp._capacity);
	}
	String1(const String1& s)//拷貝構造
	{
		_capacity = strlen(s._str);
		_size = _capacity;
		_str = new char[_capacity + 1];//+1存'\0'
		strcpy(_str, s._str);
	}

	String1& operator=(String1 s)//賦值 s是臨時變數
	{
		swap(_str, s._str);
		swap(_size, s._size);
		swap(_capacity, s._capacity);
		return *this;
	}

	bool operator < (const String1& s)//小於運算子過載
	{
		const char *str1 =this->_str;
		const char *str2 = s._str;
		while (*str1&&*str2)
		{
			if (*str1 < *str2)
				return true;
			else if (*str1 > *str2)
				return false;
			else
			{
				str1++;
				str2++;
			}
		}
		if (*str2)//這時*str1='\0'
		{
			return true;
		}
		else //*str2='\0' 或者*str1=*str1='\0'
			return false;
	}

	bool operator==(const String1& s)
	{
		const char *str1 =this->_str;
		const char *str2 = s._str;
		while (*str1&&*str2)
		{
			if (*str1 == *str2)
			{
				str1++;
				str2++;
			}
			else
				return false;
		}
		if (*str1 == '\0'&& *str2 == '\0')
			return true;
		else
			return false;
	}

	bool operator <= (const String1& s)
	{
		return ((*this < s) || (*this == s));
	}

	bool operator > (const String1& s)// >
	{
		return !(*this <= s);
	}

	bool operator >= (const String1& s)
	{
		return !(*this < s);
	}
	bool operator != (const String1& s)
	{
		return !(*this == s);
	}
	//注:運算子過載進行賦用時,沒有函式壓棧,即沒有呼叫函式,由於是類內函式,預設為行內函數,直接展開
	void Expand(size_t n)
	{
		if (n > _capacity)
		{
			char *tmp = new char[n + 1];//多開一個存'\0'
			strcpy(tmp, _str);//需要將原資料拷到新空間
			delete[]_str;
			_str = tmp;
			_capacity = n; //_capacity不包括'\0'
		}
	}
	void PushBack(char ch)//尾插
	{
		//if (_size == _capacity) //需要增容
		//{
		//	Expand(_capacity * 2);//開2倍空間 即this->Append(_capacity * 2)
		//}
		//_str[_size] = ch;
		//_size++;
		//_str[_size] = '\0';//注意加上'\0'
		Insert(_size, ch);//直接用插入
	}

	void Append(const char *str)//拼接字串
	{
		//size_t len = strlen(str);
		//if (_size + len > _capacity)//==時剛剛好,因為_capacity不算'\0'
		//{
		//	Expand(_size + len);
		//}
		//strcpy(_str + _size, str);//strcpy有拷貝'\0'
		//_size += len;
		Insert(_size, str);//直接用插入
	}

	String1 operator+(const char* str) //+運算子過載
	{
		String1 tmp(*this); //拷貝構造
		tmp.Append(str);
		return tmp; //拷貝構造
	}
	String1& operator+=(const char* str) //+=運算子過載
	{
		Append(str); //即this->Append(str)
		return *this;
	}

	void Insert(size_t pos, char ch)//在座標pos處插入ch
	{
		assert(pos <= _size);//等於就是尾插
		if (_size == _capacity)
		{
			Expand(_capacity * 2);
		}
		int end = _size;//_str[_size]是'\0'
		//將資料向移
		while (end >= (int)pos)
			//需要將pos強轉為int,否則型別提升,當pos=0,end不會小於0,陷入死迴圈
		{
			_str[end + 1] = _str[end];
			end--;
		}
		_str[pos] = ch;
		_size++;
	}

	void Insert(size_t pos, const char* str)//在座標pos處插入字串str
	{
		assert(pos <= _size);//等於時是拼接
		size_t len = strlen(str);
		if (_size + len > _capacity) //== 時剛剛好,因為_capacity不算'\0'
		{
			Expand(_size + len);
		}
		int end = _size;
		while (end >= (int)pos)
		{
			_str[end + len] = _str[end];
			end--;
		}
		for (size_t i = 0; i < len; i++)
		{
			_str[pos++] = str[i];
		}
		_size += len;
	}
	void Erase2(size_t pos, int len = 1)//在座標為pos位置刪除長度為len字串
	{
		assert(pos < _size);
		int cur = pos;
		while (_str[cur + len])
		{
			_str[cur] = _str[cur + len];
			cur++;
		}
		_str[cur] = '\0';
		_size -= len;
	}
	void Erase1(size_t pos, int len = 1)//在座標為pos位置刪除長度為len字串
	{
		assert(pos < _size);
		int end = pos + len;
		int tail = _size - pos - len;
		for (int i = 0; i <= tail; i++)
		{
			_str[pos] = _str[end];
			pos++;
			end++;
		}
		_size -= len;
	}
	int Find(const char* sub)//查詢子串第一次出現的位置
	{
		int subindex = 0;
		int strindex = 0;
		int cur = strindex;
		int sublen = strlen(sub);
		while (cur< _size)
		{
		    strindex = cur;
			subindex = 0;
			while (subindex < sublen)
			{
				if (_str[strindex] == sub[subindex])
				{
					strindex++;
					subindex++;
				}
				else
					break;
			}
			if (subindex == sublen)
				return cur;
			else
				cur++;
		}
		return -1;	//沒有找到
	}
	
	~String1() //解構函式
	{
		if (_str)//為空就不析構
		{
			delete[] _str;
			_str = NULL;
		}
	}

	char *c_str()
	{
		return _str;
	}
private:
	char *_str;
	size_t _size;//標識大小 否則要用strlen,以空間換時間
	size_t _capacity; //標識容量,這樣可以一次開合理空間,否則一次開一次,以空間換時間
};

字串增刪查改測試用例

void Test4()
{
	String1 s1("hello");
	cout << s1.c_str() << endl;
	String1 s2(s1);
	String1 s3;
	s3 = s2;
	s3.PushBack('m');
	cout << s3.c_str() << endl;//hellom
	s3 = s1 + " world";
	cout << s3.c_str() << endl;//hello world
	s1 += " world";
	cout << s1.c_str() << endl;
	//在+運算子過載中,拷貝構造了兩次,對於字串來說拷貝構造代價較大,
	//而+=運算子過載中,沒有拷貝構造,所以一般將+運算子過載寫成下列形式
	//若給s3加" happy",下列形式只有一次拷貝構造
	String1 s4(s3);//拷貝構造
	s4 += " happy";
	cout << s4.c_str() << endl; //hello world happpy
	/*s3 += "happy everday";
	if (s3 != s4)
		printf("yes\n");
	else
		printf("no\n");*/
	s4.Insert(2, 'm');
	cout << s4.c_str() << endl;
	s4.Insert(3, "pick");
	cout << s4.c_str() << endl;
	s4.Erase1(8, 3);
	cout << s4.c_str() << endl;
	cout << s4.Find("happy") << endl;
	cout << s4.Find("pick") << endl;
	cout << s4.Find("world") << endl;
	cout << s4.Find("hello") << endl;

}

這裡寫圖片描述
擴充套件Resize和Reserve
兩個函式均為類內函式

void Reserve(size_t n)//增容到n
	{
		if (n > _capacity)
		{
			Expand(n); //Expand一般為私有
		}
	}
	void Resize(size_t n, char ch = '0')//將容量設定為n,即可以縮容,也可以擴容,並且初始化
	{
		if (n < _size)
		{
			_size = n;//直接將_size設定為n
			_str[_size] = '\0';//注意是字串
		}
		else // _size<n<_capacity  或者n>_capacity
		{
			if (n > _capacity) //若n>_capacity,先增容
				Expand(n);
			//現在_size<n<_capacity  或者n>_capacity初始化邏輯一樣
			for (size_t i = _size; i < n; i++)
			{
				_str[i] = ch;
			}
			_size = n;
			_str[_size] = '\0';
		}
	}

有一個面試題為將0-N的數字拼接為字串?
插入圖片

void Test5()
{
	const size_t N = 10000;
	char buff[128];
	int begin1 = GetTickCount();
	String1 s1;
	for (size_t i = 0; i < N; i++)
	{
		_itoa(i, buff, 10);//將整形轉為十進位制字串
		s1 += buff; //依次將字串加在後面
	}
	int end1= GetTickCount();
	cout << end1 - begin1 << endl;//20313 也就是20秒
	//將大部分時間用在+=的開空間上了,如果我們能預先將空間開好,時間將會大大縮短 
	int begin2 = GetTickCount();
	String1 s2;
	s2.Reserve(1 * 9 + 2 * 90 + 3 * 900 + 4 * 9000 + 5 * 90000 + 1);
	//在這裡根據N直接定死空間,可以寫演算法表示這個公式開合適空間
	for (size_t i = 0; i < N; i++)
	{
		_itoa(i, buff, 10);//將整形轉為十進位制字串
		s2 += buff; //依次將字串加在後面
	}
	int end2= GetTickCount();
	cout << end2 - begin2 << endl; //31 即還沒有1秒
}

寫時拷貝
儘管深拷貝可以解決多次析構同一空間造成程式崩潰、一個改變會另一個,但是深拷貝需要頻繁的開空間;寫時拷貝是淺拷貝,但是有一個變數(count)記錄該空間有多少物件指向,可以解決多次析構同一空間。
插入圖片
接下來將會從count型別為靜態,malloc,和_str共用空間具體分析。
_count為static int

class String
	{
	public:
		String( const char* str=" ")//建構函式
			:_str(new char[strlen(str)+1])
		{
			strcpy(_str, str);
			_count = 1;//構造一個即該空間有一個物件指向
		}
		String(const String& s)//寫時拷貝
			:_str(s._str)//淺拷貝
		{
			_count++;
		}
		~String()
		{
			if (--_count == 0)//表明是最後一個物件
			{
				delete[]_str;
				printf("析構\n");
			}
		}
	private:
		char *_str;
		static int _count;
	};

	int  String::_count = 0;//靜態變數在類外初始化

插入靜態count圖片
只會析構一次,由監視可以看到_count的變化,_count是公用的。
_count是malloc

class String 
	{
	public:
		String(const char* str = " ")//建構函式
			:_str(new char[strlen(str) + 1])
			, _pcount(new int(1))//申請空間並初始化為1
			{
				strcpy(_str, str);
			}
		String(const String& s)//寫時拷貝
			:_str(s._str)
			,_pcount(s._pcount)
		{
			(*_pcount)++;
		}
		~String()
		{
			if (--(*_pcount) == 0)
			{
				delete[] _str;
				delete _pcount;
				printf("析構\n");
			}
		}
	private:
		char* _str;
		int * _pcount;
	};

析構兩次
插入圖片
count和_str在同一空間

class String
	{
	public:
		int& GetRefCount() //出了作用域還存在
		{
			return *((int*)(_str - 4));//找到計數
		}
		String(const char* str = "")//構造
			:_str(new char[strlen(str)+5]) //+5:其中
		{
			_str += 4;
			strcpy(_str,str);
			GetRefCount() = 1;
		}
		String(const String& s)//寫時拷貝
			:_str(s._str)
		{
		   GetRefCount()++;
		}
		String& operator = (String& s)//賦值
		{
			if (_str != s._str)
			{
				if (--GetRefCount() == 0)
				{
					delete[] (_str-4); //需要將前面計數空間釋放
				}
				_str = s._str;
				++GetRefCount();
			}
			return *this;
		}
		~String()
		{
			if (--GetRefCount() == 0)
			{
				delete[](_str - 4);
				printf("析構\n");
			}

		}
	private:
		char* _str;
	};

插入圖片
解決淺拷貝一個修改影響另一個
由於淺拷貝共用一個空間,一個改變會改變另一個,所以當要“寫”時,如果該空間有多個物件指向則重開空間。

void CopyOnWrite()
		{
			if (GetRefCount() > 1)
				//證明該段空間還有其他物件,不能隨便寫,就要為該物件重新開空間
			{
				String tmp(_str); //tmp出作用域會調解構函式
				swap(_str, tmp._str);//而在交換後,tmp析構時就將寫之前的計數器減1
				/*--GetRefCount();
				char *tmp = new char[strlen(_str) + 5];
				strcpy(tmp + 4, _str);
				_str = tmp + 4;
				GetRefCount() = 1;*/
			}
		}

		char& operator[](size_t pos)
		{
			CopyOnWrite();
			return _str[pos];
		}

這裡寫圖片描述
對於寫時拷貝也有字串的增刪查改,第一醫院增刪改需要新增CopyOnWrite();查詢時不用。
在寫時拷貝中,只有需要“寫”時才開空間,“讀”不開空間。