日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

effective c++_【阅读笔记】Effective C++()

發布時間:2025/3/13 c/c++ 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 effective c++_【阅读笔记】Effective C++() 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

全文參考自Effective C++, Scott Meyers
程序來自本人
https://github.com/hxz1998/ccl

1. 讓自己習慣C++

C++高效編程守則視狀況而變化,取決于使用C++的哪一部分。

C++四大塊:

  • C
  • Object-Oriented C++:面向對象
  • Template C++:泛型編程,模板元編程
  • STL:容器,迭代器,算法,函數對象

2. 盡量以const,enum,inline替換 #define

  • 對于單純常量,最好使用 const 或者 enums 來替換 #define
  • 對于形似函數的宏(macros),最好使用 inline 函數來替換 #define

有一種方法,可以獲得宏帶來的效率,以及一般函數帶來的可預料行為以及類型安全性(Type Safety),例如:

template<typename?T>inline?T?callWithMax(const?T?&a,?const?T?&b)?{
????return?a?>?b???a?:?b;
}

int?main()?{
????int?a?=?callWithMax<int>(1,?2);
????cout?<endl;
}

3. 盡可能使用 const

  • 聲明為 const 可以讓編譯器幫助檢查錯誤。
  • const 可以施加于任何作用域內的對象、函數參數、函數返回類型、成員函數。
  • 編譯器強制實施bitwise constness,但編寫程序時,應該使用“概念上的常量性”conceptual constness。
  • 當 const 和 non-const 成員函數有實質等價的實現時,要用 non-const 版本去調用 const 版本,這樣可減少代碼重復。

const 如果出現在 * 左邊,那么表示被指物是常量;如果在 * 右邊,那么表示指針是常量;如果出現在兩邊,那么表示指針和被指物都是常量。例如:

int?main()?{
????int?a?=?0,?b?=?1;
????int?const?*p1?=?&a;
????int?*const?p2?=?&a;

????*p1?=?2;????//?不行!因為 p1 指向的內容是常量
????*p2?=?2;????//?可以,p2?自身是常量,p2?只能指向a,但是?a?中的內容可以變
????p1?=?&b;????//?可以,p1?指向另一個內容,并聲稱這個內容不可變
????p2?=?&b;????//?不可以,p2?自身是常量,不能指向其他東西了
}

試著習慣這樣的寫法:

void?f1(const?int?*i);?//?指向一個不能修改內容的?i
void?f2(int?const?*i);?//?一貓貓一樣

這倆寫法效果是一樣的,都是指向一個內容不可變的數據(指針本身可以再修改指向的對象)。

對于第三點,一個很好的例子如下:

class?Text?{
private:
????std::string?text;
public:
????const?char?&operator[](std::size_t?pos)?const?{
????????return?text[pos];
????}

????char?&operator[](std::size_t?pos)?{
????????return?const_cast<char?&>?(?????????//?使用?const_cast?去掉?const?聲明
????????????????static_cast<const?Text?&>???//?使用?static_cast?把?*this?轉換成?const?對象
????????????????(*this)[pos]);??????????????//?使用?(*this)[]?方法返回(這個時候是?const?結果,經過?const_cast?去掉?const?聲明
????}
};

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

  • 為內置對象進行手工初始化,因為C++并不能保證完全初始化好它們。
  • 構造函數最好使用成員初值列(member initialization list),而不要在構造函數本體內使用賦值操作(assignment)。
  • 初始列列出的成員變量,其排列順序要和它們在類聲明中的一致。
  • 為免除“跨編譯單元初始化次序”問題,使用 local static 對象來代替 non-local static 對象。

如果成員變量是 const 的或者 references 的,那么它們一定需要有初值,不能被賦值。例如:

class?X?{
????const?int?val;
????int?&re_val;
public:
????X(int?val_,?int?&re_val_)?:?val(val_),?re_val(re_val_)?{}
};

基類總是比派生類要先初始化好,例如:

class?X?{
????const?int?val;
????int?&re_val;
public:
????X(int?val_,?int?&re_val_)?:?val(val_),?re_val(re_val_)?{
????????cout?<"X?initialization..."?<endl;
????}
};

class?Y?:?public?X?{
public:
????Y(int?val,?int?&re_val)?:?X(val,?re_val)?{
????????cout?<"Y?initialization..."?<endl;
????};
};

