GeekBand C++面向對象高級編程(上)1
1. C vs C++
C++對與C最大的區別就是C++把數據和函數包在一起,使得數據/函數變成對象的屬性/方法,這樣帶來了極大的好處,不像C語言函數/變數(一部分)都是全局的,面向對象編程給開發效率帶來了好處
c++是在c語言基礎上發展來的c++兼容c,在c語言基礎上增加了一些升級(畢竟兩個+):
(1)c++的類型檢查更加嚴格
(2)c++擴展了c
面向對象編程(以類的方式來組織代碼)
運算符重載(一種函數的特殊表現形式)
異常處理(一種新的錯誤表現和處理形式)
泛型編程(通用類型和演算法編程)
使用class complex創建對象的三種形式

2.C++ program代碼的基本形式

2.1
使用自己寫的頭文件,需要使用雙引號也可以在引號中加上路徑
例如:#include 「../include/complex.h」
尖括弧的頭文件表示使用系統默認路徑的頭文件
使用時需要注意:
(1)c++程序推薦以.cpp結尾,可以是.c .C .cc .c++
(2)標準的c++ 頭文件 沒有.h
#include <iostream>
#include <string>
#include <list>
標準的c語言頭文件 去尾加頭
#include <stdio.h> =======> #include <cstdio>
#include <time.h> ========>#include <ctime>
#include <string.h> =======> #include <cstring>
系統c的頭文件 和c語言中使用頭文件的方式相同
#include <unistd.h>
#include <pthread.h>
用戶自定義頭文件 寫的時候帶.h 導入正常導入
#include "data.h"
2.2 頭文件中的防衛式聲明complex.h

在寫一個工程文件的時候,一般都會寫自己的頭文件,然後在.cpp文件中使用的時候包含他,對與頭文件的包含最好不要規定先寫誰在寫誰,那樣對開發的人負擔有點大(而且並沒有必要),為了在包含頭文件的時候不發生重複聲明/定義,所以加上防衛式聲明是很有必要的。
#ifndef,#endif 是宏定義條件編譯
預處理的時候遇到#ifndef,編譯器會檢查是否define過_COMPLEX_這個宏,如果沒有執行#define,最後遇到#endif結束。
假設這個頭文件被重複包涵,而且沒有加上條件編譯
編譯器會報錯:

如果寫了防衛式條件編譯,就會跳過第二次聲明內容,error也就消失了。
2.3 頭文件的布局

一般在頭文件中寫一個類的聲明/定義,就按照上圖從上往下寫。
前置聲明:
假設你寫了一個頭文件如下

然後包含在一個.cpp文件中,然後編譯會出現以下錯誤:

這是應為編譯器在編譯時是按照從上往下的順序編譯的,然而complex先前沒有出現過,編譯器會不認識他,所以產生一個error。
這類錯誤就是由於沒有前置聲明造成的,前置聲明的作用就是為了讓編譯器提前知道有這麼一個類叫complex,從而避免error
當然啦,最終要的是後面的兩個聲明和類定義,因為前置聲明後,聲明的函數或者類型完全可以放在類聲明之後。
類定義就是類的具體實現,頭文件不僅僅是聲明(大部分工作是聲明),也可以把一些函數的定義(實現)寫在頭文件中。
2.4 class的聲明

class complex 是類的頭,complex是類的名字
{}大括弧括起來的就是類的身體,這裡面就要開始去設計,我的類應該具備什麼樣的數據,我應該準備什麼樣的函數,才能滿足使用者對這個類的使用需求。
這個時候就出現一個問題,現在complex類中的數據是double類型,但是也有可能需要int類型,float類型的數據,這個時候如果沒有一個東西來提供一個通用的解決方法,那麼我們就要把類寫3遍(double,int,float)
然而C++還是比較仁慈的,他提供了一種通用解決方案:
2.5 class template 模板

在class head上面加上template<typename T>
告訴編譯器T是一個類型名,占時沒有確定,在使用的時候讓使用者決定T是什麼類型(見最後一行),這樣就達到了泛型的效果
3.inline函數
一個函數如果在class內部完成定義,那他就已經是inline函數,例如:

如果函數的定義在class外部那麼就不是inline函數,但是就是想把函數變成inline函數需要在頭文件中(類外)直接定義,不能在別的.cpp文件中定義,因為inline函數在編譯的時候可能被編譯器替換成{...}之間的東西(變成語句),以此來減少函數調用的開銷(壓棧等操作),所以在編譯器鏈接外部函數的時候會找不到函數的符號,然後出現以下錯誤:

