第7章字串與指標 - 資訊傳播工程學系

Download Report

Transcript 第7章字串與指標 - 資訊傳播工程學系

第7章 字串與指標
第7章 字串與指標
在本章中,我們再次使用陣列。但是與其
像在第六章中一般考慮元素中所含的都是
數值的陣列,我們將會看每一個元素中都
含有一個單一字元的陣列。如這些陣列的
最後一元素中是空字元(\0),則這些字元陣
列稱為字串。
第7章 字串與指標
7.1 宣告,初始化及列印字串並了解記憶體的
安排
7.2 決定有關字串及字元的資訊與使用PRINTF
7.3 二維字元陣列
7.4 由鍵盤與檔案讀取字串
7.5 指標變數與陣列變數
7.6 在一個宣告中進行初始化
7.7 傳遞字串給使用者定義的函數
7.8 動態記憶存取
7.1 宣告,初始化及列印字串
並了解記憶體的安排
課題:
 字元陣列
 初始化單一字元
 初始化字串
 列印字串
 記憶體安排
7.1 宣告,初始化及列印字串
並了解記憶體的安排
1. 什麼是一個字串?
2. 當一串由雙引號所封包的字元被寫在程
式的本體中,會如何看待它呢?
3. 我們如何才能不必逐個字元地初始化字
串?
4. 我們可以使用像cc=“This is a string
constant,also called a string literal.”;般
的指定敘述來儲存字串到陣列cc[ ]中嗎?
7.1 宣告,初始化及列印字串
並了解記憶體的安排
5.
6.
7.
8.
我們可以用什麼函數來列印一個單一字元?
我們可以用什麼函數來列印一個字串到螢幕
上,而它們是如何運作的?
我們如何列印一個字串到一個檔案中?
如果我們寫了
char aa,ee[2];
aa='g';
strcpy(ee,"g");
則aa記憶體中的內容和ee[ ]記憶體中的內容
會相同?
7.1 宣告,初始化及列印字串
並了解記憶體的安排
9. 我們如何在我們的變數表中顯示字元陣
10.
11.
12.
13.
列?
在位址被列印出來之後,我們可以了解
本課程程式中的字串在記憶體中儲存的
位置嗎?
記憶體的安排是何時完成的?
當程式執行時會發生什麼?
我們可以在宣告中初始化字元陣列就向
對數值陣列做的一樣嗎?
7.1 宣告,初始化及列印字串
並了解記憶體的安排
14. 為什麼我會想要在執行時期縮小、放大
或重新安排記憶體區塊?
15. 你可以在如何才能這麼做的細節方面給
我們一些提示嗎?
16. 每一個和ANSI C相容的編譯器都用相
同的方式安排記憶體嗎?
17. 我們可以總結我們所學到的C列印字元
/ 字串的函數嗎?
7.1 宣告,初始化及列印字串
並了解記憶體的安排
什麼是一個字串?一個字串是一個字元的陣列其中包含
結尾空字元(\0)。譬如說,我們在本課程程式中使用敘述
bb[0]='C';
bb[1]='a';
bb[2]='t';
bb[3]='\0';
以字元儲存了一個字串在字元陣列bb[ ]中,記得一個陣
列的元素是儲存在緊接且漸增的記憶體位址中。正因如
此,一個字串是由儲存在緊接的記憶體儲存格中的字元
碼(通常是ASCII或EBCDIC)所組成。最後一個記憶體
儲存格含有視作一個單一字元的脫離序列\0。
7.1 宣告,初始化及列印字串
並了解記憶體的安排
當一串由雙引號所封包的字元被寫在程式的本體
中,會如何看待它呢?C把它視為如同一個位
址。C實際上以儲存字串的第一個記憶體儲存格
的位址工作。我們隨後會更加詳細地看字串常數
所使用的位址。目前你只需要記得在程式本體中
C把字串常數視為一個位址。譬如說,每當我們
使用字串作為函數的引數時,我們也可以使用位
址。位址通常會由沒有括號的陣列名稱所代表。
7.1 宣告,初始化及列印字串
並了解記憶體的安排
我們如何才能不必逐個字元地初始化字
串?使用指定敘述逐個字元地初始化字串
是挺麻煩的。幸好C有庫存函數可使得初始
化字串容易些。譬如說,原型在檔案
string.h中的函數strcpy,把一個從某個位
址開始的字串複到以另一個位址開始的記
憶體儲存格。
7.1 宣告,初始化及列印字串
並了解記憶體的安排
我們可以使用像cc=“This is a string constant, also
called a string literal.”;般的指定敘述來儲存字
串到陣列cc[ ]中嗎?不行,這是一個常見的錯
誤。我們不能用這個指定敘述因為C從指定敘述
右邊的文字字串看到一個位址。所以這樣子的敘
述導致C嘗試把一個位址儲存到 cc 所指示的位
置。但是陣列cc[ ]是宣告來儲存字元而不是位
址,所以指定敘述不能運作。
7.1 宣告,初始化及列印字串
並了解記憶體的安排
我們可以用什麼函數來列印一個單一字元?我們
先前示範過如何使用putchar及printf函數列印單
一字元在螢幕上所以在此不再重覆。C有putc及
fputc函數可以用來列印單一字元到檔案中。在使
用上它們基本上是相同的。舉例來說敘述
putc(a,outfile);
fputc(a,outfile);
導致變數a所表示的字元被列印到檔案指標outfile
所指向的檔案中 。
7.1 宣告,初始化及列印字串
並了解記憶體的安排
我們可以用什麼函數來列印一個字串到螢幕上,
而它們是如何運作的?我們可以用puts及printf
把一個字串列印在螢幕上。舉例來說,敘述
puts(bb);
printf("%s\n",bb);
導致在識別字bb所標示的位址的字串被列印在螢
幕上。puts函數把陣列bb[ ]的元素逐一列印在螢
幕上直到它碰到空字元為止(空字元不會被列印
出來),此時雖然沒有明確的告訴它它也會列印
一個換行字元。
7.1 宣告,初始化及列印字串
並了解記憶體的安排
我們如何列印一個字串到一個檔案中?我
們可以使用fputs或者fprintf函數。譬如說,
兩個敘述
fputs(bb,outfile);
fprintf(outfile,"%s\n",bb);
都導致字元陣列bb[ ]被列印到outfile所指
向的檔案。
7.1 宣告,初始化及列印字串
並了解記憶體的安排
如果我們寫了
char aa,ee[2];
aa='g';
strcpy(ee,"g");
則aa記憶體中的內容和ee[ ]記憶體中的內容會相同?不會,而且這說
明了字串的一個基本特性。以下的圖中描述了記憶體儲存格中的內
容; 顯然可見,陣列型式中結尾空字元。因為這個差異,我們不能
視aa為一個字串。
7.1 宣告,初始化及列印字串
並了解記憶體的安排
本課程程式中一維字元陣列的三維格式
7.1 宣告,初始化及列印字串
並了解記憶體的安排
在位址被列印出來之後,我們可以了解本課程程式中的字串在記憶
體中儲存的位置嗎?可以的,我們注意到,依照漸增的次序,記憶
體位置及對應的字串名稱是
FF28
FF8C FFF0 FFF5
dd
cc
bb
aa
我們可以轉換這些十六進位值為十進位值並由這些十進位值了解所
顯示的位址之間共有多少位元組。轉換這些為十進位得
65320
65420
65520 65525
dd
cc
bb
aa
在dd與cc之間我們有65420-65320=100個位元組,在cc與bb之間我們
有65520 -65420=100個位元組,在bb與aa之間我們有65525 - 65520=5
個位元組。陣列cc[ ]與dd[ ]被宣告為具有100個元素,bb[ ]具有4個元
素,而aa只有1個元素。位址顯示它們所有的記憶體是緊密地排在一
起。
(續下頁)
7.1 宣告,初始化及列印字串
並了解記憶體的安排
(接上頁)
至於本程式先前的編譯及執行,我們顯示記憶體佔用的
次序(以線性型式而不是表格的型式) , 如下圖。
7.1 宣告,初始化及列印字串
並了解記憶體的安排
記憶體的安排是何時完成的?當程式被編譯時,C由宣告
決定有多少變數及陣列的記憶體儲存格需要被設定以及
所有被宣告的資料結構所需要的記憶體區塊的大小。因
為這是在編譯時完成的,我們必須要確定我們陣列的大
小是任何可能問題所需的最大值。換句話說,我們不能
在執行時期沒有使用更進階的技巧就把陣列宣告的大小
放大以便容納更多的資料。記住變數的位址在編譯時期
已經設定好(不是絕對位址而是所謂的相對位址,意即變
數相對於其他已存在的變數的儲存格所在)。我們不能在
執行時期移動這些記憶體儲存格集。如果我們嘗試這麼
做的話會當掉我們的程式。
7.1 宣告,初始化及列印字串
並了解記憶體的安排
為什麼我會想要在執行時期縮小、放大或重新安排記憶體區塊?我
們想要這麼做是為了更有效率地使用記憶體。譬如說,如果我們在
一個程式中的宣告要能容納最大可能的問題,則我們會發現有些電
腦因為記憶體不足的緣故而容納不下程式。如果是這個狀況,則我
們不可能不修改宣告並重新編譯程式就能在這些系統執行我們的程
式。這對商業軟體而言是不切實際的。處理問題更好的方法是在編
譯時期使記憶體集合理地小,讓所有的現代電腦都能處理它。然
後,如果要跑大的問題的話,讓程式在執行時期需要的話預留其他
的記憶體(譬如說,當需要使用一個大的陣列時)。如此做的話,在只
有小量記憶體的電腦上,我們只需列印出一個錯誤訊息說明電腦需
要更多的記憶體才能執行這個大的問題。小的問題在小的系跑或大
的問題在大的系統跑就不用列印錯誤訊息,如此我們就能寫一個能
在不同大小系統上使用的程式。
7.1 宣告,初始化及列印字串
並了解記憶體的安排
我們可以總結我們所學到的C列印字元/字串的函數嗎?
可以的;以下是一個一覽表。
使用%c的putchar及printf
使用%c的putc,fputc及fprintf
使用%s的puts及printf
使用%s的fputs及fprintf
列印一個字元到螢幕
列印一個字元到檔案
列印一個字串到螢幕
列印一個字串到檔案
7.2 決定有關字串及字元的資訊與
使用PRINTF
課題 :
 %s轉換指示
 決定字串的資訊的字串函數
 決定字元的資訊的單一字元函數
