C++ Primer Plus12章 類和動態內存分配

12章 類和動態內存分配

在這裏插入圖片描述

1. 動態分配內存的原因

爲了避免大量的內存被浪費,一般採用在程序運行時,而不是編譯時,確定諸如使用多少內存的問題。

C++使用newdelete運算符來動態控制內存。

2. 在構造函數中使用new的注意事項
2.1 在構造函數中使用new,在析構函數中必須使用delete

1.爲何需要分配內存:

如果不分配內存,直接用str=s,只會保存參數字符串的地址,並沒有創建備份。

2.使用new分配內存的位置:

使用new分配的內存,位置在中;對象中僅保留了該位置的地址信息。

3.使用析構函數的原因:

當對象被刪除時,對象本身所佔用的內存被釋放,但是對象成員指針所指向的內存不會被自動釋放。因此,需要在析構函數中使用delete語句,從而在對象過期析構函數被調用時,釋放函數中new分配的內存。

4.newdelete的使用方法:

/*聲明*/
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 newdelete必須兼容。new對應deletenew []對應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.指針指向已有的對象

Shortestfirst不創建新的對象,只是指向已有的對象,因此不需要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;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章