C教學

34
現現 ,現現現現現現現現 , 現現 Compiler 現現現現現現現現現現現現,現現現現現現,現現現現現現現現 ,現現現現現現現現現 現現現現 ,現現現現現現現現現現現現現, 現 現現現現: 現現現現現現現現現現現現現現現,現現現現現現現現現現現現現現現現現 現現現現現現現現現現現(現現現), 現現現現 ,現現現現現現 現現 現現現現現現現現現現(type),現現現現現現現現現現現現現現現現現現,現現現現現現現現 現現現現現現現現現現現現現現現現現現現現, 現現現現現現現現現, 現現現現現現現現現, 現現 C 現現現現現現現現, 現現 =(assigement)現現現現現現現現現現現現現現現現現, 現現現現現現現現現現現"現現現現現"現現現現現"現現現現現"現 >現現現現, 現現 現現現現>現現現現現現 , 現現現現現現現現現, 現現現現現現現現現現現, 現現現現現現現現現現, 現現現現現現現現現現現現現現現現現現現,現 現現,現現,現現現現現現現現現現現現現現 運運運運運運運運運運運運運運運運運運運運運運運運運,運 運運運運(Data Type) C 現現現現現現現現現現現現 現現 現現現現 現現現現 現現現現 現現現現 現現 16 現 32 int -2147483648 ~ 2147483647 8 char -128 ~ 127 16 short -32768 ~ 32767 32 long -2147483648 ~ 2147483647 64 long long 16 現 32 unsigned int 0 ~ 4294967295 8 unsigned char 0 ~ 256 16 unsigned short 0 ~ 65535 32 unsigned long 0 ~ 4294967295 64 unsigned long long 現現現 32 float 10 ^-38 ~10 ^38 64 double 10 ^-308 ~10 ^308 現現 8 char -128 ~ 127 現現現現現現現現現現,現現現現現現 IEEE 現現現,現現現現現現現現現現現現 char,現 C 現現現現,現現 ASCII code 現現現現現現現現現現現現 type varname; type varname = constant; // 現現現現現 initial value 1

Transcript of C教學

Page 1: C教學

現在的數位電腦幾乎都以二進位形式儲存資料。如果事先不知道二進位的編碼意義,則無法解釋這些二進位資料的意義。因此我們必須透過程式語言定義的資料型別, 告訴 Compiler 變數內所存放的資料有多大,如何去解釋它,如何翻譯含有該變數的運算指令等問題。只要是資料,不論是存放在主記憶

體內的變數,或是存放於暫存器的運算結果, 都有型別。而型別和運算符號間的關係如下:

運算符號只能作用於規定的型別上,並非所有的型別和運算符號組合都合法。 若運算符號需要兩筆資料(如乘法), 則這兩筆資料必須是同一個型別,運算的結果也是同一個

型別。 我們可在資料前面加上(type),也就是以圓括弧將型別名稱括起來的形式,將該資料強迫轉型。

由於我們常常會把不同型態的數值做算數運算, 如把整數加上浮點數, 或短整數加上長整數, 因此 C 語言為了方便起見, 除了=(assigement)會將右側的型別轉成左側變數的型別外, 在做算數

運算時會自動將"型別比較小"的數值轉成"型別比較大"的數值。其中浮點數型別>整數型別, 位元長度長的>位元長度短的。值得注意的是, 這類的型別自動轉換, 不同程式語言的規則不同, 且會破壞

型別檢查機制, 容易讓程式撰寫者發生不容易被發現的錯誤,因此能免則免。

變數,常數,以及運算符號可以組合成運算式。運算式執行完畢後會在暫存器上留下該運算式的執行結果,此結果也具有型別。

資料型別(Data Type)

C 語言所定義的資料型別如下

型別 符號位元 位元長度 表示方法 數值範圍

整數

16 或 32 int-2147483648 ~

21474836478 char -128 ~ 12716 short -32768 ~ 32767

32 long-2147483648 ~

214748364764 long long

16 或 32 unsigned int 0 ~ 4294967295

8 unsigned char 0 ~ 25616 unsigned short 0 ~ 6553532 unsigned long 0 ~ 4294967295

64unsigned long

long

浮點數 有32 float 10^-38~10^38

64 double 10^-308~10^308

字元 有 8 char -128 ~ 127

1

Page 2: C教學

以上的整數使用二補數,浮點數則採用 IEEE 的標準,相關資料請見電腦系統概論。至於 char,對 C 語言來說,採用 ASCII code 的編碼方式。

變數的宣告是以

type varname;type varname = constant; // 給定初始值 initial value

的形式來宣告 varname 的型態。常數的部分,其表示法為

123 表示十進位的 int

0123 表示八進位的 int

0x123 表示十六進位的 int

123L 表示十進位的 long

123.0 表示十進位的 float

123.0L 表示十進位的 double

123.0e-8 表示 123*10-8 的 float

'a'表示 char

'\n'表示 char 的換行符號(new line)

'\t'表示 char 的 Tab

'\b'表示 char 的 Backsapce

'\r'表示 Carriage Return

'\f'表示 Line Feed

'\\'表示\

'\''表示'

變數宣告的前面也可以加上 const 這個保留字,以表示此變數只能在宣告時給定初始值,以後就不能再改了

const double PI = 3.14159;

2

Page 3: C教學

運算符號(Operator)

算術(Arithmetic)運算符號

運算符號 功能敘述 運算符號 功能敘述+ 加 * 乘- 減 / 除% 餘數    

++ 加一 -- 減一

以上除了%只能用於整數外,其餘符號不論整數或浮點數均可使用。++和--是所謂的 unary operator,只能用於變數,可放在變數的前或後面,意義如下

x++ 此運算式的結果為 x 的數值,此外 x 變數的內容會加 1

++x 此運算式的結果為(x 的數值+1),此外 x 變數的內容會加 1

x-- 此運算式的結果為 x 的數值,此外 x 變數的內容會減 1