int?main()?{
????int?re_v?=?1;
????Y?y(1,?re_v);
????//?>:?X?initialization...
????//????Y?initialization...
}

由于定義于不同編譯單元內的 non-local static 對象的初始化順序并未明確定義,因此會出現這樣情況:

  • 定義在 File1.hh 中一個靜態全局變量 tfs
  • 在 File2.cc 中使用 tfs

那么如果 File1.hh 中的 tfs 還沒初始化好呢,File2.cc 中就想使用了,那么就會出現大問題!例如下面這個例子:

//?File1.hh

#include?
using?namespace?std;
class?FileSystem?{
public:
????size_t?numDisks()?const?{?return?0;?}
};
extern?FileSystem?tfs;

//?File2.cc

#include?
#include?"File1.hh"
using?namespace?std;
class?Directory?{
????size_t?disks;
public:
????Directory()?{?disks?=?tfs.numDisks();?}
????size_t?getDisks()?const?{?return?disks;?}
};
int?main()?{
????Directory?directory;
????cout?<}

這個時候編譯器就會報錯:

CMakeFiles\local_static.dir/objects.a(File2.cc.obj):File2.cc:(.rdata$.refptr.tfs[.refptr.tfs]+0x0):?undefined?reference?to?`tfs'

很明顯,在 local_static 目錄中沒有找到該引用,因此報錯了,那么該怎樣做呢?

使用方法(類似于工廠方法)來獲取這個值,而不是依賴編譯器初始化

例如:

//?File1.hh

class?FileSystem?{
public:
????size_t?numDisks()?const?{?return?0;?}
};
FileSystem?&getFS()?{
????static?FileSystem?tfs;
????return?tfs;
}

//?File2.cc
class?Directory?{
????size_t?disks;
public:
????Directory()?{?disks?=?getFS().numDisks();?}
????size_t?getDisks()?const?{?return?disks;?}
};
int?main()?{
????Directory?directory;
????cout?<}

這樣一來,就不用擔心了?。

不過,這樣還是有另外一個問題,例如多線程環境下還是有不確定情況,處理這種麻煩情況的做法之一是:在單線程啟動階段,手動調用一遍所有的 reference-returning 方法。這樣可以消除與初始化有關的“競速形式(race conditions)”

5. 了解C++默默編寫并調用哪些函數?

編譯器可以暗自為 class 創建 default 構造函數、copy 構造函數,copy assignment 操作符,以及析構函數。

首先,開門見山地說,C++默認編寫了默認構造函數、默認析構函數、拷貝構造函數,以及拷貝賦值函數,而且它們默認都是 inline 的。當然,這些函數的默認創建在一定時期是失效的,例如:

  • 默認構造函數:當提供了一個構造函數后,編譯器不再為類提供默認構造函數,而且默認。
  • 默認析構函數:當提供了一個析構函數后,編譯器就不再提供默認析構函數,默認析構函數是 non-virtual 的。
  • 拷貝構造函數:只要沒提供,而且滿足可拷貝構造的條件,那么就提供,否則不提供。
  • 拷貝賦值函數:只要沒提供,而且滿足可拷貝復制的條件,那么就提供,否則不提供。

上面說了兩個條件,那么具體是什么條件呢?

5.1 可拷貝構造&可拷貝賦值?

先來看一下滿足這倆條件的例子:

template<typename?T>
class?NamedObject?{
private:
????T?objectValue;
????string?name;
public:
????NamedObject(string?n,?T?val)?:?name(n),?objectValue(val)?{}
????NamedObject(const?NamedObject?&rhs)?{
????????objectValue?=?rhs.objectValue;
????????name?=?rhs.name?+?"?copy?";
????}friend?ostream?&operator<const?NamedObject?&rhs)?{
????????os?<"?"?<????????return?os;
????}
????NamedObject?&operator=(const?NamedObject?&rhs)?{
????????objectValue?=?rhs.objectValue;
????????name?=?rhs.name?+?"?=?";return?*this;
????}
};int?main()?{string?newDog?=?"newDog";string?oldDog?=?"oldDog";NamedObject<int>?od(oldDog,?1);NamedObject<int>?nd(newDog,?2);cout?<"?:?"?<endl;??//?>:?2?newDog?:?1?oldDog
????nd?=?od;????cout?<//?>:?1?oldDog?=
}

那么此時,即便自己不提供拷貝構造以及拷貝賦值構造操作符,編譯器也會對成員變量進行遞歸的拷貝賦值過來。但是在遇到成員變量是 const 或者 reference 類型時,編譯器就兩手一攤,無能為力了(具體可參考Effective C++, 3th, P37)。

例如下面的例子:

template<typename?T>
class?NamedObject?{
private:
????const?T?objectValue;
????string&?name;
public:
????//?其他函數都一樣
????NamedObject(const?NamedObject?&rhs)?{
????????objectValue?=?rhs.objectValue;
????????name?=?rhs.name?+?"?copy?";
????}
????NamedObject?&operator=(const?NamedObject?&rhs)?{
????????objectValue?=?rhs.objectValue;?//?不能對一個 const 對象賦值!
????????name?=?rhs.name?+?"?=?";return?*this;
????}
};int?main()?{string?newDog?=?"newDog";string?oldDog?=?"oldDog";NamedObject<int>?od(oldDog,?1);NamedObject<int>?nd(newDog,?2);
????nd?=?od;????//?>:?error:?use?of?deleted?function?'NamedObject&?NamedObject::operator=(const?NamedObject&)'
}

當然,這只是編譯器不再提供了而已,用戶自己還是可以設計如何去復制拷貝以及構造拷貝的,這完全取決于自己怎么處理成員變量。

除此之外,如果基類把拷貝構造函數設置成了 private 那么在派生類中也是沒辦法操作的。

6. 若不想使用編譯器自動生成的函數,那該明確拒絕

為駁回編譯器自動(暗自)提供的機能,可將相應的成員函數聲明為 private 并且不予實現?;蛘呤褂美^承 Uncopyable 這樣的基類。

如果不想讓一個類支持拷貝構造或者賦值構造,那么我們可以將函數聲明但不實現,例如這樣子:

class?Uncopyable?{
private:
????Uncopyable(const?Uncopyable&);
????Uncopyable&?operator=(const?Uncopyable&);
};

當然,對于每一個想實現這個功能的類都能去單獨這樣聲明,不過,還可以使用繼承方法去實現,例如:

class?Uncopyable?{
protected:
????Uncopyable()?=?default;
private:
????Uncopyable(const?Uncopyable?&);
????Uncopyable?&operator=(const?Uncopyable?&);
};
class?SubClass?:?public?Uncopyable?{
????//?默認不允許拷貝構造和賦值運算符
};
int?main()?{
????SubClass?s1,?s2;
????s1?=?s2;????//?error!
}

7. 為多態基類聲明virtual析構函數

  • 帶多態性質的基類應該聲明一個 virtual 析構函數。
  • 如果類帶有任何 virtual 函數,那么它就應該擁有一個 virtual 析構函數。
  • 如果類的設計目的不是用來做基類的,那么就不應該聲明 virtual 析構函數。

當使用基類指針指向派生類對象時,沒有問題,但是要是想把這個基類指針給刪掉,這時候問題就來了,例如下面這個例子:

class?BaseClass?{
private:
????char?*name;
public:
????BaseClass(int?size)?{
????????name?=?new?char[size];
????????for?(int?i?=?0;?i?'a';
????}
?//?基類的析構函數
????~BaseClass()?{?delete?name;?}
};

class?DeriveClass?:?public?BaseClass?{
private:
????char?*count;
public:
????DeriveClass(int?size)?:?BaseClass(size)?{
????????count?=?new?char[size];
????????for?(int?i?=?0;?i?'b';
????}
????//?派生類的析構函數
????~DeriveClass()?{?delete?count;?}
};
int?main()?{
????//?多態用法,基類指針指向派生類對象,沒毛病
????BaseClass?*obj?=?new?DeriveClass(16);
????//?刪除基類指針,出現了問題!
????delete?obj;
????return?0;
}

上面的程序乍一看看不出個毛病來,現在對 BaseClass *obj = new DeriveClass(16); 設置斷點,進行單步調試,可以觀察到構造函數過程是:

new?DeriveClass(16)
????|
BaseClass(16)
????|
DeriveClass(16)
????|
????end

這個順序完全正確,先構造基類再構造派生類嘛,執行完后,內存狀態是這樣的:

可以得知操作系統給這兩個對象中的成員分配內存到了 name : 0x1061980 和 count : 0x10619c0 。

那么執行 delete obj; 時,順序是這樣的:

delete?obj
????|
~BaseClass()
????|
????end

從上面可以看出來,竟然只執行了基類的析構函數,而沒有執行派生類的析構函數,那么這時的內存表示是怎么樣的?見下圖:

由此可見,在不經意間,就造成了內存泄漏問題,那么該如何解決這個問題呢?

很簡單,只需要把基類的析構函數聲明為 virtual 就可以了,這樣強制去執行子類的析構函數。

不過,這樣還是有兩種結果,例如下面是一種結果:

class?BaseClass?{
????//?其他都一樣
????virtual?~BaseClass()?{?delete?name;?}
};
class?DeriveClass?:?public?BaseClass?{
????//?其他都一樣
????~DeriveClass()?override?{?delete?count;?}
};

這個時候,是先執行的派生類析構函數,再執行基類析構函數。

另一種結果是:

class?BaseClass?{
????virtual?~BaseClass()?{?delete?name;?}
};
class?DeriveClass?:?public?BaseClass?{
????//?刪掉了自己的析構函數
};

這個情況下,才是先執行基類析構函數,再執行派生類析構函數。

8. 別讓異常逃離析構函數

  • 析構函數絕對不要拋出異常,如果一個被析構函數調用的函數可能拋出異常,析構函數應該捕捉任何異常,然后吞下他們(不傳播)或結束程序。
  • 如果接口使用者需要對某個操作函數運行期間拋出的異常做出反應,那么 class 應該提供一個普通函數(而不是在析構函數中)執行操作。

即便C++允許析構函數拋出異常,但是最好不要這樣做。當然,吞掉異常也是有爭議的,比如“草率地結束程序”可能會帶來更嚴重的問題,或者“不明確的行為帶來的風險”可能會帶來不安全的問題等等,具體問題具體分析是比較好的。

但是,通??梢蕴峁┮粋€讓用戶在析構函數前控制異常的機會,例如使用“雙重保險”來盡最大化確保問題得到解決。

9. 絕不在構造函數和析構函數過程中調用virtual函數

在構造和析構期間不要調用 virtual 函數,因為這類調用從不下降至派生類。

不管怎樣,都不應該在構造函數和析構函數內部去調用一個 virtual 函數,因為這樣的操作是不可預估的,帶來意想不到的結果。為什么這樣?因為在基類中,構造函數執行階段或者析構函數執行階段只能看到基類的內容,所以在派生類中實現的程序,是不可用的。下面這句話直白且有效的指出了問題的所在:

在基類(base-class)構造期間,virtual 函數不是 virtual 函數。

也正是因為這樣一個“對象在 derived class 構造函數開始執行前,不會成為一個 derived class 對象”的規則,所以最好在構造期間對 virtual 函數視而不見。

那么如何科學有效地去解決這個問題?當然是在基類中把需要在構造函數內執行地函數設置成非 virtual 函數。

總之就是,在基類構造和析構期間調用的 virtual 函數不可下降至派生類。

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

令賦值(assignment)操作符(=)返回一個 reference to *this。

為什么這樣做呢?是因為可以實現類似于這樣的程序:

a?=?b?=?c?=?10;

因此,我們在編寫類的 operator= 操作符時,可以寫成:

class?BaseClass?{
public:
????BaseClass?&operator=(const?BaseClass?&rhs)?{
????????//?隨便干點什么
????????return?*this;?//?關鍵在于這里
????}
};

當然啦,也可以不做返回,不過既然這是一個好的實踐,那么沒有確切的理由不去做,最好就去做。

11. 在 operator= 中處理“自我賦值”

  • 確保當對象自我賦值時,operator= 有可預估的行為。其中需要注意的包括比較“來源對象”和“目標對象”的地址、精心周到的語句順序以及拷貝交換。
  • 確定任何函數如果操作一個以上的對象,而其中多個對象是同一個對象時,其行為仍然正確。

簡而言之,就是需要考慮操作符兩邊是否是同一個對象,因為如果是同一個對象,會出現類似下面的問題:

class?BaseClass?{
private:
????char?*data;
public:
????BaseClass?&operator=(const?BaseClass?&rhs)?{
????????delete?data;
????????data?=?rhs.data;
????????return?*this;
????}
};
int?main()?{
????BaseClass?baseClass;
????baseClass?=?baseClass;
}

自己給自己賦值,沒毛病,但是在運算符函數的 delete data 卻帶來了問題,因為它刪除掉了自己的內存空間,卻在下面那行 data = rhs.data 又想用了,而這時系統已經收回了這塊空間,這樣一來操作系統肯定是不干的,所以程序就報錯了。

那么該如何解決呢?這樣:

class?BaseClass?{
private:
????char?*data;
public:
????BaseClass?&operator=(const?BaseClass?&rhs)?{
????????//?多一個檢查是否是自己的操作就可以了,也稱證同測試
????????if?(&rhs?==?this)?return?*this;
????????delete?data;
????????data?=?rhs.data;
????????return?*this;
????}
};

12. 復制對象時勿忘其每一個成分

  • 拷貝函數應該確保復制了“對象內的所有成員變量”以及“所有的 base class 成員”。
  • 不要嘗試以某個拷貝函數去實現另一個拷貝函數,應該將兩者共同的部分抽取到一個新的函數中去完成,然后由兩個拷貝函數共用。

一般而言,如果自己不聲明拷貝構造函數和拷貝賦值操作符,那么編譯器會幫自己生成的,但是!重點來了!如果選擇了自己去聲明定義,那么麻煩事就來了(因為即便可能出錯編譯器也不會告訴你)。

尤其是一個類派生自基類的時候,就需要小心謹慎地去處理基類的對象,然而有些是 private 的,因此復制起來比較麻煩,這個時候可以使用這樣的方式來解決問題:

  • 對于拷貝構造函數,在初始化列表中顯式地去調用基類的拷貝構造函數,然后在子類的拷貝構造函數內部處理好自己的問題。
  • 對于賦值拷貝操作符,在合適的位置顯式調用基類的 operator=() 函數。

具體例子見下面:

class?BaseClass?{
private:
????string?name;
public:
????BaseClass()?=?default;
????BaseClass(int?sz,?char?c)?{?name?=?string(sz,?c);?}
????BaseClass(const?BaseClass?&rhs)?:?name(rhs.name)?{}
????BaseClass?&operator=(const?BaseClass?&rhs)?{
????????if?(this?==?&rhs)?return?*this;
????????this->name?=?rhs.name;
????????return?*this;
????}
????friend?ostream?&operator<const?BaseClass?&rhs)?{
????????os?<????????return?os;
????}
};
class?DeriveClass?:?public?BaseClass?{
private:
????int?age;
public:
????DeriveClass(int?a,?int?sz,?char?c)?:?BaseClass(sz,?c),?age(a)?{}
????//?必須要調用基類的拷貝構造函數,否則不會拷貝構造完全
????DeriveClass(const?DeriveClass?&rhs)?:?age(rhs.age),?BaseClass(rhs)?{}
????DeriveClass?&operator=(const?DeriveClass?&rhs)?{
????????if?(&rhs?==?this)?return?*this;
????????age?=?rhs.age;
????????//?如果不調用下面這句,將會出現沒有拷貝基類 name 值的問題!
????????BaseClass::operator=(rhs);
????????return?*this;
????}
????friend?ostream?&operator<const?DeriveClass?&rhs)?{
????????os?<"\t"?<????????return?os;
????}
};
int?main()?{
????DeriveClass?d1(18,?3,?'1');
????DeriveClass?d2(20,?5,?'2');
????DeriveClass?d3(d1);
????d1?=?d2;
????cout?<endl?<endl?<????/**
?????*?正常輸出:
?????*?22222????20
?????*?22222????20
?????*?111??????18
?????*/
????/**
?????*?如果按照前兩點建議,那么出現這樣的情況概不負責;
?????*?111??????20
?????*?22222????20
?????*??????????18
?????*/
}

總而言之,一旦選擇了自己去完成拷貝構造函數復制拷貝操作符,那么就別怪編譯器不厚道了,需要自己去謹慎操作。


軟考之后終于可以靜下心來看看書了?(開)?(心)

總結

以上是生活随笔為你收集整理的effective c++_【阅读笔记】Effective C++()的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。