C++基礎知識面試必備、複習細節 (2)
類基礎知識
- c語言強調面向過程,c++強調面向對象
- 定義public說明符之後的成員在整個程序內可被訪問;定義private說明符之後的成員只可以被類的成員函數訪問,即隱藏了實現細節;定義protected說明符則說明該類或該類的子類、友元可以訪問
- 使用struct和class定義類的唯一區別在於默認的訪問權限不一樣,struct的默認訪問權限爲public,class的默認訪問權限爲private
- 封裝的優點:確保用戶代碼不會無意破壞封裝對象的狀態,且被封裝類的具體實現可以在不影響用戶代碼的前提下完成修改
- sizeof一個空類的結果是1,如果有虛函數則會增加一個虛函數指針的大小,靜態類數據成員不影響類大小,類成員的大小隻受其變量的影響,函數存儲在公共代碼區
- 類中成員函數使用名字順序:優先在成員函數中查找該名字聲明,如果沒有找到再去類中查找,因此如果形參的變量名和類的變量名相同則默認爲形參的,訪問類的變量可以用this指針訪問
- this指針:指向當前類對象,是一個 * const
- 繼承是"is a"關係,組合是"has a"關係
構造函數和析構函數
-
構造函數爲類分配內存和初始化,析構函數則是釋放內存
-
構造函數和析構函數不返回任何類型
-
構造函數可以被多次重載,參數列表不同即可
-
構造函數可以通過在函數參數列表後加上初始化器更方便地進行初始化
-
默認的拷貝構造函數只是進行成員變量的簡單賦值,因此如果有內存分配則需要自己重新定義拷貝構造函數
-
如果有自己進行內存分配,則需要自己寫析構函數
-
構造時從上到下:類先調用父類的構造函數再調用子類的構造函數;
析構時從下到上:類先調用子類的析構函數再調用父類的析構函數;
-
如果沒有顯式定義構造函數和析構函數,系統會隱式構造默認構造函數。只有類沒有聲明任何構造函數時系統纔會生成默認構造函數
-
派生類構造函數名(總參數列表) : 父類構造函數名(參數列表) //如果不調用父類構造函數,則使用默認構造函數
拷貝控制
-
拷貝構造函數的參數是自身對象類型的引用(必須是引用,否則豈不是永遠陷入了拷貝的無限循環)
-
可以通過重載賦值運算符’='實現賦值拷貝
-
通常,拷貝函數的調用時機爲:函數的參數爲類的對象或函數的返回值是類的對象時
-
拷貝分爲淺拷貝和深拷貝:
-
淺拷貝:按字節拷貝。這在一些情況會產生問題:如果要拷貝的類中有指針變量或數組之類的,則按字節拷貝會直接按字節進行拷貝,因此就會出現兩個指針指向同一內存的情況,當一個類析構時會釋放內存,則另一個指針將指向一個未知區域,造成內存泄露,數組也是同理,當一個類析構時會釋放內存,則造成了內存泄漏,因此如果有指針或者數組之類變量,需要採用深拷貝。
-
深拷貝:每個對象共同擁有自己的資源,必須顯式提供拷貝構造函數和賦值運算符。
深拷貝在拷貝數組或指針時將重新開闢新的區域進行存儲然後拷貝值
-
淺拷貝只是對指針的拷貝,拷貝後兩個指針指向同一個內存空間,深拷貝不但對指針進行拷貝,而且對指針指向的內容進行拷貝,經深拷貝後的指針是指向兩個不同地址的指針。
-
如果要對指針變量進行淺拷貝,可以採用 std::shared_ptr 解決該問題
-
友元
-
友元函數:在類中,用在外部函數申明前加關鍵字friend,可以允許這個外部訪問本類protected 和 private 的成員。友元函數不是類成員函數,是類外的函數,但是可以訪問所有成員
class Point{ public: friend void fun(Point t);//友元函數 private: int a; protected: int b; }; void fun(Point t) { t.a = 100; t.b = 2000; cout << t.a << " " << t.b << endl; }
-
友元類:可以定義一個class是另一個的friend,以便允許第二個class訪問第一個class的 protected 和 private 成;即如果類A是類B的友元類,則A就可以訪問B的所有成員(成員函數,數據成員)
class Point{ friend class Line; //友元類聲明方式 private: int x; };
-
友元的關係是單向的而不是雙向的。如果聲明瞭類 B 是類 A 的友元類,不等於類 A 是類 B 的友元類,類 A 中的成員函數不能訪問類 B 中的 private 成員。
-
友元的關係不能傳遞。如果類 B 是類 A 的友元類,類 C 是類 B 的友元類,不等於類 C 是類 A 的友元類
繼承與多態
-
面向對象編程的三個基本概念:數據抽象(類)、繼承、動態綁定(多態)
-
派生類從基類中繼承數據和函數,派生類可以從基類中繼承public和protected對象,基類的private只有基類本身和友元可以訪問。protected對外等價於private,對子類等價於public
定義方式:在類後寫類派生列表指明從哪個籍類繼承
class Bulk_quota:public Quota { public: xxx private: xxx }
-
繼承分爲公有繼承、私有繼承和受保護繼承
公有繼承:則外部也可以訪問父類的public函數和變量:public成員保持不變,private成員不可見,protected成員也保持不變
私有繼承:除了自己和友元,其他都不能訪問父類的函數和變量:繼承的public和protected變成了private
受保護繼承:自己、友元、子類可以訪問父類的函數和變量,外部不可訪問:原先的public變成了protected,private保持不變
-
基類通常都要定義一個虛析構函數,即使該函數不執行任何操作
-
因爲派生類都包含有基類的部分,所以可以將派生類的對象當成基類對象來使用,基類的指針或引用可以綁定到派生類對象。因此基類的指針指向的對象可能與其動態類型不一致。不可以將基類轉換爲派生類
Quota item; //基類 Bulk_quota bulk; //派生類 Quota *p=&item; //將基類指針指向基類對象 p=&bulk; //基類指針指向派生類對象 Quota &r=bulk; //基類引用綁定派生類對象
-
每個類控制自己的初始化過程,首先初始化基類的部分,然後再按照順序依次初始化派生類成員
不要做重複工作:基類的變量通常調用基類的構造函數初始化,不要在派生類中直接初始化
Bulk_quota(cosnt string& book,double p,double disc): Quota(book,p),discount(disc){};
-
可以在類名後加一個關鍵字final防止繼承
-
友元關係不可以被繼承
-
多態:靜態多態(編譯時期確定行爲,包括函數重載、模板、運算符重載等) 動態多態(運行時期纔可以確定行爲,如虛函數)
-
虛函數:在函數聲明前加一個virtual,虛函數是允許被其子類重新定義的成員函數
-
構造函數不可以聲明爲虛函數,因爲還沒過完成構造此時還沒有虛函數表
-
基類的析構函數通常寫成虛函數:防止內存泄漏。想去借助父類指針去銷燬子類對象的時候,不能去銷燬子類對象。假如沒有虛析構函數,釋放一個由基類指針指向的派生類對象時,不會觸發動態綁定,則只會調用基類的析構函數,不會調用派生類的。派生類中申請的空間則得不到釋放導致內存泄漏。
-
虛函數表(vtable):每個類都擁有一個虛函數表,虛函數表中羅列了該類中所有虛函數的地址,排列順序按聲明順序排列
虛表指針(vptr):每個類有一個虛表指針,當利用一個基類的指針綁定基類或者派生類對象時,程序運行時調用某個虛函數成員,會根據對象的類型去初始化虛指針,從而虛表指針會從正確的虛函數表中尋找對應的函數進行動態綁定,因此可以達到從基類指針調用派生類成員的效果。
-
存在虛函數的類至少有一個(多繼承會有多個)一維的虛函數表叫做虛表(virtual table),屬於類成員,虛表的元素值是虛函數的入口地址,在編譯時就已經爲其在數據端分配了空間。編譯器另外還爲每個類的對象提供一個虛表指針(vptr),指向虛表入口地址,屬於對象成員。在實例化派生類對象時,先實例化基類,將基類的虛表入口地址賦值給基類的虛表指針,當基類構造函數執行完時,再將派生類的虛表入口地址賦值給基類的虛表指針(派生類和基類此時共享一個虛表指針,並沒有各自都生成一個),在執行父類的構造函數。
C++多態的實現過程,可以得出結論:- 有虛函數的類必存在一個虛表。
- 虛表的構建:基類的虛表構建,先填上虛析構函數的入口地址,之後所有虛函數的入口地址按在類中聲明順序填入虛表;派生類的虛表構建,先將基類的虛表內容複製到派生類虛表中,如果派生類覆蓋了基類的虛函數,則虛表中對應的虛函數入口地址也會被覆蓋,爲了後面尋址的一致性。
-
純虛函數和抽象基類:
-
class CShape { public: virtual void Show()=0; //用等於0表示純虛,不對函數進行定義 };
-
聲明純虛函數後,則該類就變成了抽象基類,抽象基類不可以被實例化,只能被繼承。
-
運算符重載
-
通常情況下,不要重載逗號、取地址、邏輯與邏輯或
-
可以用友元函數方式重載,也可以類函數方式重載
-
重載輸入輸出時,通常返回 istream 和 ostream的引用,這樣有左值作用,可以連續輸入輸出
-
//重載輸出運算符 << ostream &operator<<(ostream &os,const Sale_data &item) { os<<otem.units_sold<<" "<<item.revenue; return os; } //重載相等運算符 bool operator==(const Sales_date &a,cosnt Sales_date &b) { return a.units_sold==b.units_sold && a.revenue==b.revenue; } //重載 != 運算符 bool operator!=(const Sales_date &a,cosnt Sales_date &b) { return !(a==b); } //重載+=運算符 Sales_date& operator+=(const Sales_date & rhs) { units_sold+=rhs.units_sold; revenue+=rhs.revenue; return *this; //返回*this即本身 }
-
實現遞增和遞減運算符時要區分前置和後置版本
前置運算符返回的是遞增或遞減後對象的引用,而後置版本則是對象遞增遞減後返回之前的拷貝,是一個值
爲了區分前置後置,在後置中有參數int,前置則無參數。這個int形參無需命名
//前置版本: //執行所需要的遞增或遞減後,返回*this Sales_date& operator++() { //執行遞增操作; return *this; //返回*this即本身 } //後置版本: Sales_date operator++(int) { Sales_date tmp=*this; //先存儲當前的變量值 //執行遞增操作; return tmp; //返回tmp即可 }
模板與泛型編程
關鍵字:template
//函數模板
template <typename T>
int compare(const T& v1,const T& v2)
{
if(v1<v2) return -1;
else if(v2<v1) return 1;
else return 0;
}
template <typename T> T foo(T* p)
{
T tmp=p;
// ....
return tmp;
}
//類模板
template <class type> class class-name
{
}
template <class T> class Stack
{
private:
vector<T> elems; // 元素
public:
void push(T const&); // 入棧
void pop(); // 出棧
T top() const; // 返回棧頂元素
bool empty() const
{ // 如果爲空則返回真。
return elems.empty();
}
};