C++ Primer筆記

序:

  學習C++最重要的的邊寫邊學。不理解一個語法知識的設計原因,就無法在腦海裏形成一個邏輯鏈,只能死記硬背也註定記不深刻。
  所以我推薦初學者先閱讀《Accelerated C++》一書。這本書是學習C++最好的入門書之一,通過一個個實例來讓讀者瞭解C++最常使用到的80%語法。剩下的20%可以通過《C++ Primer》來補充。我這次的筆記就是在記錄那剩下的20%。

變量類型

理解變量類型
  • 類型修飾符(*或&)僅僅修飾單個變量
  • 從右向左閱讀r的定義。離變量名最近的符號對變量的類型有最直接的影響。
int *p, q;   //p是int型指針,q是int
int *&r = p;  //r是對指針p的引用
int (*a)[n];  //數組指針:指向int數組的指針
int   *a[n]; //指針數組:[ ]的優先級高,a是一個數組,存放int*類型元素。
聲明、定義、初始化
  • 變量能且只能被定義一次,但是可以被聲明多次。
  • 值初始化是值使用了初始化器(即使用了圓括號或花括號)但卻沒有提供初始值的情況。
    • 注意,當不採用動態分配內存的方式(即不採用new運算符)時,寫成int a();是錯誤的值初始化方式,因爲這種方式聲明瞭一個函數而不是進行值初始化。如果一定要進行值初始化,必須結合拷貝初始化使用,即寫成int a=int();值初始化和默認初始化一樣,對於內置類型初始化爲0,對於類類型則調用其默認構造函數。
extern int i;           //聲明i
int j;                  //聲明並定義j
extern double pi=3.1416;//定義並初始化,此時雖然有extern語句,但是因爲加了等號,所以成爲定義
int *p=new int();       //值初始化
vector< string> vec(10);//值初始化
數組的特殊性
  • 數組不允許拷貝和賦值
  • 在很多用到數組名字的地方,編譯器會自動把其替換成一個指向數組第一個元素的指針。使用auto時是這樣,decltype時這種轉變不會發生
int a[] = {0,1,2};
int a2[] = a;    //錯誤:不允許使用一個數組初始化另一個數組
a2 = a;          //錯誤:不允許把一個數組直接賦值給另一個數組
auto ia(a);      //ia是一個整型指針,指向a的第一個元素
decltype(a) ia3={1,2.3}; //decltype(a)返回的類型是由3個元素構成的數組
類型轉換
  • 無符號類型注意不能賦值成負數。否則值實際爲對這個負數取模。
  • 賦值給帶符號類型一個超出它表示範圍的值時,結果是未定義的。
  • 顯式類型轉換(cast)
    • static_cast:處理具有明確定義的類型轉換,不包含底層const。,這種強制轉換只會在編譯時檢查。 如果編譯器檢測到您嘗試強制轉換完全不兼容的類型,則static_cast會返回錯誤。
    • reinterpret:處理非關聯的類型轉換,通過改變對象的位模式。例如 pointer 和 int的無關類型的轉換。
    • const_cast:只能改變運算對象的底層const,
    • dynamic_cast:在運行時檢查基類指針和派生類指針之間的強制轉換。

作用域

作用域嵌套
  • 內層作用域可以重定義外層作用域已有的名字。
  • 但可以通過作用域操作符來訪問外層變量。::是作用域操作符。全局作用域沒有名字,所以::name是特指全局作用域裏的name變量。

const限定符

const基本對象
  • const對象一旦創建後其值就不能改變,所以const對象必須初始化
  • 默認情況下const對象僅在文件內有效,不同文件需要定義同名的const變量,它們是獨立的。
  • 如果想只在一個文件內定義,可以使用extern關鍵字。定義語句前加extern關鍵字
//文件1中定義,該常量能被其他文件訪問
extern cnost int bufSize = fcn();
//文件2中的聲明,與文件1中是同一個
extern const int bufSize;
const與引用
  • 對const的引用,簡稱常量引用,即把引用綁定到const對象上。底層const
  • 想要引用常量必須使用常量引用,但常量引用可以引用非常量對象。
  • 允許爲一個常量引用綁定非常量的對象、字面值,甚至一般表達式。而非常量引用是不可以的。
