1. 程式人生 > >C++面向物件程式設計(陳維興 林小茶)精講

C++面向物件程式設計(陳維興 林小茶)精講

面向物件設計主要特徵是程式=物件+訊息,物件是基本元素,物件接收到訊息後,啟動有關方法完成操作。

面向物件程式設計的基本特徵有:抽象、封裝、繼承和多型。

c++支援編譯時的多型和執行時的多型,編譯時的多型通過函式過載實現,執行時的多型通過虛擬函式實現。

c++通過對c進行擴充,是面向過程程式設計和麵向物件設計的混合型程式設計語言。

c++程式一般由類的宣告和類的使用兩大部分組成。

c++對c的擴充:

l  c++除了保留c的進行輸入/輸出操作時常使用的printf()和scanf()函式外,新增標準輸入流物件cin和標準輸出物件cout。cin遇到空格,就認為本資料輸入結束並檢查輸入資料與變數的匹配情況。c++增加了操縱符對輸出資料格式進行控制,如換行操縱符endl。

l  在c語言中全域性變數必須宣告在任何函式之前,區域性變數必須集中在可執行語句之前,而c++允許變數宣告可與可執行語句交替出現。

l  在c++中,結構名、聯合名、列舉名都是型別名。可以像定義物件那樣,定義結構體、聯合體、列舉。不必在前面冠以struct、union、enum。

l  c語言建議為每一個函式建立函式原型,c++必須建立函式原型,以便在編譯時進行型別檢查。

l  c語言常用#define 來定義常量,但是容易出錯,c++推薦用const修飾符。

l  行內函數以空間換時間的方式降低函式呼叫時的系統開銷,但要求行內函數體內不含有負雜的控制語句,也不能過長,免得程式編譯後體量過大。c++中一個函式在類體內則被隱式的指定為行內函數,也可以用inline顯式的指定行內函數。

l  可以定義帶有預設引數的函式。

l  使用者可以過載函式,即在同一作用域中,可以定義函式名相同的函式,只要函式引數型別不同或者引數個數不同或者兼而有之。

l  作用域識別符號::c語言中函式內如有和全域性變數同名的區域性變數,則全域性變數會被遮蔽無法訪問,而c++中可以在變數名前面使用::表明是使用全域性變數。

l  計算機記憶體會被分為四個區:程式程式碼區、全程資料區、棧、堆。只有堆可由使用者分配和釋放。c語言中使用函式malloc函式和free來動態管理記憶體,c++提供了運算子new、delete來做同樣的工作。

l  引用,c++中引用就是變數的別名,故又稱別名。引用和原變數名指向記憶體中的同一區域。

l  傳遞函式引數的三種情況,1、將變數名作為函式引數,這是“傳值呼叫”,是單向傳遞,在函式執行過程中,形參的變化不會傳遞給實參,2、指標變數作為函式引數,這是“傳址呼叫”,是雙向傳遞,3、引用作為函式引數,也是“傳址呼叫”雙向傳遞。

l  通常一個函式不能直接用在賦值運算子的左邊,例如

index(2)=25;這是不允許的,但是使用引用返回函式值就可以這麼寫了:

int& index(int i)

{

 return a[i];

}

index(2)=25;這是允許的,相當於a[2]=25;要注意的是引用不是一種資料型別,所以不能定義引用的引用、引用的指標、引用的陣列。

 

類的構成:類和結構體的擴種形式十分相似,類生命中包括資料和函式,分別稱為資料成員和成員函式,資料成員和成員函式有公有、保護、私有三種訪問許可權。一般情況下,類體中只給出成員函式原型,而函式體的定義放在類外。需要注意的是不能在類宣告中給資料成員賦初值。

