C++ 構造函數 析構函數 虛函數
我們在平時的生活中一般會總結出一些規律,早上起牀會刷牙洗臉,晚上會洗澡睡覺,這些都成了慣例。使用瓶裝調味品時先將瓶蓋打開,用完後將瓶蓋蓋上。這是一種好習慣。但是有些人不同,他們往往偷懶,一個常常不刷牙不洗臉不洗澡的人會有體味,東西放得亂七八糟的人生房間很不整潔。這些都是我們不希望看到的。當然編程中我們也不希望代碼亂七八糟。
使用一個未初始化的變量簡直就是災難,使用一個未初始化的指針將導致崩潰。這是我的忠告。在C++中初始化不會有附加的效果,不會降低效率,我們要做的是養成好習慣,產生一個對象的時候就將它初始化。 對於
Object.Init(); //初始化,構造
Object.Free(); //釋放,析構
這樣的調用並不是很困難,要記住他也不是難事,但是誰都不能保證他永遠不會忘記,更糟糕的是
Object.Init();
Object.Free();
沒有配對使用
Object.Init();
Object.Free();
Object.Free();
或
Object.Init();
Object.Init();
Object.Free();
會帶來什麼樣的結果,誰也不知道,而且這樣的錯誤,編譯器不會報錯。這是多麼可怕的錯誤,一個程序員最怕遇上的就是這樣的邏輯錯誤,它可能爲了找這樣的一個錯誤花上一整天時間。 讓我們看看有什麼好的辦法。
一個對象按時間來分析,一般有三個階段,出生,活動,死亡。與我們要做的有什麼相關之處呢,初始化,運行,釋放。很好,對照一下,我們發現在對象出生的時候初始化,死亡的時候釋放,如果這一切能用這樣的機制來操作,我們就再也不用擔心會由於忘記或錯誤的使用帶來麻煩了。
C++裏就提供了這樣的機制。使用他有個約定
class Object{
public:
Object(); //與類同名的函數,該函數沒有返回值,叫做構造函數
~Object(); //類似的,在構造函數名前加一個取反符號,叫做析構函數
};
構造函數將在對象產生的時候調用 析構函數將在對象銷燬的時候調用
調用的過程和實現方法由編譯器完成,我們只要記住他們調用的時間就行了,而且他們的調用是自動完成的,不需要我們控制。
#include <iostream>
using namespace std;
class Object{
public:
Object(){ cout < < "Object ON! " < < endl; }
~Object(){ cout < < "Object OFF! " < < endl; }
};
void main()
{
Object o;
}
運行結果
Object ON!
Object OFF!
構在函數和析構函數確實的執行了
現在我們來一個應用的例子
一個字符串類,它需要保存字符串的內容,但是它不知道字符串的大小,那麼設計這個字符串類的時候,保存字符串的成員變量就不能用固定大小的數組,而是用可以間接操作數組的指針。 #include <iostream>
#include <string.h>
using namespace std;
class string{
private:
char * data;
public:
string(){ data = NULL; }
string( char * str )
{
cout < < "Copy string: " < < str < < endl;
data = new char[ strlen(str) + 1 ];
memcpy( data , str , strlen(str) + 1 );
}
char * Data(){ return data; }
~string()
{
if( data )
{
cout < < "Free string: " < < data < < endl;
delete data;
}
}
};
void main()
{
{
string s( "abcd ");
cout < < "Show String: " < < s.Data() < <endl;
}
cin.get();
}
Copy string: abcd //執行了string::string( char * str ) 構造函數
Show String: abcd
Free string: abcd //由於在{}中產成的對象是臨時對象,它的生命期在}後就結束了,所以string::~string() 析構函數被調用
申請內存和釋放內存的操作自動完成了,構造函數和析構函數的目的在於一個類可以象普通類型一樣初始化和釋放,從而保證了封裝。 上面的例子有兩個構造函數,這麼什麼大不了的,我們看過《面面俱到----重載》得都知道,重載的把戲。
要注意的是構造函數可以有參數,在繼承中如何處理呢。
class mystring : public string{
public:
mystring( char * str ):string( str ){ }
}
mystring( char * str ):string( str )
記住這樣的形式,給自己的父類傳遞函數就用這樣的書寫格式,這是一個約定。
構造函數後面加上一個:表示後面是一個初始化序列,說它是一個序列是因爲它可以初始化多個成員變量,在初始化序列裏調用向父類傳遞參數是爲了保證類的產生的順序,先產生父類,然後是子類。使用初始化有個好處就是可以提高效率。
string(){ data = NULL; }
可以改寫成
string():data(NULL){ }
他的作用是產生成員變量char * data時將他的值置爲NULL。從而少了data = NULL;這步操作。
注意,這裏構造和析構有一個順序問題,就是構造時應該從基類開始按繼承的層次順序調用,析構的時候順序正好相反。這樣處理是因爲,子類可能在構造函數裏使用父類的成員變量,如果父類還沒有創建,那就會有問題,而析構的時候,如果父類先析構,也會有這樣的問題。
析構函數還有一個能否正確運行的問題。 #include <iostream>
using namespace std;
class One{
public:
One(){ cout < < "One ON! " < < endl; }
~One(){ cout < < "One OFF! " < < endl; }
};
class Two : public One{
public:
Two(){ cout < < "Two ON! " < < endl; }
~Two(){ cout < < "Two OFF! " < < endl; }
};
class Three : public Two{
public:
Three(){ cout < < "Three ON! " < < endl; }
~Three(){ cout < < "Three OFF! " < < endl; }
};
void main()
{
Three three;
}
運行結果
One ON!
Two ON!
Three ON!
Three OFF!
Two OFF!
One OFF!
正確
void main()
{
Three * three = new Three;
delete three;
}
運行結果
One ON!
Two ON!
Three ON!
Three OFF!
Two OFF!
One OFF!
正確
void main()
{
One * three = new Three;
delete three;
}
運行結果
One ON!
Two ON!
Three ON!
One OFF!
不好了,Two和Three的析構都沒有運行,怎麼會這樣,原來One * three指出了指針指向的是一個One類的對象。如何得到正確的結果呢,如果能讓One類記住被繼承後的變化就好了。
對了!虛函數,在《後入爲主----虛函數》中可以知道,虛函數有這個特性,不信試試看。
class One{
public:
One(){ cout < < "One ON! " < < endl; }
virtual ~One(){ cout < < "One OFF! " < < endl; }
};
void main()
{
One * three = new Three;
delete three;
}
運行結果
One ON!
Two ON!
Three ON!
Three OFF!
Two OFF!
One OFF!
正確
這個特點很重要,我們要牢牢記住,我們稱這種方法爲“虛析構”,在多態裏運用非常廣泛,也是編寫可複用代碼的一個重要技巧。
構造和析構的作用機制就是自動化,簡化編程的複雜度。還有要記住的是,在一個類的構造函數裏分配了的資源儘量要記得在該類的析構函數裏釋放,當然也允許提前釋放,你可以在析構函數裏判斷它是否已經釋放,如果沒有就釋放。這就是----由始至終,它間接的描述了一個對象的生和死(記住這一點很重要,因爲我以後會講到如何運用這個特性控制對象的生死)。
使用一個未初始化的變量簡直就是災難,使用一個未初始化的指針將導致崩潰。這是我的忠告。在C++中初始化不會有附加的效果,不會降低效率,我們要做的是養成好習慣,產生一個對象的時候就將它初始化。 對於
Object.Init(); //初始化,構造
Object.Free(); //釋放,析構
這樣的調用並不是很困難,要記住他也不是難事,但是誰都不能保證他永遠不會忘記,更糟糕的是
Object.Init();
Object.Free();
沒有配對使用
Object.Init();
Object.Free();
Object.Free();
或
Object.Init();
Object.Init();
Object.Free();
會帶來什麼樣的結果,誰也不知道,而且這樣的錯誤,編譯器不會報錯。這是多麼可怕的錯誤,一個程序員最怕遇上的就是這樣的邏輯錯誤,它可能爲了找這樣的一個錯誤花上一整天時間。 讓我們看看有什麼好的辦法。
一個對象按時間來分析,一般有三個階段,出生,活動,死亡。與我們要做的有什麼相關之處呢,初始化,運行,釋放。很好,對照一下,我們發現在對象出生的時候初始化,死亡的時候釋放,如果這一切能用這樣的機制來操作,我們就再也不用擔心會由於忘記或錯誤的使用帶來麻煩了。
C++裏就提供了這樣的機制。使用他有個約定
class Object{
public:
Object(); //與類同名的函數,該函數沒有返回值,叫做構造函數
~Object(); //類似的,在構造函數名前加一個取反符號,叫做析構函數
};
構造函數將在對象產生的時候調用 析構函數將在對象銷燬的時候調用
調用的過程和實現方法由編譯器完成,我們只要記住他們調用的時間就行了,而且他們的調用是自動完成的,不需要我們控制。
#include <iostream>
using namespace std;
class Object{
public:
Object(){ cout < < "Object ON! " < < endl; }
~Object(){ cout < < "Object OFF! " < < endl; }
};
void main()
{
Object o;
}
運行結果
Object ON!
Object OFF!
構在函數和析構函數確實的執行了
現在我們來一個應用的例子
一個字符串類,它需要保存字符串的內容,但是它不知道字符串的大小,那麼設計這個字符串類的時候,保存字符串的成員變量就不能用固定大小的數組,而是用可以間接操作數組的指針。 #include <iostream>
#include <string.h>
using namespace std;
class string{
private:
char * data;
public:
string(){ data = NULL; }
string( char * str )
{
cout < < "Copy string: " < < str < < endl;
data = new char[ strlen(str) + 1 ];
memcpy( data , str , strlen(str) + 1 );
}
char * Data(){ return data; }
~string()
{
if( data )
{
cout < < "Free string: " < < data < < endl;
delete data;
}
}
};
void main()
{
{
string s( "abcd ");
cout < < "Show String: " < < s.Data() < <endl;
}
cin.get();
}
Copy string: abcd //執行了string::string( char * str ) 構造函數
Show String: abcd
Free string: abcd //由於在{}中產成的對象是臨時對象,它的生命期在}後就結束了,所以string::~string() 析構函數被調用
申請內存和釋放內存的操作自動完成了,構造函數和析構函數的目的在於一個類可以象普通類型一樣初始化和釋放,從而保證了封裝。 上面的例子有兩個構造函數,這麼什麼大不了的,我們看過《面面俱到----重載》得都知道,重載的把戲。
要注意的是構造函數可以有參數,在繼承中如何處理呢。
class mystring : public string{
public:
mystring( char * str ):string( str ){ }
}
mystring( char * str ):string( str )
記住這樣的形式,給自己的父類傳遞函數就用這樣的書寫格式,這是一個約定。
構造函數後面加上一個:表示後面是一個初始化序列,說它是一個序列是因爲它可以初始化多個成員變量,在初始化序列裏調用向父類傳遞參數是爲了保證類的產生的順序,先產生父類,然後是子類。使用初始化有個好處就是可以提高效率。
string(){ data = NULL; }
可以改寫成
string():data(NULL){ }
他的作用是產生成員變量char * data時將他的值置爲NULL。從而少了data = NULL;這步操作。
注意,這裏構造和析構有一個順序問題,就是構造時應該從基類開始按繼承的層次順序調用,析構的時候順序正好相反。這樣處理是因爲,子類可能在構造函數裏使用父類的成員變量,如果父類還沒有創建,那就會有問題,而析構的時候,如果父類先析構,也會有這樣的問題。
析構函數還有一個能否正確運行的問題。 #include <iostream>
using namespace std;
class One{
public:
One(){ cout < < "One ON! " < < endl; }
~One(){ cout < < "One OFF! " < < endl; }
};
class Two : public One{
public:
Two(){ cout < < "Two ON! " < < endl; }
~Two(){ cout < < "Two OFF! " < < endl; }
};
class Three : public Two{
public:
Three(){ cout < < "Three ON! " < < endl; }
~Three(){ cout < < "Three OFF! " < < endl; }
};
void main()
{
Three three;
}
運行結果
One ON!
Two ON!
Three ON!
Three OFF!
Two OFF!
One OFF!
正確
void main()
{
Three * three = new Three;
delete three;
}
運行結果
One ON!
Two ON!
Three ON!
Three OFF!
Two OFF!
One OFF!
正確
void main()
{
One * three = new Three;
delete three;
}
運行結果
One ON!
Two ON!
Three ON!
One OFF!
不好了,Two和Three的析構都沒有運行,怎麼會這樣,原來One * three指出了指針指向的是一個One類的對象。如何得到正確的結果呢,如果能讓One類記住被繼承後的變化就好了。
對了!虛函數,在《後入爲主----虛函數》中可以知道,虛函數有這個特性,不信試試看。
class One{
public:
One(){ cout < < "One ON! " < < endl; }
virtual ~One(){ cout < < "One OFF! " < < endl; }
};
void main()
{
One * three = new Three;
delete three;
}
運行結果
One ON!
Two ON!
Three ON!
Three OFF!
Two OFF!
One OFF!
正確
這個特點很重要,我們要牢牢記住,我們稱這種方法爲“虛析構”,在多態裏運用非常廣泛,也是編寫可複用代碼的一個重要技巧。
構造和析構的作用機制就是自動化,簡化編程的複雜度。還有要記住的是,在一個類的構造函數裏分配了的資源儘量要記得在該類的析構函數裏釋放,當然也允許提前釋放,你可以在析構函數裏判斷它是否已經釋放,如果沒有就釋放。這就是----由始至終,它間接的描述了一個對象的生和死(記住這一點很重要,因爲我以後會講到如何運用這個特性控制對象的生死)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.