|
假設我們要開發一個String類,它可以方便地處理字符串數據。我們可以在類中聲明一個數組,考慮到有時候字符串極長,我們可以把數組大小設為200,但一般的情況下又不需要這么多的空間,這樣是浪費了內存。對了,我們可以使用new操作符,這樣是十分靈活的,但在類中就會出現許多意想不到的問題,本文就是針對這一現象而寫的。現在,我們先來開發一個Wrong類,從名稱上看出,它是一個不完善的類。的確,我們要刻意地使它出現各種各樣的問題,這樣才好對癥下藥。好了,我們開始吧!
Wrong.h:
#ifndef WRONG_H_ #define WRONG_H_ class Wrong { private: char * str; //存儲數據 int len; //字符串長度
public: Wrong(const char * s); //構造函數 Wrong(); // 默認構造函數 ~Wrong(); // 析構函數 friend ostream & operator<<(ostream & os,const Wrong& st); }; #endif
Wrong.cpp:
#include <iostream> #include <cstring> #include "wrong.h" using namespace std; Wrong::Wrong(const char * s) { len = strlen(s); str = new char[len + 1]; strcpy(str, s);
}//拷貝數據
Wrong::Wrong() { len =0; str = new char[len+1]; str[0]='\0';
}
Wrong::~Wrong() { cout<<"這個字符串將被刪除:"<<str<<'\n';//為了方便觀察結果,特留此行代碼。 delete [] str; }
ostream & operator<<(ostream & os, const Wrong & st) { os << st.str; return os; }
test_right.cpp:
#include <iostream> #include <stdlib.h> #include "Wrong.h" using namespace std; int main() { Wrong temp("天極網"); cout<<temp<<'\n'; system("PAUSE"); return 0; } |
運行結果:
天極網
請按任意鍵繼續. . .
大家可以看到,以上程序十分正確,而且也是十分有用的。可是,我們不能被表面現象所迷惑!下面,請大家用test_wrong.cpp文件替換test_right.cpp文件進行編譯,看看結果。有的編譯器可能就是根本不能進行編譯!
test_wrong.cpp:
#include <iostream> #include <stdlib.h> #include "Wrong.h" using namespace std; void show_right(const Wrong&); void show_wrong(const Wrong);//注意,參數非引用,而是按值傳遞。 int main() { Wrong test1("第一個范例。"); Wrong test2("第二個范例。"); Wrong test3("第三個范例。"); Wrong test4("第四個范例。"); cout<<"下面分別輸入三個范例:\n"; cout<<test1<<endl; cout<<test2<<endl; cout<<test3<<endl; Wrong* wrong1=new Wrong(test1); cout<<*wrong1<<endl; delete wrong1; cout<<test1<<endl;//在Dev-cpp上沒有任何反應。 cout<<"使用正確的函數:"<<endl; show_right(test2); cout<<test2<<endl; cout<<"使用錯誤的函數:"<<endl; show_wrong(test2); cout<<test2<<endl;//這一段代碼出現嚴重的錯誤! Wrong wrong2(test3); cout<<"wrong2: "<<wrong2<<endl; Wrong wrong3; wrong3=test4; cout<<"wrong3: "<<wrong3<<endl; cout<<"下面,程序結束,析構函數將被調用。"<<endl; return 0; } void show_right(const Wrong& a) { cout<<a<<endl; } void show_wrong(const Wrong a) { cout<<a<<endl; } |
運行結果:
下面分別輸入三個范例:
第一個范例。 第二個范例。 第三個范例。
第一個范例。
這個字符串將被刪除:第一個范例。
使用正確的函數: 第二個范例。 第二個范例。
使用錯誤的函數: 第二個范例。
這個字符串將被刪除:第二個范例。
這個字符串將被刪除:?= ?=
wrong2: 第三個范例。 wrong3: 第四個范例。
下面,程序結束,析構函數將被調用。
這個字符串將被刪除:第四個范例。
這個字符串將被刪除:第三個范例。
這個字符串將被刪除:?=
這個字符串將被刪除:x =
這個字符串將被刪除:?=
這個字符串將被刪除:
現在,請大家自己試試運行結果,或許會更加慘不忍睹呢!下面,我為大家一一分析原因。
首先,大家要知道,C++類有以下這些極為重要的函數:
一:復制構造函數。
二:賦值函數。
我們先來講復制構造函數。什么是復制構造函數呢?比如,我們可以寫下這樣的代碼:Wrong test1(test2);這是進行初始化。我們知道,初始化對象要用構造函數。可這兒呢?按理說,應該有聲明為這樣的構造函數:Wrong(const Wrong &);可是,我們并沒有定義這個構造函數呀?答案是,C++提供了默認的復制構造函數,問題也就出在這兒。
(1):什么時候會調用復制構造函數呢?(以Wrong類為例。)
在我們提供這樣的代碼:Wrong test1(test2)時,它會被調用;當函數的參數列表為按值傳遞,也就是沒有用引用和指針作為類型時,如:void show_wrong(const Wrong),它會被調用。其實,還有一些情況,但在這兒就不列舉了。
(2):它是什么樣的函數。
它的作用就是把兩個類進行復制。拿Wrong類為例,C++提供的默認復制構造函數是這樣的:
Wrong(const Wrong& a) { str=a.str; len=a.len; } |
在平時,這樣并不會有任何的問題出現,但我們用了new操作符,涉及到了動態內存分配,我們就不得不談談淺復制和深復制了。以上的函數就是實行的淺復制,它只是復制了指針,而并沒有復制指針指向的數據,可謂一點兒用也沒有。打個比方吧!就像一個朋友讓你把一個程序通過網絡發給他,而你大大咧咧地把快捷方式發給了他,有什么用處呢?我們來具體談談:
假如,A對象中存儲了這樣的字符串:“C++”。它的地址為2000。現在,我們把A對象賦給B對象:Wrong B=A。現在,A和B對象的str指針均指向2000地址。看似可以使用,但如果B對象的析構函數被調用時,則地址2000處的字符串“C++”已經被從內存中抹去,而A對象仍然指向地址2000。這時,如果我們寫下這樣的代碼:cout<<A<<endl;或是等待程序結束,A對象的析構函數被調用時,A對象的數據能否顯示出來呢?只會是亂碼。而且,程序還會這樣做:連續對地址2000處使用兩次delete操作符,這樣的后果是十分嚴重的!
本例中,有這樣的代碼:
Wrong* wrong1=new Wrong(test1); cout<<*wrong1<<endl; delete wrong1; |
假設test1中str指向的地址為2000,而wrong中str指針同樣指向地址2000,我們刪除了2000處的數據,而test1對象呢?已經被破壞了。大家從運行結果上可以看到,我們使用cout<<test1時,一點反應也沒有。而在test1的析構函數被調用時,顯示是這樣:“這個字符串將被刪除:”。
再看看這段代碼:
cout<<"使用錯誤的函數:"<<endl; show_wrong(test2); cout<<test2<<endl;//這一段代碼出現嚴重的錯誤! |
show_wrong函數的參數列表void show_wrong(const Wrong a)是按值傳遞的,所以,我們相當于執行了這樣的代碼:Wrong a=test2;函數執行完畢,由于生存周期的緣故,對象a被析構函數刪除,我們馬上就可以看到錯誤的顯示結果了:這個字符串將被刪除:?=。當然,test2也被破壞了。解決的辦法很簡單,當然是手工定義一個復制構造函數嘍!人力可以勝天!
Wrong::Wrong(const Wrong& a) { len=a.len; str=new char(len+1); strcpy(str,a.str); } |
我們執行的是深復制。這個函數的功能是這樣的:假設對象A中的str指針指向地址2000,內容為“I am a C++ Boy!”。我們執行代碼Wrong B=A時,我們先開辟出一塊內存,假設為3000。我們用strcpy函數將地址2000的內容拷貝到地址3000中,再將對象B的str指針指向地址3000。這樣,就互不干擾了。
大家把這個函數加入程序中,問題就解決了大半,但還沒有完全解決,問題在賦值函數上。我們的程序中有這樣的段代碼:
Wrong wrong3; wrong3=test4; |
經過我前面的講解,大家應該也會對這段代碼進行尋根摸底:憑什么可以這樣做:wrong3=test4???原因是,C++為了用戶的方便,提供的這樣的一個操作符重載函數:operator=。所以,我們可以這樣做。大家應該猜得到,它同樣是執行了淺復制,出了同樣的毛病。比如,執行了這段代碼后,析構函數開始大展神威^_^。由于這些變量是后進先出的,所以最后的wrong3變量先被刪除:這個字符串將被刪除:第四個范例。很正常。最后,刪除到test4的時候,問題來了:這個字符串將被刪除:?=。原因我不用贅述了,只是這個賦值函數怎么寫,還有一點兒學問呢!大家請看:
平時,我們可以寫這樣的代碼:x=y=z。(均為整型變量。)而在類對象中,我們同樣要這樣,因為這很方便。而對象A=B=C就是A.operator=(B.operator=(c))。而這個operator=函數的參數列表應該是:const Wrong& a,所以,大家不難推出,要實現這樣的功能,返回值也要是Wrong&,這樣才能實現A=B=C。我們先來寫寫看:
Wrong& Wrong::operator=(const Wrong& a) { delete [] str;//先刪除自身的數據 len=a.len; str=new char[len+1]; strcpy(str,a.str);//此三行為進行拷貝 return *this;//返回自身的引用 } |
是不是這樣就行了呢?我們假如寫出了這種代碼:A=A,那么大家看看,豈不是把A對象的數據給刪除了嗎?這樣可謂引發一系列的錯誤。所以,我們還要檢查是否為自身賦值。只比較兩對象的數據是不行了,因為兩個對象的數據很有可能相同。我們應該比較地址。以下是完好的賦值函數:
Wrong& Wrong::operator=(const Wrong& a) { if(this==&a) return *this; delete [] str; len=a.len; str=new char[len+1]; strcpy(str,a.str); return *this; } |
把這些代碼加入程序,問題就完全解決,下面是運行結果:
下面分別輸入三個范例:
第一個范例 第二個范例 第三個范例
第一個范例
這個字符串將被刪除:第一個范例。
第一個范例
使用正確的函數:
第二個范例。
第二個范例。
使用錯誤的函數:
第二個范例。
這個字符串將被刪除:第二個范例。
第二個范例。
wrong2: 第三個范例。 wrong3: 第四個范例。
下面,程序結束,析構函數將被調用。
這個字符串將被刪除:第四個范例。 這個字符串將被刪除:第三個范例。 這個字符串將被刪除:第四個范例。 這個字符串將被刪除:第三個范例。 這個字符串將被刪除:第二個范例。 這個字符串將被刪除:第一個范例。
關于動態內存分配的問題就介紹到這兒,希望大家都能熱愛編程,熱愛C++!
|