7.2 決定有關字串及字元的資訊與
使用PRINTF
1. 在本課程程式中,轉換指示%s、%40s、
2.
3.
4.
5.
%-40s及%40.10s的意義為何?
使用printf來列印字串常數的方法是什
麼?
strlen函數是做什麼的?
strlen有什麼作用?
sizeof運算子是作什麼用的?
7.2 決定有關字串及字元的資訊與
使用PRINTF
6.
7.
8.
9.
在本課程程式中,我們把sizeof(many_lines)
除以sizeof(char)。為什麼?
在算式sizeof(many_lines)中,陣列名稱
many_lines中並沒有括號。這是表示sizeof實
際上是在處理位址嗎?
什麼是BUFSIZ?
strcpy與strlen兩者都是使用位址作為輸入引
數。是不是大多數的字串函數都是使用位址
而不是變數名稱作為輸入引數?
7.2 決定有關字串及字元的資訊與
使用PRINTF
10. isdigit函數是做什麼的?
11. isdigit在我們的程式中可以用來做什麼?
12. tolower函數是用來做什麼的?
13. 使用puts來列印新的一行的方法是什麼?
14. 在C函數庫中有其他的字元函數嗎?
7.2 決定有關字串及字元的資訊與
使用PRINTF
在本課程程式中,轉換指示%s、%40s、%-40s及%40.10s的意義為何?這
些轉換指示的運作就像那些用在整數型別d一般。格式是
%旗標 欄位寬度 . 精準位數 s
其中旗標、欄位寬度、小數點及精準度是可忽略的。
7.2 決定有關字串及字元的資訊與
使用PRINTF
使用printf來列印字串常數的方法是什麼?結果
是一個樣子奇怪的printf敘述,舉例來說,可以
使用以下的格式:
(接下頁)
7.2 決定有關字串及字元的資訊與
使用PRINTF
(續上頁)
在這個敘述中,我們有格式化字串作為第一個字串常數,以及三個要被列印
的常數。就如所示在格式化字串中有三個轉換指示。他們和隨後的字串依照
次序相互對應。這使得第一個%s成為“This is”的格式化指示而第二個%s成
為“one methd for printing”的格式化指示。第三個%s對應於“string
constants\n”。雖然我們習慣於在printf敘述中看到要被列印的變數,但事實
上在其中我們也可以有字串常數。注意到第一個逗號之前的字串是格式化字
串。記住,一個程式本體中的一個字串實際上代表一個位址,而第一個逗號
之後的字串是一般的字串。C基本上把程式本本體中的字串常數換成字串常
數在記憶體的位址。所以,雖然我們寫的是一個字串常數,我們實際上是指
出一個位址,而對一個位址,%s是正確的轉換指示。
要小心的是我們在第一個逗號之前可以有數個格式化字串。譬如說,我們可
以寫
7.2 決定有關字串及字元的資訊與
使用PRINTF
strlen函數是做什麼的?函數strlen是用來決定某一個字元陣列中實
際儲存的字元的數目。它之所以能如此是經由使用陣列第一個元素
的位址作為搜尋空字元開始。它數著記憶體儲存格的數目直到它碰
到空字元。使用strlen的格式是
strlen(位址);
其中位址告訴strlen從那裏搜尋起。函數傳回一直到但不包含字串中
的空字元的位元組或記憶體儲存格的數目。雖然毫無疑問傳回的值
是一個整數,但是為了強調這一點,我們還是小心地在傳回值之前
使用(int)轉型算子。舉例來說,敘述
occupied=(int)strlen(many_lines)
把字串many_lines中字元的數目指派為變數occupied的值(不含空字
元)。
7.2 決定有關字串及字元的資訊與
使用PRINTF
sizeof運算子是作什麼用的?sizeof運算子計算用來儲存
某一個變數或變數型別的位元組數目。至於它是一個運
算子或是一個函數一點都不重要,除了一點就是我們不
需要總是用括號把引數圍起來。在這教材中,我們會在
所有的情況中使用括號以避免因不使用而引發的問題。
使用sizeof的格式是
sizeof(名稱)
其中名稱可以是一個變數名稱或型別。
7.2 決定有關字串及字元的資訊與
使用PRINTF
在本課程程式中,我們把sizeof(many_lines)除以sizeof(char)。為什
麼?這允許我們決定保留給陣列的元素位置的個數。如果我們要知
道有多少元素保留給一個陣列,光知道保留的位元組數是不充份的
。我們必須把位元組數除以陣列元素的型別所用的位元組數,因為
ANSI C除了字元之外並沒有明確定下每一個型別所用的位元組數目
。譬如說,某一個C的實例中整數可能用兩個位元組而別的可能用
四個位元組。為了使程式碼具可攜性,我們除以元素型別的位元組
數目。依據定義,C給一個字元一個位元組。所以sizeof(char)是永
遠等於1而我們在程式中實際上並不需要除以sizeof(char)。但是我
這麼做是讓你了解當你要決定保留給一個陣列的元素個數時你該除
以元素型別的sizeof。
7.2 決定有關字串及字元的資訊與
使用PRINTF
什麼是BUFSIZ?就如你所猜的,BUFSIZ是一個代表輸
入緩衝區大小的巨集常數。它是定義在stdio.h之中而且是
你的C實例所用的大小。ANSI C要求它至少有256個位元
組。一般的大小是512。它給我們可以經由我們的標準輸
入流,stdin,傳輸的最大字元數。這是有用的因為我們
知道沒有一個以這種方式傳輸的輸入字串可以超越這個
大小。所以,我們可以用巨集BUFSIZ來定由stdin(通常
是指鍵盤)讀取的字元陣列的大小。隨後在本章中,我們
會發現在我們的C實例中一次由鍵盤讀取的字串不能超過
512。
7.2 決定有關字串及字元的資訊與
使用PRINTF
strcpy與strlen兩者都是使用位址作為輸入引數。是不是大
多數的字串函數都是使用位址而不是變數名稱作為輸入引
數?是的,和使用一個變數名稱作為一個輸入引數的數學
庫存函數不同,一個字串函數通常使用一個字串或者字元
陣列的第一個元素的位址作為輸入引數。所以,使用字串
函數時,我們必知道如何在C中引用位址,有多少記憶體
是和某一個位址有關,以及在我們的程式中記憶體被處理
方式。
7.2 決定有關字串及字元的資訊與
使用PRINTF
isdigit函數是做什麼的?這個函數處理一個單一字元而不
是一個字串。這個函數決定用作引數的字元是不是
0,1,2,3,4,5,6,7,8或9。如果不是,這個函數會傳回0。如果
是,這個函數會傳回一個非零整數。舉例來說,在本課
程程式中,isdigit是用在
if ( isdigit (many_lines[0] ) ) puts(many_lines);
這一行中。引數,many_lines[0]是字元T。所以isdigit函
數傳回0值,該值在if敘述被用來指示假,而導致puts敘
述沒有被執行。
7.2 決定有關字串及字元的資訊與
使用PRINTF
tolower函數是用來做什麼的?這個函數處理的也是單一字元,不是
字串。如果tolower的引數是大寫字母,這個函數會傳回字母的小寫
。它並不修改引數本身。譬如說,在本課程程式中,我們使用了以
下的敘述
cc=tolower(many_lines[0]);
putchar(cc);
puts(many_lines);
因為many_lines[0]是大寫字母T,tolower(many_lines[0])傳回小寫字
母t。設定敘述把t指派給變數cc。這在敘述putchar(cc);的輸出結果中
可以驗證。但是因為tolower函數並不修改引數many_lines[0],
many_lines[]的第一個字元仍然是T。這在敘述puts(many_lines);
的輸出結果中可以驗證。
7.2 決定有關字串及字元的資訊與
使用PRINTF
使用puts來列印新的一行的方法是什麼?如果我們正在
列印的字串中有換行字元(\n)的話,則會開啟新的一行。
換行字元將不會被列印出來。給定字串
"This sentence \ncovers two lines."
以下是使用puts列印的。
This sentence
covers two lines.
其他的脫序列,如\t,嵌於字串中時的運作和以上類似。
7.2 決定有關字串及字元的資訊與
使用PRINTF
C函數庫中的字元函數
(接下頁)
7.2 決定有關字串及字元的資訊與
使用PRINTF
(續上頁)
7.3 二維字元陣列
課題:
初始化二維陣列中的字串
 列印二維陣列中的字串
