Post view

C++ 物件導向程式設計:4.陣列與指標

物件導向程式設計 蘇言霖

4.  陣列與指標


■  一般陣列與物件陣列
所謂陣列(array) 就是相同型別資料的集合,比如整數陣列、字串陣列、結構陣列、物件陣列等。例如:

 int apple[120];  //記錄一箱蘋果
 book Acer[5678]; //記錄宏碁的圖書

C++ 陣列的用法和C 語言完全相同, 120 是指陣列的個數,元素範圍是 0-119 。C++ 不會檢查陣列元素是否有超過範圍,如果使用 apple[120] 或 apple[130] C 和C++ 都允許,但不會得到正確結果。不檢查陣列註標的原因很簡單,只是希望增加程式的執行速度,如果程式不會出錯,或者程式員在適當的地方追加檢查指令,就不需要由C++ 負責隨時監督陣列。
我們也可以在C++ 宣告二維以上的各種陣列,比如:

 int apple[100][10]; //二維整數陣列
 char x[10][20][30]; //三維字元陣列
 book acer[100][10]; //二維物件陣列


陣列可以像一般變數或物件在宣告的同時設定初值,比如:


 int ibm[5] = {10, 20, 30};
 int pc[] = {10, 20, 30, 40, 50};
 int x[2][3] = { {10, 20, 30},{40, 50, 60} };

ibm 有5個元素,只有前面0-2三個元素指定初值,其餘則預設為 0。如果您希望C++ 自動計算陣列的個數,C 和C++ 都提供了一個省略規則:當宣告陣列變數時有指定初始值,陣列的最高維數可以省略不寫,C 和 C++ 會自動計算。


因此 pc 陣列將會得到5 個元素。比較特別的是二維以上陣列的初值設定,我們必須在每一組高維數的前後再加上一對內層的 {} 和逗號。如果可以清楚的知道 x[0][0] 是 10,x[1][1] 是 50。如果只要設定第二維的第 0 個元素,可以寫:


 int x[][5]={ {10}, {20}, {30} };

須注意的是,陣列設定元素的個數不能超過我們指定的陣列大小。 array.cpp 是一個簡單的 C++ 陣列範例。const int element=10; 在C 語言表示一個不能改變內容的變數,並非真正的常數,或稱為「僅讀變數」。由於陣列宣告的個數必須是整數常數(int 或char均可),不能是其他型別常數、變數、物件等。因此同樣的陣列宣告方式,在C 語言會產生Constant expression required的錯誤訊息。C++ 也有類似的看法,但是在大多數的情況,會是一個真正的常數。


C++ 沒有限制陣列的宣告維數,要宣告 10 或 20 維陣列都不成問題,但是整個陣列變數所佔用的空間不能超過 64K(其實有 60K 就不錯了),改用huge 巨大模式,陣列也無法超過執行時可用記憶體空間(少於 600K)。 即使您的電腦安裝 8MB 以上的主記憶體也於事無補。


■  字串
C 和C++ 最有意思的地方就是,雖然沒有內建「字串變數」這種常用的資料型別,卻可以在程式中使用字串!比如像:


 cout << "Hello C++ !";

由一對 "" 括住的文字就叫做字串( string)。如果我們把字串的每一個元素分開來看,則每一個資料都是一個字元,即,'H', 'e', 'l'...。試問具有相同字元型別元素的集合是什麼?答對了,就是字元陣列!C 和C++ 把字元陣列視為一個字串。例如:


 char filenmae[10];
 char yes[]={ 'O', 'k', 0 };

字串規定必須用 ASCII 碼為 0 的特殊符號來代表字串結束記號。請注意 '0' 表示 0 這個字元,而 0 則表示數字 0。上面我們宣告了一個yes 字串,但如果要設定很長的字串,這種設定字串初值的方式似乎遜斃了。通常我們可以簡寫:


 char yes[] = "Press Ok button";
 char s[10][80]; //二維字元陣列

其中二維的字元陣列又稱為「一維字串陣列」,前面的三維字元陣列就是「二維字串陣列」以此類推。

■  陣列與函式

如果我們希望傳一個二維陣列給某個加總函式計算總和,可以設計成 arrf.cpp。


sum 函式可以加總任何一個3*3 大小的整數陣列(其實是n*3 均可)。我們只要告訴sum 要處理的陣列名稱即可,比如 sum(apple)。您不能寫sum(apple[0][0]) 這表示僅處理apple 陣列的 [0][0] 這一個整數元素,而非整個 apple 陣列。


