C/C++程式設計細節(三)——類、繼承、模板、運算子過載
1、類、派生類
C++中類的概念很重要,重要到什麼程度呢?把class/struct看成和main同一個等級,為什麼這麼說呢?因為,C++中不允許全域性變數獨立於類外,
所以,在一個C++檔案中,除了標頭檔案,就是class和main了。當然這只是其中一個原因。另外,類可以看做一種型別,和C中struct類似的一種型別。但又有一定的區別。下面具體分析。
不論是類還是派生類,都是類,具有類的屬性。關於類,將從以下幾個方面進行闡述:
(1)成員變數、成員函式
成員變數是與類相關的變數,成員函式是對成員變數操作的函式。關於成員變數和成員函式的認識,可以通過sizeof 來認識。sizeof是一個運算子,用於計算資料型別的大小。sizeof(int);int a = 4;sizeof a ;
a)空類、空結構體的大小為1個位元組;如果只有成員函式,則還是隻佔用1個位元組,因為類的成員函式獨立於類的儲存空間;
b)只含有虛擬函式的類的大小為4,或8(64位);
c)要注意const,static,它們儲存在初始化資料區;
【總結】:類表面上包含成員函式和成員變數,但在其儲存空間,只包含成員變數,成員函式獨立於類的儲存空間,存放在程式碼區。
(2)成員變數的訪問方式
a、內部訪問:由類中的成員函式對類的成員進行訪問;(類訪問)
b、外部訪問:在類外,由類的物件對類的成員進行訪問。(物件訪問)
訪問標號有三種,內部訪問及友元函式可以訪問類中所以成員;外部訪問只能訪問類的共有成員。
(3)static:作用是限定作用範圍
【類外static】類外使用static其實就是C的範疇,和C中使用技巧一樣。
A、static區域性變數:在棧空間定義的,但是存在於全域性變數區
和棧空間的區域性變數比,其特點是:只初始化一次,存放地址不變,再次呼叫時,保持上一次的值不變。
B、static全域性變數:以全域性變數的方式定義,當然存在於全域性變數區
和一般的全域性變數相比,其特點是:不能用extern宣告為引用,在其他檔案中呼叫,保持上一次模組呼叫後的值
【類內static】C++特有
A、static成員變數:雖然定義在類內,但是存放在全域性變數區,與物件無關,屬於類;static成員變數要在類的定義外面初始化。
B、static成員函式:不含有this指標,所以不能呼叫本類的成員函式和成員變數,只能訪問該類的靜態成員變數,屬於類。
此外,還要注意:
A、靜態成員變數屬於類,而不是屬於某個特定的物件,它是由該類的所有物件共享的,因此不能在類的構造方法中初始化
B,靜態成員屬於該類所有物件公有,可以被類的物件呼叫,可以用類名直接呼叫
C,靜態成員受private的限制
D,該類的靜態函式只能訪問該類的靜態成員變數
E,常量成員才不能修改,靜態成員變數必須初始化,但可以修改,如果沒有顯式初始化,會被程式自動初始化為0
(4)const:作用是防止一個變數值改變。
【類外const】
const變數,也稱為只讀變數,定義時,必須初始化。編譯階段做到只讀。C的範疇
const物件, 只讀物件,只能呼叫const型的成員,那麼如何對這些成員進行初始化呢,答案是:初始化列表。C++範疇
【類內const】
const成員變數:在類中其實只是宣告這個變數為const型,初始化工作在建構函式的初始化列表中進行。
不能再建構函式的函式體裡對const變數進行賦值。
const成員函式:只讀成員函式,該函式不能修改該物件的成員變數
建構函式、解構函式中不能使用const成員函式。
【const與函式】
const修飾形參,表示這個形參在改函式中不能進行修改。
const修飾返回值,表示這個反回值不能修改。
此外,要注意:
A,c++中,宣告const int i,是在編譯階段做到 i只可讀的。不可以修改。
const char str1[]=”abc”; const char str2[]=”abc”; const char *p1 = “abc”; const char *p2 = “abc”;
str1和str2地址不同,P1和P2地址相同
B,常量指標調只能用常量函式
class A { public: virtual void f() { cout<<"A::f()"<<endl; } void f() const { cout<<“A::f() const"<<endl; } } class B : A { public: void f(){ cout<<“B::f()”<<endl; } } g(const a* a) { a->f(); } int main() { A* a = new B(); a->f(); g(a); delete a ; }
C、引用:引用為物件起了另外的一個名字,該物件是已經存在的物件,引用必須初始化,有型別
(6)建構函式:作用就是為了方便進行類的成員變數的初始化 。所以在函式體裡最好製作變數初始化相關的操作。
分為:無參建構函式(預設建構函式),有參建構函式,拷貝建構函式。在其函式體內可以進行:
A、初始化類的成員變數;
B、申請堆空間。
初始化時必須採用初始化列表一共有三種情況
A、繼承來的物件(繼承時呼叫基類建構函式)
B、const成員
C、引用成員
程式輸出:~B ~A ~A ~Aclass A { public: A() { } ~A() { cout<<"~A"<<endl; } }; class B:public A { public: B(A &a):_a(a) { } ~B() { cout<<"~B"<<endl; } private: A _a; }; int main(void) { A a; //很簡單,定義a的時候呼叫了一次建構函式 B b(a); }
注意兩點:1)建構函式的呼叫順序:類建構函式 > 子類成員變數建構函式 > 子類建構函式
解構函式與之相反
2)注意_a(a)是對B的私有成員_a的初始化
3)解構函式一般定義為虛擬函式,建構函式不能是虛擬函式
則建構函式中,成員變數一定要通過初始化列表來初始化的是b,c。class A { ... private: int a; }; class B : public A { ... private: int a; public: const int b; A &c; static const char* d; B* e; }
分析:const成員要在建構函式中初始化。
程式輸出:012class A { public: A() { printf(“0”); } A(int a) { printf(“1”); } A& operator=(const A& a) { printf(“2”); return*this; } } int main() { A al; al=10; }
分析: A a1; //呼叫A預設建構函式
a1=10; //型別不匹配,呼叫建構函式A(int)進行隱式轉化,之後將引用傳給operator=()
(7)解構函式:作用是對成員進行清理工作。
A、棧物件,編譯器自動清理。在函式執行結束時,自動呼叫。
B、堆物件,new產生,必須手動清理,通常在解構函式中使用delete實現其析構。
C、全域性物件,構造時先呼叫建構函式,析構時也會呼叫解構函式,但是程式中看不到呼叫解構函式。
(8)友元函式:
A,定義在類中的友元函式的作用域在全域性作用域,在main函式中可以直接呼叫;
(9)行內函數inline
A,行內函數就是在編譯階段時,將程式中出現的行內函數的地方用行內函數的函式體來代替。因此,可以避免呼叫函式產生額外的時間開銷,一般用於加快程式執行速度。
B,因為將程式碼嵌入到類中,所以可能導致可執行檔案的變大或者變小。
C,在編譯的時候做型別檢查。
(10)虛擬函式
A,虛擬函式與普通函式的主要區別是具有執行時多型性,跟是否內聯無關的。所以,虛擬函式可以是inline函式,加上inline後起不到inline的作用。
B,建構函式不能宣告為虛擬函式
建構函式不能包含虛擬函式
解構函式可以宣告為虛擬函式
解構函式不能包含任何函式,所以不能包含虛擬函式
C,虛擬函式表、虛表指標
有虛擬函式的類,前四個位元組是虛表指標,指向虛表。
class Test{ public: int a; int b; virtual void fun() {} Test(int temp1 = 0, int temp2 = 0) { a=temp1 ; b=temp2 ; } int getA() { return a; } int getB() { return b; } }; int main() { Test obj(5, 10); // Changing a and b int* pInt = (int*)&obj; *(pInt+0) = 100; *(pInt+1) = 200; cout << "a = " << obj.getA() << endl; cout << "b = " << obj.getB() << endl; return 0; }
程式碼輸出:200 10
2、基類與派生類
(1)派生類對基類成員的訪問形式:
a、內部訪問:由派生類中新增的成員對基類繼承來的成員的訪問。
b、外部訪問:在派生類外,由派生類的物件對從基類繼承來的成員的訪問。
(2)訪問控制
基類成員在派生類的訪問屬性取決於繼承方式以及這些成員本來在基類中的訪問屬性
a、基類的私有成員無論什麼繼承方式,在派生類中均不可以直接訪問
b、在公有繼承下,基類的保護成員和公有成員均保持原訪問屬性
c、在保護繼承方式下,基類的保護和公有成員在派生類的訪問屬性均為保護屬性
d、在私有繼承下,基類的保護和公有成員在派生類中的訪問屬性均為私有屬性
(3)建構函式的執行先執行父類,再執行子類。解構函式的析構順序想反。
子類可以繼承父類所有的成員變數和成員方法,但不繼承父類的建構函式。因此,在建立子類物件時,為了初始化從父類繼承來的資料成員, 系統需要呼叫其父類的構造方法。
3、模板
(1)編譯時檢查資料型別,保證了型別安全。
(2)函式模板必須由編譯器根據程式設計師的呼叫型別例項化為可執行的函式。
4、運算子過載
友元函式運算子過載與成員運算子過載的區別是:友元函式沒有this指標,而成員函式有,因此,在兩個運算元的過載中友元函式有兩個參
數,而成員函式只有一個。可以總結如下:
1.對雙目運算子而言,成員函式過載運算子的函式引數表中只有一個引數,而用友元函式過載運算子函式引數表中含有兩個引數。
對單目運算子來說,成員函式過載運算子的函式引數表中沒有引數,而用友元函式過載運算子函式引數表中含有一個函式。這個問題要搞清
楚,有一個this指標的問題。。。
2.雙目運算子一般可以用友元函式過載和成員函式過載,但有一種情況只可以用友元函式過載。即:雙目運算子左邊的變數是一個常量,而
不是物件!!!這點很重要的額。
而關於運算子的過載,記著:
1.對於單目運算子,建議選擇成員函式;
2.對於運算子“=,(),[],->”只能作為成員函式;
3.對於運算子“+ =,-=,/=,*=,&=,!=,~=,%=,<<=,>>=”建議過載為成員函式;
4.對於其他運算子,建議過載為友元函式。
那麼下面這個題的答案也就很明顯了:
1、將x+y*z中的“+”用成員函式過載,“*”用友元函式過載應該寫為:?
答案為:x.operator+(operator*(y,z))
2、若要過載+、=、<<、=和[]運算子,則必須作為類成員過載的運算子是哪些?
答案:=和[]