■ 一般陣列與物件陣列
所謂陣列(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();
}
其實在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);
}