陣列參數的宣告方式是否與您所學有點不同?其實這才是標準的宣告方式,由於可省略陣列的最高維數,而且希望 sum 可以處理任何n*3 大小的陣列,通常會宣告成arrf2.cpp 的樣子。我們在程式裡加了一些指令,把陣列的元素的內容加一。

這可以解釋C++不會把整個apple陣列複製到array 陣列,而是直接處理 apple 陣列本身。array只是apple的代名詞,傳入函式的只是apple 陣列的起始位址,使 array 知道apple 的起始位址。這好像一位導遊把整個團交給另一位領隊時,只要告訴他這個團旅遊的地址以及人數即可。

或許您會覺得到本期的陣列和字串似乎滿簡單的?的確如此。通常初學者在學習陣列和字串的時候,並不會覺得有特別困難的地方,但只要一開始談指標,就像吃錯藥一般臉色都變了!個個眉頭深鎖擠眉弄眼痛苦不堪!為了使大多數讀者真正學會指標,這一期我們把難度降低了許多,希望您做個深呼吸放鬆心情,看看我們如何正式的介紹「指標」。

■  指標

所謂指標(pointer) 就是存放記憶體位址的一種整數變數,和一般整數變數完全一樣,在宣告指標變數時,必須在變數名稱的前面加上一個 * 號表示。


指標通常用來表示記憶體的某個位址,在真實環境下可以經由指標存取 1MB的任何一個位址。由於 80X86 系列 CPU 硬體設計的關係,在 64K(即 65536)以內的位址,稱為近程區段,搭配近程指標即可,凡是向前或向後超過64K 範圍都必須改用遠程指標。C++ 預設是近程指標。


什麼是記憶體位址?您可以想像成家裡的信箱號碼、書的頁碼等,如果您對近程或遠程不了解,可以把64K 當做馬路上的近距離里程,比如規定方圓一公里內就叫做近程,如果要到一公里以外的地方,就叫做遠程。通常初學者學不好指標的原因其實很簡單,並不是指標太難學,而是對記憶體似懂非懂,並沒有澈底了解所致。限於篇幅,請參考記憶體相關文章。

譬如我們希望直接取用記憶體位址編號2468的一個位元組。無論您使用任何一種等級的一般變數,比如:

 static int a = 10;

數字10究竟是存到記憶體的那一個位址?我們沒有必要知道,也沒有理由知道,若真的斤斤計較乾脆回去用組合語言好了。

但有時候會希望直接取用某個位址的記憶體要怎麼辦?指標就是最好的工具。透過指標去存取記憶體,就叫做指標間接定址法。暫時先忘記指標,讓我們來看一個很特別的程式。


程式 point1.cpp 是一個非常驚人的範例,我們把指標當做一般整數變數來使用!程式的 (int *) 和 (int) 只是型別轉換,並不會影響資料內容,由程式執行結果我們可以知道:


      指標變數 = 整數變數

也就是說,int *a; 和 int a; 是相等的,只是型別有點不同。既然如此,C 和C++ 為什麼要多設計出另一種資料型別?這是為了幫使用者自動計算記憶體位址,以及間接的操作一般變數、物件等。int *p1;  表示 p1 是一個指向整數空間的指標, int 在目前為 2 個位元組的整數,因此 p1++ 事實上是加 2,向前移動一個int 單位,因此程式的答案會是 34 而不是 32。


如果我們把 int *p1 改成 char *p1 ,*p2; 並修改相關的型別轉換,則執行結果就是 32,改成 float 則 x = 38。現在您只要把指標當做一種特別的整數即可。但問題是指標有什麼用途呢?什麼時候一定要使用指標,而不能使用一般的整數變數?這個問題並不好回答!


其實函式裡,自動等級的變數就是使用間接定址法,以指標的方式用CPU 堆疊來存取資料,如 auto a = 10; 並非您想像的只是簡單的把 10 存入 a。 point1. cpp 的例子是把指標當做整數使用,其實整數變數也可以當做指標!很複雜是不是?錯了!請回頭看看:指標=整數,這句話的真義。


通常我們會使用指標的地方,是希望更靈活的處理資料,比如前面的陣列(陣列就是一個指標常數)、程式執行時動態記憶體配置的變數或物件,這種臨時產生的物件是沒有名稱的,不可能在編譯時期用直接操作。或者是希望同一個指令可以執行不同的函式(函式指標),這些就是指標的用途以及必須使用指標的地方!


