感謝侯捷老師的悉心講授的課程,讓我在對很多東西上有了更深層次的認識。
我呢,是一個非計算機專業畢業的本科生,畢業后帶著對程序感興趣的后知后覺開始學習編程,也不是抱著以工作的目的導向去的,學的比較雜,也缺乏系統性。算算日子,距離第一行java代碼已經過去兩年有余了,對于飛CS的我來說,堅持到今天也算不易。但是用了這么就的“面向對象編程”,但自己其實不能太說清其本質到底為何物。這也算是我正規劃的第一步吧,再次感謝Boolan網和侯捷老師。那么我也就來簡單的分享一下我所學到的知識吧,畢竟非專業出身,如果其中有錯誤的地方,希望大家能夠指出來,謝謝大家。
Class是怎么來的
對于面向對象語言來說可能很重要的任務就是和class打交道吧(至少我接觸的java、c++、python都是和class打交道的),但是在C語言中卻沒有見到過這個東西,最多也就有struct而已。
其實class對于認識世界來說是更加一致的,比如我們通常會把生物劃分為動物、植物、微生物,把動物又劃分為哺乳類、兩棲類等等。我們在認識世界的時候往往會把具體的事物,比如貓、狗、人等抽象看待,找出其共性,再把不同點加入到他們自己的屬性中去,就形成了“界門綱目科屬種”這樣的生物學劃分規律。所以,也就是說,類和類之間應該有關系(比如人和貓都屬于哺乳類動物),那么這些關系之間會有一些想通的屬性(比如,人和貓都喝奶長大,都會運動等等)。但,不同種類也具有特殊的屬性,比如貓有毛,人就沒有等等。
而軟件也應該對現實事物的一種抽象表現形式的描述,那么類就可以很好的把各種屬性進行隔離并描述不同類之間的關系。比如,人和貓都具備喝奶的屬性,但貓具備豐富的毛發,而人卻不具備這個屬性。因此,C++中使用class來隔離部分數據,將不同的數據分隔開來。同樣,針對不同的屬性,也就應該具有針對這個屬性的響應操作方法(成員函數),比如,貓濃密毛發這個屬性,那么他就具備“舔毛”的這個操作毛發的方法(函數),人沒有濃密的毛發則就不需要這個函數了。
因此,C++相較于C增加重要的概念class,用這個概念來讓程序能夠使用更加抽象的方式(屬性+操作屬性的函數(方法))來描述這個世界。
什么是面向對象(Object Oriented)
其實無論Java還是C++的編程過程中,最重要的需要設計各種各樣的class,對于Java來說,C++的class要復雜一些,C++ class可以分為“帶有指針成員變量的的class”和“不帶指針變量的class”。而,對于每次設計出來的單一class來說,這就屬于一種“基于對象(Object Based)”的編程。
如果再拿剛才我說的人和貓來看,對于我們需要抽象一個class來描述這個類別的時候,我們這個過程,其實就是基于對象(Object Based)的過程,比如為了描述貓咪,而建立了一個class貓咪。如果為了實現某一個復雜的過程,我們往往設計一個class是不夠的。比如我們養貓的過程來說吧,在構造這個人和貓的系統的時候,顯然需要class人,也需要class貓咪,這兩種class之間從抽象的角度來看是不是有一些共通之處呢?如果說吃的不一樣我們在這里先不談,那么喝的水總歸是一樣的吧,呼吸的空氣總歸也是一樣的吧,如果為了描述人和貓分別獨立設計兩個class,也許并沒有這個必要,所以,再進一步抽象的時候,我們也許會得到(這只是為了描述這個過程,不一定非常貼切,誰讓我養貓呢,低頭抬頭看到的全是貓)一個class 動物,這時候把水和空氣作為一種通用屬性放在動物類中,而進一步設計class貓和class人的關系時,就只需要通過class動物這個類來描述他們倆之間的關系了比如對我來說一定class人里面少不了鏟貓砂,擼貓等等這一類特有的函數(方法);class貓中存在搗亂、賣萌這類特有的函數了。而喝水、吃飯、呼吸、睡覺(貓一般一天能睡十八九個小時,真羨慕他們,有吃有喝有睡,而我必須掙錢養他們。。。。好像說著說著class人中又多了一個方法。。。)這類方法(函數)雖然動物類就有,但是人和貓畢竟都還是有區別,這時候又可以通過覆蓋這些方法來表示共性中的不同點。
因此,面向對象相較于面向過程來說,就是進一步對數據和操作方法的抽象,通過進一步的抽象來描述多個class之間的關系的抽象方法。
C++程序的基本形式
說了那么多關于基于對象和面向對象的故事,那么C++程序到底是由什么東西構成呢
-
C++程序可以分類兩種:.cpp(C++文件)和.h(頭文件)
而頭文件往往也分為兩部分,一部分為class的聲明(Classes Declaration),另外一部分則為標準庫(Standard Library),其中標準庫部分里面包含了大量的算法,可以讓我們在設計類的時候不需要重復造輪子。
-
那么.cpp和.h如何關聯在一起呢?
是通過#include來引入標準庫或這頭文件。其中如果是引入標準庫的部分,使用的是<>來引入,系統中的標準庫文件,比如#include <iostream.h>。如果引入的是C語言的標準庫,可以使用cname或者name.h的方式引入,比如#include <stdio.h>或這#include <cstdio>;
如果是自己所編寫的頭文件需要使用""來引入進來,比如#include "complex.h",一般情況下,這個表示的是和cpp文件在同一個目錄下的.h文件,如果.h文件在cpp文件目錄的某個文件夾中,需要使用#include "/dir/xxx.h"來引入了。
對于C和C++的輸出有什么區別呢?
C++
#include <iostream>
int main()
{
int i = 7;
std::cout << "i= " << i << endl;
return 0;
}
- C語言
#include <stdio.h>
int main()
{
int i = 7;
printf("i=%d \n", i);
return 0;
}
-
頭文件編寫的防御式聲明
#ifndef __COMPLEX__ #define __COMPLEX__ //防御式的聲明 ......... #endif
-
目的:
- 可以讓使用者更加自由的include這個頭文件
- 防止同一個程序中重復的導入這個頭文件
-
頭文件的布局
#ifndef __COMPLEX__ #define __COMPLEX__ //前置聲明(forward declarations) class ostream; class complex; complex& __doapl(complex* ths, const complex& r); //類-聲明(class declarations) class complex { .... }; //類-定義 complex::function .... #endif
-
-
以complex類(復數類)舉例說明類-聲明的定義
class complex //class head { //class body public: //訪問級別access level為public的部分可以被外部直接訪問的部分 complex (double r = 0, double i = 0) : re(r), im(i) { } //complex () : re(0), im(0) { } //構造函數的重載,但是由于有參數的函數有默認值,所以不能共同存在 complex& operator += (const complex&); //有些函數在body之外定義 double real() const { return re; } //有些函數再次直接定義 double imag() const { return im; } void imag(double i){ im = i; } //函數的重載 private: //訪問級別access level為private的部分只能被class內部和friend的函數直接訪問 double re, im; //數據應該放在private里面,以達到封裝的效果 friend complex& __doapl(complex* ths, const complex& r); }
-
inline(內聯)函數
- 在類本體內所定義的函數
例如(之前定義的class):
double imag() const { return im; }
- 不在本體內定義的函數,使用inline關鍵字修飾的函數(是否能成為inline函數具體情況需要由編譯器來決定,屬于對編譯器的建議)
例如:inline double imag(const complex& x) { return x.imag(); }
- 在類本體內所定義的函數
-
構造函數(Constructor)
complex (double r = 0, double i = 0) //默認實參 : re(r), im(i) //初始列,構造函數專有的設置參數初值的方法,效率比直接賦值要高 { } complex () : re(0), im(0) { } //構造函數的重載
構造函數會在創建對象的時候被自動調用
函數名和類型相同,并且無返回值
如果創建對象時沒有傳遞參數,會調用默認參數的構造函數
使用初始列設置初值的效率高于在函數體中賦值的過程
構造函數可以重載(overloading)
-
構造函數可以放在private中,作為私有的,可以作為singleton的設計模式(內存中只有一個對象)
//singleton class A{ public: static A& getInstance(); setup(){.........} private: A(); A(const A& rhs); .... }; A& A::getInstance() { static A a; return a; }
-
常量成員函數
double real() const {return re;}
- 對于不改變數據的函數,應該添加const關鍵字
- 如果不添加const關鍵字,則使用者創建一個常量c1
const complex c1(2, 1)
會報錯
-
參數傳遞:pass by value vs. pass by reference (to const)
- pass by value:將數據打包整體傳傳遞過去(如果對象比較大,會是效率降低)
- pass by reference:引用在底部為指針,傳遞效率相當于傳遞指針
- 盡量傳遞引用,而不要傳遞值
- 傳遞引用被修改,則原始值也被修改
- 如果為了提高效率,但不希望原始內容被修改,則應該使用const修飾
complex& operator += (const complex& x)
-
返回值傳遞:return by value vs. return by reference (to const)
返回值盡量使用return by reference
如果返回的結果實在函數中創建的,則不能以reference返回,因為隨著函數結束而結束,不能以reference返回,否則會獲取已被銷毀的對象
-
傳遞著無需知道接收者是以reference的形式接收
inline complex& __doapl(complex* ths, const complex& r) { .... return *ths;//返回值要求為reference,則此處返回實際內容即可 //傳遞著無需知道接收者是以reference的形式接收 }
-
友元(friend)
對于被friend修飾的函數,可以直接取得private中的成員
-
相同class的各個object互為友元(friend)
//定義 class complex { public: complex(double r = 0, double i = 0):re(r), im(i){} int func (const complex& param){return param.re + param.im} private: double re, im; }
{ //調用 complex c1(2, 1); complex c2; c2.func(c1); //可以直接訪問c1中的成員(相同class的不同對象互為友元) }
-
操作符重載(
c2 += c1;
)- 成員函數類型
- 二元操作符,具有兩個操作數,系統會將操作符作用在左邊身上,如果左邊操作數有定義,系統能夠找到對應的操作符重載
- 對于成員函數類型的操作符重載,編譯器在編譯時會為該函數自動添加this指針,該this指針就相當于操作符左側的操作數,所以在定義函數時并不需要傳入兩個參數,只需要傳入右側的操作數即可
-
例如
c2 += c1;//操作符作用在c2上
-
定義方法
inline complex& complex::operator += (const complex& r) { return __doapl(this, r); //返回值的設計要求主要是為了滿足鏈式調用的情況 //返回值可以滿足這樣的要求:c3 += c2 += c1; //過程是c2 += c1先運算,然后產生返回值,再和c3運算 } //實際上函數會被編譯器添加this指針 //complex::operator+= (this, const complex& r) //其中c2 += c1;調用時,相當于將c2以this的身份傳入,c1以另外的參數傳入
-
- 非成員函數類型(無this)
- 由于是非成員函數,則編譯器并不能幫助函數添加this指針,則則參數列表中,需要兩個形式參數,分別來表示操作符左右兩側的操作數
inline complex operator + (const complex& x, const complex& y) { return complex(real(x) + real(y), imag(x) + imag(y)); } inline complex operator + (const complex& x, double y) { return complex(real(x) + y, imag(x); } inline complex operator + (double x, const complex& y) { return complex(x + real(y), imag(y)); //此處生成了新的復數對象,輸入內部變量,所以返回值不能是reference } //為了方便例如7+c1的情況,所以不講該函數設計為成員函數,而是使用全局函數來定義
- 成員函數類型