【C++】String中的深淺拷貝問題;深拷貝的兩種寫法(傳統寫法、現代寫法)

一、淺拷貝問題

淺拷貝

首先定義一個Sting類,它包含一個成員變量,一個char*的指針。 

namespace CPP
{
	class String
	{
        public:
	private:
		char* _str;
	};
}

對於String類的拷貝構造函數及operator=函數來說,當用一個String對象拷貝構造或賦值給另一個String對象時,就是將這個對象裏的指針的值賦值給另一個對象裏的指針。將一個指針值賦值給另一個指針,就會使得兩個指針指向同一塊空間,這就產生了淺拷貝(值拷貝)。 

namespace CPP
{
	class String
	{
	public:
		//拷貝構造---淺拷貝
		String(const String& s)
			:_str(s._str)
		{}
		//賦值
		String& operator=(const String& s)
		{
			if (this != &s)
			{
				_str = s._str;
			}
			return *this;
		}
		//析構函數
		~String()
		{
			if (_str)
			{
				delete[] _str;
			}
		}
	private:
		char* _str;
	};
	void TestString1()
	{
		String s1("hello");
		String s2(s1);//未寫拷貝構造函數時,使用默認的,爲淺拷貝(值拷貝)
		cout << s2.c_str() << endl;

		String s3("world");
		s2 = s3;
		cout << s2.c_str() << endl;
	}
}

這時其實程序已經崩潰了。 問題在於以下兩個方面:

① 兩個(或兩個以上)指針指向同一塊空間,這個內存就會被釋放多次;(例如下面定義了一個String對象s1,以淺拷貝的方式拷貝構造了一個String對象s2,則s1和s2裏面的指針_str就會指向同一塊空間;當出了作用域,s2先調用析構函數,而上面代碼中析構函數裏面進行了空間的釋放,也就是這個空間就會被s2釋放,接下來會調用s1的析構函數,也會去釋放這塊空間,釋放一塊已經不屬於我的空間就會出錯) 

② 另一方面,當兩個指針指向同一塊空間時,一旦一個指針修改了這塊空間的值,另一個指針指向的空間的值也會被修改。 

二、深拷貝及其兩種寫法(傳統、現代)

1.傳統寫法

若用一個s2對象拷貝構造給s3對象,s3(s2)以及s4賦值給s3,s3=s4,當涉及到淺拷貝的問題時:

  • 對於拷貝構造函數來說,s3先開一塊和s2一樣大的空間;然後把s2的內容原封不動的拷貝下來,再讓s3的_str指向這段新開的空間。出了作用域,各自釋放各自的。
  • 而對於賦值運算符重載函數來說s3已經存在,則必須先釋放s3的空間然後才讓s3開與s4一樣大的空間(否則就會導致s3裏面的指針沒有釋放),然後再把s4拷貝過去。

 

 

優點:直觀。

//深拷貝--傳統寫法
		//s3(s2) s3是this指針,s2是s
		String(const String& s)
		{
			_str = new char[strlen(s._str) + 1];//_str是this指針,新開一個s2大小的空間
			strcpy(_str, s._str);//把s2中的各個字節拷貝給this(s3)
		}

		//賦值 s3=s4
		String& operator=(const String& s)
		{
			if (this != &s)//判斷不要自己給自己賦值
			{
				delete[] _str;//因爲不知道空間大小,爲避免內存泄漏,先釋放掉s3原來的空間
				_str = new char[strlen(s._str) + 1];
				strcpy(_str, s._str);
			}
			return *this;
		}
void TestString1()
	{
                String s2("hello");
		String s3(s2);//未寫拷貝構造函數,使用默認的,即爲淺拷貝(值拷貝)
		cout << s3.c_str() << endl;

		String s4("world");
		s3 = s4;
		cout << s3.c_str() << endl;
	}

 

2.現代寫法(推薦)

調用其他的函數來實現自己的功能

  • 本質:讓別人去開空間,去拷數據,而我將你的空間與我交換就可以。 
  • 實現:例如用s2拷貝構造一個s3對象s3(s2),可以通過構造函數將s2裏的指針_str構造一個臨時對象tmp(構造函數不僅會開空間還會將數據拷貝至tmp),此時tmp就是我們想要的哪個對象,然後將新tmp的指針_ptr與自己的指針進行交換。
  • 至於賦值,就可以直接調用swap()交換~ 

對於構造函數來說,因爲String有一個帶參數的構造函數,則用現代寫法寫拷貝構造時可以調用構造函數,而對於沒有無參的構造函數的類只能採用傳統寫法(開空間然後拷數據)。