--x 此運算式的結果為(x 的數值-1),此外 x 變數的內容會減 1

邏輯(logic)運算符號

運算符號 功能敘述 運算符號 功能敘述> 大於 < 小於

>= 大於等於 <= 小於等於== 等於 != 不等於&& logic AND || logic OR! logic NOT

C 語言裡並沒有 boolean 資料型態(只有 true 和 false 兩種值),條件是否成立完全是看運算式的結果,若為 0 表示不成立,不為 0 表示成立。這一點設計得不好,很容易讓人犯錯,誤把邏輯運算寫成數值運算而不自覺。像 java 語言就提供了 boolean 資料型態,且邏輯運算和數值運算不能混用,以免程式

撰寫者犯錯。

AND 兩者為真才為真,其餘皆為假

OR 兩者為假才為假,其餘皆為真

位元(Bit)運算符號

運算符號 功能敘述 運算符號 功能敘述

3

Page 4: C教學

& bit AND << left bit shift| bit OR >> right bit shift

^ bit XOR ~ 1 的補數

所謂位元運算符號,其作用的大小是 bit,而不是整個數值。因此要了解這類符號的運算結果,必須先把數值轉成二進位(2 補數)。以下範例內的==>表示計算的過程, 且為說明起見, 假設這些數字為

8bits

2 & 3 ==> 00000010 & 00000011 ==> 00000010 ==> 相當於 2

2 & 1 ==> 00000010 & 00000001 ==> 00000000 ==> 相當於 0

2 | 1 ==> 00000010 |000000 01 ==> 00000011 ==> 相當於 3

XOR 表示兩者相同時為 0,兩者不同時為 1

2 ^ 3 ==> 00000010 ^ 00000011 ==> 00000001 ==> 相當於 1

~2 ==> ~00000010 ==> 11111101 ==> 相當於-3

left bit shift 是把每一個 bit 都向左移,右邊補 0

2 << 1 ==> 00000010 << 1 ==> 00000100 ==> 相當於 4

right bit shift 是把每一個 bit 都向右移,左邊補上最左邊的 bit

2 >> 1 ==> 00000010 >> 1 ==> 00000001 ==> 相當於 1

-3 >> 1 ==> 11111101 >> 1 ==> 11111110 ==> 相當於-2

指標(Pointer)陣列(Array)與函數(Function)運算符號

運算元 功能敘述 運算元 功能敘述

& 取變數的地址 ->透過結構指標取結構成

員數值* 透過指標取數值 [] 取陣列元素數值

FunctionName() 函數呼叫 . 結構變數.結構成員

以上運算符號的用法和意義,請見後面章節詳述

其他運算符號

運算元 功能敘述 運算元 功能敘述4

Page 5: C教學

=將右邊的值複製到左邊的

變數(type)

將右邊的數值轉換成 type型別

+=將右邊的數值加上左邊的數值然後指定給左邊的變

數?:

若?左邊成立則做:左邊否則做:右邊

,合併兩個運算視為一個敘

述sizeof(type)

傳回 type 所需要的 byte數

(運算式) 表示()內優先運算

= 將右邊的數值指定(assigement,也就是複製)給左邊的變數

+= 將將右邊的數值加上左邊的數值然後指定給左邊的變數,其餘如-= *= /= %= &= ^= |= <<= >>=的意義也都相同,只是第一個運算符號不同而已

, 將兩個運算式結合成一個,以後面運算式的數值作為結果。比較常見的用途是在 for(;;)迴圈,由於以分號分開的部分只能有一個敘述,若想放進兩個運算式,就可以使用","如下

for (i=0, j=2; i<n && j<m; i++, j++);

另一個,的用途是在變數宣告時,若要一個敘述內宣告好幾個變數可以用

int x, y, z;

c? p1: p2 相當於 if(c) p1 else p2 這個條件敘述的縮寫,範例如下

(x==0) ? 0 : 1/x;

 

運算優先權與結合順序

運算式可以由任意的運算符號,變數,以及常數所組成。而電腦執行程式時只能依序執行指令,因此運算式中運算符號的執行順序,必須定義得非常明確,編譯程式才能幫我們做正確的翻譯。程式語言中

有關定義運算符號的執行順序,是由優先權(Priority)和結合順序(Association)兩者構成的。優先權越高的越早執行,相同優先權的符號,則看其結合順序。若為左結合(Left Association)表示左邊的符號先做,若為右結合(Right Association)表示右邊的符號先做。下表是 C 語言定義的優先權和結合順序,上方的列的優先權高於下方的列,同一列的運算順序則由表最右側的結合順序決定。例如第三列乘法*的優先權高於第四列+法,而同為第三列的*/%則視其在運算式中的出現順序決定。

運算元結合順序

( ) [ ] -> .               左到右! ~ ++ -- + - * & (type sizeof    右到左

5

Page 6: C教學

) * / %                 左到右+ -                   左到右

<< >>                   左到右< <= > >=               左到右

== !=                   左到右&                     左到右^                     左到右|                     左到右

&&                     左到右||                     左到右?:                     右到左= += -= *= /= %= ^= |= <<= >>=   右到左,                     左到右

第二優先權裡的+-*&是所謂 unary operator,其作用的目標只有一個。此處+表示正號,-表示負號,*表示由指標取值,&表示取變數的地址。以下是幾個運算式計算的過程:

2+3+4*5 其中*為第三優先權左結合,+為第四優先權左結合,因此*先做,再來是左邊的+,最後中間的+,整個的執行過程

2+3+4*5==>2+3+20==>5+20==>25

所以最後的結果是"int 25"

2+3+4*5.0 此處要注意的是 5 變成了 5.0,也就是說這些常數並不是同一個型別,因此過程變成

2+3+4*5.0 ==> 2+3+4.0*5.0 ==> 2+3+20.0 ==> 5+20.0 ==> 5.0+20.0 ==> 25.0

最後結果為"float 25"

假設 int x; float y;

