天天看點

C++ 高品質程式設計指南讀書筆記

第四章 入門

1,全局變量的初始化不要依賴另一個全局變量。因為無法判斷順序。

2,每一個源代碼檔案就是一個最小的編譯單元,每一個編譯單元都能獨立編譯而不需要知道其他編譯單元的存在及其編譯結果。好處:公開接口、隐藏實作,減少代碼修改後重新編譯的時間。

3,一個低級資料類型對象總是優先轉換為能夠容納的下它的最大值的、占用記憶體最少的進階類型對象。

4,for循環中,如果計數器從0開始計數,則建議for語句的循環控制變量的取值采用前閉後開區間,這樣更直覺。for(int i = 0;I < N;i++)循環N次。

5,for循環周遊多元數組。C++中二維數組以先行後列存儲在連續記憶體中,先列後行效率高,因為外層循環走過每一行,在走到特定行時,内層循環走過該行包含的每一列的所有元素,顯然内層循環執行時位址跳躍的幅度很小,都是連續地依次通路每一個記憶體位置相鄰的元素,沒有跨行存取。

第五章 常量

1,常量分為#define定義的宏常量和const定義的常量。#define是預編譯僞指令,在編譯階段前就替換為所代表的字面常量了,是以宏常量在本質上是字面常量。C語言中,const常量是值不能修改的變量,有存儲空間。但是C++中,基本資料類型的const常量在符号表中,不配置設定存儲空間,ADT/UDT的const對象會配置設定存儲空間。const常量有資料類型宏常量沒有資料類型,編譯器會對const常量進行類型檢查,不會對宏常量進行類型檢查,const常量可以調試,宏常量不可調試。

2,在C語言中,const常量是外連接配接的,也就是不能在兩個編譯單元中同時定義一個同名的const常量,或者把一個const常量定義在一個頭檔案而在多個編譯單元同時包含該頭檔案。但在C++中,const常量是内連接配接的,編譯器會認為它們是不同的符号常量,為每個編譯單元分别配置設定存儲空間。不要在頭檔案中初始化字元串常量,這樣包含了這個頭檔案的每一個編譯單元會為那一長串的字元串字面常量建立一個獨立的拷貝項,空間開銷很大。

3,常量分為:#define常量,const常量,enum常量。

4,類中的cosnt: const資料成員 隻在某個對象生存期内是常量,而對于整個類而言卻是可變的。因為類可以建立多個對象,不同的對象其const資料成員的值可以不同。是以不能在類的聲明中初始化const資料成員,因為類的對象沒被建立時,編譯器不知道const資料成員的值是什麼。const常量隻能在初始化清單初始化。但是static const常量可以直接在頭檔案初始化。

第六章 函數

1,函數調用方式有三種:1,過程式調用,2,嵌套調用,如;lcm(lcm(a,b),c).3,遞歸調用:自己調自己。

2,參數傳遞方式:1,值傳遞 2,位址傳遞 3,引用傳遞。如果是指針傳遞且僅作為輸入用,則應在類型前加const,防止指針指向的記憶體單元被無意修改。如果是以值傳遞的方式傳遞對象,則宜改用const &來傳遞,這樣不會調用對象的構造和析構函數,提高效率。

3,傳回值。

A, 不要省略傳回值類型,如果沒有傳回值,應聲明為void類型。

B, 如果函數的傳回值時一個對象,有些場合下可以用“傳回引用”替換“傳回對象值”,以提高效率,而且還可以支援鍊式表達,但有些場合隻能傳回對象值。

例如重載運算符= 和 +,=傳回引用,沒有内容複制的過程,+傳回對象值,函數内建立一個臨時對象temp,如果傳回引用,在函數結束後引用會失效。

C, return語句不可傳回指向堆棧記憶體的指針或引用,因為該記憶體單元在函數體結束時會自動釋放,如char str[] = “haha”; return str.但是const char *p=”hahaha”;return p,是對的,因為字元串常量儲存在程式的靜态資料區。

