Try   HackMD

C 語言自學經典

本教學採用 HackerRank 以及 自編題目 作為課後練習系統


基礎輸入輸出

#include <stdio.h> int main(int argc, const char *argv[]) { printf("Hello World!\n"); return 0; }

輸出如下圖所示

$ gcc helloworld.c -o helloworld
$ ./helloworld
Hello World!
$

上述程式碼展示了基礎的 program Hello World
可以看到程式碼由2部分所組成

  • 標頭檔(Header file)
    • printf 函式實作定義在 stdio.h 這個檔案中

      stdio

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
      standard input/output

    • printf 這個函式主要的功能就是將引號內的字符打印到 標準輸出裝置(e.g. 螢幕)

      printf

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →
      print format

  • 主程式(Main function)
    • 主要程式邏輯
    • 程式執行順序是由上到下執行
    • return 0 表示程式完美執行結束

scanf 這個函式則是自 標準輸入裝置(e.g. 鍵盤) 讀取輸入

#include <stdio.h> int main(int argc, const char *argv[]) { int num; scanf("%d", &num); printf("%d\n", num); return 0; }

上述例子展示了如何從標準輸入裝置讀取一個整數到程式中並把它顯示在螢幕上
一開始必須要定義一個 變數(variable),用以儲存輸入的數字
值得注意的是,scanf 第二個參數在前面必須加一個 & 的符號,表示將數字寫入變數當中
%d 代表整數的意思,不同型態的變數需要使用對應的符號,以下舉幾個常見的例子

%d 整數
%f 單精度浮點數
%lf 雙精度浮點數

對於輸入的數字,我們可以對其進行四則運算,考慮以下例子

#include <stdio.h> int main(int argc, const char *argv[]) { int num, num2; scanf("%d", &num); num2 = num * 2; printf("%d\n", num2); return 0; }

輸出如下

$ gcc temp.c -o temp
$ ./temp
10
20
$

基礎條件判斷式

基礎語法為

if (expression) { dosomething }

expression 為 條件式,只有當條件式為 true 的時候,才會執行 dosomething
一般條件式會採用比較符號(e.g. 大於 小於 等於)來撰寫條件式
值得注意的是,等於的寫法與一般數學表示法不同
a=1 與 a==1 不同

  • a = 1
    • 將 a 賦值為 1
  • a == 1
    • 此為比較條件式,其意義為 a 有沒有等於 1

除此之外,若是要用到多個條件是一起判斷,要用到 And(&&) 以及 Or(||)
a & 1 與 e1 && e2 不同,or 也是同理

  • a & 1
    • bitwise 運算
  • e1 && e2
    • 此為條件式,其意義為 e1 以及 e2 皆是否為真
1 < 0 false
10 == 10 true
1 < 0 && 10 == 10 false
1 < 0 || 10 == 10 true

考慮撰寫一個判斷奇數偶數的程式

取餘數符號使用(%)

#include <stdio.h> int main(int argc, const char *argv[]) { int num; scanf("%d", &num); if (EXPRESSION) { printf("奇數\n"); } else { printf("偶數\n"); } return 0; }

EXPRESSION 為下列何者

  • num % 2
  • num % 2 == 1

參考: In c, in bool, true == 1 and false == 0?

課後練習1 - 解答

撰寫一程式用以判斷是否為閏年
輸入為年分,輸出 yes(為閏年), no(不為閏年)
詳細輸出可參考以下

$ gcc leapyear.c -o leapyear
$ ./leapyear
2020
yes
$

迴圈

對於要執行多次的程式碼,可以使用迴圈進行簡化
基礎語法

for (initialization; expression; afterthought) { dosomething }

當 expression 成立的時候,才會一直重複執行 dosomething
考慮以下程式碼

#include <stdio.h> int main(int argc, const char *argv[]) { int i; for (i = 0; i < 10; i++) { printf(" %d", i); } printf("\n"); return 0; }

執行結果

$ gcc loop.c -o loop
$ ./loop
 0 1 2 3 4 5 6 7 8 9
$

初始化條件一開始 i = 0

執行第一次時,i =  0 條件為 i < 10 -> 成立
執行第一次時,i =  1 條件為 i < 10 -> 成立
...
執行第一次時,i =  9 條件為 i < 10 -> 成立
執行第一次時,i = 10 條件為 i < 10 -> 不成立

除了基本的 for-loop,也有另外兩種迴圈語法

while (expression) { dosomething } --- do { dosomething } while (expression)

expression 一樣是判斷條件式,與上述介紹邏輯相同
不一樣的是,do-while 迴圈是 先做再判斷

課後練習
For Loop in C
Sum of Digits of a Five Digit Number
Printing Pattern Using Loops

