C++筆記
-
C++概述
- C++兩大編程思想
- 面向對象:三大特性
- 封裝
- 繼承
- 多態
- 泛型編程
- 面向對象:三大特性
- C++兩大編程思想
-
雙冒號作用域運算符
- ::如果前面沒有任何作用域,代表使用全局作用域
-
命名空間
-
用途:解決命名衝突
-
可以存放變量、函數、結構體、類 …
-
必須聲明在全局作用域下
-
可以嵌套命名空間
-
是開放的,可以隨時向命名空間下添加新的成員,同名命名空間會合並
-
可以匿名
namespace{ int a = 100; //相當於在變量前加了一個關鍵字 static }
-
可以起別名
namespace veryLongName{ int a = 100; } void test(){ namespace veryShortName = NameveryLongName; cout << veryShortName::a << endl; }
-
-
using
- using聲明:當就近原則和using聲明同時出現時,需要避免二義性
- using編譯指令:如果有就近原則,那麼優先使用就近原則;存在多個並且有同名出現,需要加作用域區分
-
C++對C語言增強
-
全局變量檢測增強
-
函數檢測增強
- 聲明函數返回值
- 形參類型 檢測
- 返回值檢測增強
- 調用時參數傳參個數檢測增強
-
類型檢測增強 malloc 返回void* C++必須強轉
-
struct增強
- C++可以放入函數
- 使用時C++可以省略關鍵字struct
-
三目運算符
- C語言返回值
- C++返回變量
-
const增強
-
全局const修飾變量 受到常量區保護,即使語法通過,運行也會報錯(C和C++效果一樣)
-
局部const修飾的變量可以間接修改成功(C可修改,C++不可修改)
{ const int b = 100; int *p = &b; *p = 200; }
-
C++中const修飾變量可以用來初始化數組 int arr[b];
-
連接屬性
- C默認外部鏈接屬性
- C++默認內部鏈接屬性
-
-
const分配內存
- 對const修飾變量取地址時會分配內存
- const前加external關鍵字後也會分配內存
- 使用變量來初始化const修飾變量
- 對於自定義數據類型,也會分配內存
-
-
引用
-
給一個內存空間起別名
-
一旦初始化後不可修改指向了
- 本質是指針常量,所以必須初始化,並且初始化後不可修改
-
先定義數組類型,在定義引用
int arr[10]; int (&Arr)[10] = arr;
-
-
參數傳遞方式
- 值傳遞
- 地址傳遞
- 引用傳遞
-
引用注意事項
- 不要返回局部變量的引用
- 如果函數返回值是引用,那麼這個函數可以作爲左值進行運算
-
指針的引用
- 通過引用技術 可以簡化指針
-
常量的引用
-
類型前加const
int & ref = 10; //錯誤 const & ref = 10; //編譯會優化,類似於: int temp = 10; const int & int ref = temp;
-
使用場景:修飾函數中形參,防止誤操作
-
-
類
- class + 名 { 成員 }
- 類構成:成員函數和成員屬性
- 作用域:public、protected、private
- public: 類內可以訪問 類外也可以訪問
- protected:類內可以訪問 類外不可以訪問(子類可以訪問)
- private: 類內可以訪問 類外不可以訪問(子類不可以訪問)
-
內聯函數
- 內聯函數引出–宏函數
- 必須保證運算完整性,加括號
- 即使加括號,有些情況依然和預期結果不符合
- 函數的聲明和實現必須同時加inline
- 內部成員函數 默認前面加了inline關鍵字
- 內聯限制(編譯器決定是否爲內聯)
- 不能存在任何形式的循環語句
- 不能存在過多的條件判斷語句
- 函數體不能過大
- 不能對函數進行取地址操作
- 內聯函數引出–宏函數
-
函數默認參數
- 在形參後面=默認值
- 所有默認參數在參數列表最右邊
- 函數的聲明和實現只能有一個有默認參數
-
佔位參數
- 函數參數列表中只寫類型,調用時必須要傳入參數
- 佔位參數也可以有默認參數
-
函數重載
- 滿足函數重載條件
- 作用域必須相同
- 函數名稱相同
- 函數的參數類型、個數或順序不同
- 返回值不可以作爲重載的條件
- 引用的重載函數
- 加const和不加const也可以作爲重載條件
- 函數重載碰到函數參數默認值時,避免二義性
- 滿足函數重載條件
-
extern “C”
-
函數重載原理,編譯器在底層會將函數名字做兩次修飾,方便內部訪問函數
-
在C++下運行C語言文件
#ifdef __cplusplus extern "C" { #endif ... #ifdef __cplusplus } #endif
-
-
類的封裝
- C語言的封裝
- 缺點:C語言下沒有做類型轉換的檢測;將屬性和行爲分離
- C++封裝
- 將屬性和行爲作爲一個整體,來表現生活中的事和物
- 將成員加權限控制
- struct和class區別在於默認權限
- struct:public
- class:private
- C語言的封裝
-
構造和析構
- 構造
- 沒有返回值,也不寫void,函數名與類型相同
- 可以有參數,可以發生重載
- 由編譯器自動調用,不需要手動調用,而且編譯器只會調用一次
- 析構
- 沒有返回值,也不寫void,函數名與類型相同 在函數名前加~
- 不可以有參數,不可以發生重載
- 由編譯器自動調用,不需要手動調用,而且編譯器只會調用一次
- 構造函數分類
- 按照參數進行分類
- 有參構造:Person(int age){ }
- 無參構造(默認構造):Person( ){ }
- 按照類型分類
- 普通構造
- 拷貝構造:Person(const Person &p){ }
- 按照參數進行分類
- 構造函數調用規則
- 系統默認提供三個函數:默認構造函數、析構函數、拷貝構造函數
- 如果提供了有參構造函數,那麼系統就不會提供默認構造函數,但依然提供拷貝構造函數
- 如果提供了拷貝構造函數,那麼系統就不會提供其他的普通構造函數了
- 構造
-
深拷貝和淺拷貝
- 系統提供拷貝構造函數只會做簡單的值拷貝
- 如果類中有屬性開闢到堆區,那麼在釋放的時候,由於淺拷貝問題導致堆區內容會重複釋放,程序崩潰
-
初始化列表
- 構造函數名( ) : 屬性 ( 值 ) , 屬性 ( 值 ) …
-
類對象作爲類成員
- 當其他類作爲本來成員,先構造其他類對象,再構造自身,釋放順序與構造相反
-
explicit關鍵字
- 防止隱式類型轉換方式來初始化化對象
-
new和delete運算符
- malloc和new、free和delete區別
- new、delete是運算符,malloc、free是庫函數
- malloc返回void *, new返回的是new出來的對象指針
- malloc需要判斷是否開闢成功,而new內部做了操作(內部會malloc數據在堆區,判斷內存是否分配成功,調用構造函數)
- malloc不會調用構造函數, 而new調用構造函數
- malloc對應釋放是free,new對應釋放是delete
- 注意
- 不要用void *去接受new出來的對象,原因是不能夠釋放
- 用new在堆區創建數組,類中必須要存在默認構造函數,否則無法創建
- 如果是數組釋放的時候要在delete後加 [ ]
- malloc和new、free和delete區別
-
靜態成員
-
靜態變量
- 數據共享
- 在編譯階段分配內存
- 在類內聲明、類外進行初始化
- 訪問方式:對象訪問、類名訪問
- 也有訪問權限
-
靜態函數
- 訪問方式:對象訪問、類名訪問
- 靜態成員函數不可以訪問非靜態成員變量
- 靜態成員函數可以訪問靜態成員變量,因爲都是共享數據
- 非靜態成員函數可以訪問靜態成員變量和非靜態成員變量
- 也有訪問權限
-
-
單利模式
- 主席類:一個類中只有唯一的一個實例對象
- 數據共享,而且只許拿到一個主席的對象的指針即可
-
成員變量和成員函數存儲
- 成員屬性算在類大小中
- 成員函數、靜態成員變量、靜態成員函數並不算在類大小中
-
this指針
- this指針指向的是被調用的成員函數所屬的對象
- *this對象本體
- this解決名稱衝突
-
空指針訪問成員函數
- 如果是一個空指針
- 可以訪問沒有this的一些成員函數
-
常函數和常對象
-
this指針本質
- 指針常量 type * const this;
- 指針的指向不可以修改,指針指向的值可以修改
-
常函數:
- 成員函數後面加const,不可以修改成員屬性
-
常對象:
-
不可以修改內部屬性
-
常對象只能調用常函數,不能調用普通成員函數
-
-
如果特殊要修改常函數或常對象:在屬性前加mutable修飾
-
-
友元
- 全局函數作爲友元函數
- 全局函數作爲本類友元函數,可以訪問私有內容
在類內部寫入:friend + 函數聲明
- 全局函數作爲本類友元函數,可以訪問私有內容
- 類作爲友元類
- 告訴編譯器一個類是本類的好朋友,那麼他就可以訪問到裏面私有內容
- 在類內部寫入:friend class 類名
- 成員函數作爲友元函數
- 告訴編譯器一個類中的成員函數是本類的好朋友,那麼他就可以訪問到裏面私有內容
- 全局函數作爲友元函數
-
加號運算符重載
- 對於內置數據類型,編譯器知道該如何進行運算
- 但是對於自定義數據類型,編譯器並不知道該如何運算
- 利用運算符重載可以解決問題
- 實現兩個類數據類型相加
- 分別利用成員函數和全局函數來實現
- 成員函數本質 p1.operator+(p2)
- 全局函數本質operator+(p1,p2)
- 簡化p1+p2
-
左移運算符重載
- 對於內置數據類型,編譯器知道如何cout進行<<運算輸出
- 對於自定義數據類型,無法輸出
- 重載左移
- 利用成員函數:失敗,原因不能讓cout在左側
- 利用全局函數:ostream& operator<<(ostream& cout, class_name &p)
-
遞增運算符重載
- 前置++
- 返回引用,class_name & operator++( )
- 後置++
- 返回值,class_name operator++(int )
- 前置++的效率略高於後置++,原因是前置不會調用拷貝構造函數
- 前置++
-
智能指針(指針運算符重載)
- 智能指針:用來託管堆區創建的對象的釋放
- 如果想讓sp對象創建當做一個指針去對待,需要重載-> *
-
賦值運算符重載
- 系統會默認給一個類添加4個函數:默認構造、拷貝構造、析構、operator=
- 由於系統提供的operator會進行簡單的值拷貝,導致如果屬性中有堆區的數據,會進行重複釋放
- 解決方案:需要重載operator=
- 返回值 class_name & operator=(const class_name & p)
-
[ ]重載
-
案例
int& class_name::operator[](int index){ return this->pAddress[index]; }
-
-
關係運算符重載
- 重載 == 和 !=
- 實現兩個自定義數據類型對比操作
-
不要重載 && 和 ||
- 原因:無法實現短路規則
- 總結 ( )、[ ]、->、= 只能卸載成員函數中進行重載
- << 和 >> 通常寫在全局函數配合友元進行重載
-
繼承
-
基本語法
- 語法:class 子類:繼承方式 父類
- 父類:基類
- 子類:派生類
-
繼承好處:可以減少重複代碼出現
-
繼承方式
- 公共繼承
- 父類中public到子類中public
- 父類中protected到子類中protected
- 父類中private到子類中private
- 保護繼承
- 父類中public到子類中protected
- 父類中protected到子類中protected
- 父類中private到子類中 訪問不到
- 私有繼承
- 父類中public到子類中private
- 父類中protected到子類中private
- 父類中private到子類中 訪問不到
- 公共繼承
-
構造和析構
- 先調用父類構造,在調用子類構造,析構順序和構造相反
- 可以利用初始化列表方法顯示制定出調用父類的哪個構造函數
-
同名成員處理
- 如果子類和父類擁有同名成員,優先調用子類成員,可以通過作用域調用父類的成員
- 同名成員函數,子類會隱藏掉父類中的所有版本,如果想調用父類中的其他版本,加上作用域即可
-
同名靜態成員處理
- 如果子類和父類擁有同名成員,優先調用子類成員,可以通過作用域調用父類的成員
- 同名成員函數,子類會隱藏掉父類中的所有版本,如果想調用父類中的其他版本,加上作用域即可
- 訪問方式有兩種
- 通過對象進行訪問
- 通過類名進行訪問
-
多繼承
- class 子類:繼承方式 父類1, 繼承方式 父類2
- 當兩個父類中有同名的成員被子類繼承後,調用時候需要加上作用域區分
-
菱形繼承(Animal父類,Sheep和Tuo同時繼承Animal,SheepTuo進行多繼承,父類 Sheep和Tuo)
- 問題1:訪問父類中的數據,需要加作用域區分具體數據
- 問題2:由於菱形繼承導致繼承的數據有一份浪費
- 解決方案:利用虛繼承 virtual Animal類屬於虛基類
- 在Sheep和Tuo類中繼承的內容爲vbptr 虛基類
- v:virtual
- b:base
- ptr:pointer
- vbptr指針 指向虛基類表 vbtable
- vbtable中有偏移量,通過偏移量可以找到唯一的一份數據
-
-
多態
- 靜態多態 – 函數重載、運算符重載
- 動態多態 – 父子之間繼承 + 虛函數
- 動態多態滿足條件
- 父類中有虛函數
- 子類重寫父類的虛函數
- 父類的指針或引用指向子類的對象
- 重寫:子類重寫新實現父類中的虛函數,必須返回值、函數名、參數一致才成爲重寫
- 子類在做重寫的時候,可以不加關鍵字virtual
- 多態原理
- 當父類中存在虛函數後,內部發生結構變化
- 多了指針 vfptr 虛函數表指針 – 指向 虛函數表 vftable
- 虛函數表內部記錄着虛函數的地址
- 當子類發生重寫後,會修改子類中的虛函數表中的函數地址,但是並不會影響父類中的虛函數表
- 多態好處
- 對擴展提高
- 組織性強
- 可讀性強
- 如果父類中有虛函數,子類並沒有重寫父類的虛函數,那麼這樣的代碼毫無意義(沒有用到多態帶來的好處,反而內部結構變的複雜了)
-
抽象類和純虛函數
- 純虛函數 語法:virtual void func( ) = 0;
- 如果一個類中有純虛函數,那麼這個類就無法實例化對象
- 有純虛函數的類,也稱爲 抽象類
- 如果子類繼承了抽象類,那麼子類必須重寫父類的純虛函數,否則子類也屬於抽象類
-
虛析構和純虛析構
- 如果子類中有屬性創建在堆區,那麼多態情況下,不會調用子類的析構代碼,導致內存泄漏
- 解決方案:利用虛析構或者純虛析構
- 虛析構:在析構前加 virtual
- 純虛析構 virtual ~函數名() = 0
- 純虛析構類內聲明、類外必須要實現
- 如果一個類中有了純虛析構函數,那麼這個類也屬於抽象類
-
向上和向下類型轉換
- 父類轉子類 向下類型轉換 不安全
- 子類轉父類 向上轉換安全
- 如果發生多態,總是安全的
-
重載 重寫 重定義
- 重載 函數重載
- 同一個作用域 函數名相同
- 參數 個數 類型 順序不同滿足條件
- 返回值不可以作爲重載條件
- 重寫
- 繼承關係
- 父類中有虛函數
- 子類可以重寫父類中的函數,返回值、函數名、參數類別都一致
- 重定義
- 繼承關係
- 非虛函數 子類重新定義 父類中同名的成員函數
- 重載 函數重載
-
函數模板
- template 告訴編譯器 T是萬能數據類型,下面緊跟的函數或者類中出現 T類型,不要報錯
- 調用模板函數
- 自動類型推到,必須讓編譯器推導出一致 T類型纔可以使用模板
- 顯示指定類型,顯示的告訴編譯類型 T的類型
- 模板使用的時候必須要告訴編譯器 T是什麼類型,否則無法使用
-
普通函數和函數模板的區別和調用規則
- 區別
- 普通函數可以隱式類型轉換
- 函數模板如果是自動類型推到的使用,是不可以發生隱式類型轉換
- 調用規則
- 如果函數模板和普通函數都可以實現調用,那麼優先調用普通函數
- 可以通過空參數列表語法來強制調用函數模板
- 函數模板也可以發生函數重載
- 如果函數模板可以產生更好的匹配,那麼優先使用的是函數模板
- 區別
-
模板機制
- 編譯器並不是把函數模板處理成能夠處理任何類型的函數
- 函數模板通過具體類型產生不同的函數(產生了 模板函數)
- 編譯器會對函數模板進行兩次編譯,在聲明的地方對模板代碼本身進行編譯,在調用的地方對參數替換後的代碼進行編譯
-
模板侷限性以及利用具體化技術解決
- 模板並不是真正的通用代碼,對於一些自定義的數據類型,模板有時不能實現效果
- 可以通過具體化實現對自定義數據類型進行操作
- template<> bool myCompare(Person & a, Person & b)
-
類模板
- template下面緊跟的是個類,那麼這個類成爲類模板
- 類模板和函數模板區別
- 類模板使用時不可以用自動類型推導,必須顯示指定類型
- 類模板中的類型可以有默認參數
- 泛型編程 – 體現在模板技術 – 特點 將類型參數化
- 類模板中的成員函數創建時機
- 類模板中的成員,並不是一開始創建出來的,而是在運行階段才創建出來
- 類模板作爲參數
- 指定傳入類型
- 參數模板化
- 整個類模板化
- 查看T類型名稱:typeid(T).name()
- 類模板遇到繼承問題及解決
- 如果父類是一個類模板,子類在做繼承時,必須指定出父類中T的類型,否則無法給父類中的T分配內存
- 類模板的份文件編寫問題以及解決
- 類模板不建議分文件編寫,因爲成員函數創建時機運行階段,使用時必須要包含.cpp纔可以
- 解決方法:將類中成員函數的聲明和實現都寫到一個文件彙總,並且將文件後綴名改爲.hpp即可(約定俗成)
- 類模板遇到友元函數
- 全局函數做友元的類內實現
- 全局函數做友元的類外實現
- 模板函數聲明 模板函數實現
-
類型轉換
- 靜態類型轉換 static_cast
- 語法:static_cast<目標類型>(原對象)
- 對於內置數據類型可以轉換
- 對於自定義數據類型,必須是父子之前的指針或者引用可以轉換
- 動態類型轉換 dynamic_cast
- 語法:dynamic_cast<目標類型>(原對象)
- 對於自定義數據類型
- 父轉子 不安全 轉換失敗
- 子轉父 安全 轉換成功
- 如果發生多態,那麼總是安全的都可以轉成功
- 常量類型轉換 const_cast
- 只能對 指針 或者引用之間使用
- 重新解釋類型轉化 reinterpret_cast
- 不建議使用 不安全
- 靜態類型轉換 static_cast
-
異常處理
- 基本語法
- 三個關鍵字:try throw catch
- try:試圖執行一段可能會出現異常的代碼
- throw:出現異常後拋出異常關鍵字 throw+類型
- catch:捕獲異常 catch(類型)
- 如果捕獲到的異常不想處理,想繼續向上拋出 throw
- 異常必須要有人處理,如果沒有處理,程序會自動調用terminate函數,使用程序中斷
- 可以拋出一個自定義類型的異常 myException
- 棧解旋:從try代碼開始,到throw拋出異常,所有棧上的對象都被釋放掉,釋放的順序和構造順序是相額反的,這個過程稱爲棧解旋
- 異常的接口聲明
- 如果只允許函數體中拋出某種異常,可以使用異常的接口聲明
- void func(類型),如果throw(空),代表不允許拋出異常
- 異常變量聲明週期
- MyException e會調用拷貝構造
- MyException &e引用方式 接受 建議使用這種方式 節省開銷
- MyException *e指針方式 接受 拋出&MyException();匿名對象,對象被釋放掉,不可以再操作了
- MyException *e指針方式 接受 拋出 new MyException();堆區創建的對象,記得手動釋放 delete e;
- 建議使用 引用的方式 去接受對象
- 異常的多態使用
- 提供基類異常類BaseException 純虛函數 virtual void printError() = 0;
- 提供兩個子類 繼承 異常基類 重寫父類中的純虛函數
- 測試 利用父類引用去接受子類對象 實現多態
- 拋出哪種異常就打印哪種異常printError函數
- 使用系統標準異常
- 標準異常頭文件 #include
- 使用系統異常類 out_of_range(char *)
- 捕獲 catch(exception &e) {cout << e.what();}
- 基本語法
-
標準輸入和輸出
- 標準輸入
- cin.get() 緩衝區中讀取一個字符
- cin.get(兩個參數) 不讀取換行符
- cin.getline() 讀取換行並且扔掉
- cin.ignore() 忽略 (N) N代表忽略字符數
- cin.peek() 偷窺 偷看1個字符然後放回去
- cin.putback() 返回 把字符放回緩衝區
- cin.fail() 緩衝區的標誌位 0:正常 1:異常
- cin.clear() cin.sync() 重置標誌位 並且刷新緩衝區
- 標準輸出
- out.put() out.write() 利用成員函數輸出內容
- 通過流成員函數
- int number = 99;
- cout.width(20); 預留20空間
- cout.fill("*"); 填充
- cout.setf(ios::left); 左對齊
- cout.unsetf(ios::dec); 卸載十進制
- cout.setf(ios::hex); 安裝十六進制
- cout.setf(ios::showbase); 設置顯示進制 基數
- cout.unsetf(ios::hex); 卸載十六進制
- cout.setf(ios::oct); 安裝八進制
- cout << number << endl;
- 使用控制符 頭文件#include
- int number = 99;
- cout << setw(20); 設置寬度
- << setfill("~") 填充
- << setiosflag(ios::showbase) 顯示進制基數
- << setiosflag(ios::left) 設置左對齊
- << hex 安裝十六進制
- << number
- << endl;
- 標準輸入
-
文件讀寫操作
- 讀寫頭文件 #include
- 寫 ofstream ofs
- ofs.open 指定打開方式 ios::out | ios::trunk
- 判斷是否打開成功 ofs.is_open
- ofs << “aaa”;
- 關閉流對象 ofs.close();
- 讀
- ifstream ifs
- 指定打開方式 ios::in
- 判斷是否打開成功 is_open
- 三種方式 可以讀文件中的數據
- 關閉流對象 ifs.close();