第* 章 指標與結構 -...

23
C 語言的最大特色就是「指標」( Pointer ),這是個讓人又愛又怕受 傷害的主題。有一句話是,學過 C,而不會指標,那只能說您看過 C。熟 悉指標的人,會將它比喻是天上那一顆最美的星星,而讓 懂的人,頭 上會冒星星,同樣是星星,但卻有 同的情境。 「指標好難喔,像無字天書,不知道該如何學,從哪一地方開始下 手,有沒有秘訣…」,常常有人會對我講這些話,並問我有沒有好方法 可以「頓悟」它。有許多人學到指標時,便裹足不前。基本原因是沒有 專書討論指標及與其相關的主題。基於此,便開始規劃,撰寫一本以指 標為核心的書籍,使得對指標不太懂的人,可以輕易 解指標的運作原 理與其應用,同時也可以讓已了解的人精益求精,更進一步探考個中的 奧秘。現在,已夢想成真,從書名「精通 C/C++ 指標:深入系統底層技 術」,就知道它是您學習與深入指標的最佳讀本。 本書內容精彩無比,除 C/C++ 語言的指標有深入的探討外,同 時也將與指標有 曲同工之妙的 reference 詳加討 ,所以本書不僅有 C 而已,還包括 C++ 程式語言,有關 reference 的主題。其實不僅 C++ 使 reference 的概念,JavaC# 以及 Python 也是以 reference 達到和 C 言的指標之功能。所以本書也加入 Java C# 以及 Python 這三種程式語 言相關議題的討論。 我們從 C 語言的指標與其息息相關的記憶體基本概 ,開始展開這 一次的快樂探險旅程,其中會經過:指標與變數、指標與陣列、指標與 函數、指標與字串、指標與結構、鏈結串列、二元搜尋樹、指標與檔案 處理等刺激關卡,希望能有效導引讀者進入指標的深層世界。同時也會 以鏈結串列、二元搜尋樹,檔案的處理等重要的主題,加以應用於本書 所談的其它程式語言,從而驗證指標與 reference 的相似功能。

Upload: others

Post on 15-Jan-2020

10 views

Category:

Documents


0 download