相同型別的物件可以相互賦值,如a=b,物件之間的賦值,僅僅是對資料成員的賦值,不同的物件的資料成員佔據不同的儲存空間而不同物件的成員函式是佔有同一個函式程式碼段,無法對他們賦值。當類體中存在指標時,使用賦值運算子進行賦值,可能會產生問題,即所謂的“淺拷貝”和“深拷貝”問題,淺拷貝時,因為含有指標,兩個物件指標指向同一塊記憶體區域,同一個物件呼叫解構函式時,該塊記憶體被釋放,當另一個也呼叫解構函式,同樣要釋放該塊記憶體,而同一塊記憶體不能被釋放兩次,這時便會出現問題,要解決該類問題,就需要使用“深拷貝”。

建構函式是特殊的成員函式,其函式名必須和類名相同,可以有任意的引數但不能有返回值甚至void也不行,它不需要使用者呼叫,在定義物件時自動執行且只執行一次,為物件分配空間,進行初始化。

c++提供了除賦值運算子之外的初始化資料成員的方法,即成員初始化列表。對於用const修飾的資料成員,或是引用型別的資料成員,是不允許用賦值語句直接賦值的,只能用成員初始化列表進行初始化。
解構函式是另一種特殊成員函式,通常用於撤銷物件時的一些清理工作,如釋放分配給物件的記憶體空間等。解構函式和建構函式名字相同,但在前面加一個波浪號(~),解構函式沒有引數也沒有返回值,而且不能過載。因此一個類中只有一個解構函式。當撤銷物件時,編譯系統會自動呼叫解構函式。

拷貝建構函式,它的作用是在建立一個新物件時,使用一個已經存在的物件去初始化這個新物件,如point p1(p2)或point p1=p2;拷貝建構函式只有一個引數,並且是同類物件的引用,每個類都必須有一個拷貝建構函式。

因為成員函式程式碼是同類所有物件共用,為了使成員函式辨別出當前呼叫自己的是哪個物件,c++引入了自引用指標this,每當建立一個物件,系統就把this指標指向該物件,當呼叫成員函式時,系統把this指標作為一個隱含引數傳遞給該函式。

物件中的資料成員有自己獨立的儲存空間,互不相干,但有時希望不同的物件可以共享一個或幾個資料成員,實現同類的不同物件間資料共享,於是有了靜態成員的概念。無論建立多少類的物件,都只有一份靜態資料成員的拷貝,從而實現同類的不同物件間的資料共享,靜態成員屬於類,所以可以用類直接訪問。靜態成員函式不是為了物件間的溝通,而是為了處理靜態資料成員。靜態成員函式沒有this指標,所以一般不訪問非靜態成員,如果要訪問非靜態成員,則需顯式的通過物件名.非靜態成員名來訪問。

有了靜態成員來實現同類不同物件間的資料共享,同樣為了實現類間的資料共享,c++引入了友元這一概念。友元是一扇通向私有成員的後門。

一個類的友元函式不是該類的成員,它可以是不屬於任何類的非成員函式,也可以是另一個類的成員函式。定義時需在友元函式前新增關鍵字friend。一個類的友元函式可以訪問該類的私有成員,但它畢竟不是該類成員,所以在定義和呼叫該類時不必像成員函式那樣在函式名前加上“類名::”。它也沒有this指標,需要顯式的通過“物件名.資料成員”才能訪問資料成員。成員函式只能訪問它所屬的類,但一個函式如果被定義為多個類的友元函式,那它可以訪問這些類的所有資料。使用友元函式時必須要慎重。

可以將一個類比如y定義為另一個類x的友元類,那麼y類中的所有成員函式都是x類的友元函式。

繼承就是從先輩處得到屬性和行為特徵,較好的解決了程式碼重用的問題。預設為私有繼承,還有公有繼承、保護繼承。基類的建構函式和解構函式不能被繼承,當建立派生類物件時,先呼叫基類的建構函式,在呼叫派生類的建構函式,撤銷派生類的物件時,順序相反。在沒有虛擬函式的情況下,如果派生類中定義了與基類成員同名的成員,稱派生類成員覆蓋了基類成員,如要在派生類中使用基類同名成員,必須使用“基類名::同名成員”的方式顯式指定,如果在物件中呼叫就用“物件名.基類名::同名成員”。