inline函數的執行效率比一般的函數要快,但是inline這個關鍵詞只是給編譯器的一個建議,編譯器到底有沒有把函數編譯成inline 函數我們不知道(一般比較短小的函數可以)
4.access level(訪問級別)
訪問級別是一個關鍵字,他說明了誰可以在什麼地方訪問class中的數據或函數,在class的設計中有下面三種(friend會影響許可權但是不算在3種之內,virtual先擱置一下)

在設計class的時候,一般按照圖中的順序寫(這只是讓代碼看上去工整可讀性高),也可以一段public,一段private如下圖:

順序沒有要求,但是如果沒有指明許可權,默認是private,和寫在private下效果一樣。

下面來看看不同的許可權有什麼區別:
public:
屬於public的聲明在類外可以直接調用,一般原則是不要把class的數據做成public,因為讓數據保持私有才能符合面向對象的編程理論之一,只有class自己才能改變自己的數據,外部只能通過class的方法(就是函數)改變類的數據,這樣保證了類中數據的安全性,數據只能通過合乎規則的方式被更新。

protected:
屬於protected的聲明,只能給class本身的函數以及從該class派生出來的子類使用(爸爸保護起來的就是要給兒子的),這個訪問級別適用與:當你設計一個函數時考慮到在本類中and繼承後子類中可以使用,但是又不想在外部被隨便調用,可以使用這個許可權(比如你爸爸的財產,只能在族內(類中的)使用,子孫後代都可以用,ps 如果是土豪,隨意分錢,請趕緊聯繫QQ2279543495)。
如果在類外使用會出現error

private:
屬於private的聲明只能被類本身的成員函數使用,private聲明在類外是可見的(名字是知道的),但是不能訪問,數據最好放在private下,因為C++強調封裝,函數也可以(如果這個函數只想在類內對數據做處理)。
在類外使用會出先error

friend:
這是個非常強大的東西(論基友的地位),屬於friend的函數不屬於類的成員函數,但是可以像成員函數一樣訪問類的private和protected成員,friend可以是函數也可以是類.
用friend class舉例:
創建friend_class.cpp
聲明class A

聲明class B

然後使用g++ -c friend_class.cpp只編譯不鏈接,出現error

接著在class A中加上friend class B,error 消失

在C++中friend函數盡量少一些,因為C++強調的是封裝,friend打破了封裝的意義。
在類內的函數為什麼可以直接使用類內的數據使用呢?
因為,相同的class的各個object互為friend。
5.constructor構造函數
絕大多數類至少有個一個構造函數,當一個類被對象創建時,構造函數被隱式的調用,他負責對象的初始化。
構造函數很特殊,一般函數都有返回類型(void也是的),而構造函數沒有,也不必有,因為構造函數就是用來創建對象的(所以構造函數名必需和類名一樣),創建對象不需要返回值。

三種會調用構造函數的創建方法:

對象被創建自動調用,如果沒有指定數值(如c2),參數就默認初始化成0;如果指定,就初始化成指定值,如果new complex[10],則構造函數被調用10次。
這初始化列表是幹啥的?以下這種方式也可以初始化變數

這樣的確可以,但是使用初始化列表會使程序更快,好多人選擇C++而不是JAVA很大程度上是因為C++更快,一個變數數值的設定有兩個階段,第一個階段是初始化,第二個是賦值,如果在大括弧裡頭寫就是賦值,效率當然沒有初始化高,所以一般使用初始化列表。初始化列表只有構造函數有。
構造函數是必要的,因為類通常包含一些結構,而結構又可能包涵很多欄位,這就需要複雜的初始化,當一個類的對象被創建時,構造函數被自動調用,減輕了程序猿的負擔(其實構造函數違背了「一切工作自己負責」的c語言原則),當然如果不想使用構造函數初始化,也可以提供介面函數來初始化。
構造函數可以寫多個(C++有overload特性),以滿足不同的初始化需求。
構造函數一般不放在private裡頭,但是也有特殊情況
5.1 Singleton 單例

這個類把構造函數放到了private裡頭,這樣外部就不能直接調用,必需通過A的初始化函數GetAinit
由於a又是static變數所以只會被創建一次,這個就是單例模式。
5.2 你的類是否需要無參構造(C++沉思錄提到的細節)
如果一個類已經有了構造函數,而你想聲明該類的對象並且不顯示的初始化他們,則必需顯式的寫一個無參構造(使用overload特性)。