TRANSCRIPT

  • 序 言 ii

    C 語言的最大特色就是「指標」(Pointer),這是個讓人又愛又怕受

    傷害的主題。有一句話是,學過 C,而不會指標,那只能說您看過 C。熟悉指標的人,會將它比喻是天上那一顆最美的星星,而讓不懂的人,頭

    上會冒星星,同樣是星星,但卻有不同的情境。

    「指標好難喔,像無字天書,不知道該如何學,從哪一地方開始下

    手,有沒有秘訣…」,常常有人會對我講這些話,並問我有沒有好方法

    可以「頓悟」它。有許多人學到指標時,便裹足不前。基本原因是沒有

    專書討論指標及與其相關的主題。基於此,便開始規劃,撰寫一本以指

    標為核心的書籍,使得對指標不太懂的人,可以輕易了解指標的運作原

    理與其應用,同時也可以讓已了解的人精益求精,更進一步探考個中的

    奧秘。現在,已夢想成真,從書名「精通 C/C++ 指標:深入系統底層技

    術」,就知道它是您學習與深入指標的最佳讀本。

    本書內容精彩無比,除了對 C/C++ 語言的指標有深入的探討外,同

    時也將與指標有異曲同工之妙的 reference 詳加討論,所以本書不僅有 C

    而已,還包括 C++ 程式語言,有關 reference 的主題。其實不僅 C++ 使

    用 reference 的概念,Java、C# 以及 Python 也是以 reference 達到和 C 語

    言的指標之功能。所以本書也加入 Java、C# 以及 Python 這三種程式語

    言相關議題的討論。

    我們從 C 語言的指標與其息息相關的記憶體基本概念,開始展開這

    一次的快樂探險旅程,其中會經過:指標與變數、指標與陣列、指標與

    函數、指標與字串、指標與結構、鏈結串列、二元搜尋樹、指標與檔案

    處理等刺激關卡,希望能有效導引讀者進入指標的深層世界。同時也會

    以鏈結串列、二元搜尋樹,檔案的處理等重要的主題,加以應用於本書

    所談的其它程式語言,從而驗證指標與 reference 的相似功能。

  • 序 言 iii

    本書的共有六篇,前五篇分別是當今很紅的程式語言,分別是 C、

    C++、Java、Visual C# 及 Python。第六篇是各種程式語言比較篇,將 C、

    C++、Java、Visual C# 及 Python 等五種程式語言,在程式語言基本架構

    上的主題做比較,希望此篇可以讓您一窺程式語言之美。

    撰寫本書的心情不知為什麼,雖然很辛苦,但覺得好愉快,因為我

    的期望快實現了,因為您可以從本書輕輕鬆鬆的了解指標的精髓,進而

    加以應用,並告訴我,「指標是天上閃亮的星星」。

    擁護我的讀者不在少數,有的在國內,有的在大陸,因此,常常會

    聽到:「老師,我是看您的書長大的」。真的有一種使命感,要撰寫一

    些進階有參考價值的書,本書完成了,真誠的希望您可以從書中獲取一

    些知識,做為您的一技之長。再一次的謝謝您的一路的相陪、支持與鼓

    勵,讓我有動力再出發,感恩。

    [email protected]

  • 指標與函數

    04

    4-1 函數初探

    4-2 兩數對調

    4-3 再論傳址呼叫

    4-4 指向函數的指標

    4-5 傳回指標的函數

    4-6 除錯題

    4-7 問題演練

    4-8 程式實作

  • Part 1 C 程式語言篇 80

    4-1 函數初探

    函數 (function)是執行某一任務的片段程式。它的好處是使程式模組化、

    重複使用、降低維護成本等功能。函數可分為兩種,一為庫存函數 (library

    function),它是編譯器所提供的,二為使用者自定函數 (user-defined

    function),它是由使用者撰寫的。本章所討論的是後者。

    函數的呼叫可分為傳值呼叫 (call by value)與傳址呼叫 (call by address)。

    傳值呼叫表示實際參數傳給形式參數的是值 (value),而傳址呼叫則是傳

    位址 (adddress)。

    我們先從一簡單的範例談起,請參閱範例程式 functionCall-5。

    範例程式:functionCall-5

    1 2 3 4 5 6 7 8 9

    10 11 12 13 14 15 16 17 18 19 20 21

    /* functionCall-5.c*/ #include double calAverage(double, double); int main() { double x, y, aver; printf("請輸入 x與 y的浮點數: ");

    scanf("%lf %lf", &x, &y); aver = calAverage(x, y); printf("x與 y的平均數為: %.2f\n", aver);

    getchar(); return 0; } double calAverage(double a, double b) { double average; average=(a+b)/2; return average; }

  • Chapter 4 指標與函數 81

    輸出結果

    請輸入 x與 y的浮點數: 12.34 45.67 x與 y的平均數為: 29.01

    程式將實際參數 x 與 y 傳送給 calAverage 函數的形式參數 a 與 b,計算

    此兩數的平均數,並指定給 average 變數後,回傳給主程式的 aver,最後

    將它輸出。注意!由於 calAverage 函數的資料型態是 double,所以函數

    的回傳值 average 的資料型態也必須是 double。由於 aver 變數用於接收

    average 變數值,所以 aver 的資料型態也必須是 double。由於實際參數傳

    給形式參數是變數值,所以稱此為傳值呼叫。

    4-2 兩數對調

    指標到底有什好處呢?我們從兩數對調的運作方式談起,請參閱範例程

    式 swapType。

    範例程式:swapType

    1 2 3 4 5 6 7 8 9

    10 11 12 13 14 15 16 17 18

    /* swapType.c */ #include void swap_by_address(int *, int *); void swap_by_value(int, int); int main() { int x=100, y=200; /* Call by value */ printf("Call by value\n"); printf("Before swapping...\n"); printf("x=%d, y=%d\n", x, y); swap_by_value(x, y); printf("After swapping...\n"); printf("x=%d, y=%d\n\n", x, y); /* Call by address */ x = 100; y = 200;

  • Part 1 C 程式語言篇 82

    19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44

    printf("Call by address\n"); printf("Before swapping...\n"); printf("x=%d, y=%d\n", x, y); swap_by_address(&x, &y); printf("After swapping...\n"); printf("x=%d, y=%d\n\n", x, y); getchar(); return 0; } void swap_by_value(int a, int b) { int temp; temp = a; a = b; b = temp; } void swap_by_address(int *a, int *b) { int temp; temp = *a; *a = *b; *b = temp; }

    輸出結果

    Call by value Before swapping... x=100, y=200 After swapping... x=100, y=200 Call by address Before swapping... x=100, y=200 After swapping... x=200, y=100

  • Chapter 4 指標與函數 83

    從輸出結果得知,利用傳值呼叫無法達到兩數對調的效果,但利用傳址

    呼叫就可以。swap_by_value()為傳值呼叫函數,如圖 4-1 所示:

    x 10

    y 20

    a 10

    b 20

    temp

    temp = a; a = b; b = temp;

    圖 4-1 傳值呼叫

    而 swap_by_address()為傳址呼叫函數,如圖 4-2 所示:

    x 10

    20

    a

    b

    temp

    temp = *a; *a = *b; *b = temp;

    &x

    &y

    圖 4-2 傳址呼叫

    4-3 再論傳址呼叫

    接下來,討論一些常以傳址方式運作的範例。

    4-3-1 找尋陣列中的最大值

    首先討論如何呼叫一函數,尋找出陣列的最大值,如範例程式 findMax

    所示。

  • Part 1 C 程式語言篇 84

    範例程式: findMax 1 2 3 4 5 6 7 8 9

    10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

    /* findMax.c*/ #include int findmax(int [], int); int main() { int i; int arr[] = {20, 10, 100, 40, 60, 80, 90}; int elements = sizeof(arr)/sizeof(arr[0]); int maxNumber = findmax(arr, elements); printf("Max( "); for(i=0; i

  • Chapter 4 指標與函數 85

    int findmax(int *, int n); /* 函數語法宣告 */ … int findmax(int *x, int n) /* 函數定義 */ { int j; int max = *x; for(j=1; j max) max = *(x+j); return max; }

    上述兩種寫法都可以。因為 [] 和 * 都是指標。我個人比較喜歡使用 *

    的方式,因為它好像天上一顆閃亮的星星。

    4-3-2 加總一維陣列的元素

    接下來,討論如何呼叫一函數,以計算一維陣列元素的總和,如範例程

    式 sumOfArray1-1 所示。

    範例程式:sumOfArray1-1

    1 2 3 4 5 6 7 8 9

    10 11 12 13 14 15 16 17 18 19

    /* sumOfArray1-1.c */ #include int sum(int [], int); int main() { int i; int arr[] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100}; int elements = sizeof(arr)/sizeof(arr[0]); int total = sum(arr, elements); printf("Sum( "); for(i=0; i

  • Part 1 C 程式語言篇 86

    20 21 22 23 24 25 26

    int sum(int x[], int n) { int j, t = 0; for(j=0; j

  • Chapter 4 指標與函數 87

    輸出結果

    Sum( 10 20 30 40 50 60 70 80 90 100 ) is 550

    從輸出結果驗證,使用

    t += x[j];

    t += *(x+j);

    是一樣的。

    4-3-3 加總二維陣列的元素

    以上是傳送一維陣列,那二維陣列應如何傳送呢?我們來探討如何呼叫

    一函數,以計算二維陣列的總和,請參閱範例程式 sumOfArray2-1。

    範例程式:sumOfArray2-1

    1 2 3 4 5 6 7 8 9

    10 11 12 13 14 15 16 17 18 19 20

    /* sumOfArray2-1.c */ #include int sum(int [][2], int, int); int main() { int i, j, row, column, total=0; int arr2[][2] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100}; int elements = sizeof(arr2)/sizeof(arr2[0][0]); row = elements / 2; column = 2; total = sum(arr2, row, column); printf("There are %d elements in the array\n", elements); printf("Sum(\n"); for(i=0; i

  • Part 1 C 程式語言篇 88

    21 22 23 24 25 26 27 28 29 30 31 32 33 34

    printf(") is %d\n", total); getchar(); return 0; } int sum(int p2[][2], int n, int m) { int i, j, t = 0; for(i=0; i

  • Chapter 4 指標與函數 89

    13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

    total = sum(arr2, row); printf("There are %d elements in the array\n", elements); printf("Sum(\n"); for(i=0; i

  • Part 1 C 程式語言篇 90

    4-4 指向函數的指標

    指向函數的指標 (pointer to function)相信有些人從未用過,或根本沒聽

    過,指標怎麼會和函數有關連呢?指向指標的函數可使得程式在應用上

    顯得更為靈活,當您要使用哪一函數,只要將指標指向此函數即可,其

    實就是將函數名稱設定給一指標,因為函數名稱是一位址(當然是在記

    憶體的位址),在未進入此主題時,讀者須分辨

    int *pf(int); -

    int (*pf)(int); -

    是不一樣的,其中表示 pf 是一函數,此函數有一參數為 int 型態,且會

    回傳一指向 int 的指標,下一節將詳加討論。而為 pf 是一指向某一函數

    的指標,此函數最後回傳 int 值。

    在處理指向函數指標時,有一些需要特別注意的地方:

    int add(int, int); int (*pf)(int, int); pf = add;

    因為 add 函數有 2 個參數,故 pf 指標也要有 2 個參數才能匹配。以下是

    不正確的使用方法:

    double add1(double, double); int add2(int); int (*pf)(int, int); pf = add1; /* 錯,因為兩者函數之參數的資料型態不同 */ pf = add2; /* 錯,因為兩者函數之參數的個數不同 */

    有了以上的認知後,咱們來研究範例程式 pointerToFunction 做了什麼事。

    範例程式:pointerToFunction

    1 2 3 4

    /* pointerToFunction.c */ #include int add(int, int); int substract(int, int);

  • Chapter 4 指標與函數 91

    5 6 7 8 9

    10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

    int multiply(int, int); int divide(int, int); int (*operation)(int, int); int main() { int x, y, output; printf("請輸入 x 與 y: ");

    scanf("%d %d", &x, &y); operation = add; output = (*operation)(x, y); printf("%d + %d = %d\n", x, y, output); operation = substract; output = (*operation)(x, y); printf("%d - %d = %d\n", x, y, output); operation = multiply; output = (*operation)(x, y); printf("%d * %d = %d\n", x, y, output); operation = divide; output = (*operation)(x, y); printf("%d / %d = %d\n", x, y, output); getchar(); return 0; } int add(int a, int b) { return a+b; } int sub(int a, int b) { return a-b; } int multiply(int a, int b) {

  • Part 1 C 程式語言篇 92

    46 47 48 49 50 51 52

    return a*b; } int divide(int a, int b) { return a/b; }

    輸出結果

    請輸入 x 與 y: 100 2 100 + 2 = 102 100 – 2 = 98 100 * 2 = 200 100 / 2 = 50

    程式中有四個函數分別處理加 (add)、減 (sub)、乘 (multiply) 、除 (divide),

    同時也宣告一指向函數的指標 operation,當要處理加法時,則將 add 指

    定給 operation。同理,要處理乘法,也只要將 multiply 指定給 operation

    便可,以此類推。程式要求使用者輸入二個數值 x、y,並將其結果存放

    於 output。

    4-5 傳回指標的函數

    前面曾提及

    int *pf(int);

    它是一回傳指標的函數 (function returns a pointer),其表示 pf 是一函

    數,此函數有一個參數,而且會回傳一指向 int 的指標。請參閱範例程式

    functionRetPointer。

    範例程式:functionRetPointer

    1 2 3 4

    /* funtionRetPointer.c */ #include int *pf(int [], int); #define MAX 5

  • Chapter 4 指標與函數 93

    5 6 7 8 9

    10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

    int k[MAX]; int main() { int total=0, n; int i[MAX] = {10, 20, 30, 40, 50}; int *ptr; ptr = pf(i, MAX); printf("ptr = %#x\n\n", ptr); printf("Sum of (\n"); for(n=0; n

  • Part 1 C 程式語言篇 94

    ptr = 0x1030 Sum of ( k[0] = 110 k[1] = 220 k[2] = 330 k[3] = 440 k[4] = 550 ) is 1650

    程式中有一函數的原型宣告,如下所示:

    int *pf(int [], int);

    此表示 pf 是一函數,有兩個參數,而且會回傳一指向 int 的指標。程式中

    的 k 是陣列名稱,表示陣列第一個元素的位址。當 pf 函數結束時,將回

    傳 k 給 main 函數的 ptr 指標變數,使得 ptr 是一指向 k 陣列第一個元素的

    位址,亦即 ptr 等同於 &k[0] (從輸出結果得知為 0x1030)。

  • Chapter 4 指標與函數 95

    4-6 除錯題

    小蔡老師出了以下程式,要請大家一起來 Debug。

    1. /* sumOfArrayBugs-1.c */ #include int sum(int *, int); int main() { int i; int arr[] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100}; int elements = sizeof(arr)/sizeof(arr[0]); int total = sum(arr, elements); printf("Sum("); for(i=0; i

  • Part 1 C 程式語言篇 96

    for(i=0; i

  • Chapter 4 指標與函數 97

    for(i=0; i

  • Part 1 C 程式語言篇 98

    5. /* functionRetPointer-bugs.c */ #include int *pf(int [], int); #define MAX 5 int main() { int total = 0, k; int i[MAX] = {10, 20, 30, 40, 50}; int *ptr; ptr = pf(i, MAX); printf("ptr=%p\n\n", ptr); printf("Sum of (\n"); for(k=0; k

  • Chapter 4 指標與函數 99

    4-7 問題演練

    1. 試申述下列敘述的意義:

    (a) int (*p)(int);

    (b) int *p(int);

    4-8 程式實作

    1. 請先定義一含有十個資料的一維陣列,再利用傳址方式將此陣列, (1)

    傳給 input 函數,以便輸入資料, (2)傳給 total 函數,計算此陣列元素

    的和,之後將總和回傳。

    2. 請先定義一含有八個資料的二維陣列 (2 列 4 行 ),再利用傳址方式將此

    陣列,(1)傳給 input 函數,以便輸入資料,(2)傳給 total2 函數,計算此

    陣列元素的和,之後將總和回傳。

    3. 撰寫兩種排序的方法,如氣泡排序與插入排序,之後以 4-4 節所論及指

    向函數的指標之方法,撰寫一主程式分別呼叫這兩個函數。

    4. 試以第三章的命令列引數,在命令列提示字元的模式下,將一陣列的

    資料以氣泡排序加以排序之。

    sort –r 或 sort -a

    r 表示以降幕(由大至小)的方式排序之,a 則表示以升幕(由小至大)

    的方式排序之。

    ACL050000_04.pdf04指標與函數4-1  函數初探4-2  兩數對調4-3  再論傳址呼叫4-4  指向函數的指標4-5  傳回指標的函數4-6  除錯題4-7  問題演練4-8  程式實作

    /ColorImageDict > /JPEG2000ColorACSImageDict > /JPEG2000ColorImageDict > /AntiAliasGrayImages false /CropGrayImages true /GrayImageMinResolution 300 /GrayImageMinResolutionPolicy /OK /DownsampleGrayImages true /GrayImageDownsampleType /Bicubic /GrayImageResolution 300 /GrayImageDepth -1 /GrayImageMinDownsampleDepth 2 /GrayImageDownsampleThreshold 1.50000 /EncodeGrayImages true /GrayImageFilter /DCTEncode /AutoFilterGrayImages true /GrayImageAutoFilterStrategy /JPEG /GrayACSImageDict > /GrayImageDict > /JPEG2000GrayACSImageDict > /JPEG2000GrayImageDict > /AntiAliasMonoImages false /CropMonoImages true /MonoImageMinResolution 1200 /MonoImageMinResolutionPolicy /OK /DownsampleMonoImages true /MonoImageDownsampleType /Bicubic /MonoImageResolution 1200 /MonoImageDepth -1 /MonoImageDownsampleThreshold 1.50000 /EncodeMonoImages true /MonoImageFilter /CCITTFaxEncode /MonoImageDict > /AllowPSXObjects false /CheckCompliance [ /None ] /PDFX1aCheck false /PDFX3Check false /PDFXCompliantPDFOnly false /PDFXNoTrimBoxError true /PDFXTrimBoxToMediaBoxOffset [ 0.00000 0.00000 0.00000 0.00000 ] /PDFXSetBleedBoxToMediaBox true /PDFXBleedBoxToTrimBoxOffset [ 0.00000 0.00000 0.00000 0.00000 ] /PDFXOutputIntentProfile (None) /PDFXOutputConditionIdentifier () /PDFXOutputCondition () /PDFXRegistryName () /PDFXTrapped /False

    /CreateJDFFile false /Description > /Namespace [ (Adobe) (Common) (1.0) ] /OtherNamespaces [ > /FormElements false /GenerateStructure true /IncludeBookmarks false /IncludeHyperlinks false /IncludeInteractive false /IncludeLayers false /IncludeProfiles true /MultimediaHandling /UseObjectSettings /Namespace [ (Adobe) (CreativeSuite) (2.0) ] /PDFXOutputIntentProfileSelector /NA /PreserveEditing true /UntaggedCMYKHandling /LeaveUntagged /UntaggedRGBHandling /LeaveUntagged /UseDocumentBleed false >> ]>> setdistillerparams> setpagedevice