7.3 二維字元陣列
1. 為什麼我們常用多維字元陣列來儲存字
2.
3.
4.
5.
串?
我們如何宣告一個二維字元陣列?
我們如何初始化一個二維字元陣列?
我們如何用printf來列印一個二維字元
陣列?
我們如何使用sizeof運算子在二維字元
陣列上?
7.3 二維字元陣列
6. 我們如何用puts與fputs來列印一個二維
字元陣列?
7. 在這些迴圈,puts與fputs列印出相同的
東西嗎?
8. 我們如何複製一個二維字元陣列的內容
到保留給另外一個二維字元陣列的記憶
體儲存格?
9. 我們把strlen函數用在一個二維字元陣
列?
7.3 二維字元陣列
10. 我們如何在我們的變數表格中表示二維
字元陣列?
7.3 二維字元陣列
為什麼我們常用多維字元陣列來儲存字串?譬如
說,使用二維陣列的一個理由是我們習慣於看二
維的文字表現方式,而我們印象中使用列行的二
維陣列與我們一般所見相符。譬如說,如果本頁
的文字有50行的縱深與80個字元寬,則我們可以
把整頁儲存在一個陣列aa[50][80]中。我們可以想
像一本書的樣子而把這個概念延伸到三維。譬如
說,一本400頁的書可以用一個三維陣列
aa[400][50][80]表示。當文字是以這種方式儲存
時是容易想像我們正在處理的東西。
7.3 二維字元陣列
我們如何宣告一個二維字元陣列?我們使用關鍵
字char宣告二維字元陣列隨後跟著一個具有兩組
括號的識別字。格式為
char 陣列名稱 [列數] [行數];
其中陣列名稱可為任何正確的識別字而列數與行
數必須為正整數常數。舉例來說,
char aa[2][90];
宣告aa[ ][ ]為一個有180個字元大小的記憶儲存格
的字元陣列。這些位格可以想像是分為2列90行。
7.3 二維字元陣列
我們如何初始化一個二維字元陣列?我們以本課程程式作為例子來
描敘。譬如說,
#define NUM_ROWS 3
#define NUM_COLS 15
char bb[NUM_ROWS][NUM_COLS];
strcpy(bb[0],"The bb array ");
strcpy(bb[1],"has ");
strcpy(bb[2],"2 strings.");
宣告bb[ ][ ]為3列,各有15行。各列被初始化來儲存以下的字:
7.3 二維字元陣列
每一個用雙引號封包的字串被存在一列裏。注意
到表列中每一個字串的長必須小於宣告的第二個
維度的大小。第二個維度的大小,15,是大於最
長的字串“The bb array \0”的大小,共14個字元
(含結尾空字元)。當你以這種方法初始化你的字
串,重要的是你不能忘記放在每一個字串結尾的
空字元。你的第二個維度的大小必須包含這個字
元。譬如說,陣列bb[][]最小的可接受大小為
bb[3][14],因為第一個字串中有14個字元。
7.3 二維字元陣列
我們如何用printf來列印一個二維字元陣列?我們可以使
用具有一個%s轉換指示的printf。我們曾說明過在一個
printf敘述中的%s會導致列印從所指示的位址開始直到遇
到空字元為止。只要指示一個二維字元陣列中一列開始
的位址,就可以列印整列。舉例來說,給定二維陣列
aa[ ][ ],迴圈
for(i=0;i<2;i++) printf("%s",aa[i]);
導致其中的兩列被列印。記住之所以可以如此做是因為
指示陣列aa[ ][ ]時只用單一的一組括號是指出一列開始的
位址。譬如說,aa[0]代表第一列開始的位址而aa[1]代表
第二列開始的位址。因為每一列都以\0結尾,只要迴圈走
過所有的列,就可以列印所有的字串。
7.3 二維字元陣列
我們如何使用sizeof運算子在二維字元陣列上?端視我們使用的運算
元為何,我們可以決定整個陣列,單一的一列或是單一的元素宣告
的大小。譬如說,對於本課程程式中的二維陣列bb[ ][ ],敘述
reserved=sizeof(bb[0]);
計算出bb[ ][ ]中第一列宣告的大小(或行數),因為運算元bb[0]只有一
組括號,表示說它是代表第一列開始的位址。從輸出結果中我們可
以看出是15,也就是宣告的行數。如果用bb作為運算元而沒有括
號,則我們可以計算出整個陣列的大小。譬如說,如果我們用了
reserved=sizeof(bb);
則reserved的值會是45,也就是保留給整個陣列bb[ ][ ]的總位元組
數。如果我們想要算出保留給bb[ ][ ]的列數,我們可以寫
reserved=sizeof(bb)/sizeof(bb[0]);
這敘述會計算45/15,也就是3,保留的列數。
7.3 二維字元陣列
我們如何用puts與fputs來列印一個二維字元陣列?puts與
fputs兩者都以一個位址作為它們的引數而且一直列印到
碰到空字元為止。所以作為引數,我們可以使用有一組
括號的陣列名稱來列印陣列中單一的一列。譬如說,
puts(bb[i]);
fputs(bb[i],outfile);
導致陣列bb[ ][ ]中的第i列被列印出來。函數puts列印輸出
到螢幕上而fputs列印到outfile所指向的檔案。如要列印陣
列bb[ ][ ]中所有的列,我們可以把以上敘述放到迴圈
中,如
for(i=0;i<NUM_ROWS;i++) puts(bb[i]);
for(i=0;i<NUM_ROWS;i++) fputs(bb[i],outfile);
7.3 二維字元陣列
在這些迴圈,puts與fputs列印出相同的東西嗎?
不,puts所產生的螢幕輸出是
The bb array
has
3 strings.
而fputs在輸出檔案列印出
The bb array has 3 strings.
由此,我們可以清楚地看出puts在每一個字串的
結尾都列印了一個換行字元而fputs並沒有。當你
使用這些函數時應當注意它們的差異。
7.3 二維字元陣列
我們如何複製一個二維字元陣列的內容到保留給另外一
個二維字元陣列的記憶體儲存格?我們在本課程的程式
中沒有示範,但是我們可以用strcpy函數。譬如說,如果
我們有兩個陣列宣告為
char cc[4][60],dd[4][60];
我們可以使用一個像
for(i=0;i<4;i++) strcpy(cc[i],dd[i]);
的迴圈來做。這個迴圈複製陣列dd[ ][ ]的內容,一次一
列,到cc[ ][ ]。要成功地複製字串,必須要有足夠的記憶
體保留給cc[ ][ ]來容納dd[ ][ ]中儲存的每一個字串。
7.3 二維字元陣列
我們把strlen函數用在一個二維字元陣列?要這
麼做的話我們需要把每個字串開始的位址傳給
strlen,也就是每一列開始的位址。所以,在本
課程程式中
occupied=strlen(bb[0]);
把bb[ ][ ]第一列開始的位址傳給strlen而strlen則
傳回該列的字元數(除了結尾空字元以外)。由
strlen傳回的整數值被指派給整數變數occupied。
7.3 二維字元陣列
我們如何在我們的變數表格中表示二維字元陣列?為了便於說明,
我們在下圖所示的三維解說中以列--行方法顯示一個二維陣列。每一
列的記憶體格被投影到後方。在這個圖中,我們沒顯示所有的記憶
體儲存格,但是有說明了每一列有多個記憶體儲存格觀念。在每一
個位格中填了一個字元。空字元是位於每一列的字串的結尾。顯示
的值是每一列第一個字元的值。
7.4 由鍵盤與檔案讀取字串
課題:
 由鍵盤讀取字串
 由檔案讀取字串
