C++線下測試回顧

題目地址

有同學說不知道怎么畫內存模型圖,我這里附幾個教程
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;
}

析構函數

先來點前奏delete p與 delete[] p的區別

下面我們來看程序


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++的東西深究起來不得了,很多事情要考慮清楚才能寫出完美無誤的代碼,而這一直是我追求的目標,我先反省下自己。
  • 今天聽老師單獨給我們分析,談到一些問題的時候要自己思考沒空做筆記,今天寫的這些都是記在腦子里的,估計會有些遺漏,還望跟我一起接受指導的同學們批評指出。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容