D,return String(s1 + s2)和String result(s1 + s2);return result;不一樣:後者是先建立result對象,調用構造函數初始化,再調用拷貝構造函數把result複制到儲存傳回值的外部存儲單元,最後在函數結束時銷毀result。前者建立一個臨時對象并傳回它,編譯器可以直接把臨時對象建立并初始化在外部存儲單元,省去了拷貝和析構。

4,當局部變量與某一全局變量同名時,在函數内部将遮蔽該全局變量,這時可以通過一進制作用域解析運算符(::)來引用全局變量,如,::g_iCount++。

5,任何能用遞歸函數實作的解決方案都可以用疊代來實作,但是對于某些複雜的問題,将遞歸方案展開為疊代方案可能比較困難,而且程式的清晰性會下降。遞歸因為反複調用函數占用了大量的堆棧空間,運作時開銷很大,但是疊代隻發生在一個函數内部,反複使用局部變量進行計算,開銷要好很多,但是遞歸更直覺,易于閱讀和了解。

6,Use const whenever you need!

第七章 指針 數組 字元串

1,指針的算術運算。

       ++,--,引用&,解引用*,==,!=。

       指針加減一個正整數i,含義不是在其值上直接加減i,還要包含指針所指對象的位元組數資訊,如:int *pInt = new int[100];pInt +=50;編譯器改寫為pInt += 50 * sizeof(int)。是以void*類型的指針不能參與算數運算。還有不能對voiud*類型的指針使用*來擷取它指向的變量。

2,數組不能從函數的return語句傳回,但是數組可以作為函數的參數。數組名其實就是這個數組的首位址。把數組作為函數參數傳遞給函數時并非把整個數組的内容傳遞進去,此時數組會退化成一個同類型的指針。

3,字元數組是元素為字元變量的數組,字元串則是以’/0’結束字元的字元數組。

     如果用一個字元串字面常量來初始化一個字元數組,數組的長度至少要比字元串長度大1,因為還要儲存/0.如char array[] = “Hello”;數組的元素其實是{‘H’,’e’,’l’,’l’,’o’,’’/0}。

4,引用和指針

A, 引用在建立的同時必須初始化,而指針在定義的時候不必初始化,可以在定義後面的任何地方重新指派。

B, 不存在NULL引用,引用必須與合法的存儲單元關聯,而指針可以為NULL。

C, 引用一旦被初始化為指向一個對象,它就不能被改變為另一個對象的引用,而指針可以任何時候改變為指向另一個對象。

D,引用的建立和銷毀不會調用類的拷貝構造函數和析構函數。

E,  引用主要用途是用來修飾函數的形參和傳回值,既有指針的效率,又具有變量使用的友善些和直覺性

第八章 進階資料類型、

1,數組作為函數參數時,會自動轉換為指針,但是包裝在struct/class中的數組,記憶體空間完全屬于該struct/class對象。如果把struct/class對象傳遞給函數時,其中的數組将全部複制到函數堆棧中,是以struct/class有數組成員時,最好使用指針或者引用傳遞該對象。

第九章 編譯預處理

第十章 檔案結構和程式版式

第十一章 命名規則

第十二章 面向對象

1,虛函數

       每一個具有虛函數的類叫做多态類,C++編譯器必須為每一個多态類至少建立一個虛函數表vtable,它其實就是一個函數指針數組,其中存放着這個類所有的虛函數的位址及該類的類型資訊。

2,override和隐藏

     派生類定義中的名字(對象或函數名)将義無反顧地隐藏掉基類中任何同名的對象或函數。基于這樣的原則,如果派生類定義了一個與其基類的虛函數同名的虛函數,但是參數清單有所不同,那麼這就不會被編譯器認為是對基類虛函數的改寫(Override),而是隐藏,是以也不可能發生運作時綁定。要想達成運作時綁定的效果,派生類和基類中同名的虛函數必須具有相同的原型。

       如:

       class IRectangle

{

       Virtual ~IRectangle(){}

       Virtual void Draw() = 0;

}

