effective C++ 筆記

雖然秋招沒找到工作,還是得繼續學習啊。

條款3 儘可能使用const

1)const char *p;是指向常量的指針,不可以改變指向的值,可以改變指向。

char * const p;是常量指針,不可改變指向,但可以改變指針指向的值。

const char * const p;指向常量的常量指針,值和指向都不可以改變。

迭代器就相當於指針,也可以使用const關鍵字

vector<int> vec;

const vector<int>::iterator iter = vec.begin();  iter的作用相當於 * const p; 

*iter = 10;沒問題,改變指針所指的物。

iter++;錯誤,iter是常量迭代器,不能改變指向的地址。

vector<int>::const_iterator cIter = vec.end(); 此時iter相當於 const *p;

*cIter = 10;錯誤,不能該表所指物。

cIter++;正確,可以改變指向的地址。

2)mutable可以使const成員函數改變成員變量的值。在必要的情況下可以使用。

3)

當成員函數有const和non const版本,功能一樣時,應在non const成員函數中調用const版本,調用方法如下:

先轉換成const數據類型使用:staic_cast<const TextBlock&>(*this)[position],在使用staic_cast<char &>將const轉換爲non-const。

條款4 確定對象被使用前已被初始化

1)

上圖所示成員變量是賦值,而不是初始化,初始化發生的更早,發生在這些成員default構造函數調用之時。

使用成員列表初始化,則省去了default構造函數先給成員變量設初值,在賦予新值這一個步驟。成員列表初始化則直接用實參做爲各個成員變量構造函數的實參,這樣效率比較高。

2)C++有着固定的成員初始化順序,基類的初始化總是在子類之前。成員變量的次序最好先聲明先初始化,否則容易遺漏。

3)爲了避免跨單元編譯之初始化順序,最好使用local static 替換 non-local static.

條款5 瞭解C++默默編寫並調用了哪些函數。

1)編譯器會給空類定義一個默認構造函數,複製構造函數,重載賦值運算符,還有默認析構函數。這些函數都是public和inline。

只有當這些函數需要被調用的時候,它們纔會被編譯器創建出來。對於賦值構造函數和賦值運算符,編譯器只是將每一個非靜態成員變量的值拷貝到目標對象。

條款6 若不想使用編譯器爲你自動創建的函數,應該主動拒絕。

1)可以將拷貝構造函數和賦值操作符設置爲私有成員。然而這種做法並不安全,因爲友元函數和成員函數仍然可以調用他們。

2)可以聲明一個不含任何數據成員的空類,將拷貝構造函數和賦值操作符設置爲私有的。

條款7 爲多態基類設置虛析構函數。

1)當一個基類指針指向子類的時候,刪除基類時,若基類析構函數爲非虛的,則只會調用基類的析構函數,造成局部刪除。

2)在類不希望被繼承的時候,盲目使用虛析構函數會增加類佔用的內存,並且降低移植性。增加的字節根據編譯器所給指針的大小,一般爲四字節。

條款8 別讓異常逃離析構函數。

1)析構函數絕對不要拋出異常,當使用容器類時,很可能會有多個異常出現,會導致程序奔潰。     

2)析構函數拋出得異常一定要在析構函數內捕捉,然後吞下他們(即什麼也不幹)或者直接讓程序結束運行。

3)如果客戶需要對某個操作函數運行期間拋出得異常做出反應,應提供一個普通成員函數來處理,而不是在析構函數中處理。

條款9 絕不再析構和構造過程中調用virtual函數

1)在基類構構造期間,虛函數不是虛的,意思就是假如在基類構造函數中調用虛函數,只會調用基類得版本,而不是繼承類得版本。因爲在基類構造之時,子類還沒有進行成員初始化,如果調用子類版本得成員函數,將會出現未知得錯誤。

2)同樣得道理適用於析構函數,子類調用析構函數先釋放子類成員,在調用父類析構函數,這時在析構函數中虛函數不虛。因爲如果調用子類的虛構函數會將已經釋放的成員在釋放一遍,這樣會造成不確定的行爲。

3)解決上述問題的辦法就是把構造函數中調用的函數改爲非虛函數,然後由子類構造函數傳遞必要的參數給父類的構造函數。

總結一下就是在構造函數和析構函數中,基類的調用不會下降爲子類,就是不會調用子類的函數。

條款10 令operator= 返回一個 reference to *this

1)

2)在賦值操作符中,返回一個指向左側的引用,這樣就可以鏈式操作。

條款11 在operator= 中處理自我賦值

1)在賦值的時候可能會有自我賦值的情況,例如 a = a,這種情況可能會出現錯誤。