//深拷貝--現代寫法
		//s3(s2) tmp開闢一個和s2相同的空間,拷過來,s2是s
		String(const String& s)
			:_str(nullptr)
		{
			String tmp(s._str);//調構造
			swap(_str, tmp._str);
		}

		////賦值 s3=s2
		//String& operator=(const String& s)
		//{
		//	if (this != &s)//判斷不要自己給自己賦值
		//	{
		//		String tmp(s._str);
		//		strcpy(_str, tmp._str);
		//	}
		//	return *this;
		//}

		//更簡單的賦值
		String& operator=(String s)
		{
			swap(_str, s._str);
			return *this;
		}
void TestString1()
	{
		
		String s2("hello");
		String s3(s2);//未寫拷貝構造函數,使用默認的,即爲淺拷貝(值拷貝)
		cout << s3.c_str() << endl;

		String s4("world");
		s3 = s4;
		cout << s3.c_str() << endl;
	}

優點:簡潔+複用

 

最後本文涉及代碼如下:

String.h

#define _CRT_SECURE_NO_WARNINGS 
#include  <iostream>
using namespace std;
#pragma once
namespace CPP
{
	class String
	{
	public:
		////無參的構造函數
		//String()
		//	//:_str(nullptr)//不可取,會有空指針問題
		//	:_str(new char[1])
		//{
		//	_str[0] = '\0';
		//}

		////帶參的構造函數
		//String(const char* str)
		//	:_str(new char[strlen(str)+1])
		//{
		//	strcpy(_str, str);// while (*dst++ = *src++)'\0'已經拷貝過去了
		//}

		//推薦使用全缺省的帶參的構造函數初始化
		String(const char* str = "")//不能給nullptr,因爲strlen會崩潰
			:_str(new char[strlen(str) + 1])
		{
			strcpy(_str, str);// while (*dst++ = *src++)'\0'已經拷貝過去了
		}

		//拷貝構造---淺拷貝
		String(const String& s)
			:_str(s._str)
		{}
		//賦值
		String& operator=(const String& s)
		{
			if (this != &s)
			{
				_str = s._str;
			}
			return *this;
		}

		//深拷貝-傳統寫法
		//s3(s2) s3是this指針,s2是s
		String(const String& s)
		{
			_str = new char[strlen(s._str) + 1];//_str是this指針,新開一個s2大小的空間
			strcpy(_str, s._str);//把s2中的各個字節拷貝給this(s3)
		}

		//賦值 s3=s2
		String& operator=(const String& s)
		{
			if (this != &s)//判斷不要自己給自己賦值
			{
				delete[] _str;//因爲不知道空間大小,爲避免內存泄漏,先釋放掉s3原來的空間
				_str = new char[strlen(s._str) + 1];
				strcpy(_str, s._str);
			}
			return *this;
		}

		//深拷貝--現代寫法
		//s3(s2) tmp開闢一個和s2相同的空間,拷過來,s2是s
		String(const String& s)
			:_str(nullptr)
		{
			String tmp(s._str);//調構造
			swap(_str, tmp._str);
		}

		////賦值 s3=s2
		//String& operator=(const String& s)
		//{
		//	if (this != &s)//判斷不要自己給自己賦值
		//	{
		//		String tmp(s._str);
		//		strcpy(_str, tmp._str);
		//	}
		//	return *this;
		//}
		//更簡單的賦值
		String& operator=(String s)
		{
			swap(_str, s._str);
			return *this;
		}

		//析構函數
		~String()
		{
			if (_str)
			{
				delete[] _str;
			}
		}

		//輸出c形式的字符串
		char* c_str()
		{
			return _str;
		}
		//計算大小size
		size_t Size()
		{
			return strlen(_str);
		}

		char& operator[](size_t pos)//返回別名,除了可讀還可寫
		{
			return _str[pos];
		}
	private:
		char* _str;
	};
	void TestString1()
	{
		String s1;
		String s2("hello");
		cout << s1.c_str() << endl;//s1是一個空對象,它的_str是一個空指針
		cout << s2.c_str() << endl;

		for (size_t i = 0; i < s2.Size(); ++i)
		{
			cout << s2[i] << " ";
		}
		cout << endl;

		String s3(s2);//未寫拷貝構造函數,使用默認的,即爲淺拷貝(值拷貝)
		cout << s3.c_str() << endl;

		String s4("world");
		s3 = s4;
		cout << s3.c_str() << endl;
	}
}

test.cpp

#define _CRT_SECURE_NO_WARNINGS 
#include <iostream>
using namespace std;
#include "String.h"

int main()
{
	CPP::TestString1();
	system("pause");
	return 0;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章