Class RectangleImpl : public IRectangle

{

       …

       Virtual void Draw(int scale){cout << “RectangleImpl::Draw(int)”<<endl;}

       Virtual void Draw(){cout << “RectangleImpl::Draw()”<<endl;}

}

Void main(void)

{

       IRectangle *pRect = IRectangle::CreateRectangle();

       pRect->Draw(); //(1)

       pRect->Draw(200); //(2)

}

上述1調用由于pRect的靜态類型為IRectangle*,是以使用IRectangle::Draw()執行靜态類型檢查,但由于pRect指向的對象實際是RectangleImpl對象,是以将綁定到RectangleImpl::Draw()。2調用,因為IRectangle沒有這個原型的函數,是以拒絕編譯,除非pRect的類型為RectangleImpl*。

如果RectangleImpl不重定義Draw():

RectangleImpl* pRectImpl = new RectangleImpl;

pRectImpl->Draw(); //(3)

pRectImpl->Draw(200); //(4)

       上述3調用無法編譯,因為Draw()被Draw(int)隐藏了。

3,C++支援運作時多态的手段有兩種,一種是虛函數機制,另一種就是RTTI。

4,多态數組

       如果能夠在數組裡放置一些多态對象的話,就可以通過一緻的接口來動态地調用他們自定義的虛函數實作了。這個想法是好的,但是會有沒意識到的問題。如:

       Shape a();

       Circle b();

       Rectangle c();

Shape myShapes[3];

       myShapes[0] = a;

myShapes[1] = b;

myShapes[2] = c;

for(int I = 0;i<3;++i)

{

       myShapes[i].Draw();

}

數組不會一次調用Shape::Draw(),Circle::Draw(),Rectangle::Draw(),因為數組的元素類型是Shape,不是Shape&或Shape*,是以會按照Shape的大小來配置設定記憶體空間,是以三者都會調用Shape::Draw()。

是以不要在數組中直接存放多态對象,而是換之以基類指針或者基類的隻能指針。

第十三章 對象的初始化 拷貝 析構

1, 初始化清單

a)       如果類存在繼承關系,派生類可以直接在初始化清單裡調用基類特定的構造函數以向它傳遞參數:

Class A

{

       A(int x);

}

              Class B : public A

              {

B(int x,int y);

}

B::B(int x,int y) : A(x)

{}

b)        類的非靜态const資料成員和引用成員隻能在初始化清單初始化,因為它們存在初始化語義,而不存在複制語義。

c)        如果一個類有另一個類的成員變量,那麼在初始化清單和在構造函數裡複制的效率是不一樣的,用初始化清單直接調用另一個類的拷貝構造函數,在構造函數裡複制先調用這個類的預設構造函數建立這個對象,再調用這個類的指派函數,顯然在初始化清單初始化效率更高。

2, 拷貝構造函數 拷貝指派函數

如果不主動編寫拷貝構造函數和拷貝指派函數,編譯器将以“按成員拷貝”的方式自動生成相應的預設函數,如果類中有指針成員或引用成員,那這兩個預設函數可能隐含錯誤。如:類string的兩個對象a,b,假設a.m_data的内容為“Hello”,b.m_data的内容為“world”,将a指派給b,預設指派函數的“按成員拷貝”意味着執行b.m_data = a.m_data,這将造成三個錯誤:1,b.m_data原來的記憶體沒有被釋放,造成記憶體洩露。2,b.m_data和a.m_data指向同一塊記憶體,a或b任何一方的變動都會影響另一方。3,在對象被析構,m_data被delete了兩次。

拷貝構造函數是在對象被建立并用另一個已經存在的對象來初始化它時調用的,而指派函數隻能把一個對象指派給另一個已經存在的對象:

String a(“hello”);

String b(“world”);

String c = a; //拷貝構造函數,因為c這時才被建立,不過最好寫成c(a)

C = a; //指派函數,因為c這個對象已經存在。

3, 派生類的基本函數

1, 派生類的構造函數應在初始化清單顯示的調用基類的構造函數。