■  實例:抓螢幕程式


假設我們想寫一個簡單的非常駐式「抓螢幕程式」,把螢幕上的文字畫面抓到一個 SCREEN.TXT 文字檔裡頭。關於文字幕的結構請參閱相關書籍,這裡省略。請想想這個程式需要寫多少指令? >200?


螢幕上每一個字都是由2 個位元組所構成,第一個位元組是字元本身,第二個則是文字的顏色,因此 80 行螢幕,每列就有160 個位元組。彩色螢幕的記憶體位址在 B800:0000,單色螢幕是 B000:0000 ,GETSCR.CPP程式不會自動判斷彩色或單色螢幕,請您自行修改 B8 或 B0。C++規定長整數的後面最好加上一個 L。


程式意外的簡單吧?我們在程式的前面宣告了一個遠程的 scrptr 指標,由於螢幕區在 640K 傳統記憶體之後,凡是超過 64K 的位址都必須使用 far 修飾詞。遠程指標就等於一個「長整數」。


scrfile.put(*scrptr); 是把scrptr 所指向的一個字元取出後交給scrfile 物件的put 函式「印」到檔案。然後把螢幕指令向前移動2 個位元組。每印完一列畫面就送出一個 '\n' 字元換列。當檔案物件scrfile 不使用時,C++ 會自動呼叫物件的解構函式把檔案關掉,除非必要我們可以不必擔心檔案的問題。


您可以把 SCREEN.TXT 讀入編輯器,就會看到執行 getscr 當時的畫面。我們並不想增加您的負擔,getscr2.cpp 只是為了證明整數可以像指標一般使用。我們希望您能夠融會貫通,真正學會指標。


指標前面為什麼要加上 static靜態修飾詞?其實也可以不加,但是效率上會差了一些。我們曾說過,auto等級的變數會使用間接的方式來存取資料,如此一來auto等級的指標反而變成「間接﹢間接」雙間接指標運算!結果在無意之中多了許多額外工作,試問程式如何跑得快?


當然您也可以在getscr2加上 static ,使程式跑得快一些。只是目前使用auto 或static您不會感覺速度的差異,因為程式太小,再加上檔案處理佔用大部份時間,您可以寫其他測試程式來試試。


為了這個原因,建議您從現在起,無論是C 或C++ 程式,在需要趕時間的地方,把自動指標之前加上static修飾詞。但不要任意改變變數的視野,把區域變數改成全域變數,在C++ 必須養成儘可能不使用全域變數的習慣。由於這個問題沒有一本C 或C++ 書提及,所以自然而然的成為我們的獨家技術。


■  參考


C++ 新增了一個新的資料型別,叫做參考(reference)。 什麼是參考?簡單的說,就是常數指標或稱為「指標常數」,也就是禁止改變內容的指標。宣告參考只要在變數名稱前面加上 &符號即可。


如果了解指標,參考就更簡單了,首先請分辨底下三個指標敘述:


 const int *p1;
 int *const p2 = &x;
 const int *const p3 = &y;


p1 是一個指向常數的指標,p2 則是一個指標常數,指向一般整數 x,p3是一個指向常數的常數指標,指向常數 y(其實是僅讀變數)。參考就像是 p2。比如宣告一個指向 x 的 ref 參考:


 int &ref = x;    ←不必用 &x !

很簡單吧,由於ref 和 p2、p3 都是指標常數,因此必須在宣告時指定初值否則會收到 ...must be initialized 的錯誤訊息。
由於指標在使用時必須經常加上* 或 & 運算子,常常會因為忘記使用造成許多不便。因此C++ 增加了參考來解決這些困擾。請參考 ref.cpp。


order 函式使用指標來完成交換 x,y 物件,swap 則使用參考來交換x,y。在主程式裡頭,我們用 sizeof 指令來顯示指標和參考型別的大小,答案是 2 bytes。既然有佔用記憶體空間,就必須稱為參考變數,而非參考常數。
既然參考本身就是一種指標常數,宣告 int &const ref 是沒有意義的,如果希望參考到一個僅讀變數,或者不希望參考改變該變數的內容,則可以宣告:


 const int &ref = x;


■  指標陣列


一群指標集合也可以組成陣列,比如底下的 ptrs 就是一個指向整數的指標陣列,陣列的元素有 100 個:


 int *ptrs[100];   //ptrs 可以指向 100 個不同位址的物件

