什麼是面向對象?
面對這個疑問,相信很多寫了多年代碼的朋友們一時也不知道怎麼回答。引用一位大神的話說:OOP編程是利用“類”和“對象”來創建各種模型來實現對真實世界的描述。
什麼是“對象”?對象,是不以人的意志爲轉移而又與自我的存在通過感性確定性進行關聯的 客體事物,是簡單的、直接性的存在、本質性的 現實。在哲學上,對象是簡單、直接的存在本質,具有現實的本質性,是知識的生成者,沒有對象就沒有知識。是感性確定性中必要的兩個條件之一,另一條件則是自我,缺一不可。對象本身是存在的,它的存在不依附任何關係。在計算機中,數據封裝形成的實體就是對象。對象是類的實例化。一些對象是活的,一些對象不是。在現實生活中一個實體就是一個對象,如一個人、一個氣球、一臺計算機等都是對象。比如這輛汽車、這個人、這間房子、這張桌子、這株植物、這張支票、這件雨衣。 概括來說就是:萬物皆對象。在面向對象的程序設計中,對象是系統中的基本運行實體,是代碼和數據的集合。 在應用領域中有意義的、與所要解決的問題有關係的任何事物都可以作爲對象,它既可以是具體的物理實體的抽象,也可以是人爲的概念,或者是人和有明確邊界和意義的東西。一個對象 即是 一個類 的 實例化後實例,一個類必須經過實例化後方可在程序中調用,一個類可以實例化多個對象,每個對象亦可以有不同的屬性。
什麼是類?一個類即是對一類擁有相同屬性的對象的抽象、藍圖、原型。在類中定義了這些對象的都具備的屬性(variables(data))、共同的方法。
面向對象編程三大特性
封裝,繼承,多態。
初識:
封裝:類定義將其說明(用戶可見的外部接口)與實現(用戶不可見的內部實現)顯式地分開,其內部實現按其具體定義的作用域提供保護。對象是封裝的最基本單位。封裝防止了程序相互依賴性而帶來的變動影響。面向對象的封裝比傳統語言的封裝更爲清晰、更爲有力。
繼承:一個類可以派生出子類,在這個父類裏定義的屬性、方法自動被子類繼承。
多態:一個接口,多種實現。一個基類中派生出了不同的子類,且每個子類在繼承了同樣的方法名的同時又對基類的方法做了不同的實現,這就是同一種事物表現出的多種形態。
深入:
繼承:
首先區分:“是”關係和“有”關係。
“是”關係表示繼承。
“有”關係表示合成。
派生類的成員函數 不能直接 訪問基類的私有成員。如果派生類能訪問基類的私有成員,則從這個派生類繼承的類也同樣能訪問這些數據。
派生類成員函數可以引用基類的 公有成員 和 保護成員,只需使用成員名即可。當派生類的成員函數重新定義基類的成員函數時,爲了從派生類中訪問基類的成員,可以在基類成員名前面加上基類名和二元作用域分解操作符 (::)
派生類 構造函數 調用 基類構造函數 時,如果 實參個數 和 類型 與 基類構造函數定義中指定的 參數個數 和 類型 不一致,就會發生編譯錯誤!
在派生類構造函數中,用成員初始化器列表初始化成員對象並顯式地調用基類的構造函數(當基類中不包含可以被隱式調用的默認構造參數),可以防止重複初始化。當調用默認構造函數時,如果數據成員在派生類的構造函數中被再次修改,就會發生重複初始化。
在派生類頭文件中用#include包含基類的頭文件。
這樣做的原因:
1、爲了讓派生類能在自己的代碼區域中,使用基類的名稱,必須告知編譯器這個基類是存在的。
2、告知編譯器能夠爲這個對象保留適當數量的內存。
3、使得編譯器能夠判斷派生類是否正確地使用了從基類繼承的成員函數。
基類的保護成員,可以由基類的成員和友元訪問,也可以由派生類的成員和友元訪問。
在現實開發中:最好用私有數據成員促進良好的軟件工程,讓編譯器解決代碼優化問題。
使用保護數據成員(protected)可能會導致兩個嚴重的問題:
1、派生類對象可以不使用成員函數而直接設置基類的保護數據成員的值,可能導致對象不一致狀態。
2、派生類成員函數的代碼編寫很可能依賴於基類的實現。增加了耦合性。
當基類只應該向它的派生類(以及友元)而不是其它客戶提供服務(即成員函數)時,最好使用protected訪問限定符。
將基類的數據成員聲明爲 私有(private)的,就使得程序員能在改變基類的實現時,不必修改派生類的實現(儘量使用這種實踐)。
雖然成員函數訪問數據成員時,比直接訪問數據稍慢一些,但是,當今編譯器會優化,可以隱式的執行許多優化操作。
在 派生類中重新定義基類的構造函數時,派生類版本通常會調用基類版本,完成部分工作,在引用基類的成員函數時,如果忘記了在函數名前加上 基類名和 ::操作符,則會導致無限遞歸。
派生類中的構造函數和析構函數
當程序創建派生類對象時,它的構造函數會立即調用基類的構造函數。首先執行的是基類的構造函數體,然後纔是派生類的成員初始化器,最後執行的是派生類的構造函數體。對於包含兩級以上的類層次,這一過程就是這樣逐級進行的。
析構順序:按照構造過程 相反 的順序依次被調用。
虛析構的意思:如果只調用了基類的析構函數 ,能夠使編譯器幫忙關聯調用相關派生類的析構函數。保證程序的正確析構。
基類的構造函數、析構函數和重載的賦值運算符並不會被派生類繼承。但是派生類的構造函數、析構函數和重載的賦值運算符,可以調用基類的構造函數、析構函數和重載的賦值運算符。
析構類似 棧 操作。
多態:
多態:需要使用基類指針句柄和基類引用句柄,而不能使用名稱句柄。
多態的關鍵:每個對象知道如何響應相同的函數調用,做正確的事情。同一消息發送到不同對象時得到的結果具有”許多形式“,因此稱爲”多態。
利用多態,可以設計和實現易於擴展的系統。多態的兩項底層技術:虛函數 和 動態綁定。
何時發生多態:當通過 基類指針 或 引用調用 虛函數時,就會發生多態----C++會動態地(即在執行時)選擇實例化對象的那個類的正確函數。
1、派生類對象的地址 賦予 基類指針,然後通過這個 基類指針調用函數,最終調用的是 基類的功能,即句柄的類型決定了會調用哪個函數。
2、不能將基類對象的地址賦予派生類指針,會編譯錯誤!!
3、通過將 基類函數 聲明爲 virtual ,引入虛函數和多態。然後,將派生類對象的地址賦予基類指針,並使用這個指針調用派生類的功能,得到的功能正是所期望獲得的多態行爲。
4、如果顯式地將 基類指針 強制轉換爲 派生類指針,則編譯器就 允許 通過這個指針訪問 只在 派生類中存在的 成員。這種技術被稱爲向下強制轉換。(必須這麼做)
虛函數:
利用虛函數,指針所指向的對象類型(不是句柄類型)決定了調用的是虛函數的哪個版本。
動態綁定:
在執行時選擇合適的函數調用,這個過程。
只能通過指針或引用才能發生動態綁定。
將基類指針指向派生類對象時,試圖用這個基類指針引用派生類纔有的成員,是錯誤的。
抽象類:
不應該實例化任何對象的類。
存在的目的:提供適當的基類,讓其它類繼承。通過將類的一個或多個虛函數聲明爲”純的“,可以使這個類成爲抽象類。
純虛函數:
純虛函數沒有提供實現,它要求派生類必須重寫這個函數才能成爲具體類。
虛函數提供了實現,允許派生類選擇是否重寫這個函數。
如果在基類中實現函數沒有什麼意義,而是希望讓它的所有派生類實現這個函數,則應該將這個函數聲明爲 純虛函數。
抽象類:
抽象類定義了類層次中的各種類的共同公共接口。
抽象類不能被實例化。
抽象類中至少需要一個純虛函數。
虛函數表:
C++編譯包含一個或多個虛函數的類時,會爲這個類建立一個vtable(虛函數表)。每當調用類的虛函數時,正在執行的程序都會利用這個vtable選擇適當的函數實現。利用多態以及使用向下強制轉換、dynamic_cast、typeid和type_info運行時類型信息RTTI 和 動態強制轉換功能。
dynamic_cast<目標類型>(原類型);
#include <typeinfo>
typeid();
虛析構函數:
如果指向派生類對象的 基類指針 使用delete運算符,顯式地銷燬這個具有非虛析構函數的派生類對象,則C++標準指定這種行爲是未定義的。解決辦法:用關鍵字 virtual 聲明基類中的析構函數。
當銷燬派生類對象時,這個派生類中的基類部分也會被銷燬,因此派生類和基類的析構函數都應該執行。基類的析構函數,會在派生類的析構函數執行之後自動執行。
可保證:當派生類對象通過基類指針刪除時,將會自動調用自定義的派生類析構函數(如果存在);
類深入研究:
先看常對象和常量成員:常對象與常量成員函數:
1、將需要修改類的數據成員的成員函數定義成const。錯誤!
2、如果將成員函數定義爲const,但它在同一個實例上調用了類的一個非常量成員函數。錯誤!
3、在常量對象上調用非常量成員函數。錯誤!。
4、常量成員函數可以被重載爲非常量的版本
5、 一旦將對象定義爲 常對象 之後,就 只能 調用類的 const 成員(包括 const 成員變量和 const 成員函數)
小插曲零散的總結:
構造函數和析構函數,不允許被聲明爲const。但可以用於初始化常量對象。
在構造函數中調用非常量成員函數,作爲常對象初始化的一部分。
常對象的”常量性‘是在 構造函數 完成了對象的初始化之後 生效的,直到 調用了對象的析構函數。
成員初始化器列表 是在執行構造函數體 之前 執行的。
常對象不能通過賦值修改,因此必須初始化它。當 類的數據成員 用const聲明時,必須使用成員初始化器,向構造函數提供類對象的數據成員的初始值。對 引用 而言,也應該這麼做。
常量數據成員(常量對象和常量變量)以及被聲明爲引用的數據成員,都必須用成員初始化器語法初始化,在構造函數體中對這些類型的數據賦值是不允許的。
友元:
類友元函數是在類的作用域外定義的,但它仍有具有訪問這個類的非公有成員的權限。獨立的函數或整個類都可以被聲明成另一個類的友元。
應將所有的友元關係聲明放在類定義體的開始部分,並且在其前面不應帶有任何訪問權限限定符。
友元關係是授予的,而不是獲得的。比如爲了使 類B 成爲 類A 的友元,類A必須顯式地將類B聲明爲它的友元。
友元關係不是對稱的、也不是傳遞的。比如類A是類B的友元,而類B是類C的友元,那麼不能說明類A與類C,類B是類A的友元。
將重載函數指定爲類的友元也是可行的,想成爲友元的每個重載函數,都必須在類定義中顯式地聲明成這個類的友元。
this指針:
成員函數 如何知道 要操作哪個對象的數據成員呢?
每個對象都可以通過一個稱爲this(C++關鍵字)的指針訪問自己的地址。
對象的this指針,並不是對象本身的一部分。所以不要用sizeof對其求值。
this指針是作爲一個隱式的實參(由編譯器)傳遞給對象的每個非靜態成員函數的。
一個有趣的應用場景:
防止對象被賦值爲自身。(當對象包含指向動態分配的內存的指針時,自身賦值會引起錯誤)。使用this指針實現串聯式函數調用。(return *this)點運算符的結合性是從左向右的。
用new 和 delete操作符實現動態內存管理
什麼是動態內存管理?
C++允許程序員在程序中控制它們的內存的分配和釋放。
堆:是爲每個程序所分配的內存區域,用於存儲動態分配的對象。
new操作符 可以用來動態分配任何 基礎類型 或 類類型 的對象。如果new 無法在內存中找到足夠空間,將會拋出異常提示出錯。
什麼是內存泄漏?
當不再需要動態分配的內存時,如果不釋放它,則可能使系統耗盡內存。
靜態類成員:
當類的所有對象只使用數據的一個副本就足夠了時,應該將這個數據聲明爲靜態數據成員,以節省空間。類的靜態成員,只具有類作用域。當不存在類的對象時,爲了訪問這個類的公有靜態成員,只需簡單地在數據成員的前面放上類名和二元作用域分解操作符(::)即可。靜態成員函數是類所提供的服務,而不是類的某個特定對象所提供的服務。不能在文件作用域的靜態數據成員的定義中包含關鍵字 static。
不能將靜態成員函數聲明爲 const,因爲const限定符表明函數不能修改它所操作的 對象 的內容,但靜態成員函數的存在和操作是獨立於類的任何對象。
如果成員函數 不需要 訪問非靜態數據成員或非靜態成員函數,則應該將他們聲明爲靜態的。
什麼是數據抽象?
客戶只關心有什麼功能,不關心這些功能怎麼實現的。
代理類:
良好的軟件工程的兩個基本原則:將接口與實現分離以及隱藏實現細節。
作用:對類的客戶隱藏類的私有數據。向類的客戶提供一個只知道類的接口的代理類,從而使得使用類的服務的客戶無法知道類的實現細節
預處理器包裝器:
#ifndef __XXX_XXX_H__
#define __XXX_XXX_H__
......
#endif
防止頭文件中的代碼被多次包含到同一個源代碼文件中
小插曲零散的總結:
三種句柄訪問類的公有成員:
1、對象名稱,2、對象引用,3、指向對象的指針。. 號表示:點成員運算符。
類的非靜態數據成員不能在聲明的時候初始化。
如果成員函數在類定義體中定義的,則編譯器會試圖通過內聯方式調用它。在類定義的外部定義的成員函數,可以使用inline關鍵字顯示的進行內聯調用,編譯器保留是否內聯的權利。
只有最簡單、最穩定的成員函數(即它的實現基本不會改變)才應該在類的頭文件中定義
//舉例子:
//一旦定義了Time類之後,它可以用作對象、數組、指針和引用聲明的一種類型:
class Time
{
};
Time sunset; //object of type Time;
Time arrayOfTimes[ 5 ]; //array of 5 Time objects
Time &dinnerTime = sunset; //reference to a Time object
Time *timePtr = &dinnerTime; //pointer to a Time object
合成和繼承的概念:
通常,類並不是從零開始,而是可以包含其它類的對象作爲自己的成員,或者從其它類派生,繼承新類能夠使用的屬性和行爲。合成:將類的對象作爲其它類的成員,稱爲合成(聚合)。繼承:從現有的類,派生新類的方法
對象的大小?
sizeof運算符,只會得到類的數據成員的 大小。因爲:編譯器只會創建成員函數的一個副本,它獨立於類的所有對象!類的所有對象共用這個副本!當然每個對象都需要擁有類數據的一個副本,所以這些數據會因爲對象而不同。
分離接口與實現:
無法簡單的通過將成員函數的聲明放在頭文件,定義放在源代碼文件來安全分離,因爲:內聯成員函數需要放在頭文件中,以便編譯器編譯客戶代碼時,客戶可看到內聯函數的定義。類的私有成員是在頭文件的類定義中列出的,以便這些成員對客戶可見,雖然客戶可能無法訪問這些私有成員。具體解決辦法:使用 “代理類” ,在類的客戶面前隱藏私有數據。
設計一個代理類:
#include <iostream>
using namespace std;
class Implementation{
public:
Implementation(int v): value(v){}
void setValue(int v){
value = v;
}
int getValue() const{
return value;
}
private:
int value;
};
class Delegate{
public:
Delegate(int v): pi(new Implementation(v)){}
void setValue(int v){
pi->setValue(v);
}
int getValue() const{
return pi->getValue();
}
private:
Implementation *pi;
};
int main()
{
Delegate d(5);
d.setValue(100);
cout<<d.getValue()<<endl;
return 0;
}
默認構造函數:
每個類最多只能有一個默認構造函數。
構造函數可以調用類的其它成員函數,但由於構造函數負責對象的初始化,因此數據成員此時可能還未處於一致狀態,在數據成員未被適當地初始化之前就使用她們,可能導致邏輯錯誤
析構函數:
當銷燬類的對象時,就會隱式地調用這個類的析構函數。析構函數本身不釋放對象的內存,它在對象的內存回收之前執行終止性的清理工作,使這些內存可以被複用,以便保存新的對象。析構函數不接收任何實參,沒有返回值。一個類只能有一個析構函數,不能重載析構。析構函數必須聲明爲public
何時調用構造函數和析構函數?
構造函數和析構函數是由編譯器 隱式 調用的。
abort函數與exit函數會迫使程序立即終止,而不允許調用任何對象的析構函數。
微妙的陷阱:
返回私有數據成員的引用。。(應該避免使用)
讓類的公有成員函數返回這個類的私有數據成員的引用。
注意:如果函數返回一個常量引用,則這個引用不能用作可修改的左值。
返回私有成員數據的引用或指針,會破壞類的封裝,並使客戶代碼依賴於類的數據的表示形式。應該避免
對象可以作爲函數的實參,也可以從函數返回。這種傳遞和返回是通過 按值 傳遞執行的,傳遞或返回的是 對象的副本。在這種情況下,C++會創建一個新對象,並使用拷貝構造器將原始對象的值複製到新對象中。對於每個類,編譯器都提供一個默認的拷貝構造器,它會將原始對象的每個成員複製到新對象的對應成員中。和逐成員賦值一樣,如果類的數據成員包含指向動態分配內存的指針,則拷貝構造函數就可能產生嚴重錯誤。
按常量引用傳遞是一種安全的、執行起來不錯的方案