淺析在類模版中構建成員函數時,使用memcpy產生的副作用

  一般情況下我們在對類模版中的成員函數進行構建時會經常對一些數據進行復制拷貝,而通常情況下我們都不提倡用memcpy進行拷貝,因爲在類模版中所傳進來的類型可以是內置類型也可以是非內置類型,除非你在成員函數中使用memcpy前進行類型萃取,否則它所帶來的副作用的後果也是很可怕的。memcpy在對內置類型可正常拷貝,而在對非內置類型拷貝時會出現淺拷貝的現象。

  下面我們可以通過一個簡單的順序表程序來分析memcpy對非內置類型所產生的副作用:

#include<iostream>
#include<string>
using namespace std;

template<typename T>
class SeqList
{
public:
	SeqList()                        //構造函數
		:_data(NULL)
		, _size(0)
		, _capacity(0)
	{}
	SeqList<T>& operator=(const SeqList<T>& s)           //賦值重載
	{
		if (this != &s)
		{
			
			_data = new T[s._capacity];
			_size = s._size;
			_capacity=s._capacity;
			memcpy(_data, s._data, _size*sizeof(T));
			/*int i=0;
			for (i = 0; i < _size; i++)
			{
				_data[i] = s._data[i];
			}*/
		}
		return *this;
	}
	SeqList(const SeqList<T>& s)                       //拷貝構造
		:_data(new T[s._capacity])
		, _size(s._size)
		, _capacity(s._capacity)
	{
	        memcpy(_data, s._data, _size*sizeof(T));
		/*int i = 0;
		for (i = 0; i < _size; i++)
		{
			_data[i] = s._data[i];
		}*/
	}
	~SeqList()                                            //析構函數
	{
		if (_data != NULL)
		{
			delete[] _data;
			_size = 0;
			_capacity = 0;
		}

	}
public:
	void Pushback(const T& d)
	{
		CheckCapacity();
		_data[_size] = d;
		_size++;
	}
	
public:
	void CheckCapacity()                 //容量檢測
	{
		if (_size == _capacity)
		{
			T *tmp = new T[2 * _capacity + 3];
			memcpy(tmp, _data, _size*sizeof(T));
			/*int i = 0;
			for (i = 0; i < _size; i++)
			{
				tmp[i] = _data[i];
			}*/
			delete[] _data;
			_data = tmp;
			_capacity = 2 * _capacity + 3;
		}
	}
	void Print()
	{
		int i = 0;
		for (; i < _size; i++)
		{
			cout << _data[i] << " ";
		}
		cout << endl;
	}
private:
	T*_data;
	int _size;
	int _capacity;
};

上面是一個簡單的順序表的模版,當我們將上模板中的拷貝構造,賦值重載和容量檢測都用memcpy實現時,並且我們給出下面的測試主函數(此時我們使用的是內置類型):

int main()
{
	SeqList<int> s;
	s.Pushback(1);
	s.Pushback(2);
	s.Pushback(3);
	s.Pushback(4);
	s.Print();
	SeqList<int> s3(s);
	s.Print();
	SeqList<int> s2;
	s2 = s;
	s2.Print();
	system("pause");
	return 0;
}

下面是運行結果:

wKioL1bykzbjOKaRAABJWn1QQrQ244.png

此時程序並沒有出現異常,而當我們將其測試主函數改爲string時,如下:

int main()
{
	SeqList<string> s;
	s.Pushback("111");
	s.Pushback("222");
	s.Pushback("333");
	s.Pushback("444");
	s.Print();
	SeqList<string> s3(s);
	s.Print();
	SeqList<string> s2;
	s2 = s;
	s2.Print();
	system("pause");
	return 0;
}

此時出現的情況是:

wKiom1bylWbyvRYBAAB_i4kZtA0263.png

沒錯! 程序崩潰了!

而且不論你使用上述三個成員函數中任意一memcpy,程序都會崩潰!

由於我使用的是VS2013;程序崩潰後的光標時打到析構函數處,因此比較容易想到是出現了淺拷貝的狀況(由於不同版本編譯器會出現不同的狀況,所以此文的分析是在VS2013這個版本基礎上建立的

  下面就以容量檢測這個成員函數爲例進行一個具體的分析,當我們進行尾插時,只有當我們插入的數據超過_capacity,纔會進行增容處理,因爲我給的初始容量是三個,所以當進行第四個插入時纔會進入到增容函數中去,下圖就是當進入到增容函數中後出現的狀況:

wKioL1bynCTQbVJTAAE7EXNiTf0951.png

可以看見當進去後執行完memcpy後,_data所指向的是一個地址,而當我把這個地址拿到後發現一個很神奇的現象,這個地址放的竟然是它:

wKiom1bynP6jBQAwAAA3JOmF1RM326.png

沒錯,是_data的地址!

然後我又看了tmp的地址,如下圖:

wKioL1bynkbQGfUkAABHDzKGQ4Q822.png

到這裏基本上就清楚了,tmp和_data是指向了同一份地址,而這個地址又是_data的,因此在上面的大圖中,當_data被釋放掉時,tmp裏面還是_data的地址,是一個已經被釋放的空間地址,而在析構函數中你又釋放了一次,因此程序會在析構處崩潰,這也是memcpy所帶來的副作用——淺拷貝現象!

  而當你將下面的for循環放開,然後將memcpy註釋掉,問題就解決了,在這是因爲這裏用的是string的operator=進行的賦值操作。

  這裏還要說明的一點是,因爲不同版本編譯器對string的構造並不一樣,因此我才一直在強調編譯的環境,可能其他的版本出現的現象並不一樣,但其原理是沒有改變的!因此在以後的類模版中我們要儘量避免使用memcpy。

  本文若有不足之處,請讀者在留言之處提醒,謝謝!

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