概念
先說下c++中的類的特性:封裝、繼承、多態
封裝:將數據和操作數據的函數綁定在一起,同時能設置訪問權限,比如類中的所有成員變量都是私有的,這就是封裝的意義。
繼承:繼承允許我們依據另一個類來定義一個類,這使得創建和維護一個應用程序變得更容易。這樣做,也達到了重用代碼功能和提高執行時間的效果。
多態:多態的方式有兩種
多態
多態分爲靜態多態(函數重載)和動態多態(函數重寫(覆蓋、虛函數))
靜態多態
靜態多態最簡單的例子 靜態多態就是函數重載,函數重載在編譯期決定用哪個
int Add(int a ,int b);
int Add(double a,int b);
當調用add函數是就會根據參數的類型來判斷用哪個函數;這個實現是在編譯的時候編譯器根據實參的類型來選擇對應的函數。
動態多態
動態多態,就是在程序運行的時候根據基類的指針(引用)的對象來決定到底用哪個類裏面的虛函數。
(這麼理解,我需要提前定義一個出門對象,根據時間來判斷到底乘坐什麼工具;如果沒有多態我就需要把每一個交通工具都定義過去,最後在根據時間判斷調用哪個交通工具函數;多態的效果就是我只需要定義一個出門對象,我根據時間來把創建一個交通工具對象賦給出門對象,最後就只要調用出門函數即可)
#include<iostream>
using namespace std;
class Goout{
public :
virtual void takevehicles(int x)=0;
};
class Bus : public Goout{
public:
virtual void takevehicles(int x){
cout<<"take bus"<<endl;
}
};
class Subway : public Goout{
public:
virtual void takevehicles(int x ){
cout<<"take subway"<<endl;
}
};
int main(){
//定義基類
Goout* go=NULL;
int i=rand();
if(i%2==1){
go=new Bus;
}
else {
go=new Subway;
}
cout<<i<<endl;
go->takevehicles(1);
delete go;
return 0;
}
動態多態的使用條件
如果用了基類的指針指向了派生類的對象,用基類的指針刪除派生類的對象的時候就需要用到虛析構函數,要不然會產生內存泄漏
不能處理友元函數 全局函數 靜態函數 構造
● 基類中必須包含虛函數,並且派生類中一定要對基類中的虛函數進行重寫。
● 通過基類對象的指針或者引用調用虛函數。
虛函數相關內容
- 虛函數指針是在什麼時候初始化的?
在構造函數進行虛表的創建和虛表指針的初始化,和構造子類對象時。
- 首先調用父類的構造函數,此時編譯器只看到了父類,因此先初始化了父類的虛表指針,使其指向父類的虛函數表。
- 執行子類的構造函數的時候,子類的虛表指針被初始化,只要父類中有虛函數表那麼就會在父類的虛表中添加虛函數指針。如果是重寫的話,那就回修改虛函數表中原有父類的虛函數指針。
3.重寫了之後,那麼如果用父類的指針指向子類的對象,當訪問重寫的虛函數時,就根據指針來判斷是訪問的父類還是子類的函數。 這就是動態綁定
虛函數時在編譯階段生成的,他一般存放再代碼段,也就是常量區。
- 派生類虛表
1.先將基類的虛表中的內容拷貝一份
2.如果派生類對基類中的虛函數進行重寫,使用派生類的虛函數替換相同偏移量位置的基類虛函數
3.如果派生類中新增加自己的虛函數,按照其在派生類中的聲明次序,放在上述虛函數之後
- 使用多態的缺陷
效率,多態的實現需要搜索虛函數表;空間的浪費。
- 虛函數表和編譯器的關係理解?
當類中聲明瞭虛函數是,編譯器會在類中生成一個虛函數表VS中存放在代碼段,虛函數表實際上就是一個存放虛函數指針的指針數組,是由編譯器自動生成並維護的。虛表是屬於類的,不屬於某個具體的對象,一個類中只需要有一個虛表即可。同一個類中的所有對象使用同一個虛表,爲了讓每個包含虛表的類的對象都擁有一個虛表指針,編譯器在每個對象的頭添加了一個指針,用來指向虛表,並且這個指針的值會自動被設置成指向類的虛表,每一個virtaul函數的函數指針存放在虛表中,如果是單繼承,先將父類的虛表添加到子類的虛表中,然後子類再添加自己新增的虛函數指針,但是在VS編譯器中我們通常看不到新添加的虛函數指針,是編譯器故意將他們隱藏起來,如果是多繼承,在子類中新添加的虛函數指針會存放在第一個繼承父類的虛函數表中。
成員變量、構造函數初始化順序
- 對於成員變量初始化的時候,和構造函數中初始化成員列表的順序無關,只和定義成員變量的順序有關。所以一般構造函數的初始化列表和定義順序相同。如 A(int a,int b,int c):_a(a),_b(b),_c©{};
- const成員變量和引用成員必須在初始化列表初始化,初始化列表的執行比構造函數體的執行先,因此如果類中存在const成員和引用成員就不能用缺省的構造函數,而且必須初始化列表來初始化。const成員不能直接初始化
class A{
public:
int &a;
int b;
const int c;
A():b(9),a(b),c(1){}//引用和常量成員變量必須要種這種形式來初始化
};
- static成員變量和函數,在類內聲明,然後再類外初始化;(不能在類內初始化,因爲如果在類內初始化那麼就意味着每個類都有對應的靜態對象,但是靜態成員是屬於整個類的,明顯矛盾了)爲了保證靜態成員只會被初始化一次,所以才把初始化放在了類外來做。 但是 靜態常量是可以在類內初始化的 static const int a=1; 這樣是沒有問題的。
- 整體順序:基類的靜態變量和全局變量->派生類的靜態變量和全局變量->基類成員變量->派生類成員變量
默認構造函數
- 空類的大小是多少?
1個字節,一個空類不包含任何信息,但是必須在內存空間中佔有一點的位置,否則就沒法使用這些實例。
- 空類中包含哪些函數?
class empty{
public:
empty();
~empty();
empty(const empty& rhs);//複製構造函數
empty& operator=(const empty& rhs);//賦值運算符
empty* operator&();//取地址運算符
const empty* operator&() const;//const的取地址運算符
};
- 默認拷貝構造函數會出現什麼問題?
默認拷貝構造函數是淺拷貝,因此指針會指向同一個內存空間,釋放指針的時候會出現問題。
比如類中含有指針指向了一個地址,在默認複製構造函數中,會使得複製後的對象指向同一個地址空間,
如果刪除一個對象同時釋放空間之後,另一個指針指向的位置就是無效的,那麼再次析構的時候就刪除了兩次對象。
爲了避免這種情況出現有兩種方式1.重寫默認複製構造函數 2.禁用默認複製構造函數,放入private中。
class Rect{
private:
int width;
int height;
int *p;
public:
Rect(){
p=new int(100);
}
~Rect(){
delete p;
}
};
- 虛函數佔得空間多大?
在32位機上,佔4個字節;但是如果需要計算類的大小的時候就要考慮對齊的問題。
說到對齊問題就可以多說一點,比如一個類
class A{
char a;
double b;
int c;
};//這個類的大小就是24個字節,因爲需要進行對齊,a雖然只佔了一個字節但是用了8個字節的空間
class A{
char a;
int c;
double b;
};//這個類的大小就是16個字節,爲什麼比上面少了8個字節呢? 因爲可以把a和c都放在一個8字節裏。
class A{
char a;
int c;
double b;
virtual void f(){};
};//這個類的大小就是24個字節,因爲後面的虛函數要用一個虛指針來指向虛函數表,佔了4個字節,因爲需要對齊所以耗費了8個字節。
- 爲什麼構造函數不能有返回值?
如果有返回值,那麼每次初始化對象的時候就會返回得到一個返回值,那麼賦值的時候就會把返回值傳遞給等號左邊的對象。
class A{
private:
int x_;
public:
A():x_(0){}
A(int x):x_(x){}
};
//假設這時候A初始化有返回值, A():x_(0){return 1;}
A a=A();//此時A()返回了1, A a=1;就變成用1又初始化了對象,這樣就混亂了,這樣語言就產生了歧義。
如果去理解的話,可以說是A()嘗試了一個A的隱藏返回值,因此就不需要其他返回值了。