const與指針
  • 指向常量的指針:想要存放常量對象的地址,只能使用指向常量的指針。底層const
  • const指針:指針本身是const,即指針初始化後的值(那個地址)不能改變。頂層const
const int a;
const int &b =a;     //常量引用,底層const
int i = 42;
const int &r = i*42; //可以將const的引用綁定到右值上;

const int *p1 = &a;  //指向常量的指針,底層const
int *const p2 = &a;  //常量指針,p2永遠指向a,頂層const

**在拷貝操作時,拷入和拷出的變量必須具有相同的底層const資格,或者兩個對象的數據類型必須能夠轉換。
一般來說,非常量可以轉換成常量,反之則不行**


異常處理

try語句塊
  • throw表達式,可以用來引發異常。
  • try語句塊處理異常,以try關鍵字開始,並以多個catch子句結束。
  • 異常類,用於在throw表達式和相關的catch子句之間傳遞異常的具體信息。

函數

尾置返回類型
  • 以auto開頭,以->返回類型結尾。
int (*func(int i))[10];         //直接寫法
auto func(int i) -> int(*)[10]; //尾置返回類型。返回一個指針,該指針指向含有10個整數的數組
typedef int arrT[10];          //arrT是一個類型別名
using arrT= int[10];            //arrT的等價聲明
arrT* func(int i);              //使用類型別名。返回類型同上
局部靜態對象
  • 局部靜態對象在程序執行路徑第一次經過對象定義語句時初始化,直到程序終止才被銷燬。在此期間即使對象所在的函數結束執行,也不會對它造成影響。它的值可以改變。
含有可變形參的函數
  • initializer_list形參:這是在標準庫中定義的一個模板類,其中的對象永遠是const值無法改變。而且拷貝或賦值一個initializer_list對象使用的是傳引用賦值。
  • 省略符形參:爲了便於C++程序訪問某些特殊的C代碼而設置的,通常不用於其他目的。
    void error_msg(initializer_list<string> il);
    error_msg({"a","b"});
    error_msg({"a","b","c"});
    void foo(parm_list, ...); //逗號是可選的

數據成員和成員函數
  • 在類體內定義的成員函數默認爲inline函數,在類體外定義的成員函數默認情況下不是內聯的。
  • 類內初始值有限制:可以放在花括號裏,或者等於號右邊,但不能使用圓括號。
構造函數
  • 默認構造函數:如果存在類內初始值用它們初始化,否則默認初始化。按類中出現的順序初始化。

    • 很多時候需要自己定義默認構造函數:
      1. 當類沒有聲明任何構造函數時,編譯器纔會自動生成默認構造函數。
      2. 如果定義在塊中的內置類型或符複合類型(比如數組和指針)默認初始化,它們的值將是不確定的。
      3. 如果類內有其他類類型的成員,且這個成員的類型沒有默認構造函數,那編譯器將無法初始化該成員。
    • = default:在C++11裏,如果需要默認行爲,可以通過在參數列表後面加上 = default來要求編譯器生成構造函數。Data() = default;
    • 當某個數據成員被構造函數初始值列表忽略時,它將以與合成默認構造函數相同的方式隱式初始化。
  • 能通過一個實參調用的構造函數定義了一條從構造函數的參數類型向類類型隱式轉換的規則。
  • 要意志隱式轉換,可以使用explicit關鍵字。explicit關鍵字定義的構造函數可以用於顯式地強制類型轉換。
友元
  • 可以把非成員函數定義爲友元,也可以把其他的類定義成友元,還可以把其他類(之前已定義)的成員函數定義爲友元。
  • 友元不具有傳遞性,每個類負責控制自己的友元類或友元函數。
  • 把一組重載函數作爲友元,需要對這組函數中的每一個進行聲明。
靜態成員
  • 通常類的靜態成員不能在類的內部初始化。然而如果靜態成員是constexper的就可以。
  • 靜態成員能用於某些場景,而普通成員不能
    • 靜態數據成員可以是不完全類型。
    • 可以使用靜態數據成員作爲成員函數的默認實參。

