一、淺拷貝問題
淺拷貝
首先定義一個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;
}