有同學說不知道怎么畫內存模型圖,我這里附幾個教程
UML類圖小結
UML類圖與類的關系詳解
類似的教程筆記網上有不少的,大家自己搜來看看把習題的類圖實現一下,鑒于文章篇幅我這里就不做畫圖教程了。
首先我們要明確下規范,寫函數內容的時候,最好使用this指針
this->width = width;
this->height = height;
而非:
width = width;
height = height;
為什么要這樣寫?這樣寫的好處是什么呢?
我們應該知道當局部變量名與全局變量同名時,全局變量會被覆蓋,我們自己寫構造函數的時候很可能會選擇用不同名的變量名來進行賦值操作,像這樣的:
width_d = width;
height_d = height;
但最好是寫成這樣的:
this->width = width;
this->height = height;
這里的this非常明確地指出了左邊的width是當前類的成員,而不至于令別人看的時候摸不著頭腦,不清楚這里的width到底哪里的東西。
我最后是這樣寫的:
this->width = width?width:0;
this->height = height?height:0;
附帶對width,height的判斷檢查。
從簡單的構造函數談起
剛拿到題的時候我是呵呵笑的,腦袋中已經有了初始模型,直到我下筆寫的時候……
說來慚愧,我寫第一個構造函數的就卡住了,磨磨蹭蹭十來分鐘過去了還是想不出怎么調用Point類里的數據成員(x, y)比較好。
原題中Rectangle類定義了一個指向Point的指針leftUp。我們可以利用這個指針來對(x, y)進行構造賦值。
起初我是這樣寫的:
x = leftUp->x;
y = leftUp->y;
……編譯器啪啪啪打臉,簡直不忍直視,大家幫忙分析下這樣賦值錯在哪里?
左邊的x,y到底是誰?代表的是Point的成員還是另外創建的數據?leftUp之前只是定義了下,這個指針并未在堆中被創建,leftUp還沒有被實例化哪里來的成員(x,y)?
很多時候就是這樣的,感覺自己挺懂的,下筆寫出來的往編譯器來一丟立馬報一堆錯。
你說我沒有實例化,那么我來搞一個
this->leftUp = Point* ptr;
this->leftUp->x = ptr->x;
this->leftUp->y = ptr->y;
這樣乍看之下似乎無錯,但大多數負責任的編譯器仍會報錯,這是為什么呢?
這里涉及到設計C++類、函數要考慮到的一些東西。當我們編寫一個函數的時候通常會默認我們調用的外部的數據是安全的,是可用的,同時為了保證自己的魯棒性我們要防止外部改動導致本函數的失效或者更為惡劣的程序崩潰。所以我們在對指針指向的類成員變量賦值時最好是這樣做:
this->leftUp = new Point(x, y);
直接在堆中new一個,同時調用Point的默認構造函數傳進(x, y)值;
你真的會拷貝構造嗎?
老師講完拷貝構造的時候我問了老刁,你拷貝類Shape里的no了嗎?我們開始都沒注意到這個值,不過細心的網友應該記著Rectangle是繼承Shape而來的。所以他默認的數據里還是有no這個成員的,你怎么能拋棄他呢?但是我們要怎么給他拷貝復制呢?
我們使用Shape(other),直接來調用父類Shape的默認構造函數。為啥可以這樣寫呢?直接調用父類不會出錯嗎?
不會的,如果你認真看了侯捷老師的視頻,應該已經知道子、父類之間會為友元。
完整的代碼如下
inline
Rectangle::Rectangle(const Rectangle& other)
: Shape(other), width(other.width), height(other.height)
{
if(other.leftUp != nullptr)
{
this->leftUp = new Point(*other.leftUp);
}
else
{
this->leftUp = nullptr;
}
}
有的人可能會將函數名后緊跟的初始化操作寫成這樣的順序:
width(other.width), height(other.height), Shape(other)
這時候李老師問了一個問題:你認為拷貝構造時的順序是怎樣的?是按照你寫的初始化的順序嗎?
幾乎所有人都回答“是”。
然而事實是有點坑爹的,構造函數開頭的
inline
Rectangle::Rectangle(const Rectangle& other)
: Shape(other), width(other.width), height(other.height)
并不是按照你寫的順序來的,而是按照編譯器定義的優先級來的,先是拷貝構造父類的數據,然后是原類里對數據定義的順序,所以你在開頭考慮寫成什么順序并沒有什么卵用,無論你寫成什么順序,他內部都已經有約定好的順序了,但是為了代碼閱讀方便,讓人一看便知拷貝構造的順序,你這里只要按照他內部約定好的順序來寫,先父類,后定義順序,權當做個順序說明好了。
最后我們要面對的是leftUp這個指針成員,很多人可能會直接這樣寫:
this->leftUp = new Point(*other.leftUp);
如果你拷貝的other里的leftUp是個空指針呢?我們還在堆里創建他干嘛?
所以這里要加個if判斷。
if(other.leftUp != nullptr)
{
this->leftUp = new Point(*other.leftUp);
}
else
{
this->leftUp = nullptr;
}
為什么我用的是nullptr而不是NULL,null或者0?你可以參考下我下面給的連接,相信你看完會總結出一個屬于自己的答案。
NULL,0,ptrnull全解析
C/C++ 中 0 與 NULL 區別是什么?
NULL VS ptrnull
賦值操作符-你不造的那些事兒
只有構造函數可以這樣
Rectangle::Rectangle(const Rectangle& other)
: Shape(other), width(other.width), height(other.height)
{
···
}
在花括號之前這樣直接初始化,賦值操作符是不可以的,這是構造函數才擁有的特例。
所以我們還是老老實實用this指針吧。不過我相信很多人會忘了在開頭寫這句判斷:
if(this == &other)
{
return *this;
}
如果他賦值操作的就是他本身,我們不加判斷的直接進行操作,在處理leftUp的時候,
this->leftUp = new Point(*other.leftUp);
已有的other.leftUp被再次指向了一個新的Point對象,但你原來的的other.left并沒有被銷毀,原來的數據不再被記錄在案,換句話說你搞丟他了,這樣便出現了內存泄露。
不過不用驚訝,李老師說“大部分程序員的c++程序里必然會出現內存泄露的問題”,所以要想成為那少數的“大牛”,就從現在開始養成良好的習慣,學習畫你的數據內存模型圖,搞清楚每一個數據的動向、聯系。確保你寫的程序萬無一失。
接下來我們要思考父類Shape要怎么寫呢?
說實話,我對處理繼承的父類的東西是一竅不通的,連上面那個構造函數Shape(other)都是看的別人的,看到這個直接歇菜了。我開始寫了個這樣的
Shape(other.no);
連我自己都不知道這是什么鬼,一提筆便暴漏出很多問題來。我們幾個C++都不曉得該怎么處理,然后有人從網上找了個答案。寫成了醬紫:
Shape::operator=(other);
李老師后來過來看到我們這樣寫還以為我們是懂的,說這是對父類操作符重載的標準寫法。
這里我們把“operator=”看做一個整體,即Shape的成員函數,然后我們直接傳入參數other,這樣便調用了shape的默認構造函數,對no進行的賦值操作,這樣做的好處是我們完全不必管Shape內部是如何實現的,以及是否發生改動,我們只管做我們的賦值操作就OK了。
下面我要給大家當下反面教材。當時我問了句有點蠢的話:李老師,你怎么確定那個Shape里“operator=”一定存在呢?他不需要定義一下嗎?
李老師笑著對我說到:看,這就是沒有好好看侯捷老師視頻的典型。
四大函數,構造、拷貝、操作符賦值、析構,一旦一個對象被創建這四個函數便被編譯器自動生成默認結構了,so……
今天李老師講的時候,提到了過了很多次解耦思想,一個函數定義的什么功能就只做什么事情,不要直接"left->x = x",搞得你很懂外面那個類是怎么實現的一樣,非要去操作底層。很多時候我們在進行團隊合作的時候,尤其是大公司,你調用的東西很可能不是你寫的,你不知道你用的那個數據何時會發生改動,所以你要保證你的通用性、穩定性。寫某個函數的時候,假設其他函數都是正確的,并且你不知道他們的內部實現,你只管調用他們的接口然后完成你當前函數要做的工作。所以說盡量用下面這種寫法,否則后患無窮。
this->leftUp = new Point(x, y);
this->leftUp = new Point(*other.leftUp);
Shape::operator = (other);
賦值操作的最后我們仍要談到喜歡逗你玩的類成員指針變量leftUp。
if(other.leftUp != nullptr)
{
if(leftUp != nullptr ) {
*leftUp = *other.leftUp;
}
else
{
leftUp = new Point(*other.leftUp);
}
}
else
{
delete leftUp;
this->leftUp = nullptr;
}
首先我們要判斷other.leftUp是否為空,如果不為空我們就準備進行賦值操作,繼續判斷當前類成員leftUp是否為空,若為空就new一個直接在堆中初始化,否則直接改變leftUp指向的內容(原來指向的會被析構函數釋放掉)。最后,若要賦值的other.leftUp為空,我們就先delete當前類中的leftUp,然后將他指向nullptr。
inline
Rectangle& Rectangle::operator=(const Rectangle& other)
{
if(this == &other)
{
return *this;
}
Shape::operator=(other);
this->width = other.width;
this->height = other.height;
if(other.leftUp != nullptr)
{
if(leftUp != nullptr) {
*leftUp = *other.leftUp;
}
else
{
leftUp = new Point(*other.leftUp);
}
}
else
{
delete leftUp;
this->leftUp = nullptr;
}
return *this;
}
析構函數
下面我們來看程序
inline
Rectangle::~Rectangle()
{
delete leftUp;
leftup = nullptr;
}
}
估計很多人只寫了個delete leftUp就完事兒了,以為這樣leftUp就被釋放指向NULL了,事實真的是這樣的?
delete只是對指針的指向空間的釋放,并不會改變指針的值,即指針不為NULL,把指針指向的空間釋放掉,但是指針的本身內容,即指向空間的地址,是不會改變的;指針為NULL時,沒有空間可釋放,也就不去釋放了,而指針依然有效,指針的內容依然是NULL,在指針的有效域結束時,指針本身所占內存自動被釋放。
Is it good practice to NULL a pointer after deleting it?
而有人的喜歡在delete之前判斷是否為空,stackoverflow有不少這類問題
Is it safe to delete a NULL pointer?
Is there any reason to check for a NULL pointer before deleting?
csdn也有類似的討論,看著還挺激烈的:)
我真是孤陋寡聞了,今天才知道NULL指針是可以直接delete的
摘一段給你們看看:
需要判斷NULL指針,不是因為要delete,而是因為要訪問該指針的內容,確保指針有效。
但是這么做還是沒辦法處理野指針,因為野指針只有在訪問時才能知道是否有問題。所以在delete之后應該立即賦值為NULL。這樣既方便以后檢查指針是否有效,也可以防止二次delete無效的指針或者棧上的地址,引起的段錯誤。
總結
- 測試能反映出很多問題,往往你并不能將你心里所想完美無誤的實現出來。
- 畫內存模型圖,李老師在以前的先下課提到過,大部分時候你感覺你的程序沒問題,編譯器也沒報錯,但是仔細一分析,其實錯誤百出。有些問題只有到達了一定量級才會被你發現,但是畫內存模型分析圖可以避免這種尷尬的事情。
- 眼高手低要不得,看幾十遍視頻也不一定有親自實現一遍程序體會的深刻。
- 其實感覺很有問題沒寫出來,C++的東西深究起來不得了,很多事情要考慮清楚才能寫出完美無誤的代碼,而這一直是我追求的目標,我先反省下自己。
- 今天聽老師單獨給我們分析,談到一些問題的時候要自己思考沒空做筆記,今天寫的這些都是記在腦子里的,估計會有些遺漏,還望跟我一起接受指導的同學們批評指出。