1、第二部分第一課:面向對象初探,string的驚天內幕
2、第二部分第二課預告:掀起了"類"的蓋頭來(一)
上一課《【C++探索之旅】第一部分第十二課:指針一出,誰與爭鋒》中,大家辛苦了。
誠然,指針是不容易啃的硬骨頭。不過,假以時日,小火慢炖,可以成為一碗上好的骨頭湯,對你的C++水平那可是大補。
好了,口水擦一擦,我們正式進入C++探索之旅的第二部分啦,激動不?剛擦完的哈喇子可不要繼續流啊。
這一部分的課程稱為:C++之面向對象
因為我們要探索"面向對象編程"(OOP: Object-Oriented Programming) 。這是一種不同於以往的編程模式。
這種方法不會立即使你的程序發生革命性的改變,而且剛開始接觸的時候你甚至會覺得這種模式沒什麼用。
目前為止我們的編程模式一直是:面向過程。也就是:我想要實現什麼功能,就寫什麼函數,聲明相應變量,等等。比如我要顯示一段信息到屏幕上,我就創建一個函數:
void showMessage(string message);
但是,相信我:慢慢地,你會發現面向對象的編程模式比面向過程更符合人類的思維。你將能更好地組織你的代碼。
注意:這一課接下來提到的"對象",其實有時是指對象,有時是指類。因為要到下一課我們才學習"類"的概念。暫時先不細分,希望通過這一課讓大家對面向對象有大致了解。
對象...有啥用啊?
你會想:這不會是又一種神秘的設計吧?難道是一個宿醉後的瘋狂程序員想出來的東西?
其實不然。我們身邊充滿著對象:你的車是一個對象,你的電腦是一個對象,你的手機是一個對象,等等。
事實上,我們所知道的所有東西都可以被看作對象。面向對象編程,就是在代碼中操作這些被稱為"對象"的元素。
下面列舉幾種平時我們在編程中常見的對象:
一個窗體:例如QQ的一個聊天對話框,畫圖軟件的主界面。
一個按鈕:比如安裝軟件時那些"下一步"的按鈕。
游戲裡的一個人物。
一首歌,一個視頻
說了這麼多,對象到底是啥呀?是一種新的變量嗎,還是一種新的函數類型?
不不不,都不是。對象是一種新的編程元素!
或者說得更具體一些,對象裡面混合了變量和函數。
可不要被這句話嚇跑了,我們一點點來探索。
想象一下... 一個對象。少年,不是讓你想象結婚對象好嘛~
還是來點實際的圖片好了,能幫助我們理解。
設想:
有一天,一個程序員決定要寫一個圖形界面的程序,這個程序可以在屏幕上顯示窗體,調整窗體大小,移動窗體,刪除窗體,在窗體裡可以畫各種圖形,等等。
要實現這些功能,代碼是比較復雜的:需要不少函數,這些函數之間互相調用。而且還需要很多變量,例如窗體位置(x坐標和y坐標),窗體的寬,高,等等。
這個程序員花了不少的時間來寫這個程序。程序有點復雜,但是他完成了。最終,他實現的代碼包含了很多的函數和變量。
當我們第一次看到他的程序時,就好像看一個化學家的一個實驗環境一樣,啥也看不懂。如下圖:
不過,這個程序員對自己的程序還是很滿意的,他甚至要把程序發布到網上,讓大家可以用他的程序來創建窗體,直接拿來用就可以了,而不需要從零開始寫代碼。
不過,有一個問題,如果你不是化學專家,你可能得花不少時間搞懂這一堆東西到底是怎麼運作的:哪一個函數最先被調用呢?為了改變窗體的大小,哪個變量要被傳遞到哪個函數裡呢?總之,我們很害怕把整個實驗環境給弄炸了。
在接到一些用戶的建議和抱怨後,這個程序員決定替用戶著想。他重新設計了他的代碼,從面向過程的模式改為面向對象的模式。
這就好比他把所有和這個化學實驗有關的東西都放到一個大方盒子裡。這個大方盒子就是我們所說的"對象"。如下圖:
在上圖中,大方盒子的一部分被設為透明,是故意的,使你可以看到裡面的景象。是的,我們的化學實驗的所有設備都在大方盒子裡。但是,實際當中,大方盒子是完全不透明的,用戶從外面看不到裡面到底有什麼。如下圖:
這個大方盒子裡存放著函數和變量(那些化學儀器,試管,燒杯,等等),但這些元素對於用戶卻是不可見的。
這樣,用戶看到的就不再是成堆的試管,燒杯,等等讓其抓狂的東西了。在大方盒子外面,我們呈現給用戶的就只有一些按鈕:一個按鈕用於"打開窗體",一個按鈕用於"改變窗體大小",一個按鈕用於"關閉窗體",等等。用戶完全不需要理解大方盒子裡面的運作原理。
因此,以面向對象的模式來編程,就是:
程序員編寫代碼(可能很復雜),然後將這些復雜的代碼都裝到一個大方盒子(對象)裡,用戶從外面不能看到裡面的實現細節。因此,對於使用這個對象的用戶來說,操作起來就容易多了:只需按下對應的按鈕,不需要精通化學也可以使用整個實驗環境提供的功能了。
當然了,上面的比喻只是一個大致概念。
這一課我們暫時還不學習廣義上怎麼創建對象。我們反其道而行之,先來使用一個對象。其實這個對象我們之前已經接觸過好多次了,就是string。
好幾課之前,我們已經說過,string這種類型和普通的int,bool,float,double等不一樣。後面的這些是C++的基礎數據類型,用於存放簡單的數據。
但string卻不是這樣,事實上它是一個對象。string這個類型背後隱藏了很多細節。
揭開string背後的神秘內幕
別看string寶寶很乖的,白天好像很正經的大公司老板Bruce Wayne(布魯斯·韋恩),其實他是蝙蝠俠,背後秘密可多了。
多虧了面向對象的編程模式,我們在第一部分課程裡就已經開始使用string了,當時我們還不知道string背後的秘密。下面我們就來揭開string神秘的面紗,看看裡面的機制大概是什麼樣。
對於電腦來說,字符其實並不存在
為什麼說string內部的機制其實比較復雜呢?
首先,我們之前把string稱為"字符串",但其實電腦並不認識string當中的那些字符。
還記得嗎?我們說電腦是一台只知道計算的大計算器而已(英語computer就是"計算機"的意思),它只認識數字!
但是,如果電腦只知道操作數字,那麼它又是如何在屏幕上顯示那麼多字符的呢?例如你現在就用屏幕在看小編寫的文章。
信息技術的先驅們早就想到了好辦法。也許你聽說過ASCII(發音是[aski])表。ASCII是American Standard Code for Information Interchange的縮寫,表示"美國信息交換標准碼"。
ASCII表就是為數字和字符的對應轉換服務的一個表格。下面給出ASCII表的一部分:
數字
字符
數字
字符
64
@
96
'
65
A
97
a
66
B
98
b
67
C
99
c
68
D
100
d
69
E
101
e
70
F
102
f
71
G
103
g
72
H
104
h
73
I
105
i
74
J
106
j
75
K
107
k
76
L
108
l
77
M
109
m
如我們所見,大寫字母A對應了65,小寫字母a對應了97。所有的英文字母都包含在這個表中。
那麼,就是說:每次電腦只要看到數字65,就會把它當作大寫字母A來處理咯?
不是的。只有我們要求電腦翻譯一個數字到字符的時候,它才會照做(果然很呆萌)。實際當中,電腦是根據我們聲明的變量的類型來決定如何"看待"每個存儲在變量中的數字的。大致規則如下:
例如,我們用int型的變量來儲存數字65,那麼電腦就把它當成一個數字來看待。
相反地,假如我們用char型的變量來儲存數字65,那麼電腦就把它當成一個字符來看待。電腦會說:嗯,這是大寫字母A。事實上,char是英語character的縮寫,表示"字符"。專門用來儲存字符的。
因此,char類型的變量儲存數字,但是這個數字會被"解釋"成字符。不過,一個char型變量一次只能儲存一個字符,那麼我們怎麼儲存一個句子呢?我們接著學習。
字符串就類似字符數組
既然char只能儲存一個字符(因為char的大小是一個字節,也就是8個bit位,正好是一個英文字符的大小,中文字符占2個字節),因此,程序員們就想到了創建char型數組。
我們已經學過數組了,也就是內存中相鄰的相同類型的變量的集合。因此,用字符數組來存放多個字符的集合(例如:一句話)是很棒的選擇,我們通常也將字符數組稱為"字符串",因為它是一串字符麼。
因此,我們只要這樣聲明一個char型數組:
char text[100];
text這個char型數組裡可以儲存100個字符。不過這個數組是靜態的,大小不可變。如果你要創建大小可以改變的"動態數組",就可以用vector這種類型啦。
例如:
vectortext;
理論上,我們可以使用靜態char型數組或動態char型數組來存放一串字符。不過這樣很不方便,因此C++的設計者決定將有關字符串的操作都封裝到一個對象裡,那就是string。
創建並使用string對象
經過剛才那一節,我們了解到:操作字符串其實並不簡單啊。需要創建一個char型數組,數組中的每一個元素是一個字符。而且,我們需要創建足夠大的數組,以便裝下我們要存放的字符串。總之,有很多方面要考慮。
這時,面向對象編程就可以派上用場了。還記得我們上面的圖片中那個大方盒子嗎?string就是這樣一個"大方盒子"。
創建一個string對象
創建一個對象和我們之前創建一個普通的變量(比如int型變量)是類似的。例如:
#include#include // 必須引入string頭文件,因為string的原型就定義在裡面 using namespace std; int main() { string mString; //創建一個string類型的對象mString return 0; }
上面的程序不復雜,我們主要關注那句創建string對象的指令:
string mString;
故此,創建對象的方法和創建一個變量是一樣的咯?
並不盡然,創建一個對象有幾種方式,我們方才演示的只是其中最簡單的一種。不過,這種方式確實和創建一個普通的變量沒什麼大區別。
這樣的話,我們如何區分對象和普通變量呢?
為了區分變量和對象,可以從命名上來看,我們有規則:
變量的類型以小寫字母開始,例如普通的變量類型int
對象的類型以大寫字母開始,例如:Car
我知道你肯定會說:你看string不就是以小寫字母開始的嗎?它也是對象的類型啊。
我承認,這是一個例外,上面的規則並不是強加給我們的。很顯然,實現string的程序員並沒有遵守這個規則。
不過,大部分的情況下,對象的類型是以大寫字母開始的。
當聲明字符串時對其初始化
為了在創建對象時就對其初始化,有多種方式,我們來看看最簡單的一種:
int main() { string mString("Hello !"); //創建一個string類型的對象mString,並初始化它的值 return 0; }
這種方式的初始化,和C++的基礎變量類型,例如int,double等,是一樣的。
除了上面的這種方式,我們也可以用以下方式來初始化我們的string對象:
string mString = "Hello !";
我們既然創建了一個string對象,並為其賦值"Hello !",我們可以來打印這個對象的內容。
int main() { string mString("Hello !"); cout << mString << endl; //顯示string的內容,就好像它是一個字符串 return 0; }
運行,顯示:
Hello !
在字符串初始化後再改變其值
現在我們已經創建了我們的字符串,並且對其賦了初值。我們還可以再改變其值。
int main() { string mString("Hello !"); cout << mString << endl; mString = "How are you ?"; cout << mString << endl; return 0; }
為了改變string變量的值,我們須要用=號(賦值符號)。
上面演示的方法,其實和之前我們使用普通變量類型時並沒太大區別。我在這裡只是要向你展示面向對象的神奇之處。
你,也就是用戶,剛才就好像按下了一個按鈕,這個按鈕的作用是發出一個命令:我要將字符串的內容從"Hello !"改寫為"How are you ?"。
雖然你的指令很簡短,但string對象內部的那些函數接到指令後卻開始忙乎起來了(讓我想到了小黃人),它們依次做了以下幾件事:
首先檢測當前存放"Hello !"的char型數組是否足夠容納"How are you ?"這個字符串。
答案是否定的。因此它們創建一個新的char型數組,大小足以容下"How are you ?"這個新的字符串。
一旦新的char型數組創建完畢,就把舊的數組銷毀,畢竟沒用了。
然後把"How are you ?"這個字符串的內容拷貝到新的char型數組裡。
看到吧,這就是面向對象編程的一個強大之處:我們完全不知道string對象裡面原來發生了這麼多事。
字符串的串聯
假如我們想要將兩個字符串的內容前後相接,合並為一個字符串。理論上說來,這是不容易實現的。但是string對象中早就設計好這樣的機制啦:
int main() { string mString1("Hello !"); string mString2("How are you ?"); string mString3; mString3 = mString1 + " " + mString2; cout << mString3 << endl; return 0; }
很簡單不是嗎?只要用加號將兩個字符串連起來就好了,至於string對象內部做了多少復雜的操作,我們"VIP用戶"根本不在乎,有權就是這麼"任性"~
字符串的比較
還要繼續學嗎?很不錯,就要這種精神。
我們可以用==和!=符號來比較字符串,分別表示相等和不相等。
int main() { string mString1("Hello !"); string mString2("How are you ?"); if (mString1 == mString2) // 顯然兩個字符串不相等,括號中條件語句為假 { cout << "兩個字符串相等." << endl; } else { cout << "兩個字符串不相等." << endl; } return 0; }
事實上,在string對象內部,這個比較的過程是兩個字符串的一個字符接一個字符來比較的(借助一個循環)。但是我們不需要在意這些細節,就是這麼潇灑。
string的一些方法
string對象的功能可不至於此。其中還有很多有用的方法,我們可以來見識幾個常用的。
屬性和方法
我們之前說過,一個對象中包含了變量和函數。不過,我們既然要學習面向對象編程了,就要用更專業的術語。
在面向對象的領域,對象中的變量被稱為"屬性",而其中的函數被稱為"方法"。只是稱呼不同而已。
你可以把對象提供的每一個方法想象成我們之前那個大方盒子外面對用戶可見的那些按鈕。
屬性和方法又被稱為"成員變量"和"成員函數"。
為了調用一個對象中的方法,我們用一種你已經見識過的方式:
object.method()
我們在對象和其成員函數之間用一個點來連接。這意味著:對於此對象,我調用它的這個方法。
理論上,我們也可以用同樣的方式來訪問成員變量(屬性)。然而,在面向對象編程中,我們盡量避免用戶直接訪問我們的私人成員變量。關於這個,有很多知識點,我們以後的課程會講到。
調用成員函數和成員變量的方式也是對象獨有的,普通的變量類型並不能這樣做。這也是除了名字之外區別對象和變量的好方法。
我們來看幾個string提供的成員函數吧。
size方法
size在英語中是"大小,尺寸"的意思。因此,size方法用於獲取string對象的大小,也就是裡面包含的字符數目。
size方法的用法很簡單,沒有參數:
int main() { string mString("Hello !"); cout << "字符串的長度是 " << mString.size(); return 0; }
運行,顯示:
字符串的長度是 7
erase方法
erase方法用於刪去字符串的內容,因為erase在英語中是"刪除,清除"的意思。
int main() { string mString("Hello !"); cout << "字符串的內容是 : " << mString << endl; mString.erase(); cout << "調用erase方法後, 字符串的內容是 : " << mString << endl; return 0; }
運行,顯示:
字符串的內容是 : Hello !
調用erase方法後, 字符串的內容是 :
正如我們預期的,調用erase方法後,字符串的內容被清空了。
其實,erase方法的效果和
?
mString = "";
是一樣的。
substr方法
substr是sub和string的縮合,sub表示"子的,副的",string就是"字符串"啦,因此,顧名思義,substr就是取得一個字符串的一部分。
substr方法的原型如下:
string substr( size_type index, size_type num = npos );
可以看到,substr的返回值也是string類型。它接收兩個參數,一個是index,表示從字符串的第幾個字符開始截取,第二個參數是num,表示截取多少個字符。
事實上,更確切地說,substr方法接收兩個參數,第一個參數是必須的,第二個參數是非必須的。就是說,第一個參數必須要提供,第二個參數假如沒有提供,那麼num就會取默認值,默認值是npos。
看到上面的原型中有 num = npos了嗎?暫時還不需要深究,這個其實是C++的一種特性,術語稱為"默認參數"。就是說,假如函數被調用時,這個參數沒有被賦予值,那麼這個參數會取等號後面的默認值(類似"備胎"的概念)。
npos這個值表示:取余下的所有字符,直到最後一個。
舉個例子吧:
int main() { string mString("Hello !"); cout << mString.substr(3) << endl; return 0; }
運行,顯示:
lo !
因為我們只給了substr一個參數,就是3,所以substr的第一個參數index就被賦值為3,而第二個參數num沒有被賦值,就會取默認值。
因此,表示從mString的第四個字符開始截取,一直截取到最後。
再來試試給第二個參數num賦值的情況。
int main() { string mString("Surprise !"); cout << mString.substr(2, 4) << endl; return 0; }
運行,顯示:
rpri
也很好理解,我們給index和num分別賦值2和4,那麼就是從mString的第3個字符開始截取,截取4個字符。
我們之前在數組的那一課已經學習過了,我們可以對string類型的對象用類似數組的方式來訪問其中某一個字符,使用中括號[]。例如:
string mString("Surprise!"); cout << mString[3] << endl; //顯示第4個字符,就是 'p'
好了,這一課就結束了。
這一課的主要目的是不想讓你對面向對象編程感到恐懼。經過此課的學習,你是否已經對面向對象有點概念了呢?事實上,在這之前,你就已經用過對象了,string和vector其實都是。
我希望你已經做好准備來創建屬於你自己的對象了。都說程序員沒有對象,那麼我們自己new一個呗~
這是下一課開始的目標。
總結
面向對象編程是設計代碼的一種方法。操作的是被稱為"對象"的元素。
對象裡面的實現細節可以很復雜,但是使用對象的人卻並不需要關心這些細節,只需要知道如何使用就可以了。這是面向對象編程的一大優勢。
一個對象是由一些屬性和方法組成的,也就是變量和函數。
我們可以調用對象的方法來實現各種需求。
在內存中,字符串的操作其實是很復雜的。為了替我們這些C++的使用者著想,C++的設計者為我們設計了精巧的對象:string。我們只需要用string來創建字符串實例,操作這些字符串,而並不需要關心內存中到底發生了什麼。
今天的課就到這裡,一起加油吧!
下一課我們學習:掀起了"類"的蓋頭來(一)