x = y = 0;因為=是右結合,因此右邊的=號先做,而 y 的型別是 float,常數 0 的型別是 int,因此過程如下

x = y = 0 ==> x = y = (float)0 ==> x = 0.0 ==> x = (int)0.0 ==> 0

最後結果為"int 0", 且 x 的內容為 0,y 變數的內容為 0.0

6

Page 7: C教學

最後強調上述範例的精神是,運算式最後的結果必然有型別,而且此結果是存放在 ALU 的暫存器內。雖然=運算符號會改變左側變數的內容,但整個運算過程的重點在於暫存器上的型態和大小。

假設 int x= 1;float y;

y = x++ ==> y = 1 ==> y = (float)1 ==>1.0

最後結果為"float 1.0", 且 x 變數的內容為 int 2,y 變數的內容為 float 1.0

範例

攝氏溫度轉華氏溫度

#include <stdio.h>int main() {

float degree = 100.0; printf("100C=%fF\n", degree*9.0/5.0+32.0);

}

華氏溫度轉攝氏溫度

#include <stdio.h>int main() {

float degree = 100.0; printf("100F=%fC\n", (degree-32.0)*5.0/9.0);

}

1+2+...+n 的總合

#include <stdio.h>int main() {

int n = 100; printf("1+2+...+%d = %d\n", n, n*(n+1)/2);

}

特別注意上述的運算式裡/2 要放到最後面,如果寫成 n/2*(n+1),從數學式子的角度看好像沒問題,但別忘了,binary operator 的兩邊必須是同樣型別的資料,而且計算的結果也是同樣的型別。因

此 n/2*(n+1)會先計算 n/2,如果 n 不能被 2 整除的話,那麼為了符合計算結果必須是整數的限制,則小數點的部份就會無條件捨去,使得計算的結果錯誤。下面的範例一樣要注意相同的問題。

12+22+...+n2 的總合

#include <stdio.h>int main() {

int n = 100; printf("1*1+2*2+...+%d*%d = %d\n", n, n, n*(n+1)*(2*n+1)/6);

}

7

Page 8: C教學

把浮點數四捨五入為整數

C 語言規定浮點數轉整數時,小數點部分無條件捨去。如果要達到浮點數四捨五入為整數的效果,可以使用下面的小技巧

#include <stdio.h>int round(float y) {

return (int)(y + 0.5);}

int main() { float x = 20.6;

printf("%f 四捨五入成為 %d\n", x, (int)(x+0.5)); printf("%f 四捨五入成為 %d\n", x, round(x));

}

8

Page 9: C教學

函數:

意思是指「有傳回值的副程式」,通常應用在一段程式重複出現多次,我們可以把它獨立成函數,以減少程式撰寫工作,易於維護,並增加程式的可讀性。

格式:

傳回值型態 函數名稱(參數 1,參數 2……)

{

函數的本體

}

傳回值型態:可以在函數名稱前加上型別宣告,其用來宣告其傳回值的資料型別。若無傳回值,則宣告其型別為 void。

函數名稱:開頭第一字元不可以是數字,其餘部份由英文字母、數字…構成。

參數:函數名稱後緊接著"( )"符號,不管是否有參數值,都一定要有此符號。

範例 1:

#include <stdio.h>int addFun(int x, int y) { return x + y;}void main() { int a = 10, b = 20, r; r = addFun(a, b); printf("%d + %d = %d \n", a, b, r);}

範例 2:

#include <stdio.h>void list(void) { printf("不傳參數、不傳回值\n");}int main( ) { list();}

範例 3:

#include <stdio.h>

9

Page 10: C教學

int add_Fun(void) { int a = 5, b = 10, z; z = a + b; return z;}int main() { int c; c = add_Fun(); printf("The result is %d \n", c);}

範例 4:

#include <stdio.h>void subtract_Fun(int i, int j) { print(" %d - %d = %d\n", i, j, i - j);}int main() { int a = 14, b = 15; subtract_Fun(a, b);}

範例 5:

#include <stdio.h>int multiply_Fun(int x, int y) { return x * y;}int main() { int a=2, b=19; printf("%d * %d = %d \n", a, b, multiply_Fun(a, b)); printf("%d\n", multiply_Fun(a, b) + multiply_Fun(2*a, 2*b)); printf("%d\n", multiply_Fun(multiply_Fun(a, b), b));}

為了讓 compiler 能夠正確編譯函數呼叫,在呼叫函數 A 前必須要先做定義函數 A,因此前面幾個範例內的函數都放在 main 的前面。某些時候由於遞迴呼叫或是其他原因,可能在定義函數 A 前就要用到該函數,則可以用以下的方法來解決:

#include <stdio.h>double multiply_Fun(double, double);int main() { double a=2, b=19, c; c = multiply_Fun(a, b); printf("%lf * %lf = %lf \n", a, b, c);}double multiply_Fun(double x, double y) { return x * y;}

10

Page 11: C教學

第二行只是定義 multiply_Fun 所需要的參數和傳回值。如果用到系統的函數庫,就要 include 相關的.h檔,其原因也是如此。讀者可以找找你所用開發環境內的 stdio.h 長甚麼樣子。

參數傳遞

C 語言定義參數傳遞的方式為"Call By Value",中文翻成傳值呼叫。其機制是將運算式的值複製到堆疊上(也就是說在堆疊上產生新的變數), 然後由被呼叫的函數在堆疊上存取該參數。這種做法的最大優點是,被呼叫者看不到呼叫者定義的區域變數,因此呼叫者不怕其變數被被呼叫者改掉。

void fun(int x, int y) { x = 5; y = 5;}void main() { int x = 0; int y = 0; fun(x, y); printf("%d %d\n", x, y);}

