第十一章 構造函數和析構函數
- 構造函數的定義與重載
1.在創建對象(new)的時候調用構造函數.可以重載多個構造函數。但必須注意保留默認構造函數,和防止出現構造的歧義。
2.構造函數不需要被用戶調用,也不能被用戶調用 - 默認構造函數(不帶參數或所有參數都有默認值)
C++中,在一個類中沒有定義構造函數,不一定會自動生成默認構造函數。只有以下四種情況時纔會生成。
1.帶有含有默認構造函數的成員類。
2.繼承自帶有默認構造函數的基類。
3.類中帶有虛函數。
4.帶有虛基類的類。
注意點:在類的對象爲數組時,編譯器會爲每個數組的元素調用默認的構造函數,所以必須使用默認構造函數初始化
#include<iostream>
using namespace std;
class Point{
public :
float x,y;
Point(){
cout<<"Create default constructer"<<endl;
}
Point(float x,float y){
this->x = x;
this->y = y;
}
~Point(){
}
};
int main(){
//Point p[5];
Point *p = new Point[5];
for(int i=0;i<5;i++){
cout<<p[i].x<<","<<p[i].y<<endl;
}
delete []p;
return 0;
}
- 拷貝構造函數
用來實現自定義類對象的賦值
定義拷貝函數時要遵循的一些規則有:
1.拷貝構造函數的名字必須與類名相同,並且無返回值。
2.只能有一個參數,且是類的一個地址引用。
3.如果不定義的話系統會自動生成一個 - 深拷貝與淺拷貝
深拷貝在拷貝的時候資源重新分配,淺拷貝則共享地址。
疑難解惑
- 派生類如何初始化基類繼承的成員?
派生類的初始化列表必須明確指出基類的初始化形式。
第12章 運算符的重載
- 運算符重載的含義和注意點。
含義:擴展運算符的功能
注意點:
1.不能重載的運算符有 . 和.* 和::和?: 和sizeof,
2.只能重載已有的運算符
3.實質是函數重載,遵循函數重載的選擇原則
4.運算符重載不影響運算符的優先級和結合性還有語法結構和操作數的個數
5.運算符重載不能改變用於內部類型的含義,對於新類型的數據不能與原功能有太大出入 - 重載運算符的形式
1.成員函數運算符
函數參數比原來的少一個(後置單目運算符除外),this隱式訪問一個對象,左邊的操作數。這也就是爲什麼後置單目運算符沒有減少的原因,其左邊沒有運算符。
2.友元函數運算符
#include<iostream>
#include<string>
using namespace std;
class Document{
public :
Document(string name){
this->name = name;
}
void getName(){
cout<<name<<endl;
}
private :
string name;
};
class Book:public Document{
public :
Book(string name,int num=0):Document(name){
this->name = name;
this->num = num;
}
void getBook(){
cout<<name<<"有"<<num<<"本"<<endl;
}
private :
string name;
int num;
};
int main(){
Book x("雜誌",12);
x.getBook();
return 0;
}
繼承與靜態成員
基類與派生類將共享該基類的靜態成員變量內存。多繼承
疑難解惑
1.在多繼承中,兩個基類具有同名變量,如何消除二義性。
使用::指定哪個類的成員
2.類不能繼承基類的哪些特徵。
構造函數,析構函數,用戶定義的new運算符,用戶定義的賦值運算符,友元關係。
第14章 虛函數與抽象類
- 虛函數的作用
在C++中虛函數是實現多態操作的主要手段之一。虛函數也是成員函數,在派生的時候被重新定義和賦予新的功能。這樣不同類對象接收同一個消息,調用相同函數名,會做出不同的反應。在java中,子類重載父類同名函數時,系統自動實現了虛函數。
#include<iostream>
using namespace std;
class base{
public :
virtual void vfunc(){
cout<<"this is base's vfunc()"<<endl;
}
void vfunc2(){
cout<<"this is base's vfunc2()"<<endl;
}
};
class derived1:public base{
public :
//重寫
void vfunc(){
cout<<"this is derived1's vfunc()"<<endl;
cout<<"這個類重寫了父類的函數"<<endl;
}
};
class derived2:public base{
public :
void vfunc2(){
cout<<"this is derived2's vfunc()"<<endl;
cout<<"這個類沒有重寫父類的函數"<<endl;
}
};
int main(){
base *p,b;
derived1 d1;
derived2 d2;
p = &b;
p->vfunc();
p = &d1;
//有用定義爲虛函數,子類重寫父類vfunc();
p->vfunc();
p = &d2;
//由於沒有使用virtual,子類不會重寫父類的vfunc2,故該變量依舊調用父類中的函數;
p->vfunc2();
d2.vfunc2();
return 0;
}
動態綁定和靜態綁定
靜態綁定是發生在編譯時期,如某函數依賴於對象的靜態類型
動態綁定是發生在運行使其,即使用虛函數時,才具備動態綁定的特徵。在Java中即爲自動轉型與多態的意義。
注意點:
執行動態綁定的只有通過地址,即只有通過指針或引用變量實現,而且還必須是虛函數。抽象類與純虛函數
純虛函數的定義
class base{
//純虛函數只聲明不定義
virtual void getName()=0;
};
抽象類的作用及要點
1.抽象類不能定義對象
2.如果一個類中的純虛函數沒有全部實現則爲抽象類
3.抽象類是描述了相同屬性的事物的一組公共操作的接口虛析構函數
使用虛析構函數是爲了當用一個基類的指針刪除派生類對象時,派生類的析構函數會被調用。抽象類的多重繼承
- 虛函數的實現機制–虛函數表(V-Table)
1.虛函數表存儲某個類的虛函數地址,及這個虛函數由哪個類繼承實現。
2.使用虛函數的過程是這樣的,通過一個對象地址尋找該表的地址,遍歷該表保存的虛函數的地址,通過地址調用相應的函數。
#include<iostream>
using namespace std;
class Base{
virtual void f(){
cout<<"this is Base::f()"<<endl;
}
virtual void g(){
cout<<"this is Base::g()"<<endl;
}
};
int main(){
typedef void (*Fun)(void);
Base b;
Fun pFun = NULL;
cout<<"虛函數表的地址是"<<(int *)&b<<endl;
cout<<"虛函數表第一個函數地址是"<<(int *)*(int *)(&b)<<endl;
//調用第一個虛函數
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
//調用第二個虛函數
// pFun = (Fun)*((int*)*(int*)(&b));
// pFun();
return 0;
}
這裏通過第一次取地址獲得,虛函數地址表。然後在取一次地址得到虛函數表中的第一個虛函數地址。
- 繼承關係的虛函數表
1.在子類中沒有覆蓋,則派生的虛函數放於之前定義的虛函數的後面
2.在子類中覆蓋的話,則將之類的虛函數替換掉原來的虛函數,其餘的不變
疑難解惑
虛函數和純虛函數使用要點
1.含有純虛函數的類不可實例化,還是抽象類
2.虛函數和純虛函數不可以有static標識,這個是動態綁定,static是靜態綁定在編程中虛函數的使用技巧
1.爲了提高程序的清晰性,最好在類的每一個層次中顯式聲明這些虛函數。
2.沒有定義虛函數的派生類簡單的繼承直接基類的虛函數
3.如果一個函數爲虛函數,那麼重新定義時沒有聲明這個虛函數,在之後類的繼承結構中都是虛函數。