2)爲了避免出現這種錯誤,我們應該在賦值前,應進行認同測試,即判斷是不是自己,是自己則直接返回當前值,不做任何操作。

if(this == &rhs) return *this;

3)也可以採用不認同方式,先將要複製的對象保存,在進行賦值,這樣就不會出現錯誤。

條款12 複製對象時,千萬別忘記了對象的每一個成分。

1)當自己編寫了複製構造函數和賦值函數,應在添加成員時,注意也在複製構造函數和複製函數裏面添加相應的語句。

2)在編寫派生類的複製構造函數和賦值函數時,此時的父類的私有成員是無法訪問的,不能進行直接賦值,應該調用父類的複製構造函數和賦值函數。

3)切記,不要再複製構造函數中調用賦值函數,反過來也一樣,會造成不明確的行爲。

條款13 以對象管理資源

1)在使用完資源後,可能會忘記釋放資源,或者語句過早的返回,拋出異常跳過釋放語句,造成內存泄漏。

2)爲了避免內存泄漏,應使用類來管理資源,這樣類的生命週期結束後會自動調用析構函數釋放資源。例如使用auto_ptr,智能指針在控制流離開區塊或函數就被釋放,會自動調用析構函數,其析構函數能使用delete釋放所指對象的內存。

條款14 在資源管理類中小心copying行爲

1)

這個條款沒看大懂。。。先放這裏吧,以後懂了再回來補上。。。

RAII,也稱爲“資源獲取就是初始化”,是c++等編程語言常用的管理資源、避免內存泄露的方法。它保證在任何情況下,使用對象時先構造對象,最後析構對象。

條款15 在資源管理類中提供對原始資源的訪問

1)許多函數需要訪問原始資源,所以每一個RAII calss應該提供一個返回原始資源的函數。

2)對原始資源的訪問有顯示轉換和隱示轉換兩種,一般而言顯示比較安全,隱示對客戶比較方便。

條款16 成對使用new和delete時要採取相同的形式

條款17 以獨立語句將newed對象置入智能指針

這個條款講的是一定要用獨立語句new對象後傳入智能指針,在將指針傳給函數調用,不然可能會出現內存泄漏。不能寫成這樣調用 fun(sharped_ptr<對象> (new 對象), fun1());fun1返回另外一個實參,這種情況下,編譯器可能先執行 new 對象,然後執行fun1(),如果fun1出現異常,這個new出來的對象就沒有置入智能指針,就會出現內存泄漏。

條款18 讓接口容易被正確使用,不易被誤用。

看的有點懵逼。。。

條款19 設計class猶如設計type

條款20 寧願傳遞常量引用(pass by reference to const)而不是值(pass by value)傳遞

1)默認情況下C++是按值傳遞對象至函數。

2)使用按值傳遞會造成許多不必要得資源浪費,例如調用構造函數,析構函數。

3)按引用傳遞時,如果不想傳入的參數被修改,最好將參數設置爲const,防止傳入得參數在函數內部被修改。

3)在已值傳遞的過程中,假如將派生類傳遞給基類參數,將會切割掉派生類的其他特性,只保留基類的特性,因爲將會已派生類做爲參數調用基類的構造函數。

4)使用引用傳遞時,不會出現切割問題。

5)在使用內置類型和STL迭代器和函數對象時,按值傳遞比較合理。

條款21 必須返回對象時,不要返回其引用

1)在使用引用時,一定要提醒自己這是一個別名,一定要想到他的另外一個名字在哪裏,如果沒有另外一個名字,那可能會出現錯誤。例如

這種情況就會出現無法定義的情況,在返回result後,result將會被釋放,在對該對象進行操作將會出現無法定義的行爲。

2)函數返回指向臨時對象的引用或者指針,都將發生不明確的行爲。因爲臨時對象在函數結束後就被釋放了。

3)在函數中並在堆中給對象分配內存,然後返回對象的引用或者指針,這樣將會造成內存泄漏。如下例子:

總結:函數中不要返回臨時對象,或在堆上創建的對象,或這靜態對象的指針或者引用。返回值就對了。

條款22 將成員變量聲明爲private

條款23 最好用非成員非友元函數替換成員函數

條款24 若所有參數皆需類型轉換,請爲此採用非成員函數。

1)當你需要爲某個函數的所有參數(包括被this指針所指的那個隱喻參數)進行類型轉換,那麼這個函數必須是非成員函數。

條款25 考慮寫出一個不拋處異常的swap函數

這個沒看懂。。。

條款26 儘可能延後變量定義式的出現

條款27 儘量少做轉型動作

1)C++四種不同轉型

const_cast通常被用來將對象的常量性質移除,它也是唯一有此能力的C++風格轉型操作符。