課後練習2 - 解答

使用 for 迴圈撰寫一程式,輸出倒金字塔
輸入數字僅可以為奇數,若發現輸入非奇數需警告使用者重新輸入

2
not odd number! 
5
*****
 ***
  *

陣列

陣列基本宣告為,中括弧中間寫上大小 e.g. int myarray[5]
陣列從0開始,所以大小為 5 的陣列,其 index 為 0 ~ 4







%0



Pointers:
Pointers:



Values:
Values:



Pointers:->Values:





Indices:
Indices:



Values:->Indices:





pointers

A

A+1

A+2

A+3

A+4

A+5



values

A[0]

A[1]

A[2]

A[3]

A[4]

A[5]



pointers:f0->values:f0





pointers:f1->values:f1





pointers:f2->values:f2





pointers:f3->values:f3





pointers:f4->values:f4





pointers:f5->values:f5





indices

0

1

2

3

4

5



考慮以下輸入5個數字到陣列中的程式實作

#include <stdio.h> int main(int argc, const char *argv[]) { int myarray[5]; for (int i = 0; i < 5; i++) { scanf("%d", &myarray[i]); } for (int i = 0; i < 5; i++) { printf("%d ", myarray[i]); } putchar('\n'); return 0; }

執行結果如下

$ gcc array.c -o array
$ ./array
1 2 3 4 5
1 2 3 4 5
$

參考上圖,要指定到陣列的某個位置,可以直接寫 myarray[位置] 以存取

所以陣列的讀寫是 常數時間 aka. O(1)

輸入的部分,一樣要記得加上 & 符號

課後練習
1D Arrays in C

課後練習3 - 解答

讀取 10 個數字並寫入陣列,輸出將奇數位子數值 * 2,偶數位子數值 / 2
參考輸出如下

$ gcc array2.c -o array2
$ ./array2
1 2 3 4 5 6 7 8 9 10
2 1 6 2 10 3 14 4 18 5
$

陣列可以有任何型態,比如 int arr[10], double arr[10], etc.
字元陣列 char arr[10] 也是有的,只不過這個在 C 語言裡面代表著 字串
輸入字串在 C 語言裡面是這樣子的寫法

#include <stdio.h> int main(int argc, const char *argv[]) { char input[10]; scanf("%s", input); printf("%s\n", input); return 0; }

輸出如下

$ gcc string.c -o string
$ ./string
hello
hello
$ 

值得注意的是,輸入字串 不用加上 & 符號(原因等到講到指標會解釋)
另一個重點是,以上例子字串大小會是 10

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
如果你的輸入大小超過 10 將會有不可預期的事情發生(可參考 緩衝區溢位攻擊之一(Buffer Overflow))
字串的內容的全貌如下







%0



Values:
Values:



Indices:
Indices:



Values:->Indices:





indices

input[0]

input[1]

input[2]

input[3]

input[4]

input[5]



values

h

e

l

l

o

\0



注意到,hello 後面有一個 \0,C 語言以 \0(讀做 null character) 這個符號作為判斷字串是否結尾
所以如果字串結尾沒有 \0 也可能會導致錯誤

字母的數值,以 Ascii 表示,對於電腦來說,萬物皆由數字形成,所以字母也是由一個數字組成,而字串則是一堆數字所組成,解析方式不同而已
所以若以數字的觀點來看,hello 的數值會是







%0



Character:
Character:



Values:
Values:



Character:->Values:





Character

h

e

l

l

o

\0



values

104

101

108

108

111

0



課堂練習
讀取大小為 10 的字串,並輸出全大寫的結果(可假設使用者不會輸入超過大小為 10 的字串,須注意定義大小須包含 null character)

課後練習4 - 解答

撰寫一程式,首先讀取測資數量(case)
測試資料中,保證不會有空白
每一個 case 當中,讀取一字串,移除字串中非英文字母之字元,字串長度最大 1000
詳見底下範例輸出

$ gcc removeChar.c -o removeChar
$ ./removeChar
2
hello world!
Test case 1: helloworld
#$%^&*this123ismystr...
Test case 2: thisismystr

提示: 需要使用兩個陣列


二維陣列,又稱為矩陣,可以想像成 每個陣列元素都是一維陣列
定義方法為 int arr[10][10]

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
有 10 個陣列元素,每個元素都是一個長度為 10 的陣列
考慮以下例子

#include <stdio.h> int main(int argc, const char *argv[]) { int arr[5][5]; for (int r = 0; r < 5; r++) { for (int c = 0; c < 5; c++) { arr[r][c] = r * 5 + c; printf("%2d ", arr[r][c]); } putchar('\n'); } return 0; }

參考輸出