當一個派生類有多個基類時,成為多繼承。多繼承要注意建構函式的書寫,同時多繼承帶來一個問題:當派生類y有兩個基類a和b,同時a和b有一個共同的基類c,這樣y就會繼承兩個c的拷貝,當要訪問c的成員時,必須顯式的指定是a還是b的成員,以免產生二義性,為了解決這個問題,c++引入虛基類的概念。上例中可以把c宣告為a和b的虛基類,即:class a:virtual 繼承方式 c和class b:virtual 繼承方式 c這樣從a和b繼承的y只會繼承c一次。

不同資料型別資料之間的自動轉換和賦值,稱為賦值相容。基類和派生類物件之間也存有賦值相容關係:在基類物件可以使用的地方都可以用派生類的物件來替代。派生類物件可以賦值給基類物件,指向基類物件的指標可以指向基類的公有派生類,但不能指向私有派生類。

所謂多型性就是不同物件收到相同的訊息,產生不同的動作。連編是把函式名和函式體的程式程式碼連線(聯絡)在一起的過程。靜態連編在編譯階段就完成了,函式呼叫速度快,效率高,但缺乏靈活性,動態連編在執行階段完成,在程式執行時才去確定呼叫哪個函式,降低了程式執行效率但增強了靈活性。c++是編譯型語言,仍採用靜態連編,好在c++引入了“虛擬函式”從而實現了靜態連編和動態連編相結合。虛擬函式是基類中的成員函式,前面加有關鍵字virtual,並在派生類中被過載,在派生類中被重新定義時其函式原型包括返回型別、函式名、引數個數、引數型別的順序,都必須和基類中的原型完全相同。

虛擬函式使用的基礎是賦值相容規則,而賦值相容規則成立的前提條件是派生類從其基類公有派生,所以要想通過定義虛擬函式來實現動態多型性派生類就必須是從基類公有派生。行內函數不能是虛擬函式,因為行內函數不能在執行中動態確定其位置,所以即使虛擬函式在類的內部定義。編譯時仍將它看成非內聯的。

基類往往表示一種抽象的概念,此時在基類中預留一個函式名,具體功能留給派生類根據需要去定義,這樣一個虛擬函式就成為純虛擬函式,格式如下:virtual 返回型別 函式名(引數表)=0;含有純虛擬函式的類稱為抽象類。抽象類的目的就是用它去建立派生類,抽象類不能例項化為物件。也不允許從非抽象類派生出抽象類,抽象類也不能做函式引數型別、返回型別或顯式轉換的型別。

運算子的過載,需要寫一個運算子函式,比如要過載”+”號,就要寫一個名為operator+的函式。運算子過載函式有兩種形式,一種是定義為它要操作的類的成員函式,另一種是定義為該類的友元函式。友元運算子過載函式如下:

class x{

……

friend 返回型別 operator運算子(形參表);

……

}

注意友元函式不是類x的成員不能直接訪問x的成員,要顯式指定“x.成員名”也沒有this指標。並不是所有的運算子都可以定義為友元運算子過載函式,如賦值運算子“=”,下標運算子“[]”,函式呼叫運算子“()”等。

對於x=x1+x2;c++解釋為x=operator+(x1,x2);

成員運算子過載函式格式如下:

class x{

……

 返回型別 operator運算子(形參表);

……

}

 

因為成員函式可以在函式體內直接訪問類x的成員,而且有this指標隱含的指向當前物件,所以對於雙目運算子,只需一個形參就可以了。

對於x=x1+x2;c++解釋為x=x1.operator+(x2);