dynamic_cast主要用來執行安全向下轉型,也就是用來決定某對象是不是屬於其繼承類型中的某個類型。它是唯一無法由舊式語法執行的動作,也是唯一耗費重大運行成本的轉型動作。

reinterpret_cast意圖執行低級轉型,實際動作可能取決於編譯器,不可移植。

static_cast用來強迫隱示轉換,例如將非const轉換爲const,或者將int轉換爲double等等。它也可以執行上述各種轉換的反向轉換。但是它不能將const轉換爲非const。

2)在派生類的虛函數中使用類型轉換將派生類指針轉換爲基類指針後調用基類的函數時,只會產生一個基類副本,然後修改該副本的值,並不會真正修改繼承之後基類的值,但是修改派生類的值會生效。可能講不太清楚,直接貼代碼吧

#include <iostream>
#include <vector>
#include <queue>
#include <cmath>
#include <stack>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;

class A
{
public:
    A(int tmp) : a(tmp){}
    virtual void change()
    {
        a+=1;
    }
    void print()
    {
        cout << "This is A" << a << endl;
    }
private:
    int a;
};

class B : public A
{
public:
    B(int tmp1, int tmp2) : A(tmp1), b(tmp2){}
    void change()
    {
        static_cast<A>(*this).change();
        b += 1;
    }
    void print()
    {
        A::print();
        cout << "This is B" << b << endl;
    }
private:
    int b;
};


int main()
{
    B testb(1, 2);
    testb.change();
    testb.print();
    return 0;
}

該程序運行結果是

在B的change函數中並沒有真正改變基類的值,只是修改了副本的值,在貼上反彙編吧,這樣理解的更詳細。

可以看到在調用類型轉換後,調用了A的複製構造函數,然後調用了A的change函數,但是改變的是副本的a的值。這樣就造成了局部修改。

3)爲了避免2中出現的情況可以直接用類名加函數調用基類的函數。

4)使用dynamic_cast轉換非常的耗費時間會影響性能,應該儘量避免使用它。

條款28 避免返回handles指向對象內部的成分

1)這個條款說的應該是不要返回類內被封裝的成員。會破壞類的封裝性,還有可能會出現指針所指物被釋放的可能。

條款29 爲異常安全而努力是值得的

1)帶有異常安全性的函數不會泄漏任何資源,不允許數據敗壞。

找到工作了,飄了一段時間,發現還是要好好學習,衝!

條款30 透徹瞭解內置(inlining)的裏裏外外

1)當爲虛函數申請inline時,會被編譯器拒絕,因爲虛函數的調用是在運行時期確定,然而inline函數要在編譯期時,用函數代碼替換,這樣編譯器就無能爲力了。大部分編譯器還會拒絕帶有遞歸和循環的inline函數。

2)在某些情況下,編譯器還是會給inline函數生成一個outlined函數本體。

inline void fun(); //創建一個inline函數
void (*pf)() = fun; //創建一個函數指針指向fun
fun(); //這個函數的調用將被inline,也就是用函數文本替換
pf(); //這種情況下就不能inline了,得爲函數創建一個函數本體,因爲它通過函數指針調用

3)使用inline函數時一定要慎重考慮,只有在那些小型,被頻繁調用得函數身上使用。

條款31 將文件間得編譯依存關係降到最低

這個把我看蒙了,可能我看的時候人是蒙得吧。。。

條款32 確定你的public繼承塑模出is-a關係

1)public繼承表示is-a關係,也就是說能對基類進行的操作也對能對派生類進行。這是很重要的一點。

條款33 避免遮掩繼承而來的名稱

1)內層作用域的名稱會遮掩外層作用域的名稱

int a; //全局變量
void fun()
{
    int a;  //局部變量
    std::cin >> a;
}

如上述例子,函數內的變量將會遮掩全局變量。

2)當繼承基類和它的重載函數,你希望重新定義或覆蓋其中一部分,你必須爲那些原本會被遮掩的部分每個都引用using聲明式。

class base
{
public:
    virtual void fun1();
    virtual void fun1(double x);
};

class derived : public base
{
    using base::fun1;
    void fun1();
};

如果不用using引入基類的函數,當調用derived::fun1(double x)時,編譯器會報錯,因爲派生類fun1()覆蓋了基類的函數包括他的重載版本。

條款34 區分接口繼承和實現繼承

1)純虛函數有兩個最突出特點:它們必須被繼承了他們的具體類重新聲明,在抽象類中一般沒有給出函數定義,也可以給出函數定義。

2)在基類中聲明一個純虛函數的意義是爲了使派生類繼承函數的接口及一份強制實現。、

條款35 考慮 virtual函數以外的其他選擇

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章