7.4 由鍵盤與檔案讀取字串
1.
2.
3.
4.
我們如何使用gets函數去讀取由鍵盤輸入的
字串?
我們如何使gets由鍵盤輸入把一個二維陣列
的每一行部份填滿?
我們如何由一個檔案讀取輸入到一個二維陣
列?
本課程程式中c[ ][ ]的迴圈把輸入精確地列印
到輸出檔案但是在列印到螢幕時每列印完一
行之後加上空白的一行。為什麼?
7.4 由鍵盤與檔案讀取字串
5. 我們可以用scanf由鍵盤讀取單一的一
行輸入嗎?
6. 在本課程程式中,輸入緩衝區中其餘的
內容是如何被讀取的?
7. 對於轉換指示我們應該記住什麼?
8. 我們現在已經討論過字元與字串輸入與
輸出的函數。我們可以針對他們做一個
總結嗎?
7.4 由鍵盤與檔案讀取字串
9. 我們先前看過如果在讀取過程中有錯誤
時scanf與fscanf會傳回EOF,而在列印
過程中有錯誤時printf與fprintf會傳回
一個負整數。字元與字串的I/O函數在
遇到錯誤時也會傳回特殊的值嗎?
10. 在表格7.2中,什麼是一個空指標而且
我們如何使用它?
7.4 由鍵盤與檔案讀取字串
我們如何使用gets函數去讀取由鍵盤輸入的字串?gets函數的格式是
gets(位址);
其中位址是要用來複製字串的第一個記憶體儲存格的位址。在很多情況裏,
位址會是一個陣列的第一個元素或一個陣列中一列的第一個元素。譬如說,
在本課程程式中,宣告以及gets函數呼叫
char a[40];
gets(a);
導致gets從鍵盤取得輸入的字串並從a[ ]的第一個記憶體儲存格開始把它複製
到保留給陣列a[ ]的記憶體中。gets函數讀取由鍵盤輸入的字串直到讀到換行
字元(由按下Enter鍵產生)而在本狀況中,讀取由鍵盤輸入的所有字元。但是
換行字元並沒有被gets複製到記憶中。而是gets捨去換行字元並插入一個空
字元到它的位置。在記憶體中空字元結束一個字串。所以底下的一行
puts(a);
把a[ ]列印到螢幕上。puts函數自動地附加一個換行字元到它所讀到的字
串。所以在列印時前進到新的一行是由puts所導致而不是由於a[ ]字串儲存
在記憶體中有一個換行字元在結尾。
7.4 由鍵盤與檔案讀取字串
我們如何使gets由鍵盤輸入把一個二維陣列的每一行部份填滿?經由
把函數放在一個迴圈裏我們可以gets一列一列讀。如我們先前所說
的,gets讀取字元直到它碰到換行字元為止。每次在鍵盤上按下一個
Enter鍵,我們就要執行一個gets的呼叫來讀取輸入的字元。要部份
填滿一個二維陣列所有的列,我們需要把一個gets函數的呼叫放在一
個會經過所有列的迴圈。gets的引數應為一個陣列中一列的第一個元
素的位址。在本課程程式中,宣告以及迴圈
7.4 由鍵盤與檔案讀取字串
導致gets在迴圈中一次一行地讀取三行輸入並把
讀取的每一行儲存到陣列b[ ][ ]中的一列。注意
到gets的引數是陣列bb[ ][ ]中一列的第一個元素
的位址。再次的,gets捨去換行字元並在每一列
最後一個普通字元之後緊接著插入一個空字元。
由輸出結果中可以看到,雖然gets捨去了換行字
元,但是puts函數所列印出來的和輸入的完全相
同。這時因為gets捨去換行字元而puts加上換行
字元,使得輸出成了輸入的回應。
7.4 由鍵盤與檔案讀取字串
我們如何由一個檔案讀取輸入到一個二維陣列?C提供fgets來讀取檔案。
fgets函數有以下的格式:
fgets (位址 , 字元數 , 檔案指標);
其中位址是要儲存字元的第一個元素的位址,字元數是比要讀取的字元的最
大數目還要多1,而檔案指標則代表輸入要讀取的檔案。譬如說,本課程程
式中的敘述
fgets(c[i],100,infile);
導致infile所指向的檔案中的頭99個字元(或直到讀到換行字元為止)被讀到陣
列c[ ][ ]中的第i列。如果讀到一個換行字元,fgets並不會把它捨去。不管是
不是新的一行,一個空字元會被放到最後一個被讀到的字元之後。所以,如
果有99個字元被讀取,則fgets會放置一個空字元到第100個記憶體儲存格。
以下的宣告及迴圈導致陣列c[ ][ ]中的每一列被輸入字元部份填滿:
char c[4][100];
for(i=0;i<4;i++)
{
fgets(c[i],100,infile);
}
7.4 由鍵盤與檔案讀取字串
本課程程式中c[ ][ ]的迴圈把輸入精確地列印到輸出檔案但是在列印
到螢幕時每列印完一行之後加上空白的一行。為什麼?這之所以發生
是因為puts函數每次列印一行到螢幕之後都會加上一個換行字元,而
fputs在列印一行到輸出檔案之後則不會加上換行字元。有註解的宣告
與迴圈為
7.4 由鍵盤與檔案讀取字串
而其輸入檔中為
We read\n
four lines of\n
text from our\n
input file.\n
在讀這個輸入檔時,fgets加上空字元但不會捨去(就如gets一般)任何
換行字元。所以,fgets把以下的放入陣列c[ ][ ]中:
7.4 由鍵盤與檔案讀取字串
puts函數加一個換行字元到每一列的結尾同時不列印空
字元。所以,puts列印了以下所示:
因為每一列的最後有兩個換行字元,我們的輸出結果中有
跳行。這裏所教的是c字串函數對換行字元及空字元的處理
不同。當你讀取與列印字串時要知道這一點。
7.4 由鍵盤與檔案讀取字串
我們可以用scanf由鍵盤讀取單一的一行輸入嗎?可以這麼做但是做法並不
直接。理由是轉換指示%s導致scanf一直讀直到遇到空格(不是空字元或換行
字元)。所以使用轉換指示的scanf只讀取一個字,而不是一整行。舉例來
說,宣告與敘述
char d[50];
scanf("%s",d);
以及鍵盤輸入
At first, not all of this string is printed.
導致scanf只讀取At這個字並且存在陣列d[ ]的開始。句子剩餘的部份留在輸
入緩衝區中依然未被讀取而位置指示器則停留在字母和其後的空格之間:
因為scanf只讀取一個字,在大多數時候你會發現用gets從鍵盤讀取一整行
的輸入比較方便。雖然在本課程程式中並沒有示範,使用轉換指示%s的
fscanf函數也是讀到遇到空格為止,所以也只讀一個字。
7.4 由鍵盤與檔案讀取字串
在本課程程式中,輸入緩衝區中其餘的內
容是如何被讀取的?使用敘述
gets(e);
gets函數讀取輸入緩衝區中其餘的內容(從
位置指示器到換行字元)並把它存到陣列e[ ]
中。
7.4 由鍵盤與檔案讀取字串
對於轉換指示我們應該記住什麼?記住,對於
printf,%s指示列印直到遇到空字元為止,而對
於scanf,%s示一直讀直到遇到空格為止。因為
字串的結尾有一個空字元,所以使用printf及%s
來列印字串是很方便的。但是使用scanf及%s來
讀取字串並不怎麼方便。上述評論對於伴隨的函
數fprintf及fscanf也成立。要確定你不會搞混
printf與scanf處理%s的方式。使用%s時printf列
印整個字串而scanf只讀一個字。
7.4 由鍵盤與檔案讀取字串
我們現在已經討論過字元與字串輸入與輸出的函數。我們可以針對
他們做一個總結嗎?下圖說明了字元輸入與輸出的函數。
7.4 由鍵盤與檔案讀取字串
我們先前看過如果在讀取過程中有錯誤時scanf與fscanf會傳回EOF,
而在列印過程中有錯誤時printf與fprintf會傳回一個負整數。字元與
字串的I/O函數在遇到錯誤時也會傳回特殊的值嗎?會的,下表總結
出這些函數在有錯誤以及沒有錯誤時所傳回的值。
7.4 由鍵盤與檔案讀取字串
在表格7.2中,什麼是一個空指標而且我們如何使用它?一個指標當
然是一個位址。但是我們可以使用強迫轉型算子(int)把一個位址轉換
為一個整數。一個空指標是一個轉換為整數後為0的位址。一個空指
標和任何不是空指標的指標一定不相等。一個使用空指標以上特徵
的方式是用來評估輸入時有沒有錯誤發生?譬如說,我們可以本課
程程式中宣告一個int型變數j並以隨後兩行
7.4 由鍵盤與檔案讀取字串
取代
fgets(c[i],100,infile);
另外一種利用fgets及gets在有錯誤時傳回空指標的方式是
使用在stdlib.h中定義的的,相當於空指標的常數巨集
NULL。這一類檢查輸入錯誤的程序是一種改良的讀取輸
入的方法。我們在本課程程式中並沒有使檢查錯誤的方
法來減低程式的複雜度。在本教材中,我們並沒有盡可
能地採用檢查錯誤的方法,原因並不是我們不贊同(事實
上我們鼓勵這麼做),而是我們要專注於其他的程式設計
特點。
7.5 指標變數與陣列變數
課題:
 陣列與指標的比較與對比
 初始化與列印陣列及指標