其實C 和C++ 把陣列視為指標常數,因此指標可以和陣列語法混合使用,比如

 int apple[10];
 int *ptr = apple;  ←不必用 &apple


*(apple+5)和 *(ptr+5) 都表示取出 apple[5]或 ptr[5] 這個元素,也就是說底下這些敘述也是相等的:

 *(apple+5) = apple[5]
 *(ptr+5)   = apple[5]
 &apple[2]  = apple+2
 &ptr[2]    = ptr+2

■  函式指標

指標是物件導向相當重要的元件,如果只能指向一般物件就未免太遜了,指標不僅可以指向函式,而且還具有動態連結功能(留待以後再討論)。
C++ 繼承了C 語言指標的所有特性,所以指標也可以指向函式,比如指向具有整數參數和整數傳回值的函式的指標:


 int (*fptr)(int x);

ClearScreen 其實可以指向任何一個沒有參數和傳回值的函式。須特別注意的是,不要寫成:


 int *fptr(int x);

這是函式原型宣告,而非函式指標。 fptr函式有一個整數參數,並且函式會傳回一個指向整數的指標。
比如指向 clrscr 函式的指標:


 void (*cls)(void) = clrscr; ← 注意
 cls();  //等於執行 clrscr();

請注意,不能寫 =clrscr(); 我們要得到 clrscr 函式的位址而不是呼叫該函式並且把傳回值設給指標。另外,使用函式指標就像是一般函式,如 cls();
底下表示函式指標所指的函式,該函式有指標參數並傳回一個整數指標:

 int *(*fptr)(int *x);

■  函式指標陣列

把函式指標集合成陣列,就叫做函式指標陣列,比如:


// fptrarr.cpp
#include <conio.h>

void main()
{
  int *(*fptr[100])(void);

  fptr[1] = (int *(*)())clrscr;
  fptr[1]();  //呼叫 clrscr() 函式
}


其實程式很簡單,我們宣告了一個函式指標陣列,陣列有100個元素,即100個函式指標。每個指標都可以指向任何:


 int *函式名稱(void);

之類的一般函式,當然也可以指向不同的函式,這裡就是硬指向clrscr,由於 clrscr函式原型和我們宣告的函式指標不同,因此 (int *(*)()) 只是一個型別轉換,才能把clrscr的位址設給 fptr[1]。其實記憶體位址只是一個數字,並沒有型別,做型別轉換是C++ 為了確定使用者要把沒有傳回值的函式,設給有傳回值的函式指標,如此而已。如果您不知道要如何寫型別轉換,別擔心,C++ 編譯器產生錯誤訊息時會告訴您,只要抄上去即可。


使用函式指標陣列來呼叫指標所指的函式時,這是您從未見過的:

 fptr[1]();   //就像一般的函式呼叫

底下這個敘述是錯誤的:


  int *(*fptr)(int *x)[100];

您或許會養成習慣,把陣列的 [100] 寫在最後,但現在的陣列名稱是fptr,因此 *fptr[100] 或 *fptr[10][20]才對。

■  指標與結構


或許您對C++ 使用 file.put('\n'); 之類的指令會覺得很怪異,請看看底下這個「C 語言」的程式:


// ptrscr.c
#include <conio.h>

typedef struct
{
   void (*cls)(void);
} demo;

void main()
{
   demo ibmpc = { clrscr };

   ibmpc.cls();
}

■  this 指標


其實在class 裡頭的每一個成員函式至少都有一個參數(即使宣告(void);)那就是 this 指標(稱為自我指標)。this 指標通常用在需要取得當時物件的實際位址,或者把自己傳回去的時候。

另一個要介紹的是C++ 新增的成員指標以及 .* 和 ->* 運算子,這些東西幾乎一般C++ 書籍都沒有提到,但限於篇幅和問題的深度,就保留下來吧。


// array.cpp
#include <iostream.h>

int main()
{
  const int element = 10;
  int apple[element] = { 10, 20, 30, 40, 50, 60, 70 };

  for (int loop=0; loop<element; loop++) {
     cout << "apple[" << loop << "] = " << apple[loop]
          << endl;
  }
  return (0);
}

蘇言霖 2013/09/10 0 6959
Comments
Order by: 
Per page:
 
  • There are no comments yet
Rate
0 votes
Post info
蘇言霖
「超級懶貓級」社群網站站長
Actions