STL

泛型算法
  • 大多定義在頭文件algorithm中,還有一部分在numeric中。
  • 因爲泛型算法,只運行在迭代器之上,而不執行容器操作。所以泛型算法永遠不會改變容器的大小。即只可能改變元素的值,或者移動元素,淡不會直接添加或者刪除元素。使用插入迭代器可以改變容器大小,但這個和算法本身無關。
  • 只讀算法
    • 通常最好使用cbegin()和cend()。但如果想用算法返回的迭代器來改變元素,就需要使用begin()和end()。
    • 那些只接受一個單一迭代器來表示第二個序列的算法,都假定第二個序列至少與第一個序列一樣長。
    • 從兩個序列讀元素的算法,構成這兩個序列的元素可以來自於不同類型的容器。如一個vector,一個list。
  • 寫容器元素的算法
    • 向目的位置迭代器寫如數據的算法假定目的位置足夠大,能容納要寫入的元素。
  • lambda表達式:[捕獲列表] (參數列表) -> 返回類型 { 函數體 }
    • 可以忽略參數列表和返回類型。但必須永遠包括捕獲列表和函數體。
    • 如果忽略返回類型,lambda根據函數體中的代碼推斷出返回類型。如果函數體只是一個return語句,則返回類型從返回表達式推斷。否則返回類型爲void。
    • 捕獲值是在創建時拷貝,而不是調用時。
  • 參數綁定,bind函數:auto newCallable = bind(callable , arglist);
    • bind函數可以看成一個函數適配器,它接受一個可調用對象,生成一個新的可調用對象來“適應”原對象的參數列表。當我們調用newCallable時,newCallable會調用callable,並傳遞給它arg_list參數。callable對象原有多少參數,arg_list就應該有多少個。

內存管理

1.靜態內存:保存局部static對象、類static數據成員以及定義在任何函數之外的變量。static對象在使用前分配,在程序結束時銷燬。
2.棧內存:保存定義在函數內的非static對象。棧對象僅在其定義的程序塊運行時才存在。
3.動態內存:程序用堆來儲存動態分配的對象,即那些在程序運行時分配的對象。動態對象的生存期由程序來控制,也就是說必須顯式銷燬。一般程序使用動態內存出於以下三種原因之一:
* 程序不知道自己需要使用多少對象
* 程序不知道所需對象的準確類型
* 程序需要在多個對象間共享數據

智能指針
  • shared_ptr:我們可以認爲每個shared_ptr都有一個關聯的計數器,通常稱爲引用計數。
    • 計數器遞增:當用一個shared_ptr初始化另一個shared_ptr,或將它作爲參數傳遞給函數,以及作爲函數的返回值。
    • 計數器遞減:當給shared_ptr賦一個新值,或者一個局部的shared_ptr離開其作用域。
    • shared_ptr在無用後任然保留的一種情況是,你將shared_ptr存放在一個容器中,隨後重排了容器,從而不再需要某些元素。這種情況應該用erase刪除。
    • get使用:(1)確定代碼不會delete指針的情況下,才使用get。(2)永遠不要用get初始化另一個智能指針或者爲另一個智能指針賦值。
  • unique_ptr:一個unique_ptr“擁有”它所指向的對象,所以不支持普通的拷貝和賦值操作。但可以拷貝或賦值一個將要被銷燬的unique_ptr。最常見的例子是從函數返回一個unique_ptr。
動態數組
int n=5;
int *pia = new int[n];//pia指向第一個int
delete [] pia;//pia必須指向一個動態數組或爲空
allocator類
  • 標準庫allocator類定義在memory中,它幫助我們將內存分配和對象構造分離開。

拷貝控制