上述的程式執行後會在螢幕上印出兩個 0,而不是兩個 5。fun(int x, int y)所宣告的 x,y 和 main 裡面所宣告的 x,y 是完全不同的變數。fun 只能看到自己的 x,y 不能看到main 裡面的 x,y。呼叫fun(x, y)時,這裡的 x,y 不是變數,而是兩個運算式,這兩個運算式就是 LOAD x 和 LOAD y 兩個機器指令。因此 Call By Value 所傳遞的不是變數的地址,而是該運算式的計算結果。以下範例就更加清楚了:

void fun(int x, int y) { x = 5; y = 5;}void main() { int x = 0; int y = 0; fun(x + 1, y + 1); printf("%d %d\n", x, y);}

fun(x + 1, y + 1)裡運算式 x + 1 不會有人把它當成變數吧,既然如此 fun(x, y)裡的 x 也一樣不是變數而是運算式。

變數的範圍

討論變數的範圍時,我們必須了解變數的兩項特質

可見範圍(Where): 對 C 語言來說就是哪些函數可以看到該變數。 存在時間(When): 對 C 語言來說就是程式執行,或函數呼叫期間。

11

Page 12: C教學

如果變數定義於函數之外,如

int x;int main() {}則該變數

Where: 程式內所有的函數都可以存取。 When: 整個程式執行期間均存在。

如果變數定義於函數內,如

int main() { int x;}則該變數

Where: 只有定義該變數的函數(此處為 main)可以存取 When: 呼叫該函數時產生,離開該函數就消滅。

由於函數可以遞迴呼叫,因此函數內的變數消滅前可能會呼叫自己而再產生新的變數。此類變數稱為local variable 或 auto variable,Compiler 會把它放在堆疊上。

如果函數可以看到兩個以上同名的變數,則運算式內的變數會以位置最近的為準,所謂位置指的是最接近的{}