假設,我們只定義了一個構造函數

除非這個類有一個不需要參數的構造函數,或者參數有默認值,否則下面這句就是非法的


編譯器會去找候選的構造函數(complex(const complex&)是拷貝構造),如果沒有符合的候選項就會出現error,有可能這是體現了類的設計意圖,希望使用者手動的輸入初始化的值。
如果一個類有了顯示的構造函數,那麼試圖生成該類對象的數組也是非法的

這樣可以禁止創建對象數組,但是需要考慮是否值得那麼做。
6.overload 重載
剛剛提到構造函數可以寫多個,那麼為啥可以這麼干呢?在C語言中大家都知道函數是不可以重載,因為函數名是唯一的,在C++中實現了這個功能,根本原因我們來看一下編譯後的彙編文件是啥樣的
先寫兩個函數

然後生成彙編文件,找到兩個函數


很明顯,在彙編裡頭函數名被編譯器改了




編譯器把參數類型的首字母也加到了函數名後頭(和返回值類型沒有關係),所以這就是overload的本質。
在使用重載函數的時候要注意,在調用的時候不要讓編譯器產生疑惑,你到底要使用那一個函數,例如:

然後出現error

一般的解決方法:改變參數的順序,拿掉默認值。(只要調用不起衝突即可)
7.Const member functions 常量成員函數
在class complex中有以下兩個函數:

在類中常常會有一種函數,他不會修改類內數據,這種函數需要加上const(表明他不會修改類內數據)。
當然如果不加也可以,但是在定義了一個常量後去調用類內函數時會出現error:

編譯出現error

在這種情況下,邏輯上我想通過real函數得到數據,這是說得通的,但是我們定一個的c1是一個常量,常量表示他是不能修改的,但是real函數沒有說明該函數不會修改類內數據(不是常成員函數),那麼real就有可能在函數中修改類內數據的值(即使你沒有那麼做,但是編譯器不知道),這樣就產生了矛盾:常量不能被修改,real函數又可能去修改常量。因此編譯器報錯。
如果碰到這種情況,你設計了類,類的使用者就要埋怨你了,你沒有設計好,const其實提高了兼容性。
8. 值傳遞 vs. 引用傳遞

類型& 變數名
complex c1;
complex& c2 = c1;
這樣就創建了引用變數。
引用變數一定要初始化,如果沒有會出現error:

8.1 引用的效率
引用在彙編中看其實就是一個指針,現在來看看彙編文件
定義三個全局變數:






指針在32位的操作系統中只有4個位元組,有時候給函數傳參時,參數可能是一個結構體,這時使用值傳遞就不明智了,假設這個結構體有100個位元組,值傳遞是直接把100個位元組複製過去,而引用(指針)只是複製4個位元組,效率大大的提高了。
引用的形式比指針更漂亮。
8.2 返回值傳遞
返回值的傳遞也有值傳遞和引用傳遞之分,什麼情況下可以使用值傳遞,什麼情況下使用引用傳遞呢?
下圖是引用傳遞

當函數返回一個東西的時候,這個被return的東西在函數外已經存在,或者是動態內存分配出來的空間,或者是在函數內部創建的static變數(static變數的空間即使函數結束,但是他沒有被回收掉,只是不能用直接使用而已,可以通過指針訪問),這些分配的空間的生命周期如果大於該函數,就可以使用引用傳遞。
一定不能使用引用傳遞一個local變數,因為他是不靠譜的,在函數中local變數會在函數結束後就被系統回收掉。
假設你使用引用傳遞一個local變數,會發生無法預知的錯誤。
編譯器也會出現warning

下圖是值傳遞

因為ths是函數中創建的local變數,生命周期只是當前函數,不能通過引用傳遞,必需使用值傳遞。
使用引用傳遞,傳遞者無需知道接收者是以引用形式接收。C++也可以使用指針傳遞,但是接收者需要明確傳過來的東西是一個指針,這明顯沒有引用方便。
8.2.1 臨時對象

這兩種創建對象是有區別的,前兩種創建的是實體,直接調用構造函數創建出來的對象就是臨時對象
出現臨時對象有三種可能(要是不對請大佬們糾正。。。)
1.存儲返回自定義類型的函數的返回值。在程序未將返回值複製到對象的時候。
2.存儲強制轉換為用戶自定義類型的結果(顯示轉換的時候)。
3.當初始化一個常量引用時,如果給定的初始化對象類型與目標引用類型不同(但是兩者 能夠相互轉換),需要產生臨時對象。
這裡只討論第一種情況,如下:

