12章 類和動態內存分配
1. 動態分配內存的原因
爲了避免大量的內存被浪費,一般採用在程序運行時,而不是編譯時,確定諸如使用多少內存的問題。
C++使用new
和delete
運算符來動態控制內存。
2. 在構造函數中使用new的注意事項
2.1 在構造函數中使用new
,在析構函數中必須使用delete
。
1.爲何需要分配內存:
如果不分配內存,直接用str=s
,只會保存參數字符串的地址,並沒有創建備份。
2.使用new
分配內存的位置:
使用new
分配的內存,位置在堆中;對象中僅保留了該位置的地址信息。
3.使用析構函數的原因:
當對象被刪除時,對象本身所佔用的內存被釋放,但是對象成員指針所指向的內存並不會被自動釋放。因此,需要在析構函數中使用delete
語句,從而在對象過期析構函數被調用時,釋放函數中new
分配的內存。
4.new
和delete
的使用方法:
/*聲明*/
private:
char * str; //字符串指針
int len; //字符串長度
static int num_strings; //對象的個數
/*構造函數使用new*/
StringBad::StringBad(const char* s)
{
len = strlen(s);
str = new char[len+1]; //分配內存
strcpy(str, s);
num_strings++; //設置對象的個數
}
/*析構函數使用delete*/
StringBad::~StringBad()
{
--num_strings; //設置對象的個數
delete [] str; //釋放內存
}
2.2 new
與delete
必須兼容。new
對應delete
,new []
對應delete []
。
如果析構函數使用delete []
,那麼默認構造函數即使只對字符串賦空值,也要使用new []
:
String::String()
{
len = 0;
str = new char[1];
str[0] = '\0'; //等同於str = nullptr;
}
2.3 多個構造函數必須使用相同的方式使用new
。
2.4 應定義一個複製構造函數,通過深度複製將一個對象初始化爲另一個對象。
1.複製構造函數的形式:StringBad(const StringBad &);
如果沒有定義的話,C++會自動提供複製構造函數。
2.複製構造函數的調用時機:
一般來說是用現有的對象初始化一個新的對象時:
//將新對象顯式地初始化爲現有的對象
StringBad string1(string2); //1
StringBad string1 = string2; //2
StringBad string1 = StringBad(string2); //3
StringBad *pStringBad = new StringBad(string2); //4
//函數按值傳遞對象,或函數返回對象時
void callme2(StringBad sb) {} //5
{return sb;}
第2或3種,可能會用複製構造函數直接創建string1
;也可能先用複製構造函數生成一個臨時對象,再將臨時對象的內容複製到string1
中。
第4種,是使用複製構造函數先創建一個匿名對象,再將新對象的地址賦值給string1
。
第5種,sb
通過複製構造函數初始化。採用複製構造函數初始化會花費時間和空間,因此一般採用引用傳遞對象。
3.默認複製構造函數的功能:
默認複製構造函數會逐個複製非靜態成員(也成爲淺複製)的值。
如果成員本身是類對象,那麼就會調用成員類的複製構造函數來複制該成員。
4.淺複製:
上圖展示了在淺複製中,數據成員被逐個複製。
淺複製可能帶來的後果是:當釋放ditto
對象時,析構函數被調用,導致str
指向的字符串Home Sweet Home
佔用的內存被釋放。在之後釋放motto
對象時,析構函數再次被調用,再次釋放str
指向的已經被釋放過的內存,可能會導致不確定的、有害的後果。
例如,使用淺複製時,函數參數按值傳遞就會出現問題:
將headline2
直接作爲函數參數來傳遞,會將headline2
逐成員複製到sb
中,包括headline2
中成員指針指向的數據地址。
當函數調用結束,局部變量sb
過期,其析構函數被調用,就會釋放掉sb
的成員指針所指向的數據。這數據也同樣是headline2
的成員指針指向的數據。
因此在headline2
過期時,調用析構函數,就會再次釋放成員指針指向的數據,導致不確定的、有害的後果。
void callme2(StringBad sb)
{
cout << sb << endl;
}
callme2(headline2); //使得析構函數被調用
5.深複製:
由於默認複製構造函數是在進行淺複製,在對象的析構函數被調用時會出現問題。因此,需要定義顯式的複製構造函數以進行深度複製。
深度複製是指,應該複製數據形成副本,並將副本的地址賦給指針成員。這樣使得每個對象都有自己的數據,而不是引用另一個對象的數據。
可以看出,深度複製的必要性在於:
一些類成員使用 new
初始化的、指向數據的指針,而不是數據本身。
6.新的複製構造函數:
StringBad::StringBad(const StringBad & st)
{
len = st.len;
str = new char [len+1];
strcpy(str, st.str);
num_strings++;
}
2.5 應定義一個賦值運算符,通過深度複製將一個對象複製給另一個對象。
1.C++會提供隱式的賦值運算實現。
2.賦值運算符的使用時機:
將已有的對象賦值給另一個對象時:
StringBad headline1("This is headline1");
StringBad headline2;
headline2 = headline1; //使用賦值運算符
StringBad headline3 = headline1; //使用複製構造函數或賦值運算符
在初始化對象時,不一定會使用賦值運算符。
3.賦值運算符的隱式實現:
賦值運算符的隱式實現也是進行淺複製,只逐個複製非靜態成員的值。因此,也會導致對同一片內存多次釋放的問題。
如果成員本身是類對象,將調用該成員類的賦值運算符來爲該成員賦值。
4.賦值運算符的重載:
深度複製版本的賦值運算符所需的工作:
1.避免自我賦值
2.釋放以前指向的內存
3.複製數據,而不是複製引用
4.返回調用對象的引用, 使得能夠實現連續賦值
StringBad & StringBad::operator=(const StringBad & st)
{
if(this == &st)
return *this;
delete [] str;
len = st.len;
str = new char[len+1];
strcpy(str, st.str);
return *this;
}
3.靜態類成員
3.1 靜態類成員的特點:
靜態類成員前有static
關鍵字。
無論創建了多少對象,程序只創建一個靜態類變量副本。
所有的類對象共享同一個靜態成員。
3.2 靜態類數據成員的初始化:
靜態成員在類聲明中聲明,在包含類方法的文件中初始化。
如果靜態數據成員是 const
整數類型或枚舉型,可以在類聲明中初始化。
初始化語句指明瞭類型、作用域解析運算符,但沒有使用關鍵字static
。
//類聲明文件中
private:
static int num_strings;
//類方法定義文件中
int StringBad::num_strings = 0;
3.3 靜態類函數成員:
1.聲明與定義:
函數聲明必須加上static
關鍵字,如果函數定義獨立於函數聲明,則函數定義不能包含關鍵字static
。
靜態成員函數只能使用靜態數據成員,因爲它不與特定的對象相關聯。
2.調用:
靜態成員函數通過類名和作用域解析運算符來調用(如果公有),不能通過對象調用,不能使用this
指針。
//函數的聲明與定義
public:
static int HowMany() {return num_strings;}
//函數的調用
int count = String::HowMany();
4.其它內容
4.1 使用 new
創建對象的過程
4.2 析構函數的調用時機
如果對象是動態變量,執行完定義該對象的代碼塊時,將調用對象的析構函數。
如果對象是靜態變量,則在程序結束時,調用對象的析構函數。
如果對象是用new
創建的,僅當顯式使用delete
刪除對象時,析構函數纔會被調用。
4.3 函數的返回對象
如果要返回局部對象,那麼必須返回對象。這種情況下,將通過複製構造函數生成返回的對象。
如果要返回一個沒有公有複製構造函數的類(如ostream
)的對象,那麼必須返回引用。
如果既可以返回對象,又可以返回對象的引用,那麼應該首選引用,因爲引用效率更高。
4.4 特殊成員函數
如果沒有定義的話,C++會自動的提供下面的成員函數:
- 默認構造函數(如果沒有提供構造函數);
大致形式StringBad::StringBad() {}
-
默認析構函數;
-
複製構造函數;
-
賦值運算符;
-
地址運算符。
隱式地址運算符返回調用對象的地址,一般與期望一致。
4.5 友元形式的比較函數
將比較函數作爲友元,便於其他類型與類類型的比較:
friend bool operator==(const String &st1, const String &st2);
對於if("love" == answer)
這樣的情況, 可以先轉換成operator==("love", answer)
,再通過只接受一個const char*
類型參數的類構造函數進行轉換,得到operator==(String("love"), answer)
。
如果不是友元函數,符號左側的操作數類型必須爲類類型,否則無法參與比較。
4.6 重載中括號運算符
將返回類型聲明爲char &
,可以對特定元素進行賦值。
char & String::operator[](int i)
{
return str[i];
}
上述方法可以實現字符的索引和修改,但沒有使用const
關鍵字,無法保證不對調用對象進行修改。
因此對於常量字符串,如果想要通過中括號進行索引,還需要提供一個新的版本:
const char & String::operator[](int i) const
{
return str[i];
}
4.7 指向對象的指針
1.指針指向已有的對象
Shortest
和first
不創建新的對象,只是指向已有的對象,因此不需要new
來分配內存,也不需要delete
來釋放內存。
String *shortest = &sayings[0];
String *first = &sayings[0];
for(i=1; i<total; i++)
{
if(sayings[i].length() < shortest->length())
shortest = &sayings[i];
if(sayings[i] < *first)
first = &sayings[i];
}
2.指針指向新創建的匿名對象
favorite
使用new
爲整個對象分配內存,調用複製構造函數創建了新對象,需要使用delete
來釋放內存。
String *favorite = new String(sayings[choice]);
delete favorite;
4.8 將鍵盤輸入行讀入String對象的方法
istream & operator(istream & is, String & st)
{
char temp[String:CINLIM];
is.get(temp, String:CINLIM);
//除去輸入失敗的情況
if(is)
st = temp; //調用重載的賦值運算符
//省略多餘字符
while(is && is.get()!='\n')
continue;
return is;
}
4.9 使用定位new
運算符
char * buffer = new char[BUFF];
JustTesting *pc1, *pc2;
//指定內存位置
pc1 = new (buffer) JustTesting;
//確保兩個內存單元不重疊
pc2 = new (buffer + sizeof(JustTesting)) JustTesting("Better Idea", 6);
//顯式地爲使用定位new運算符創建的對象調用析構函數
//需要用正確的刪除順序——與創建順序相反的順序
pc2->~JustTesting();
pc1->~JustTesting();
//釋放buffer所在區域
delete [] buffer;