拷貝、賦值、與銷燬(三五法則)
  • 拷貝構造函數:如果一個構造函數的第一個參數是自身類類型的引用,且任何額外參數都有默認值,則爲拷貝構造函數。
    • 一般此函數的第一個參數都是const的引用。
    • 因爲在很多中情況下都會被隱式使用,所以通常不應該是explicit的。
    • 編譯器可以略過對拷貝構造函數的調用,直接創建對象
    • 調用場景:
      • 用=定義變量時
      • 將一個對象作爲實參傳遞給非引用類型的形參
      • 從一個返回類型爲非引用類型的函數返回對象
      • 用花括號列表初始化一個數組中的元素或者一個聚合類的成員
      • 某些類類型還會對它們所分配的對象使用拷貝初始化,例如標準庫容器使用insert或push成員
  • 賦值運算符
    • 名爲operator=的函數。
    • 某些運算符,包括賦值運算符,必須定義爲成員函數。
    • 賦值運算符通常應該返回一個指向其左側運算對象的引用。
  • 析構函數
    • 名字由波浪號接類名組成,它沒有返回值,也不接受參數。
    • 因爲不接受參數,所以不能被重載,對一個給定的類只會有唯一一個析構函數。
    • 當指向一個對象的引用或指針離開作用域時,析構函數不會執行
    • 析構函數的函數體自身不直接銷燬成員。成員是在析構函數體之後隱含的析構階段中被銷燬的。按初始化的逆序銷燬。
    • 調用場景:只要當一個對象被銷燬,就會自動調用其析構函數。
      • 變量在離開其作用域時被銷燬。
      • 當一個對象被銷燬時,其成員被銷燬。
      • 容器(標準庫容器和數組)被銷燬時,其成員被銷燬。
      • 對於動態分配的對象,當指向他的指針應用delete運算時被銷燬。
      • 對於臨時對象,當創建它的完整表達式結束時被銷燬。
  • 注意事項
    • 需要析構函數的類也需要拷貝和賦值操作。需要拷貝操作的類也需要賦值操作,反之亦然。
    • 可以定義刪除的函數來阻止拷貝:cpp a(const a&) = delete;
      • 析構函數通常不能是刪除的。如果是,則不能定義該類型的變量,不能釋放指向該類型的動態分配對象指針。
      • 合成的構造函數和拷貝控制成員可能是刪除的。本質上是類中含有一些成員不能被默認構造、拷貝、賦值與銷燬。
  • 右值引用:通過&&來獲取,只能綁定到一個將要銷燬的對象。
    • 右值:返回非引用類型的函數,連同算術、關係、位以及後置後置遞增遞減運算符,都會生成右值。
    • 可以將一個const的左值引用綁定到右值上。
    • 可以顯示地將一個左值爲對應的右值引用類型。也可以通過標準庫move函數,來獲得綁定到左值上的右值引用。
int &&rr1 = 42;             //ok
int &&rr2 = rr1;            //錯誤:表達式rr1是左值!
int a = 11;
int &ll1 = a;
int &&ll2 = static_cast< int&&>(ll1); //ok,顯式轉換
int &&rr3 = std::move(rr1); // ok
  • 移動構造函數:但類似拷貝構造函數,移動構造函數的第一個參數是該類類型的一個右值引用。
    • 不拋出異常的移動構造函數和移動賦值運算符必須標記爲 noexcept。
    • 引用限定符:放成員函數參數列表後。用來指出this可以指向一個左值或右值。 P483
  • 移動賦值運算符
StrVec::StrVec(StrVec &&s) noexcept; //移動構造函數
StrVec &StrVec::operator=(StrVec &&rhs) noexcept; //移動賦值運算符
  • 三/五法則:一般來說,如果一個類定義了任何一個拷貝操作,它就應該定義所有五個操作。

重載運算符與類型轉換

重載
  • 通常情況下,不應該重載逗號、取地址、邏輯與和邏輯或運算符。詳細:選擇作爲成員還是非成員 P493。
  • 如果類同時定義了算數運算符和相關的複合賦值運算符,則通常情況下應該用符合賦值來實現算術運算符。
  • 區分前置和後置運算符