$ gcc arr.c -o arr
$ ./arr
  0  1  2  3  4
  5  6  7  8  9
 10 11 12 13 14
 15 16 17 18 19
 20 21 22 23 24
$

需要注意的是,二維陣列是 一整塊連續記憶體(意即 可以使用一維陣列方式存取)
將上述改寫成

#include <stdio.h> int main(int argc, const char *argv[]) { int arr[5][5]; int *arr2 = (int *)arr; for (int i = 0; i < 25; i++) { arr2[i] = i; printf("%2d ", arr2[i]); } putchar('\n'); }

課後練習5 - 解答

使用二維陣列撰寫一程式,將數字以右旋轉的方式排列,達到如下圖效果
輸入大小介於 1n20

n=3
1 2 3
8 9 4
7 6 5

--
n=5
 1  2  3  4  5
16 17 18 19  6
15 24 25 20  7
14 23 22 21  8
13 12 11 10  9

副程式

如同 main function 一樣,我們也可以自己定義 function
撰寫副程式的意義在於,對於程式的可讀性有很高的提升
定義方式如下

void myfunction(int param) { }

一個副程式必須要有

  • 回傳值型態(必須要有)
    • 沒有回傳值,寫 void 即可
  • 函式名稱(必須要有)
  • 參數(不一定要有)

以下為一個基本副程式範例

#include <stdio.h> void hello() { printf("hello\n"); } int main(int argc, const char *argv[]) { hello(); return 0; }

在需要呼叫函式的時候,寫上函式的名稱,傳上相對應的參數即可

考慮以下簡單例子

#include <stdio.h> void swap(int a, int b) { int tmp; tmp = a; a = b; b = tmp; } int main(int argc, const char *argv[]) { int a = 1, b = 2; printf("a:%d b:%d\n", a, b); swap(a, b); printf("a:%d b:%d\n", a, b); return 0; }

試問,輸出結果為何?

$ gcc swap.c -o swap
$ ./swap
a:1 b:2
a:1 b:2
$

讓我們來仔細理解這段 code 的原因,一個程式的執行記憶體空間配置如下

計算機組織當中有提到呼叫函式內部實作為

.text main: #assume value a is already in $t0, b in $t1 add $a0,$0,$t0 # it's the same function as move the value add $a1,$0,$t1 jal addthem # call procedure add $t3,$0,$v0 # move the return value from $v0 to where we want syscall addthem: addi $sp,$sp,-4 # Moving Stack pointer sw $t0, 0($sp) # Store previous value add $t0,$a0,$a1 # Procedure Body add $v0,$0,$t0 # Result lw $t0, 0($sp) # Load previous value addi $sp,$sp,4 # Moving Stack pointer jr $ra # return (Copy $ra to PC)

所以可以得知,function call 的時候,會將歷史資料儲存起來(儲存當前狀態)
因此,回到 swap 這個例子,雖然名字都是 a
但是,他們不一樣
就如同世界上很多名字一樣的人,他們本質上屬於不同的個體
以資訊術語來說就是,資料儲存地址不一樣

c.f. 作業系統 context-switch
是不是也是需要儲存當前 process 的狀態,以便於回復的時候資料不會出現錯誤。同樣的道理

補充
function call 會儲存在 stack
memory allocation 會儲存在 heap

所以,當 function call 到 swap 裡面,其實這個 a 是 副本
swap 的 a 是一個變數名字相同,數值相同,但地址不一樣的變數
當 function 執行完畢之後,這個 swap 的 a 就不復存在
所以,事實上,swap function 是正常運作的,只是因為 swap 的 a 執行完後不復存在,所以看起來才沒有改變

Bitwise 運算

在電腦程式語言當中,撇除四則運算,有一種運算叫做 bitwise 運算
意即在 bit level 做運算
考慮 a && 1a & 1 的差別

  • 前者做的是比較運算
  • 後者做的是邏輯運算

假設 a=1010=10102
a & 1 的意思是,0b1010 與 0b1 做邏輯 AND 運算

1 0 1 0
0 0 0 1
-- and
0 0 0 0

同理若是 a | 1 做邏輯 OR 運算

1 0 1 0
0 0 0 1
-- or
1 0 1 1

a ^ 1 做 XOR 運算

1 0 1 0
0 0 0 1
-- xor
1 0 1 1

課堂練習
0x12340000 & 0x00005678 = ?
0x12340000 | 0x00005678 = ?
0x100 && 0x001 = ?

除了上述運算子之外,還有 <<, >> 運算子
用法是

printf("%d\n", 1 << 31);

這個意思代表將 1 往左邊位移 31 個 bit
所以這個值是 231

  • <<
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    左移出的位元會被丟棄,右側會補上 0
  • >>
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    右移出的位元會被丟棄