一般來說,雙目運算子可以被過載為友元函式活成員函式,但也有例外只能用友元函式,比如一個類的物件和一個整數或其他類物件相加的成員函式:

如果是x和整數i相加 x=x1+i;是正確的,因為c++解釋為x=x1.operator+(i);

但x=i+x1;卻出錯,因為c++解釋為x=i.operator+(x1);但是i是整數,並沒有成員函式operator+所以編譯出錯。

為了解決這一問題,只能定義兩個友元函式:

class x{

……

friend 返回型別 operator運算子(x& x1,int i);

friend 返回型別 operator運算子(int i, x& x1);

 

……

}

利用函式過載來解決運算子兩邊運算元交換的問題。

對於“++”和“--”等分字首用法字尾用法的運算子用“int”區分,沒有int是字首用法,有是字尾用法。

class x{

……

friend 返回型別 operator運算子(x& x1);//字首

friend 返回型別 operator運算子( x& x1,int);//字尾

 

……

}

為了解決“淺拷貝”帶來的“指標懸掛”問題,可以過載賦值運算子”=”,引入深拷貝。

型別轉換分為標準型別(如int,float,double,char等)間的轉換和類型別和標準型別間的轉換。準型別間的轉換c++已經自帶了方法轉換,不需使用者在寫方法。

類型別和標準型別間的轉換有兩種方法1、通過轉換建構函式進行型別轉換2、通過型別轉換函式進行型別轉換。

在程式設計的過程中經常出現這樣的情況:兩個或多個函式的函式體完全相同,差別僅在於它們的引數型別不同,為了提高程式碼的可重用性和可維護性,c++提出了模板概念。

在c語言中可以用巨集定義#define,但是巨集定義避開了型別檢查,容易出錯。

模板可以實現引數型別引數化,即把資料型別定義為引數,從而實現程式碼重用。模板分為函式模板和類模板,他們分別用來建立模板函式和模板類。

函式模板是建立一個通用函式,其函式返回型別和引數型別不具體指定,用一個虛擬的型別來代表,這個通用函式就是函式模板,系統呼叫函式時根據實參的型別取代模板中虛擬型別。

格式如下:

template  <typename 型別引數>

返回型別 函式名(模板形參表)

{

函式體

}

或者

template <class 型別引數>

返回型別 函式名(模板形參表)

{

函式體

}

一般為了與類宣告中的class區分,一般用第一種格式,c++中的型別引數一般是t、type等。,typename和class用來表名後面的引數是一個虛擬的型別名。函式模板需要例項化一個模板函式才能呼叫,當編譯系統發現一個函式呼叫

函式名(模板實參表)

就會根據模板實參表中的型別生成一個函式即模板函式。模板函式中的函式體與函式模板函式體相同。函式模板可以和同名的非模板函式過載,呼叫順序是先尋找一個引數完全匹配的非模板函式,如果有就呼叫沒有就尋找函式模板將其例項化,如例項化模板函式成功就呼叫它。

對於類的宣告也可以採用類似的方法,使用類模板可以簡化那些功能相同而資料型別不同的類的宣告。格式如下:

template <typename 型別引數>

class 類名{

類成員宣告

};

template <class 型別引數>

class 類名{

類成員宣告

};

類模板定義物件的格式是:

類名<資料實際型別> 物件名(引數表);

c++支援c語言的輸入輸出系統之外,還定義了一套面向物件的輸入輸出系統。“流”是資料從一個源留到目的的抽象,負責資料的生產者和消費者之間建立聯絡並管理資料的流動。i/o流類庫中各種類的宣告包含在相應的標頭檔案中如iostream、fstream、strstream、iomanip。

類ios是流的基類是抽象類。流類定義的物件稱為流物件,c++中有幾個預定義好的流物件:標準輸入流物件cin、標準輸出流物件cout、非緩衝型標準出錯流物件cerr和緩衝型標準出錯流物件clog。

