(Boolan) C++面向對象高級編程(一)

感謝侯捷老師的悉心講授的課程,讓我在對很多東西上有了更深層次的認識。

我呢,是一個非計算機專業畢業的本科生,畢業后帶著對程序感興趣的后知后覺開始學習編程,也不是抱著以工作的目的導向去的,學的比較雜,也缺乏系統性。算算日子,距離第一行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
    
    • 目的:

      1. 可以讓使用者更加自由的include這個頭文件
      2. 防止同一個程序中重復的導入這個頭文件
    • 頭文件的布局

       #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關鍵字,則使用者創建一個常量c1const 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的情況,所以不講該函數設計為成員函數,而是使用全局函數來定義
      
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容