另外還有 not,不過這個 not 很容易跟另外一個 not 搞混

  • !
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    這個是 logical operator
  • ~
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    這個是 bitwise operator

差別在於 logical 是針對數值這層做操作
而 bitwise 是在 bit 層
考慮以下例子

a = 0x1010
!a = 0
~a = 0x0101 = 5

這邊有一個概念,邏輯算子與 bitwise 算子不一樣,對於位移的部分有以下差異

  • 邏輯位移
  • 算術位移
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
    當遇到負號的時候,左邊補上 1

課堂練習
已知 ascii 字元的大小寫部分數字差異是 32
而數字 32 正好是 25,因此可以用 bitwise 算子將其改寫得很漂亮
撰寫一程式,輸入一字串,將大寫改小寫,小寫改大寫。輸入字串長度: 1n20
提示: 需要使用 xor 運算子

課後練習6 - 解答

撰寫一程式
輸入一 int 整數,將其前半部分與後半部分交換
意即: 0x1234 5678 要變成 0x5678 1234
以 printf("0x%08x\n", num); 來顯示 16 進位

指標

好所以,指標究竟是啥,還記得 scanf 變數前面都要加一個 & 符號嗎?
還記得 function call 傳入變數是 副本 嗎?

變數本身佔有一定的記憶體空間,
將變數傳入 function 中是做副本,也就是 數值一樣但記憶體空間不同的變數對吧
要想要在其他地方更改變數,就必須將 變數地址傳入 function 中才可以做改變
& 符號就是取出地址的方法,讀做 address of

當我們在做 scanf 的時候,本質上是 function call
所以當你做

int d; scanf("%d", &d)

實際上是把 d 這個變數的地址傳入到 scanf 這個 function 中,這樣才會改到 main 裡面的 d
所以在 scanf 中的 d 是 地址 對吧,要存取到該地址上的數值,必須要採用 * 符號(讀做 derefernece)

#include <stdio.h> int main(int argc, const char *argv[]) { int num = 10; int *ptr = &num; printf("%d %d\n", num, *ptr); return 0; }

執行結果

$ gcc ptr.c -o ptr
$ ./ptr
10 10
$

所以 副程式 裡面的 swap function 要怎麼改才會真正的有作用呢?

#include <stdio.h> void swap(AAA2, BBB2) { int tmp; tmp = *a; *a = *b; *b = tmp; } int main(int argc, const char *argv[]) { int a = 1, b = 2; printf("a:%d b:%d\n", a, b); swap(AAA1, BBB1); printf("a:%d b:%d\n", a, b); return 0; }

試問

  • AAA1 = ?
  • AAA2 = ?
  • BBB1 = ?
  • BBB2 = ?

回顧陣列篇,以上我們講的都是固定大小的陣列,如果輸入的陣列長度不固定那該怎麼辦?
malloc(memory allocation) 動態配置記憶體空間就派上用場了,基礎用法是

int *arr = (int *)malloc(10 * sizeof(int))

malloc 裡面擺放的是,陣列大小,包含

  • 幾個數值( 10 )
  • 這個數值的大小為和( sizeof(int) )

前面 (int *) 是強制轉型,malloc 回傳值型態會是 (void *) 所以必須將其轉型成對應型態
中間大小的部分,白話一點就是,我要 10 個大小的 int,然後每個 int 的大小是 sizeof(int)

針對每個型態,他的大小都不盡相同(e.g. int(8 byte), double(8 byte), char(1 byte) etc.)
查詢每個型態的大小,可以用 sizeof 幫你查找

然後使用方式就跟以前學過的一樣,參考下列範例

#include <stdio.h> #include <stdlib.h> // malloc definition int main(int argc, const char *argv[]) { int *arr; int num; printf("請輸入陣列大小: "); scanf("%d", &num); arr = (int *)malloc(num * sizeof(int)); for (int i = 0; i < num; i++) { scanf("%d", &arr[i]); } for (int i = 0; i < num; i++) { printf("%d ", arr[i]); } putchar('\n'); free(arr); return 0; }

執行結果如下

$ gcc malloctest.c -o malloctest
$ ./malloctest
5
5 6 7 8 9 10
5 6 7 8 9 10
$

注意到,在 第 21 行 有一個 free
凡是手動配置記憶體的變數,都需要手動釋放記憶體


回顧輸入字串時,不必使用 & 符號

char arr[10]; scanf("%s", arr);

因為 arr 本身是一個指標,指向該陣列的開頭
記得一件事,如何完整描述一個陣列,你只需要

  • 開頭
  • 大小

就可以了

課後練習

tags: clang