cout中常用的成員函式:cout.put(a)輸出一個字元a;

cin中常用的成員函式:cin.get(ch)功能是從輸入流讀取一個字元(包括空白符)賦給字元型變數ch;cin.getline(字元陣列,字元個數n,終止識別符號)或cin.getline(字元指標,字元個數n,終止識別符號)功能是從輸入流讀取n-1個字元,如果提前讀到終止識別符號就提前結束最後總要插入一個字串結束標誌’\n’。cin.ignore(n,終止字元)功能是跳過輸入流中的n個(預設1個)字元或遇到指定的終止字元(預設是eof)提前結束跳躍。

流基類ios中定義了一些進行輸入輸出格式控制的成員函式,檢視書籍,除此之外c++提供了另一種輸入輸出格式控制的方法,稱為操縱符,查閱書籍,使用者也可以自定義操縱符。格式如下:

輸出流:

ostream &操縱符名 (ostream &stream)

{

自定義程式碼

return stream;

}

輸入流:

istream &操縱符名 (istream &stream)

{

自定義程式碼

return stream;

}

檔案流用來處理外存檔案,根據檔案中資料的組織形式,檔案可分為兩類:文字檔案和二進位制檔案。文字檔案又稱ASCii檔案,一個位元組存放一個ASCII程式碼代表一個字元,二進位制檔案則是記憶體中的儲存形式原樣寫到外存中形成的檔案,比如整數100,在文字檔案中是以‘1’、‘0’、‘0’三個字元的ASCII的程式碼存放的,佔3個位元組,而在二進位制檔案中就是100的二進位制形式01100100存放的,佔一個位元組。

C++進行檔案操作的一般步驟:1、為要進行操作的檔案建立一個檔案流物件,2、開啟檔案,如果不存在就建立該檔案,3、進行讀寫操作,4、關閉檔案。用到的類ifsteam(用於檔案輸入)、ofsteam(檔案輸出)、fsteam(檔案輸入輸出)。

成員函式:

檔案流物件.open(檔名,使用方式):以特定方式開啟檔案

檔案流物件.open():關閉檔案

檔案流物件<<資料:寫入檔案

檔案流物件>>變數:讀取資料

檔案流物件.read(char  *buf,int len):讀取len個字元到buf陣列

檔案流物件.write(const char  *buf,int len):將buf陣列中len個字元寫入檔案。

檔案流物件.eof():檢測是否到達檔案尾。

隨機讀寫函式:看書籍。

名稱空間是由程式設計者命名的一個記憶體區域,用來處理程式中同名衝突問題。定義格式如下:

Namespace 空間名

{

程式碼

}

C語言中沒有名稱空間,如果C++使用的帶副檔名.h的標頭檔案,不必使用名稱空間。如果C++使用的不帶副檔名.h的標頭檔案就要指定標頭檔案所在的名稱空間。

程式中的常見錯誤分為:編譯時的錯誤和執行時的錯誤。編譯時的錯誤主要是語法錯誤,執行過程的錯誤統稱為異常,對異常的處理就是異常處理。

C++中處理異常的辦法是:如果執行一個函數出現異常,可以不在本函式處理而是甩給上一級也就是函式的呼叫者,一直可以逐級上傳一直到最高階,如果最高階也無法處理,系統會呼叫系統函式terminate(),有它呼叫abort終止程式。

C++異常處理機制有檢查、丟擲和捕獲三個部分:try(檢查)、catch(捕獲)、throw(丟擲)

格式如下:

Throw 表示式:

Int  i;

Throw  i;因為i是整型變數,所以throw丟擲的是整型異常;

Throw丟擲的異常會由catch捕獲。

Try

{

被檢查的語句

}

Catch(異常型別宣告1)

{

進行異常處理的語句1

}

Catch(異常型別宣告2)

{

進行異常處理的語句2

}

……

Try和catch必須配套使用。

本書主要內容就是這些。