StrBlobPtr operator++();    //前置版本
StrBlobPtr operator++(int); //後置版本
函數對象
  • 如果類定義了調用運算符,則該類的對象稱作函數對象
  • lambda是一種簡便的函數對象。
  • 調用形式指明瞭調用返回的類型以及傳遞給調用的實參類型,如:int< int, int>。
  • 標準庫function類型,可以用來保存一系列調用形式相同的可調用對象:function< int(int, int)>
類型轉換符
  • 類型轉換運算符是類的一種特殊成員函數,它負責將一個類類型的值轉換成其他類型。
    cpp operator type() const;
  • 類型轉換函數必須是成員函數,它不能聲明返回類型,形參列表也必須爲空。類型轉換函數通常應該是const。
  • 編譯器只能執行一個用戶定義的類型轉換,但是隱式的用於定義類類型轉換可以置於一個標準類類型轉換之前或者之後,並與其一起使用。
  • 顯式的類型轉換運算符:定義成顯式的,將不能用於隱式類型轉換。但存在一個例外:即如果表達式被用作條件,則編譯器會顯式的類型轉換自動應用於它。
  • 類型轉換的二義性。P519

面向對象程序設計

OOP面向對象編程
  • 核心思想:數據抽象、繼承、動態綁定。
  • override和final關鍵字
    • override:使用override標記某個虛函數,如果該函數並沒有覆蓋已存在的虛函數,將報錯
    • final:
      1.在類的後面跟final關鍵字,防止被繼承。
      2.把某個函數定義成final,之後任何想覆蓋該函數的操作都報錯。
虛函數
  • 某個函數被聲明爲虛函數,則在所有派生類都是虛函數。
訪問控制與繼承
  • 訪問權限
    • public:可以被任意實體訪問
    • protected:只允許子類及本類的成員函數訪問
    • private:只允許本類的成員函數訪問。子類永遠無法訪問
  • 繼承方式
    • public繼承:基類的成員遵循原有的訪問說明符。
    • pravate繼承:所有成員都變爲私有的。
    • protected繼承:將基類中public成員變爲子類的protected成員,其它成員的訪問權限不變。
  • 對於代碼中某個給定的節點而言,只有基類定義的公有成員是可訪問的,則派生類向基類的類型轉換也是可訪問的。
  • 不能繼承友元關係。
  • 可以用using聲明改變基類中非私有成員的訪問權限。using可以繼承構造函數。
派生類的拷貝控制成員函數
  • 派生類必須使用基類的構造函數來初始化基類部分成員。
  • 當派生類定義了拷貝或移動操作時,該操作負責拷貝或移動包括基類部分成員在內的整個對象
  • 析構函數只負責銷燬派生類自己分配的資源

模板與泛型編程

  • 非類型模板參數:表示一個值而不是一個類型;非類型模板參數的實參必須是常量表達式。
  • 保證傳遞給模板的實參支持模板所要求的操作,以及這些操作在模板中能正確工作,是調用者的責任。
  • 令模板自己的類型參數稱爲友元。
  • 爲模板類型定義別名。
  • 可以有默認模板實參
  • 成員模板:一個類可以包含模板成員函數。
  • 控制實例化:當模板被使用時纔會實例化,當相同的實例出現在多個對象文件中,每個文件就會有一個該模板的實例,造成開銷浪費。可以用extern來實例化聲明,表達在程序的其他位置有一個定義。定義必須只有一個。
  • 模板特例化:在template後跟一個空尖括號,因爲模板參數在函數參數中被指定爲特定類型。本質是實例化一個模板。
template< unsigned N, unsigned M> //非類型模板參數

template< class Type> class Bar{
friend Type; //將訪問權限授予用來實例化Bar的類型 Type
}

template< typename T> using twin = pair< T,T>;
twin< string> authors;    //authors是一個pair< string, string>

extern template class Blob< string>; //實例化聲明
template class Blob< string>;        //實例化定義,在本文件中實例化。

命名空間

  • 命名空間可以不連續
  • 模板特例化必須定義在原始模板所屬的命名空間中
  • 內聯命名空間:其中的名字可以直接被外層命名空間使用。
  • 未命名的命名空間:關鍵字namespace後緊跟括號。未命名的命名空間中定義的變量擁有靜態生命週期。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章