2, 如果基類是多态類,那麼必須把基類的析構函數定義為虛函數,這樣就可以像其他虛函數一樣實作動态綁定,否則可能造成記憶體洩露。

#include <stdafx.h>

#include <iostream>

class Base

{

public:

       Base() {}

       ~Base() { std::cout << "Base::~Base()" << std::endl; }

};

class Derived : public Base

{

public:

       Derived() {}

       ~Derived() { std::cout << "Derived::~Derived()" << std::endl; }

};

int main(void)

{

       Base* p = new Derived();

       delete p;

       return 0;

}

              如果基類的析構函數不是虛函數,派生類的析構函數就不會調用,造成記憶體洩漏,上述輸出為Base::~Base(),将析構函數改為虛函數後,輸出為:Derived::~Derived()

Base::~Base().

3, 在編寫派生類的指派函數時,注意不要忘記對基類的資料成員重新指派,可以通過調用基類的指派函數來實作。如Base::operator=(other)。因為不能直接操作基類的私有成員。

4,對象的構造和析構次序

       首先調用每一個基類的構造函數,然後調用成員對象的構造函數,而每一個基類的構造函數又将首先調用它們各自基類的構造函數,直到最根類。是以,任何一個對象總是首先構造最根類的子對象,然後逐層向下擴充,直到把整個對象構造起來。

       析構函數會嚴格按照與對象構造相反的次序執行,資料成員的初始化次序跟它們在初始化清單中的順序沒有關系,隻跟它們在類中聲明的順序有關。顯然,如果每個構造函數的初始化清單各成員的順序不可能完全相同,如果按照這個順序,那析構函數就不會得到唯一的逆序了。

第十四章 函數的進階特性

1, 重置 覆寫(override) 和隐藏

重載:具有相同的作用域、函數名字相同、參數類型、順序或數目不同。

覆寫:派生類重新實作了基類的成員函數。不同的作用域,函數名稱相同,參數清單相同,基類函數是虛函數。

虛函數的覆寫有兩種方式:完全重寫和擴充。擴充指派生類虛函數首先調用基類的虛函數,然後再增加新的功能,完全重寫則不調。

隐藏:a)派生類的函數與基類的函數同名,但是參數清單有差異,此時,不論有無virtual關鍵字,基類的函數在派生類中都将被隐藏。b)派生類的函數與基類的函數同名,參數清單也相同,但是基類函數沒有virtual關鍵字,此時,基類的函數在派生類中将被隐藏。

隐藏這個玩意 哎。

2, i++ ++i

int b = ++a; 

相當于:

a += 1;

int b = a;i

int b = a++;

相當于:

Int temp = a;

a += 1;

int b = temp;

temp.~int();

3,内聯函數

       用内聯函數替代宏:

A) 宏容易出錯,不可調試。

B) 内聯函數可以調試,在程式的Debug版本,沒有真正的内聯,編譯器像普通函數那樣為它生成含有調試資訊的可執行代碼,在Release版本才真正内聯。

C) 宏不能操作類的私有成員。

D)内聯函數以代碼膨脹為代價,省去了函數調用的開銷,進而提高程式的執行效率,如果函數體的代碼比函數調用的開銷大得多,那inline的收益會很小。

E)  以下情況不宜用inline:

如果函數體内代碼過長,使用内聯将導緻代碼膨脹過大。

如果函數體内出現循環或其他複雜的控制結構,那執行函數體内代碼的時間比函數調用的開銷大得多,内聯的意義不大。

4,類型轉換函數

A) 類的構造函數

Class Point2D : public Point

{

        Public:

               Point2D(const Point& p);

}

Point2D的構造函數可以将一個Point對象自動轉換為一個Point2D對象。

B) 自定義類型轉換運算符

類型轉換運算符以operator關鍵字開始,緊接着目标類型名和()。

Class Point

{

        Public:

               Point(float x);

               Operator float() const

               {return m_x;}

        Private:

               Float m_x;

}

Point p(100.25);

Coust << p << endl;