在這裡返回引用是錯誤的,因為complex(a,b)是一個臨時對象,他的生命周期是當前函數,所以不能使用引用傳遞必需使用值傳遞。
9.操作符重載
在C++裡頭操作符事實上被看作一個函數,它可以被重新定義,這是一個很好也很煩的語言特性(煩是因為比較難。。。)
9.1操作符重載的語法:
(1)全局操作符重載函數

(2)成員操作符重載函數

在c語言中要把兩個結構體相加,一般需要調用函數(在函數中一個一個加起來),C++中使用了操作符重載後,就能很自然的把兩個類對象進行加減乘除等操作,比調用函數自然多了。
上面兩個函數的作用是一樣的但是成員函數比全局的少一個參數(complex* ths),這是因為所有的成員函數都有一個隱藏的參數this。
9.1.0 this指針
this指針和他的英文意思一樣,就是指向當前對象的指針,在構造函數中這個指針代表,正在被構建的對象的地址。
例如,當函數參數與類內數據重名的時候:

在成員函數中這個指針,指向正在調用這個函數的對象的地址
其實類內的operator +=有個隱藏的this指針,我們寫函數的時候不需要手動把this加上,也不能加上,加上就出現error

編譯器會自動把c2的地址給this指針,然後在函數內就能直接使用類內參數了。
this指針作為return的值也是可以的,返回的內容就是當前類對象。

9.1.1二元運算符重載 L#R
編譯器會先去L對象對應的類型中找一個,
成員函數叫 operator # (R),沒有就去全局函數中找一個
全局函數叫 operator # (L, R),最後綜合選擇優先調用(寫兩個沒有意義)。
可以重載的二元運算符:
+ - * / += *= /= % << >> ==
例如:

9.1.2一元運算符重載 #O
默認操作符在前 操作數在後
編譯器會先去O對象對應的類型中找一個,
成員函數叫 operator # (),如果找不到就去全局找一個
全局函數叫 operator # (O),最後綜合選擇優先調用(寫兩個沒有意義)。
可以重載的一元運算符:
! ~ -(負號) ++ --
例如:

注意後--

9.1.2.1 啞元
在c++中有類型無名子的函數參數稱為啞元,一般在函數升級中使用

啞元起到了佔位,兼容老版本作用。

9.1.3 operator << and operator >>
C++有自己的一套I/O程序和概念,iostream提供了I/O介面,讓程序設計更符合面向對象的理念。
C++中使用<<和>>操作符來代替C語言中的printf(),scanf()這一類函數。<<和>>操作符在C中是左移位和右移位操作符,他們被重載成C++的I/O,好處是顯而易見的輸出再也不要考慮佔位符與類型的問題(吐槽格式化輸出,還是c好),與使用函數相比可以連續操作,操作符的左結合性保證了數據鏈。
然而只靠標準庫裡頭的基本數據類型是不夠的,我們如果想在輸出自己類對象的時候很自然(和輸出基本類型一樣)的話,我們需要手動重載<< 和 >>.
先聲明class complex

<< 輸出運算符重載

也可以這麼寫

os是一個流對象的容器,他的作用就是接收後面的東西(把os看作一個頭尾貫通的容器,c.real()這些就是送入容器里的水),然後ostream把os裡頭的東西全部列印到屏幕上,因為流是不停在變的所以不能使用拷貝,必需用引用ostream&。
返回值使用引用傳遞是為了讓運算符能連續操作,如果寫void只能執行一次,對與其他操作符也是一樣的,返回引用就能實現連續操作。
>> 輸入運算符重載


輸入運算符重載因為要改變數據所以第二個參數不能使用const修飾。
註:
(1)<< 和>>重載必需寫在類外,因為類內的那種使用形式,不符合通常的用法(object << cout,object >> cin 這樣用,太不舒服)
(2)運算符重載後的優先順序和原先是一樣的,所以原來怎麼用還是怎麼用,只是對象從基本類型變成了更複雜的類對象而已(運算符做的更多了)。
推薦閱讀:
※C++中的deleting destructor
※static_cast<const char*>("Fuck GTK+")
※用CPUID檢測各大OJ測評機所用的CPU(以及日常黑BZOJ)
