C++面向對象程序設計
- 在C++語言中,當我們使用基類的引用或指針調用一個虛函數時將發生動態綁定,即JAVA中的多態
- 基類通常要定義一個虛析構函數,即使該函數不執行任何實際操作,也是如此。
- 在C++中基類的成員函數有兩種:1、基類希望派生類進行重寫的函數;2、基類希望派生類直接繼承而不用改變的函數。對於前者,基類將其定義爲虛函數。當指針或引用調用虛函數時,該調用將被動態綁定(即多態)。
- 任何構造函數之外的非static函數都可以是虛函數。關鍵字virtual只能出現在類內部的函數聲明語句之前,而不能出現在類外部的函數定義語句之前。如果基類把一個函數聲明成虛函數,則該函數在派生類中隱式地也是虛函數。
- 如果成員函數沒有被聲明成虛函數,則其解析過程發生在編譯時而非運行時。所以在繼承層次中,該函數只有一個,不能被重寫成多個版本。
- 派生類必須重新聲明基類中所有的虛函數。派生類可以在這樣的函數前加上關鍵字virtual,但並非非得這樣做。因爲一旦某個函數被聲明成virtual,則在所有派生類中它都是虛函數。
- 如果派生列表中基類的訪問控制符爲public,我們可以將派生類型的對象綁定到基類的引用或指針上。
- 派生類雖然有基類的數據成員,但是也只能通過基類的構造函數來初始化它們。
class A {
public:
int a;
A(int p):a(p) {}
}
class B : public A {
public:
int b;
B(int p1, int p2):A(p1), b(p2) {}
}
//B的構造函數先由A的構造函數初始化a,然後執行A的構造函數體,接着初始化b,最後執行B的構造函數體
//派生類聲明時,聲明中包含類名但是不包含它的派生列表
class B : public A;//錯誤
class B;//正確
- 如果某個類被當作基類,則該類必須被定義,而不能僅僅被聲明。
class A; //僅僅聲明瞭A類
class B : public A {
...
} //錯誤,A類必須被定義
- 從派生類向基類的類型轉換隻對指針或引用類型有效
- 基類向派生類不存在隱式的類型轉換
- 當我們用一個派生類對象爲一個基類對象初始化或賦值時,只有該派生類對象中基類的部分會被拷貝、移動、賦值,它的派生類部分將被忽略掉。所以得到的基類對象無法強制轉換成派生類對象。
class A {}
class B : public A {}
B b;
A a(b);//實際上,調用的是拷貝構造函數,即A::A(const A&)
a = b;//實際上,調用的是拷貝賦值運算符,即A::operator=(const A&)
- 普通函數如果我們不會用到它,可以只聲明不定義。但是虛函數必須有定義,不論我們是否使用了它。
- 只有虛函數纔可以被覆蓋,若類的某個虛函數的定義用final修飾,則該類的派生類就不能再覆蓋此虛函數了。
class A {
public:
virtual void f1() const;
void f2() {
cout << "A's f2 is running." << endl;
}
virtual void f3() {
cout << "A's f3 is running." << endl;
}
}
class B : public A {
public:
void f1() const final;//B類覆蓋了A中的f1函數,且不允許自身的派生類再覆蓋
void f2() {
//B中的f2,並沒有覆蓋A中的f2。
cout << "B's f2 is running." << endl;
}
void f3() {
//B中的f3,覆蓋了A中的f3。
cout << "B's f3 is running." << endl;
}
}
class C : public B {
public:
void f1();//錯誤,B已經將f1聲明成final了
}
int main() {
A a;
B b;
A *pointA = &b;
a.f2();
b.f2();
cout << "pointA->f2:";
pointA->f2();
a.f3();
b.f3();
cout << "pointA->f3: ";
pointA->f3();
cout << "pointA = &a, pointA->f3: ";
pointA = &a;
pointA->f3();
}
//輸出結果如下:
/**
A's f2 is running.
B's f2 is running.
pointA->f2:A's f2 is running.
A's f3 is running.
B's f3 is running.
pointA->f3: B's f3 is running.
pointA = &a, pointA->f3: A's f3 is running.
*/
- 虛函數也可以擁有默認實參,如果通過基類的引用或指針調用函數,使用默認實參時,傳入派生類函數的將是基類函數定義的默認實參。所以如果虛函數使用默認實參,則基類和派生類中定義的默認實參最好一致。
- 如果一個派生類的虛函數需要調用它的基類版本,必須使用作用域運算符,否則該調用將被解析成對派生類版本自身的調用。
- 純虛函數的聲明是在虛函數聲明語句的分號前加上“=0”,另外我們可以爲純虛函數提供定義,但函數體必須在類外部。
class A {
public:
virtual void doSomeThing();
}
class B : public A {
public:
void doSomeThing() = 0;//將doSomeThing聲明爲純虛函數
}
//純虛函數的函數體定義在類的外部
void B::doSomeThing() {
cout<<"B is doing some thing"<<endl;
}
- 含有未提供定義的純虛函數的類是抽象基類。我們不能創建抽象基類的對象。派生類必須自己給出這些純虛函數的定義,否則它們仍然是抽象基類。
- 派生類的成員和友元可以訪問派生類對象中由基類繼承而來的protected成員,而無法訪問一個基類對象中的protected成員。
class A {
protected:
int i;
}
class B : public A {
public:
friend void visit(A&);//不能訪問A::i
friend void visit(B&);//能訪問B::i
}
void visit(A& a) {
cout << a.i << endl;//錯誤,不能訪問A對象的i
}
void visit(B& b) {
cout << b.i << endl;//正確,能訪問B對象的i
}
- 派生類的內部或友元對繼承而來的基類成員的訪問權限是由基類對成員的訪問控制符決定的;派生類的外部(如該派生類的派生類或其它地方)對繼承而來的基類成員的訪問權限是由派生列表的訪問控制符決定的。
- 派生類向基類的類型轉換
- 友元不能繼承
class BaseA {
friend class BaseB;
protected:
int prot_data;
}
class A : public BaseA {
private:
int data;
}
class BaseB {
public:
virtual int f(BaseA baseA) {
return baseA.prot_data;//正確,BaseB是BaseA的友元
}
int f2(A a) {
return a.data;//錯誤,BaseB不是A的友元
}
int f3(A a) {
return a.prot_data;//正確,BaseB是A中基類部分的友元
}
}
class B : public BaseB {
public:
int f(BaseA baseA) {
return baseA.prot_data + 1;//錯誤,友元不能繼承,所以B不是BaseA的友元
}
}
- 名字查找先於類型檢查。如果派生類的成員與基類的某個成員同名,則派生類將在其作用域內隱藏該基類成員。即使派生類成員和基類成員的形參列表不一致,也仍然會被隱藏掉。
struct Base {
int memfcn();
};
struct Dervied : Base {
int memfcn(int); //隱藏基類的memfcn
};
Dervied d;
Base b;
b.memfcn();//調用Base::memfcn
d.memfcn(10);//調用Derived::memfcn
d.memfcn();//錯誤:參數列表爲空的memfcn被隱藏了。因爲Derived定義了一個名爲memfcn的成員,編譯器找到這個名字後,就不再繼續查找了。
d.Base::memfcn();//正確,調用Base::memfcn
- 派生類的函數覆蓋基類的函數,必須同時滿足以下兩點:
1、基類的函數是虛函數
2、派生類的函數名以及形參列表必須和基類的完全一樣,派生類的返回類型也必須和基類函數匹配
缺少任意一條,派生類的同名函數都會隱藏基類的函數
派生類的函數是否覆蓋基類函數,與該基類函數的訪問控制符無關。
用using導入基類中指定的重載函數時,必須保證所有的重載函數對派生類都是可訪問的。
class A {
private:
virtual void f();
}
class B : public A {
//using A::f; 錯誤,f是private,B不可以訪問
private:
void f();//照樣會覆蓋A::f()
}
- 如果一個類需要定義析構函數,則也必須爲它定義拷貝構造函數和拷貝賦值運算符。但是基類的析構函數並不遵循上述規則,因爲一個基類總是需要析構函數,而且要將它設置成虛函數,此時爲了定義這樣的一個析構函數而將函數體設置成空。所以無法憑此判斷該基類還需要賦值運算符或拷貝運算符。
派生類中刪除的拷貝控制與基類的關係
1、如果基類中的默認構造函數、拷貝構造函數、拷貝賦值運算符或析構函數是被刪除的函數或者不可訪問,則派生類中對應的成員將是被刪除的,原因是編譯器不能使用基類成員來執行派生類對象基類部分的構造、賦值或銷燬操作。
2、如果在基類中有一個不可訪問或刪除掉的析構函數,則派生類中合成的默認和拷貝構造函數將是被刪除的,因爲編譯器無法銷燬派生類對象的基類部分。當爲派生類自定義拷貝或移動構造函數時,默認情況下,是由基類默認構造函數初始化派生類對象的基類部分。如果我們想拷貝(或移動)基類部分,則必須在派生類的構造函數初始值列表中顯式地使用基類的拷貝(或移動)構造函數。
class Base {
/** ...*/
}
class D : public Base {
//拷貝基類成員
D(const D &d):Base(d) {
/**...*/
}
//移動基類成員
D(D&& d):Base(std::move(d)) {
/**...*/
}
}
//如果按照如下定義,則拷貝出來的對象會很奇怪:它的Base成員被賦予了默認值,而D成員的值則是從其它對象拷貝得來的。
D(const D &d) {
/**...*/
}
- 派生類的合成拷貝構造函數會使用基類的拷貝構造函數。
- 派生類自定義的賦值運算符必須顯式調用基類的賦值運算符才能爲其基類部分賦值。
//Base::operator=(const Base&)不會自動調用
D & D::operator=(const D &rhs) {
Base::operator=(rhs);//爲基類部分賦值
/**...*/
return *this;
}
- 派生類的構造函數(含拷貝、移動構造函數)、賦值運算符、析構函數的合成版會自動調用基類中對應的操作。至於基類的這些操作是合成版還是自定義的都無關緊要,只要它們可以訪問,並且不是被刪除的即可。
- 派生類自定義的構造函數(含拷貝、移動構造函數)會自動調用基類的無參構造函數,自定義的析構函數會自動調用基類的析構函數。
- 派生類自定義的拷貝構造函數、移動構造函數、賦值運算符,不會自動調用基類的拷貝構造函數、移動構造函數、賦值運算符。
- 對象銷燬的順序正好與其創建的順序相反:派生類析構函數首先執行,然後是基類的析構函數,以此類推,沿着繼承體系的反方向直至最後。
- 如果構造函數或析構函數調用了某個虛函數,則執行的是構造函數與析構函數所在類型的虛函數版本。若構造函數與析構函數是間接調用的虛函數的話(即構造函數或析構函數調用A函數,A函數又調用了B函數,而B函數是虛函數),也遵循上述規定。
- 調用upper_bound函數可以跳過multimap和multiset中關鍵字相同的元素,例子見《C++ primer》的15.8.1中“定義Baseket的成員”
- 類的構造函數初始化步驟:
1、若有初始化列表,按初始化列表初始化
2、若無初始化列表,但在類內聲明時就賦予了初始值,則按該初始值初始化
3、若無初始化列表也無聲明時的初始值,則類類型按無參構造函數初始化,基本類型的數據的無定義
class A {
public:
A() {
cout << "the A's creater without parameter is used." << endl;
}
}
class B {
public:
int i = 1;
int k;
A a;
B(){}
B(int j):i(j){}
}
int main() {
B b, b2(2);
cout << "b.i = " << b.i << endl;
cout << "b2.i = " << b2.i << endl;
cout << "b.k = " << b.k << endl;
}
/**
輸出結果:
the A's creater without parameter is used.
the A's creater without parameter is used.
b.i = 1
b2.i = 2
b.k = 15315 //k的值未定義
*/
- 列表內容