C) 類型轉換運算符

static_cast<dest_type>(src_obj)

const_cast<dest_type>(src_obj)

reinterpret_cast<dest_type>(src_obj)

dynamic_cast<dest_type>(src_obj)

5,const 成員函數

       任何不會修改資料成員的成員函數都應該聲明為const,當編寫const成員函數時不小心寫下了試圖修改資料成員的代碼,或者調用了非const成員函數,編譯器将指出錯誤。Const跟在函數尾巴。

第十五章 異常處理和RTTI

1, RTTI

A) typeid

if(typeid(device) == typeid(Television))

{

  Television *p = static_cast<Television*>(&device);

}

B) dynamic_cast<>

Television& tv = dynamic_cast<Television&>(device);

第十六章 記憶體管理

1, 記憶體配置設定方式

A) 靜态存儲區域配置設定。記憶體在程式編譯的時候就已經配置設定好了,這些記憶體在程式的整個運作期間都存在,如全局變量、static變量。

B) 在堆棧配置設定。函數内的局部變量建立在堆棧上,函數結束時這些存儲單元自動釋放。

C) 堆或自由存儲空間配置設定,也叫動态配置設定。使用malloc new申請的記憶體,程式員自己掌握釋放記憶體的時機,用free delete。

D)原則:如果使用堆棧存儲和靜态存儲就能滿足應用要求,就不要使用動态存儲。

2, 常見的記憶體錯誤

A) 記憶體配置設定未成功,卻使用它。

在使用記憶體之前判斷指針是否為NULL,如果是new申請的記憶體,用捕獲異常來處理。

B) 記憶體配置設定成功,但是還沒有初始化就使用了它。

C) 記憶體配置設定成功并且已經初始化,但操作越過了記憶體的邊界。

D)忘記釋放記憶體或者隻釋放了部分記憶體。這樣會造成記憶體洩露。

E)  釋放了記憶體卻還在使用它。

3, 指針參數如何傳遞記憶體

A)

Void GetMemory(char*,int num)

{

        P = (char*)malloc(sizeof(char) * num);

}

編譯器總是為函數的每個參數制作臨時副本,指針參數p的副本是_p,編譯器使_p=p。如果函數修改了_p指向的内容,那麼p指向的内容也就被修改了,這就是指針可以作為輸出參數的原因。但是這裡,_p申請了記憶體空間并指向它,但是p沒有指向它,是以GetMemory并不會輸出任何東西,反而會造成記憶體洩露,因為沒有調用相應的free來釋放記憶體。

B)使用指向指針的指針或指針的引用來實作在函數中動态配置設定記憶體

Void GetMemory(char **p,int num)  //或者char *&p

{

        *p = (char*)malloc(sizeof(char) * num);

}

參數為指向指針的指針,編譯器會建立一個_p副本,跟p指向同一個指針,函數内*_p跟*p是同一個指針,修改*_p也就修改了*p。

C) 使用傳回值來傳遞動态記憶體

Char* GetMemory(int num)

{

         Char *p = (char*)malloc(sizeof(char) * num);

         Return p;

}

這裡return傳回的不是堆棧上的記憶體,是以不會在函數結束的時候被釋放。

       D)

              Char* GetString(void)

              {

                     Char p[] = “hello world”;

                     Return p;

}

數組p在堆棧中,函數結束後會被釋放。

       E)

              Char* GetString(void)

              {

                     Char *p = “hello world”;

                     Return p;

}

字元串常量位于靜态存儲區,在程式的整個生命周期都有效無論什麼時候調用GetString傳回的都隻是一個隻讀的記憶體塊的位址,可以把傳回值改為const char*防止無意中的修改。

4,malloc free是C中的庫函數,不會調用構造函數和析構函數,new delete是C++的運算符,會自動調用類的構造函數和析構函數。

第十七章 STL

1,六大元件:容器,存儲配置設定器,疊代器,泛型算法,函數對象,擴充卡。

轉載于:https://www.cnblogs.com/litmin/p/8572657.html