double x = 3.14;int main() { int x = 0; x = 100; // 此處的 x 是指上一行的整數 x}

C 語言有兩個關鍵字 static 和 extern 可用來改變變數的存取範圍

static int x;int main() {}

Where: 此 C檔案內的函數可看到 x,其他的原始程式檔都看不到 When: 整個程式執行期間均存在。

int main() { static int x;}

Where: 只有定義該變數的函數(此處為 main)可以存取 When: 整個程式執行期間均存在。

12

Page 13: C教學

extern 則表示此變數是在別的檔案內宣告(給空間),此處只是要讓 Compiler 能夠翻譯相關的運算式,但在此處並不分配空間。這種用法主要在大型專案管理中,有許多程式設計人員撰寫程式,需要用到共同的全域變數,如果每個人定義一次,則 Linker 會抱怨該變數宣告了一次以上。若都加上 static,雖然可以產生執行檔,但實際上大家用的不是同一個變數。正確的做法是:

global.h檔內定義extern int x;global.c 內定義int x;其他人的.c檔內#include "global.h"

巨集(Macro)

C 的前置處理器(Preprocessor)有一個#define命令,可用來取代原始程式內的某些字串:

#define PI 3.14159main() { double r1 = 3.0L, r2 = 5.0L; printf("Circle(3) area = %lf", 2 * PI * r1 * r1); printf("Circle(5) area = %lf", 2 * PI * r2 * r2);}

就相當於將程式寫成

main() { double r1 = 3.0L, r2 = 5.0L; printf("Circle(3) area = %lf", 2 * 3.14159 * r1 * r1); printf("Circle(5) area = %lf", 2 * 3.14159 * r2 * r2);}

define 不但可以做簡單的字串取代,還可以加上參數以完成複雜的字串取代工作

#define max(A, B) ((A) > (B) ? (A) : (B))main() { int x, p=3, q=5, r=2, s=7; x = max(p+q, r+s);}

上述程式相當於

main() { int x, p=3, q=5, r=2, s=7; x = ((p+q) > (r+s) ? (p+q) : (r+s));}

13

Page 14: C教學

這種巨集是由 preprocessor 透過字串取來達成的,和正常的函數呼叫完全不同。在下面的例子中,如果不知道 square 是巨集的話,就會搞不清楚為何跑出來的結果不正確了

#define square(x) x * xmain() { int z = 3; printf("%d\n", square(z + 1));}

印出來是 7 而不是 16喔

遞迴:

意思是重覆呼叫執行自己本身的程式片段,直到符合終止條件為止。撰寫遞迴程式的精神是

要先知道邊際條件(最簡單情形)的解法。 定義函數的參數和傳回值。 如何將大小為 n 的問題以更小的問題來解答。 在做遞迴呼叫前,別忘了先檢查邊際條件,以免造成無窮呼叫下去的情況。

求 1+2+3+...+n

解析

邊際條件是 n=1 時,總合為 1 該函數可定成 int sum(int n) sum(n) = n + sum(n - 1)

int sum(int n) { if (n == 1) { return 1; } return n + sum(n - 1);}

算 1*2+2*3+3*4+…+(n-1)*n 之和

/* 程式功能: 用遞迴求算 1*2+2*3+3*4+…+(n-1)*n之和 */#include int sum(int n);void main() { int n; printf("Input the number n: "); scanf("%d",&n); printf("1*2+2*3+3*4+...+(n-1)*n=%d", sum(n));}

14

Page 15: C教學

int sum(int n) { if (n == 1) { return 0; } else { return sum(n-1)+n*(n-1); }}

輸入兩數字 A, B,利用遞迴求得 A 的 B 次方

#include <stdio.h>power(int a, int b);void main(void) { int x, y; printf("Please input two number:"); scanf("%d %d", &x, &y); printf("\n%d^%d = %d", x, y, power(x, y));}int power(int a, int b) { switch(b) { case 0: return 1; case 1: return a; default: return (a * power(a, b - 1)); }}

兩個整數 m,n 的最大公因數

解析

如果 n==0,則最大公因數為 m 如果 n 不等於 0,則最大公因數為 gcd(m,n)==gcd(n, m%n)

int gcd(int m, int n) { if (n == 0) { return m; } return gcd(n, m % n);}

費式數列

解析

費氏數列的定義為 Fn=n, if n<= 1 Fn=Fn-1+Fn-2, if n > 1。 因此遞迴的邊際條件(最簡單情況為)0,1。

int fab(int num) {

15

Page 16: C教學

if (num <= 1) { return num; } return fab(num - 1) + fab(num - 2);}

Ackerman 函數

A(m, n)定義為

1. n+1, if m = 0 2. A(m-1, 1), if n = 0 3. A(m-1, A(m, n-1)), otherwise

怎麼寫?

河內塔

在河內塔的問題中,有三根柱子,n 個大小不一樣的碟子,一開始時所有 n 個碟子以大下小上排在某根柱子上。在一次只能移動一個碟子,且不違反大下小上的原則下,如何把這 n 個碟子全部移到另一根柱子上。

再次回憶遞迴的要點

已知邊際條件(最簡單的情況)如何解 假設某函數已經能解大小為 n 的問題,也就是要決定此函數的參數和傳回值。 如何利用大小為 n 的解法,來解更大的問題。

解析

當河內塔的碟子數為 0 時,問題已解(沒東西好搬了)。 假設三根柱子以 int from,to,another 來表示,碟子數為 n 時要這些碟子從 from柱子搬到 to柱子,解此問題的函數為 move(n,from,to,another)。 如何解 n+1 個碟子的問題呢? o 我們可以把 n 個碟子由 from搬到 another, o 把最底下的由 from搬到 to o 把 n 個碟子由 another搬到 to 為甚麼上面的搬法沒問題?當我們搬上面 n 個碟子時,留在 from柱子上的是最大的一個碟子,因此不論我們如何搬動 n 個碟子,一定不會違反規則,也就是說可以把最底下的當作不存在,好像地板一樣。

/** * 河內塔的解法 */#include <stdio.h>

16

Page 17: C教學

void move(int n, int from, int to, int another) { if (n > 0) { // 記得遞迴程式要先寫邊際條件才能作遞迴呼叫 move(n - 1, from, another, to); printf("move %d to %d\n", from, to); // 以印出訊息代表搬動的過程 move(n - 1, another, to, from); }}int main() { int n=0; // 一直讀入數字並執行 move,直到使用者輸入的數字小於等於 0 為止 while (scanf("%d",&n) != EOF && n > 0) { move(n,1,2,3); }}

17

Page 18: C教學

寫一程式輸入 5 個整數數字,計算其總合和平均。解析:

1. 需要 1 個變數儲存輸入的數字,稱此變數為 inputNum 好了 2. 需要 1 個變數紀錄到目前為止所有 inputNum 的總和,稱此變數為 sum,其初始值為 0 3. 以迴圈執行 5次,每次輸入數字到 inputNum,並把總和放到 sum,迴圈執行的次數以變數 i 來代表 4. 平均數為 sum/5

#include <stdio.h>int main { int i; // 紀錄迴圈執行次數 int inputNum; // 儲存目前輸入的數值 int sum = 0; // 儲存到目前為止的總和 for (i = 1; i <= 5; i++) { // 此迴圈執行 5次 printf("Please Input Number %d: ",i); // 提示使用者輸入第 i 個數字 scanf("%d", &inputNum); // 讀入整數到 inputNum sum = sum + inputNum; // 加總到 sum 變數 } printf("Sum is %d, average is %lf\n", sum, (double)sum / 5); // 輸出結果}

寫一函數輸入參數 int n,傳回 1+2+3...+n 的總合。解析:

1. 要想辦法拜訪 1,2,3...n 的每一個數字一次 2. 可用 for(i=1; i <= n; i++)的形式達成上述目標 3. 拜訪到這些數字時,就把它們加起來

int sum(int n) { int i; // 紀錄目前要處理的數字 int sum = 0; // 紀錄到目前為止的總和 for (i = 1; i <= n; i++) { sum = sum + i; } return sum;}

寫一函數輸入參數 int n,傳回 1+3+5...+n 的總合。解析:

1. 要想辦法拜訪 1,3,5...n 的每一個數字一次,也就是從 1開始每次加 2 2. 可用 for(i=1; i <= n; i+=2)的形式達成上述目標 3. 拜訪到這些數字時,就把它們加起來

int sum(int n) { int i; // 紀錄目前要處理的數字

18

Page 19: C教學

int sum = 0; for (i = 1; i <= n; i += 2) { // i+=2 讓 i往後面走 2 個 sum = sum + i; } return sum;}

寫一函數於螢幕上畫出九九乘法表。解析:

1. 總共有 i=1..9列 j=1..9 行,對第 i列第 j 行元素來說,其數值為 i*j

void nine() { int i, j; // i 表示第 i列(橫向),j 表示第 j 行(縱向) for (i = 1; i <= 9; i++) { for (j = 1; j <= 9; j++) { printf("%3d", i * j); } printf("\n"); }}

使用 printf 撰寫一函數輸入參數 int size,並在螢幕上印出正方形,size=3 的樣子如下

*********

解析

1. 螢幕上的游標只能由上而下,由左而右,無法回頭。 2. 此圖形共有 size列,每列有 size 個*,因此可用兩層迴圈來做。 3. 要讓一個敘述執行 size次,可用 for(i = 1; i <= size; i++)的形式來達成

/** * print out rectangle * @param size length of the rectangle */void print_rectangle(int size) { int i, j; // 第 i列,第 j 行 for (i = 1; i <= size; i++) { // 印出第 i列 for (j = 1; j <= size; j++) { // 第 i列有 size 個* printf("*"); } printf("\n"); }}

使用 printf 撰寫一函數輸入 int size,並在螢幕上印出斜一邊的三角形,size=3 的樣子如下

19

Page 20: C教學

******

解析

1. 螢幕上的游標只能由上而下,由左而右,無法回頭。 2. 此圖形共有 1到 size列,第 i列有 i 個*,因此可用兩層迴圈來做。

void print_right_triangle(int size) { int i, j; // 第 i列,第 j 行 for (i = 1; i <= size; i++) { // 印出第 i列 for (j = 1; j <= i; j++) { // 第 i列有 i 個* printf("*"); } printf("\n"); }}

使用 printf 撰寫一函數輸入 int size,並在螢幕上印出等腰的三角形,size=3 的樣子如下

* ********

解析

1. 總共有 1..size列,對第 i列而言,有 size-i 個空格,以及 2*i-1 個*

void print_equilateral_triangle(int size) { int i, j; for (i = 1; i <= size; i++) { // 印出第 i列 for (j = 1; j <= size-i; j++) { // 第 i列 有 size-i 個空格 printf(" "); } for (j = 1; j <= 2 * i - 1; j++) { // 以及 2*i-1 個* printf("*"); } printf("\n"); }}

上述問題的另一個想法是

1. 總共有 1..size列,對第 i列而言,總共有 size+i-1 個符號,其中 1..size-i 是空白,size-i+1..size+i-1 是*

20

Page 21: C教學

void print_equilateral_triangle(int size) { int i, j; for (i = 1; i <= size; i++) { // 印出第 i列 for (j = 1; j < size + i; j++) { // 每一列有 size+i-1 個符號 if (j <= size - i) { // 在 size-i 左邊(含)的符號是空白 printf(" "); } else { // 其他的是* printf("*"); } } printf("\n"); }}

使用 printf 撰寫一函數輸入 int size,並在螢幕上印出等腰的三角形,size=3 的樣子如下

* ******** *** *

解析

1. 這個形狀的上半部和前面一題是一模一樣的,下半部則是倒過來了,所謂倒過來的意思是,上半部是依 1..n 的順序列出,下半部則是依 n-1..1 的順序列出,因此可以寫成兩個雙重迴圈,且這兩個迴圈的內迴圈完全一樣,只是外面的改為 i 由 n-1..1

void print_diamond(int size) { int i, j; for (i = 1; i <= size; i++) { // 先印出上面的 size列 for (j = 1; j < size + i; j++) { if (j <= size - i) { printf(" "); } else { printf("*"); } } printf("\n"); } for (i = size - 1; i >= 1; i--) { // 在印出下面的 size-1列 for (j = 1; j < size + i; j++) { // 這部分和上半部是一樣的 if (j <= size - i) { printf(" "); } else { printf("*"); } }

21

Page 22: C教學

printf("\n"); }}

寫一函數求兩個整數的最大公因數,解析:

1. 此函數需要兩個參數 x,y 2. 當 y 不能整除 x 時,將 x 設成為 y,y 設為 x%y, 重複此步驟直到 x%y 為 0 3. 此時 y 就是這兩個數的最大公因數

int gcd(int x, int y) { int tmp; // 如果 x < y 則下面的迴圈執行第一次時就會交換 x,y 了 while (x % y != 0) { tmp = y; y = x % y; x = tmp; } return y;}

寫一函數求費氏數,解析:

1. F(n)=n, if n<=1; 2. F(n)=F(n-1)+F(n-2), otherwise 3. 可定義兩變數 fn_1,fn_2 表示最近兩個找出的費氏數 4. 下一個費氏數依定義為 fn_1 + fn_2 5. 找到最新的費氏數後,最近的兩個費氏數就變成了 fn_1+fn_2 以及 fn_1 6. 以變數 i紀錄目前要求的是哪一個費氏數 7. 以變數 tmp 作為更新最新兩個費氏數所需的記憶體空間

int fab(int n) { int fn_1 = 1, fn_2 = 0; // 紀錄最近找到的兩個費氏數 int i, tmp; // i 表示目前要找 F(i) if (n <= 1) return n; for (i = 2; i <= n; i++) { tmp = fn_1; // 先把 fn_1紀錄在 tmp fn_1 += fn_2; // 最新的費氏數是前面兩個相加 fn_2 = tmp; // 第二新的就是原先的 fn_1 } return fn_1;}

22

Page 23: C教學

struct 結構型態

{

欄項資料型態 欄項變數名稱;

欄項資料型態 欄項變數名稱;

欄項資料型態 欄項變數名稱;

   :     :

} 變數Ⅰ,變數Ⅱ……;

示意圖:

範例:

struct Student_PersonalData { char name[4]; int age; char address[30];} SP_Data;

應用範例:

#include <stdio.h>#include <string.h>void main() { struct Student_Perosnal_Data {

23

Page 24: C教學

char name[10]; int age; char address[50]; char interest[11]; } stu; strcpy(stu.name,"張三"); stu.age = 25; strcpy(stu.address, "南投縣埔里鎮大學路一號"); strcpy(stu.interest, "basketball"); printf("The student's name is: %s\n", stu.name); printf("The student's age is: %d\n", stu.age); printf("The student's address is: %s\n", stu.address); printf("The student's interest is: %s\n", stu.interest);}

上述的 struct Student_PersonalData 一經定義以後,就可以比照C 的內建資料型別來宣告和處理。

struct 內也可以其他的 struct

struct Student_Detail { int age; char *name; char *address;};struct Student_Data { int stuid; struct Student_Detail detail;};void main() { struct Student_Data x; x.stuid = 100; x.detail.age = 20; x.detail.name = "Johnson Lee"; x.detail.address = "Nation Chi Nan University";}

用於 struct 的運算符號

在如下的結構定義裡,next 前面的*不可省略,否則就遞迴定義了,Compiler 將無法決定 struct list的大小。

struct list { int data; struct list *next; // a pointer to struct list};

struct list listOne, listTwo, listThree;

listOne.next = &listTwo;

24

Page 25: C教學

listTwo.next = &listThree;// 以下想要由 listOne 設定到 listThree 的 datalistOne.next.next.data = 0; // 這不合法, 因為.的左邊必須是 struct,不可以是 pointer(*(*listOne.next).next).data = 0; // 這樣寫才對

你會發現上面的例子中, 如果 struct 裡面有 pointer to struct, 而我們想要用該 pointer 來存取結構成員時, 就必須很小心的用*和()來表達。由於結構成員包括指向結構的指標(define a pointer to struct in a struct), 是很常見的事情, 這樣的(*(*listOne.next).next).data 語法既難寫又難懂, 因此 C 語言定義了->運算符號。此符號的左邊是一個 pointer to struct, 右邊則是該 pointer 指到的結構成員。->為第一優先權左結合, 因此

(*(*listOne.next).next).data = 0; //這樣寫才對listOne.next->next->data = 0; // 這樣寫更漂亮

動態空間分配

所謂動態空間分配指的是,在執行期間由程式向作業系統或程式庫要求後才分配的空間,這塊記憶體區域稱為 Heap(堆積)。C 語言的動態空間分配主要透過 malloc 和 free 兩函數來處理。這兩個函數的宣告如下:

void *malloc(size_t size);void free(void *ptr);透過 malloc()所分配出來的空間必須由使用者呼叫 free()才能歸還給系統。初學者常犯的錯誤之一,就是忘了用 free()歸還空間,這會造成程式佔用太多記憶體,此現象稱為 memory leakage。相反的,如果空間已用 free()歸還了,卻還試著去使用那塊記憶體,則會發生 Segmentation Fault (core dumped)的錯誤。

Linked Stack

typedef struct items { int data; struct items *link;} ITEM;

typedef struct stack { ITEM *top;} STACK;

void initStack(STACK *s) { s->top = NULL;}

void pushStack(STACK *s, int y) { ITEM *x; // x will point to the new ITEM x = (ITEM *) malloc(sizeof(ITEM)); // allocate memory for the new ITEM x->data = y; // store data x->link = s->top; // x->link points to where s->top points s->top = x; // stack's top points to x

25

Page 26: C教學

}

int popStack(STACK *s) { ITEM * x = s->top;

int d = x->data; s->top = s->top->link; free(x); return d;}int stackIsEmpty(STACK *s) { return s->top == NULL;}void main() { STACK s; int i; initStack(&s); for (i = 1; i < 10; i++) { pushStack(&s, i); } while (!stackIsEmpty(&s)) { printf("%d\n", popStack(&s)); }}

Linked Queue

typedef struct items { int data; struct items *link; // points to next element} ITEM;

typedef struct queue { int size; ITEM *front, *rear;} QUEUE;

void initQueue(QUEUE *q) { q->size = 0; q->front = q->rear = NULL;}int queueIsEmpty(QUEUE *q) { return q->front == NULL;}int queueLength(QUEUE *q) { return q->size;}void addQueue(QUEUE *q, int y) { ITEM * x = (ITEM *) malloc(sizeof(ITEM)); x->data = y;

x->link = NULL; if (q->front == NULL)

26

Page 27: C教學

q->front = x; else q->rear->link = x; q->rear = x; q->size++;}int deleteQueue(QUEUE *q) { ITEM * x = q->front; int rel = x->data; q->front = x->link; if (q->front == NULL) q->rear = NULL; q->size--; free(x); return rel;}void main() { QUEUE q; int i; initQueue(&q); for (i = 1; i < 10; i++) { addQueue(&q, i); } while (!queueIsEmpty(&q)) { printf("%d\n", deleteQueue(&q)); }}

以下範例定義了矩陣結構,並透過動態空間分配的方式來做矩陣的加法和乘法

/** * Author: Shiuh-Sheng Yu * Department of Information Management * National Chi Nan University * Subject: 矩陣相加與相乘 * Toolkit: gcc * Modified Date:2002/08/20 */#include <stdio.h>// 以巨集(macro)定義矩陣元素和動態分配空間的對應關係// 所謂巨集指的是經由 preprocessor(前置處理器)取代原始檔內的字串#define M(x,i,j) *(x->data + i*x->col + j)

// 定義 MATRIX 為 struct matrix *// 也就是說 MATRIX之型態為 a pointer to struct matrix// 至於 struct 則是 C 語言讓使用者 "自訂型態" 的關鍵字typedef struct matrix { int row, col; double* data;

27

Page 28: C教學

} *MATRIX;

/** * 由檔案讀入一個矩陣 */MATRIX readMatrix(FILE* f) { int x, y, i, j; char keyword[256]; MATRIX m; /* read in keyword "matrix" */ fscanf(f, "%255s", keyword); if (strcmp(keyword,"matrix")!=0) { printf("keyword error: %s",keyword); return NULL; } // 動態分配一塊 struct matrix 大小的空間 m = (MATRIX) malloc(sizeof(struct matrix)); /* read in matrix dimension to x y */ fscanf(f,"%d", &x); fscanf(f,"%d", &y); m->row = x; m->col = y; m->data = (double*)malloc(x * y * sizeof(double)); /* read in x*y double and store them to m->data */ for (i = 0; i < x; i++) { for (j = 0; j < y; j++) { fscanf(f,"%lf",m->data + i*y + j); } } return m;}

/** * 印出矩陣的內容 */void printMatrix(MATRIX x) { int i, j; for (i = 0; i < x->row; i++) { for ( j= 0; j < x->col; j++) { printf("%lf", M(x,i,j)); } printf("\n"); }}/** * 矩陣相加 * 傳回一新矩陣為 x,y之和 */MATRIX addMatrix(MATRIX x, MATRIX y) { int i, j;

28

Page 29: C教學

MATRIX m; // 檢查兩矩陣的大小是否能相加 if ((x->row != y->row) || (x->col != y->col)) { printf("Matrix dimension mismatch.\n"); return NULL; } // 產生新矩陣所需的記憶體空間 m = (MATRIX) malloc(sizeof(struct matrix)); m->row = x->row; m->col = x->col; //產生存放資料所需的空間 m->data = (double*)malloc(m->row * m->col * sizeof(double)); // 進行矩陣的加法運算 for (i = 0; i < m->row; i++) { for (j = 0; j < m->col; j++) { M(m,i,j) = M(x,i,j) + M(y,i,j); // 使用 macro } } return m;}MATRIX multiplyMatrix(MATRIX x, MATRIX y) { /* 自己練習看看吧 */

}/** * 將動態分配矩陣的空間還給系統 */void freeMatrix(MATRIX x) { free(x->data); free(x);}

int main() { char buf[100]; MATRIX a, b, c; // 持續讀入運算符號 // stdin 定義於 stdio.h, 代表 standard input. 在沒有透過作業系統重新指定 // 的情形下, 一般為鍵盤 for (; fscanf(stdin,"%99s",buf) != EOF;) { if (buf[0] == '+') { if ((a = readMatrix(stdin)) == NULL) { break; // 有錯誤則跳離開最接近的迴圈或 switch 敘述(此處為 for迴圈) } printMatrix(a); if ((b = readMatrix(stdin)) == NULL) { break; } printf("+\n");

29

Page 30: C教學

printMatrix(b); printf("=\n"); if ((c = addMatrix(a, b)) == NULL) { break; } printMatrix(c); printf("\n"); freeMatrix(a); // 釋放動態分配的矩陣空間 freeMatrix(b); freeMatrix(c); } else if (buf[0]=='*') { /* 練習看看吧 */ } else { printf("Operator error\n"); break; } }}

說明:

C 語言對於字串的定義非常簡單,由 0 結尾的字元陣列就是字串。常數字串的寫法是由雙引號將字串的內容括起來, 以下的變數 p,q,r 都可當作字串:

int main() { char *p = "abc"; // 指標宣告, "abc"這個陣列會放在記憶體的"常數區塊",不允許被修改 char q[] = {'a','b','c',0}; // 陣列宣告,同時給予初始值 char r[] = "abc"; // 陣列宣告, 此空間就放在堆疊上 q[0] = '!'; // q 字串變成 "!bc" r[0] = '!'; // r 字串變成 "!bc" *p = '!'; // 這在 Unix 系統上會造成 Segmentation fault (core dumped)}

常見的字串操作函數

由於 C 語言對字串的定義非常簡單,因此需要一些函數幫我們操作字串。常見的範例如下列,每個範例又可能列出數種寫法,以幫助讀者熟悉指標和陣列的操作

字串長度

int strlen(char s[]) { int i; for (i = 0; s[i] != 0; i++) ; return i;}int strlen(char *s) { int i;

30

Page 31: C教學

for (i = 0; *s != 0; i++, s++) ; return i;}int strlen(char *s) { char *e; for (e = s; *e != 0: e++) ; return e - s;}

字串複製

void strcpy(char d[], char s[]) { int i; for (i = 0; s[i] != 0; i++) { d[i] = s[i]; } d[i] = 0;}void strcpy(char s[], char d[]) { int i; for (i = 0; d[i] = s[i]; i++) ;}void strcpy(char *s, char *d) { for (; *d = *s; s++, d++) ;}

字串相加(String Concatenation)

將"abc"加上"def"變成"abcdef"

\void strcat(char *s1, char *s2) { int i, j; for (i = strlen(s1), j = 0; s1[i] = s2[j]; i++, j++) ;}

字串比對

int strcmp(char *s1, char *s2) { for (; *s1 && *s1 == *s2; s1++, s2++) ; return *s1 - *s2;}

反轉字串(把"abcde"變成"edcba")

void reverse(char *s) { int i, j; char c; // 假設字串長度為 n,將 0 和 n-1調換,再換 1 和 n-2,直到字串中間為止

31

Page 32: C教學

for (i = 0, j = strlen(s) - 1; i < j; i++, j--) { c = s[i]; s[i] = s[j]; s[j] = c; }}

印出 base 進位系統的加法表和乘法表

假設大於 10 的數字以 A-Z 來代表 base 進位系統內不能有大於 base-1 的數字出現 必須有一函數可把整數 val,用 base 進位系統的數字表達出來

/** * 將 val 以 b 進位系統來表達,結果存放在字串 s 裡 */void convert(char s[], int val, int b) { int i = 0; int digit; while (val / b > 0) { digit = val % b; // 計算最後一位的大小 // 大於 10 的數字以 A-Z 來代表 s[i++] = (digit < 10) ? digit + '0' : digit - 10 + 'A'; val /= b; } s[i++] = (val < 10) ? val + '0' : val - 10 + 'A'; s[i] = 0; reverse(s);}int main() { char buf[5]; int i, j; int base = 10; printf("Please input base:"); scanf("%d", &base); // 印出 base 進位系統的加法表 printf("%d 進位系統的加法表:\n", base); for (i = 1; i < base; i++) { for (j = 1; j < base; j++) { convert(buf, i + j, base); printf("%3s", buf); } printf("\n"); } // 印出 base 進位系統的乘法表 printf("%d 進位系統的乘法表:\n", base); for (i = 1; i < base; i++) { for (j = 1; j < base; j++) {

32

Page 33: C教學

convert(buf, i * j, base); printf("%3s", buf); } printf("\n"); }}

注意空間分配問題

由於 C 語言的字串定義為以'\0'結尾的字元陣列, 因此設計人員必需小心處理陣列空間不足的問題. 以下面程式碼為例:

main() { int x = 0; char s1[5] = "abc"; int y = 0; char s2[] = "defghijklmno"; strcat(s1, s2); printf("x = %d, s1 = '%s', y = %d\n", x, s1, y);}請你試試執行的結果.

又如下面的例子, 如果使用者輸入的字串很長, 也會讓程式出現錯誤.

main() { int x = 0; char s1[5]; int y = 0; printf("Please input a string with it's length smaller than 5:"); scanf("%s", s1); printf("x = %d, s1 = %s, y = %d\n", x, s1, y);}解決此問題的方法是告訴 scanf 可以輸入的上限 main() { int x = 0; char[5] s1; int y = 0; printf("Please input a string with it's length smaller than 5:"); scanf("%4s", s1); printf("x = %d, s1 = %s, y = %d\n", x, s1, y);}

33