7.5 指標變數與陣列變數
我們已經看到在處理字串時常常使用字串中第一個元素的位址儲
存在一個陣列會比較方便些。譬如說,假設我們有10,000個句子,
每一個都是一個單獨的字串。如果我們把每一個字串第一個元素
的位址存在一個陣列a[10000]中,則列印所有的句子只需用一個迴
圈從i = 0 i = 9999,執行puts(s[i])。雖然這不是一般常用的作法,
我們通常會發現建立一個指向字串開始的指標的陣列是比較方便
的。記住,宣告一個指標變數時,我們在宣告中使用 *。譬如,
char *aa;
在記憶體中保留空間給一個名為aa的單一指標變數。因為用的是關鍵
字char,aa被指定來存放一個字元的位址。一個如
char *cc[5];
的宣告再記憶體中保留五個位格。每一個位格被指定來存放一個字元
的位址。
7.5 指標變數與陣列變數
1. 解釋宣告
char *aa;
char bb[25];
的差異。
2. 解釋宣告
char *c[5];
char dd[5][30];
的差異。
3. 為什麼敘述aa=ee,cc[0]=ff,及cc[1]=ee是恰
當的?
7.5 指標變數與陣列變數
4. 為什麼敘述bb=ee,dd[0]=ff及dd[1]=ee
是不恰當的?
5. 為什麼敘述strcpy(bb,ee),
strcpy(dd[0],ff),及strcpy(dd[1],ee)是
恰當的?
6. 為什麼敘述strcpy(aa,ee),strcpy(cc
[0]),ff)及strcpy(cc[1],ee)是不恰當的?
7. 在執行完本程式的第一節之後我們對記
憶體的想像是如何?
7.5 指標變數與陣列變數
8. 我們如何使用指標變數aa及puts來列印
指標變數來列印儲存在ee中的字串?
9. 我們如何使用指標陣列cc [ ]及puts來列
印儲存在ee及ff中的字串?
10. 我們如何使用指標變數aa及putchar( )
來列印儲存在ee中的字串?
11. 我們如何使用指標陣列cc [ ]及putchar( )
來列印儲存在ee及ff中的字串?
7.5 指標變數與陣列變數
12. 為什麼C允許陣列符號和指標變數一起
使用?
13. 我們可以在指定敘述中使用單一的字元
嗎?
14. 本課程的要點是什麼?
7.5 指標變數與陣列變數
解釋宣告
char *aa;
char bb[25];
的差異。第一個宣告為指標變數aa保留了
單一的記憶體位置。第二個宣告則保留了
25個記憶體位置給字元值。但是除非我們
在程式中把它們填滿,否則這些記憶體儲
存格依然是空的。
7.5 指標變數與陣列變數
解釋宣告
char *c[5];
char dd[5][30];
的差異。第一個宣告為單一的指標變數保
留五個記憶體位置。第二個宣告保留五個
各含30個字元的記憶體區塊。除非我們在
程式中填滿它們,否則所有這些記憶體依
然是空的。
7.5 指標變數與陣列變數
為什麼敘述aa=ee,cc[0]=ff,及cc[1]=ee是恰當的
?這些敘述當然是指定敘述,把指定敘述右邊算
式的位置放到左邊所指示的記憶體位置。因為aa
是一個指標變數而cc[0]及cc[1]試一個指標陣列變
數中的元素,我們可以儲存位址到任何這些位置
。因為ee與ff指示的是位址,這些指定敘述可以
很容易地執行。所以我們是在把位址儲存到預備
用來接受位址的記憶體儲存格中。
7.5 指標變數與陣列變數
為什麼敘述bb=ee,dd[0]=ff及dd[1]=ee是不恰當
的?因為bb不是一個指標變數,我們不能把一個
位址儲存到任何由bb所指示的記憶體位置。這個
敘述很顯然不合理,因為沒有括號的bb指示的是
陣列bb[ ]第一個元素的位址。這個位址是在編譯
時被設定,我們不能在程式本體中改變它或嘗試
改變它。同時,因為dd[0]及dd[1]並不是一個指
標陣列中的元素,我們不能在這些位格中儲存位
址。
7.5 指標變數與陣列變數
為什麼敘述strcpy(bb,ee), strcpy(dd[0],ff),及
strcpy(dd[1],ee)是恰當的?這些敘述導致由ee及ff
所指示的字串中現存的字元被複製到以bb,dd[0]
及dd[1]做為起始位址的記憶體儲存格中。這些敘
述是恰當的是因為bb[ ] 保留有25個記憶體儲存格
而dd[0]及dd[1]個保留有30個記憶體儲存格。這
表示ee[ ]及ff[ ]中所有的字元可以儲存到bb[ ]及
dd[ ][ ]中。
7.5 指標變數與陣列變數
為什麼敘述strcpy(aa,ee),strcpy(cc [0],ff)及
strcpy(cc[1],ee)是不恰當的?我們首先考慮strcpy(aa,ee)
。因為我們先前不曾指派一個隨後至少有25個記憶體儲
存格的位址給aa,所以我們不能使用這個敘述。如果我
們之前把一個有預留記憶體的位址指派給aa,則我們就可
以使用這個敘述。要小心的是,如果我們之前指派給aa
的位址隨後保留的記憶體儲存格不足所需(在本狀況中是
25),則敘述strcpy(aa,ee)會覆蓋了其他的記憶體儲存格。
這一類的錯誤會在你的程式中導致重大的問題。注意到
你這樣使用的話C編輯器不一定會指出一個錯誤。隨後在
本章中,我們會說明如何在執行期在aa之後保留記憶體空
間並且安全地使用這一類敘述。
7.5 指標變數與陣列變數
相似的情形也在strcpy(cc [0],ff)及strcpy(cc [1],ee)
中存在。因為cc [ ]只不過是一個五個指標的的陣
列,我們的空間只足夠存五個位址。這個宣告並
沒有保留空間來實際地儲存字元元素。如果要安
全地以指標變數及指標陣列的元素做為第一個引
數來使用strcpy敘述,我們必須確定第一個引數
所代表的位址有保留記憶體。
7.5 指標變數與陣列變數
在執行完本程式的第一節之後我們對記憶體的想像是如何?在執行
完本課程程式的第一節之後,我們對記憶體的想像就如下圖所示。
7.5 指標變數與陣列變數
我們如何使用指標變數aa及puts來列印指標變數
來列印儲存在ee中的字串?因為我們已經用敘述
aa=ee;
把ee[ ]中第一個字元的位址儲存到aa中,我們可
以使用puts(需要一個位址做為引數)以及aa就如
同敘述
puts(aa);
來列印儲存在ee中的字串。
7.5 指標變數與陣列變數
我們如何使用指標陣列cc [ ]及puts來列印儲存在
ee及ff中的字串?因為我們已經用敘述
cc [0]=ff;
cc [1]=ee;
把ff[ ]及ee[ ]的第一個字元的位址儲存到cc [0]及
cc [1]中,我們可以使用puts以及cc [0]與cc [1]做
為引數
puts(cc [0]);
puts(cc [1]);
來列印ff及ee中的字串。
7.5 指標變數與陣列變數
我們如何使用指標變數aa及putchar( )來列印儲存在ee中
的字串?因為putchar( )需要一個單獨的字元元素做為引
數,我們必須透過aa(使用敘述aa=ee;被指派取得ee[ ]的
位址ee[ ]中的每一個元素。或許看起來有些奇怪,但C
允許指標變數用陣列的符號存取一個陣列中各別的元素
。這就是說,aa[0]存取ee[0],aa[1]存取ee[1],而其他
的可以用相同方式存取。所以敘述及迴圈
aa=ee;
for(i=0;i<10;i++) putchar(aa[i]);
列印儲存在陣列ee[ ]中的字串(也就是“This is a”)的頭十
個
字元。
7.5 指標變數與陣列變數
我們如何使用指標陣列cc [ ]及putchar( )來列印儲存在ee及ff中的字
串?再次的,我們必須使用儲存在指標陣列cc [ ]中的位址來取用陣
列ee[ ]及ff[ ]中各別的元素。C允許使用陣列符號來做這件事情。換
句話說,當我們用敘述
cc [0]=ff;
把ff[ ]的位址儲存到cc [0]時,cc [0][0]對應於陣列ff[ ]的第一個字元
cc [0][1]對應於陣列ff[ ]的第二個字元,以此類推到整個陣列
。所以這個迴圈
cc [0]=ff;
cc [1]=ee;
for(i=0;i<2;i++)
{
for(j=0;j<10;j++) putchar(cc [i][j]);
putchar(‘\n’);
}
7.5 指標變數與陣列變數
為什麼C允許陣列符號和指標變數一起使用?C
允許使用陣列符號是因為,當一個程式被編譯
時,C把陣列符號轉換為指標符號。你應該記得
單元算子 * 是用在指標符號而括號[ ]是用在陣列
符號。不管用的是哪一種符號,我們可以存取某
一個特定位址的元素或者是某一給定的位址之後
的一些元素。譬如說,在本課程程式中aa[5]存取
的位址是儲存在保留給aa的記憶體之後的五個元
素。我們在這裏不會進入細節的部份而是留待以
後的課程。
7.5 指標變數與陣列變數
我們可以在指定敘述中使用單一的字元嗎?可以的,雖
然我們在本課程程式中沒有示範,但是指定敘述如這些
敘述修改bb[ ]。
bb[5]='5';
bb[6]=dd[0][3];
這些敘述修改bb[ ]。
aa[2]='p';
cc [1][6]=bb[2];
這些敘述修改ee[ ]。
是完全可接受的。只有字串時我們才需要使用函數如
strcpy( )進行初始化或修改。如果我們在本課程程式中在
初始化後執行上述的指定敘述,則字串會成為
7.5 指標變數與陣列變數
陣列
bb[ ]
ee[ ]
字串
This et a sample string
Thps ii a sample string
記住,和一個單一的指標變數一起(如aa)C允許使
用一維陣列型式的符號存取一個單一字元。和一
個指標陣列一起(如cc)C允許使用二維陣列型式的
符號存取一個單一字元。
7.5 指標變數與陣列變數
本課程的要點是什麼?你應該了解到
1. 要初始化一個字元陣列,我們使用strcpy。要
初始化指標變數及指標陣列,我們使用指定
敘述。
2. 雖然我們用不同的方法來初始化字元陣列及
指標(如第1點所述),我們使用相同的方法來
存取字串及個別的字元。換句話說,我們可
以在使用字元陣列及指標存取字串及個別的
字元時使用陣列符號(括號)。
7.6 在一個宣告中進行初始化
課題:
 在一個宣告中初始化字串
 在一個宣告中進行初始化與在程式本體
中進行初始化的差異
7.6 在一個宣告中進行初始化
1. 我們如何在一個宣告為一個指標變數保
留記憶體並指出它所指向的字串?
2. 在記憶體中aa所指向的字串位置在哪?
3. 從它們導致字串被儲存來看,下列兩個
宣告有何不同?
char *aa={"We can use a pointer to a string constant."};
char bb[ ]={"We can also use an array."};
7.6 在一個宣告中進行初始化
4. 給定這些儲存方法,我們如何列印所儲
存的字串?
5. 我們如何存取aa所指向的字串中各別的
字元?
6. 我們如何宣告及初始化一個指標變數的
陣列並指向許多字串?
7. 在記憶體中cc所指向的字串是位於哪裏?
7.6 在一個宣告中進行初始化
8. 以它們使得字串被儲存來看,以下兩個
宣告有何不同?
char *cc[ ]={"We can","use an array","of pointers to a constant."};
char dd[ ][11]={"Or we can","use a 2-D","array"};
9. 給定這些儲存方法,我們如何列印所儲
存的字串?
10. 我們如何存取cc所指向的字串中個別的
字元?
7.6 在一個宣告中進行初始化
11. 我們可以在程式本體中使用指定敘述
aa="We can use a pointer to a string constant.";
bb="We can also use an array.";
嗎?
7.6 在一個宣告中進行初始化
我們如何在一個宣告為一個指標變數保留記憶體
並指出它所指向的字串?我們可以宣告及初始化
一個指向一個字串常數的指標變數。譬如說,
宣告
char *aa="We can use a pointer to a string constant.";
宣告了字元指標變數aa來儲存字串
"We can use a pointer to a string constant.“
第一個字元的位址。
7.6 在一個宣告中進行初始化
從它們導致字串被儲存來看,下列兩個宣告有何不同?
char *aa={"We can use a pointer to a string constant."};
char bb[ ]={"We can also use an array."};
第一個,使用指標變數aa,導致字串常數"We can use a pointer to a
string constant.“的位址被儲存到aa的記憶體儲存格。第二個宣告導
致字串“We can also use an array.“被實質地儲存保留給陣列bb[ ]的
記憶體儲存格中。下圖顯示了記憶體的想像圖。我們可以看到,雖
然兩個宣告都使得字串被儲存在記憶體中,但是儲存的方式是很不
一樣的。注意到沒有名稱是和字串常數"We can use a pointer to a
string constant.“有關。我們取用這個字串的唯一辦法就是透過它的
位址。
7.6 在一個宣告中進行初始化
本課程程式aa與bb的記憶體想像圖
7.6 在一個宣告中進行初始化
給定這些儲存方法,我們如何列印所儲存的字串?我們
可以使用puts函數。記得puts函數需要字串第一個字元
的位址作為列印的引數。在這個程式中,兩個敘述
puts(aa);
puts(bb);
把字串列印在螢幕上。注意到兩個敘述之間相似處。它
們在這兩個狀況都可以運作因為aa與bb都代表位址。因
為aa是一個指標變數,表示式aa所代表的變數aa的值是
一個位址,是字串常數開始的位址。因為bb是一個有括
號的陣列名稱,它也是代表位址,開始的第一個記憶體
儲存格的位址。所以它們的敘述和結果看起來相似。
7.6 在一個宣告中進行初始化
我們如何宣告及初始化一個指標變數的陣列並指
向許多字串?我們可以宣告及初始化一個指標陣
列讓其中的元陣指向不同的字串常數。格式是
char *陣列名稱 [ ]={“字串_1”,“字串_2”,“字串_3”,看需要多少字串};
你可以由理解單一的一組括號是用來指出我們是
在宣告一個一維陣列來加強你的記憶。* 號指出
我們在陣列中儲存的是位址。
7.6 在一個宣告中進行初始化
以它們使得字串被儲存來看,以下兩個宣告有何不同?
char *cc[ ]={"We can","use an array","of pointers to a constant."};
char dd[ ][11]={"Or we can","use a 2-D","array"};
第一個,使用指標陣列cc,導致字串中第一個字元的位址
而不是字元本身被儲存到它的記憶體儲存格中。所以字
串常數"We can"、"use an array"及"of pointers to a
constant.“中的字元是儲存在記憶體中儲存常數的地方。
相對的,第二個宣告導致字串"Or we can"、"use a 2-D“
及"array"中的字元被儲存到保留給陣列的記憶體中。
7.6 在一個宣告中進行初始化
由這裏可以觀察到字串常數第一個元素的位址是儲存在cc[ ]中。虛線指出它
們之間的關聯。換句話說,cc[0]=0114,cc[1]=011B,cc[2]=0128。
7.6 在一個宣告中進行初始化
給定這些儲存方法,我們如何列印所儲存的字串
?我們可以基於要列印的字串數目在一個迴圈中
使用puts函數;在目前狀況下是3次。記得puts函
數是以要被列印的字串第一個字元的位址作為引
數。所以使用cc[0]、cc[1]及cc[2]作為puts的引數
導致cc[ ]所指向的三個字串被列印出來。再者,
對一個二維陣列而言,陣列中個別的列的位址是
使用陣列名稱及只有一組括號給定。所以dd[0]、
dd[1]及dd[2]代表dd[ ][ ]中每一列的位址並應作
為使用puts列印每一列時的引數。
7.6 在一個宣告中進行初始化
我們如何存取cc所指向的字串中各別的字
元?我們像先前的課程一般使用陣列符
號。譬如說,cc [1][4]代表第二個字串中的
第五個字元‘a’。注意到雖然cc[1]被宣告
為
一個一維指標陣列但是C允許cc[ ]使用二維
陣列的符號。
7.6 在一個宣告中進行初始化
我們可以在程式本體中使用指定敘述
aa="We can use a pointer to a string constant.";
bb="We can also use an array.";
嗎?第一個的答案是可以而第二個不可以。第一個是允
許的因為封包在雙引號中的字串是代表位址而我們可指
派一個位址給指標變數aa。第二個是不允許的是因為在
編譯階段bb被給定一個位址(因為它是一陣列)。一個新的
位址在執行時期是不可以給bb的。C是有一些混淆因為它
允許宣告:
char bb[ ]=“We can also use an array.”;
但它不允許指定敘述bb="We can also use an array.";
7.7 傳遞字串給使用者定義的函數
課題:
 計算一個陣列中的列數及元素個數
 傳遞陣列及指標給函數
7.7 傳遞字串給使用者定義的函數
1.
2.
3.
4.
5.
我們四個不同型的字串宣告是怎麼寫的?
我們如何把這四種類型傳遞到一個函數中?
function1的引頭是怎麼寫的?
在function1的呼叫中,我們指出我們傳遞了四
個位址。每一個引數都和其他的很相似因為它
只不過是該項目的識別字或名稱。那為什麼在
函數引頭中的對應引數彼此間是如此不同?
為什麼我們先前不用擔心這一點?
7.7 傳遞字串給使用者定義的函數
6. 為什麼我們應該記得幫忙正確地寫我們
的函數呼叫、引頭及原型?
7. 已知我們要傳遞位址到function1,
main及function1的記憶體區域是如何?
8. 我們如何決定bb[ ][ ]的列數以及dd[ ]的
元素個數?
9. 為什麼計算bb[ ][ ]的列數以及dd[ ]元素
的數目是必須的?
7.7 傳遞字串給使用者定義的函數
10. 什麼是指標算術而它對指標陣列hh[ ]是
怎麼做的?
7.7 傳遞字串給使用者定義的函數
我們四個不同型的字串宣告是怎麼寫的?四種類型先前我們都曾研
究過。在以下的宣告中,aa[ ]是一個一維字元陣列,bb[ ][ ]是一個二
維字元陣列,cc是一個字串常數的指標,而dd[ ]是一個字串常數的
指標陣列。
char aa[ ]="One-dimensional array.";
char bb[ ][LENGTH]={"Two","dimensional","array."};
char *cc="Pointer to string constant.";
char *dd[ ]={"Array","of pointers","to string","constants."};
在目前的狀況中,我們把每一個都在宣告初始化了。一維陣列帶有
一組括號。括號之間是空的因為我們已經把陣列初始化。二維陣列
帶有兩組括號其中第一組裏是空的因為我們已經把陣列初始化。第
二組裏有常數巨集LENGTH。雖然陣列已經初始化了,但是除了最
左邊的大小以外其餘的大小必須要給定。在宣告中字串常數的指標
是以 * 標示。指標陣列同時由*及括號所示。括號之間是空的因為我
們已經把陣列初始化。
7.7 傳遞字串給使用者定義的函數
我們如何把這四種類型傳遞到一個函數中?在函數呼叫
function1(aa,bb,cc,dd,num_rows_bb,num_elems_dd);
中,我們傳遞了
aa[ ]第一個元素的位址
bb[ ][ ]第一個元素的位址
cc的值,而這個值是一個位址因為cc是一個指標變數
dd[ ]第一個元素的位址
注意到,在傳遞給function1時,我們只需列出每一個的
名稱。所以,儘管每一種宣告的方式都不同,但是我們
把它們傳遞給一個函數時方式都很類似。
7.7 傳遞字串給使用者定義的函數
function1的引頭是怎麼寫的?在你的程式中使用函數時正確地寫下
引頭是很重要的。在目前狀況中,這是很直接的因為是和對應的引
數在main中的宣告平行的。在以下的表格中,我們指出main中的宣
告以及function1的引頭:(注意到表格中左右兩行相似之處)
7.7 傳遞字串給使用者定義的函數
為什麼我們應該記得幫忙正確地寫我們的函數呼叫、引頭及原型?
記住每一個引數我們只能傳遞一個位址或值。我們不能使用單一的
引數傳遞整個陣列給一個函數。如果我們要傳遞一個位址,在函數
呼叫中我們就使用一個位址。但是,在接收位址時,在函數引頭中
我們必須不只是指出我們接收的是一個位址。為了使函數能夠運用
位址,我們必須指示足夠的資訊以便C正確的執行所謂的指標算術。
譬如說,我們必須指出位址是一個列長度為某一個值的二維陣列的
起始。我們是經由在函數引頭與原型中使用括號或*來指出函數接收
的是位址。括號與*安排的方式指出C應如何在函數中執行指標算
術。譬如說,兩組括號指出位址是一個二維陣列的而第二組括號中
的值指出列的長度。這指出指標算術所要遵行的規則。然後函數本
體中我們使用的符號導致這一類型的指標算術的執行。
7.7 傳遞字串給使用者定義的函數
在圖表中,我們傳遞了四個位址與兩個值給function1。這些位址與
值被複製到保留給function1的記憶體區域中。
7.7 傳遞字串給使用者定義的函數
已知我們要傳遞位址到function1,main及function1的記憶體區域是
如何?
7.7 傳遞字串給使用者定義的函數
在上圖中,我們說明了記憶體的安排。從這個圖中注意
到function1的記憶體區域中含有4個位址(作為值)。在它
的記憶體區域中沒有陣列。但是function1可以運用所有
的陣列因為函數引頭中指示的型別已經給了足夠的資訊
來正確地運用這些陣列。同時也注意main與function1的
記憶體區塊之間的關聯。虛線顯示在main中aa、bb及dd
的位址是分別儲存在function1中ee、ff及hh的記憶體儲存
格中。同時cc的值,該值是一個位址,儲存在function1
中gg的記憶體儲存格中。注意到因為function1有這些位
址,所以它可以修改這些陣列。在你自己的函數中,傳
遞了位址之後,你可以把這些位址運用在函數如puts、
gets等正如同我們在本章先前所述。
7.7 傳遞字串給使用者定義的函數
我們如何決定bb[ ][ ]的列數以及dd[ ]的元素個數?我們使用sizeof算
子來取得它們宣告的大小譬如說,sizeof(bb)及sizeof(dd)分別計算出
保留給bb與dd的總位元組數。對於bb[ ]而言,知道了一個字元佔據
一個位元組之後,我們可以經由把總共的大小除以一列的長度來取
得列的數目。計算bb[ ][ ]列的數目的敘述為:
num_rows_bb=sizeof(bb)/LENGTH;
因為dd[ ]是一個一維陣列,我們把總共大小除以一個元素的大小就
可以得到元素的數目。我們每一個元素是一個char的位址而不是數
值的型別如int或double。所以我們除以 sizeof(char *)。雖然看起來
有點怪,但是sizeof(char *)告訴我們保留來存一個字元的位址的位元
組數。以下的敘述計算dd[ ]的元素數目:
num_elems_dd=sizeof(dd)/sizeof(char *);
一個char *典型的大小是2位元組。這是int *、double *以及其位址的
共通的大小。
7.7 傳遞字串給使用者定義的函數
什麼是指標算術而它對指標陣列hh[ ]是怎麼做的?指標算術是以位
址進行的算術(如加與減)。因為我們先前沒有談到這一點,所以我們
在這裏描述一下。譬如說,你可以假想你住在一條街上而街上每一
幢房子的位址都和相鄰的房子差1。計算哪一幢房子位址距離你的房
子3是很容易的。你只需要在你自己的位址上加3。C以類似方式運作
。從一個已知的記憶體儲存格位址決定另外一個記憶體儲存格的位
址,只需要在已知的記憶體儲存格位址上加(或減)。為了說明指標算
術,考慮以下兩個C認為相等的:
hh[1]
*(hh+1)
function1的引頭(圖7.8)指出hh是一個位址陣列的位址。每一個陣列
的元素佔了2個位元組(如前所述)。所以C處理hh+1時如同hh+2個位
元組。如果我們看本課程程式的記憶體使用的圖,我們看到hh的值
是FFEC,也就是說hh+1是FFEE。
7.7 傳遞字串給使用者定義的函數
雖然在圖中並沒有顯示出來,FFEE是dd[ ]第二
個元素的位址。所以*(hh+1)是和*(FFEE)相同。
這給我們dd[ ]第二個元素的值,這個值是位址
0126。使用puts如
puts(*(hh+1));
或
puts(hh[1]);
是和使用puts(0126)相同,都列印出字串
"of pointers"。
7.8 動態記憶存取
課題:
 在執行時期保留記憶體
 使用calloc、malloc及realloc
7.8 動態記憶存取
如果試著用固定大小的陣列來讓你的程式解盡可能多的
問題是有問題的。原因是你的程式可能在某些有足夠多
記憶體的電腦系統上可以處理這些固定大小的陣列但在
那些缺乏足夠記憶體的系統上則不行。雖然你只用程式
來處理少量的資料,但是你的程式可能在某些系統不能
運作。要使你的程式在記憶體多與少的系統上都能運
作,與其使用固定大小的陣列,不如依你要解的問題的
大小來保留記憶體。換句話說,譬如你要用字元陣列來
儲存大與小的工程報告,你可以為長的報告保留大量的
記憶體而為短的報告保留少量的記憶體的電腦上執行程
式儲存長的報告時則會失敗。
7.8 動態記憶存取
在本課程中,我們示範執行動態記憶體存取的函數(calloc, malloc,
realloc及free)的使用。為了在程式中有效運用它們,必須宣告能允
許我們運用動態記憶體特色的資料結構。在本課程中,我們宣告了
兩個動態儲存用的陣列:一維字元陣列aa[19]以及一維指標陣列
bb[22]。這兩個陣列會和只在程式中使用標準儲存方法時用的固定大
小陣列cc[22][19]相對比。在本程式中,我們用陣列aa[ ]及bb[ ]來存
放所有cc[ ][ ]所包含的資訊。在解釋中,我們說明了運用aa[ ]及bb[ ]
時所需的記憶體只比實際需要稍多但使用cc[ ][ ]時則可能保留了大量
記憶體而沒有實際佔用。我們故意地把cc[ ][ ]的大小定為比這個特定
例子所需的大,因為通常我們用固定大小的陣列來處理我們所預見
最大可能的問題而不是針對某特定問題的需要。我們會發現,使用
動態儲存,保留的記憶體只需比實際需要的稍多,比標準的儲存方
法更能有效率地使用記憶體。
7.8 動態記憶存取
calloc函數是做甚麼的?
malloc函數是做甚麼的?
realloc函數是做甚麼的?
free函數是做什麼的?
如果calloc、malloc或realloc不能依照要
求保留記憶體會發生什麼?
6. 在觀念上,空間是在記憶體中那裏保
留?
1.
2.
3.
4.
5.
7.8 動態記憶存取
7. 使用記憶體管理函數建立的儲存和使用
固定大小陣列建立的儲存有何不同?
8. 使用本課程程式中的動態記憶體存取牽
涉到哪些步驟?
7.8 動態記憶存取
calloc函數是做甚麼的?calloc函數在程式值行時期保留記
憶體。所保留的記憶體數量是由傳遞給calloc的引數所決
定的。呼叫calloc的格式是
calloc(元素個數,每個元素位元組數)
保留的記憶體數量是等於元素個數和每個元素位元組數
的乘積。舉例來說,在本課程程式中,
calloc(xx,sizeof(char));
導致大小為sizeof(char)(也就是一個位元組)的xx個
位元素(在本程式執行時xx為8)的記憶體被保留(同時
記憶體區域中所有位元全都初始化為0)。換句話說,這
個calloc的呼叫保留了8個位元組的記憶體。
7.8 動態記憶存取
calloc函數傳回所保留的第一個元素的起始位
址。譬如說,
bb[0]=(char*)calloc(xx,sizeof(char));
導致所保留的記憶體的起始位置被儲存到指標
陣列bb[ ]的第一個元素中。強迫轉型算子
(char*)導致由calloc所傳回的指標和bb[ ]的元
素同型。但是如果我們處理的是數值陣列,則強
迫轉型算子(int *)或(double *)可以和calloc
一起使用。
7.8 動態記憶存取
malloc函數是做甚麼的?malloc函數在執行時期
保留記憶體。所保留記憶體的量是由傳給malloc
的單一引數所決定的。呼叫malloc的格式是
malloc(位元組數)
所保留的記憶體數量是等於位元組數。舉例來
說,在本課程程式中,敘述
yy=xx*sizeof(char);
把xx(值等於儲存的字串的長度)乘以儲存單
一字元所需要位元組數(為一個位元組)。所以
yy代表儲存字串所需要的位元組數。
7.8 動態記憶存取
然後
malloc(yy);
導致yy個位元組的記憶體被保留。malloc函數傳回所保留
的第一個元素的起始位置。譬如說,
bb[1]=(char *)malloc(yy);
導致所保留的記憶體的起始位址被儲存到指標陣列bb[ ]
的第二個元素中。強迫轉型算子(char *)導致由malloc所
傳回的,成為一個字元的指標,和bb[ ]的元素同型。但
是,如我們處理的是數值陣列,則強迫轉型算子
(int *)或(double *)可以和malloc一起使用。
7.8 動態記憶存取
realloc函數是做甚麼的?realloc函數修改先前malloc或
calloc的呼叫中所保留的記憶體的量。格式是
realloc(指標,位元組數);
所保留的記憶體的量為位元組數必須在呼叫realloc之前計
算出來。所保留的記憶體位置是由指標所指示。引數指
標必須由先前的calloc或malloc的呼叫中所傳回。譬如
說,
realloc(bb[1],yy);
導致在bb[1]所指示的位置保留了yy個位元組的記憶體。
儲存在bb[1]的位址是先前由malloc所傳回所以是一個呼
叫realloc的合適引數。如果位元組數是少於先前calloc或
malloc為該指標所保留的位元組數,則realloc可以成功地
保留記憶體中該指標所指示的空間。
7.8 動態記憶存取
在目前的情況中,記憶體中的內容在呼叫realloc之後依然
不變。但位元組數較先前calloc或malloc為該指標保留的
位元組數為大,使得realloc可能不能保留相同的記憶體區
段。如果是這個情形,則realloc會在一個新位置保留記憶
體。函數傳回該新位置的指標。所以,
bb[1]=(char *)realloc(bb[1],yy);
導致記憶體被重新配置並且把記憶體區段的起始位置被
儲存到bb[1]。如果執行這個敘述之後儲存在bb[1]的位址
和執行這個敘述之前儲存在bb[1]的位址不同,則realloc
把第一個位址記憶體中的內容複製到第二個。就和calloc
與malloc一樣,強迫轉型算子導致realloc傳回的指標成了
一個字元的指標,和bb[ ]中的元素同型。但是如果我們
處理的是數值陣列時,強迫轉型算子(int *)或(double *)可
以和realloc一起使用。
7.8 動態記憶存取
free函數是做什麼的?free函數取消先前calloc、malloc或
realloc所保留的記憶體。格式是
free(指標)
其中指標是要取消保留的記憶體區段的位址。譬如說,
free(bb[1]);
導致由realloC保留在使用bb[1]指示的位址的記憶體區段
可供其他的calloc或malloc呼叫使用。在呼叫free之後儲
存bb[1]中的位址依然沒有改變;但是我們不能使用這個
位址儲存資訊因為這個位址的記憶體不再被保留。如果
我們想要在後續的運作中使用bb[1],我們需要呼叫calloc
或malloc(不能是realloc)並且把傳回的值儲存在bb[1]中。
一旦記憶體不再需要就把它釋放掉是良好的習摜。不要
依賴作業系統在終止執行後再行釋放記憶體。
7.8 動態記憶存取
在觀念上,空間是在記憶體中那裏保留?ANSI C在使用動
態記憶存取函數方面並沒有指示記憶體該如何配置以及配
置時以何種方式安排。所以不同的編譯器處理的方式都不
同。在這裏,我們描敘一個記憶體的常見的想像,雖然不
是很精確,但是足夠你想像一些記憶體的問題以及在程式
設計方面的影響。記憶體管理函數可以被想像成是在有時
候稱為堆積(heap)的記憶體區域裏保留空間。在觀念
上,C可以被想成把記憶體分成四個區域:
 一個區域稱為堆疊(stack)
 一個區域稱為堆積(heap)
 一個區域保留給全域變數
 一個區域保留程式指令
7.8 動態記憶存取
後三者的記憶體儲存格有低的位址而第一個有高的位址。
區域3和4在執行時期不會增長或縮小,但是區域1和2會。
一個你可以用來想像這一個觀念的圖像是在下圖中。
7.8 動態記憶存取
在上圖中,程式指令與全域變數是在左邊低位址所在的區
域。它們是封包在實心的線裏,表示在程式執行時期有固
定的大小。堆積座落於緊接著這兩者的地方而堆疊是位於
最右邊尾部的高位址區域。當記憶體由記憶體管理函數在
堆積中保留時,堆積會向右增長。當記憶體被釋放時,它
就會縮小。當函數被呼叫以及記憶體被保留來容納和有函
數有關的變數及資料結構時堆疊就向左增長。在函數執行
完畢後,除非有另外指示,否則這些記憶體會被釋放而堆
疊會縮小。如果堆積或堆疊需要太多的記憶體,則這兩者
可能交會,而導致發生不正常並且終止執行。但是這個特
殊類型的系統允許兩個區域的記憶體分別自立增長並且擁
有佔據所有可用的記憶體的可能性。
7.8 動態記憶存取
使用記憶體管理函數建立的儲存和使用固定大小陣列建立
的儲存有何不同?在下圖中,我們展示了我們對具有固定
大小二維陣列的記憶體的想像。一個固定大小的陣列的特
徵是,雖然保留了相當數量的記憶體,對某一特定問題而
言並不是所有的記憶體都被佔據。原因是這個記憶體區段
的大小是在編譯時期建立而且它的大小應該大到足以容納
實際使用中最大的陣列。一個大小為[22][19]的陣列,類似
本課程程式中的cc[ ][ ],在圖中被顯示出來。我們勾劃出
在某一次執行程式時這個陣列有被佔據的域,陣列其餘的
地方依然是空的。這導致有相當部份的記憶體是沒有用到
的。如果程式使用的系統是大到足以容納所有固定大小的
陣列的話是可以接受的。
7.8 動態記憶存取
7.8 動態記憶存取
但是,如果不是這樣的話,則下圖中說明的動態儲存會
比較好些,因為它幾乎用完它所保留的記憶體。在這個
圖中,我們只展示在堆積中記憶體是如何被保留的,其
中堆積裏的記憶體位址是儲存在堆疊內的一個指標陣列
中。同時,在堆疊內顯示了一個標準的陣列(譬如說是
char型)。這個陣列是用來儲存一列隨後會移到堆積裏的
資訊。如果我們比較上圖與下圖,我們會發現兩種方式
都儲存了相同數量的資訊。但是,我們同時也發現,因
為上圖中固定大小的陣列有相當的部份是空的,相對之
下,下圖中的動態儲存系統使用的記憶體少了許多。
7.8 動態記憶存取
7.8 動態記憶存取
使用本課程程式中的動態記憶體存取牽涉到哪些步驟?我
們在下圖中展示了一部份的過程。首先我們從cc[ ][ ]暫時
儲存一個字串到字元陣列aa[ ]。然後我們決定字串的長度
並把長度傳給calloc。calloc函數在堆積中保留足夠長度的
記憶體並設定所保留的部份的起始位址。這個位址隨後被
儲存到指標陣列bb[ ]中。這些動作是由以下的敘述進行:
strcpy(aa,cc[0]);
xx=strlen(aa);
bb[0]=(char *)calloc(xx,sizeof(char));
在這三個敘述執行完畢之後,堆積中保留的記憶體沒有儲
存任何東西。我們把aa[ ]中的字串使用敘述
strcpy(bb[0],aa);
儲存到堆積中的位址bb[0]。所以我們只需要四個敘述就能
運用C的動態記憶體管理能力。
7.8 動態記憶存取