本教學採用 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部分所組成
printf
函式實作定義在 stdio.h
這個檔案中
stdio
standard input/outputImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
printf
這個函式主要的功能就是將引號內的字符打印到 標準輸出裝置
(e.g. 螢幕)
printf
print formatImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
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 不同
除此之外,若是要用到多個條件是一起判斷,要用到 And(&&)
以及 Or(||)
a & 1 與 e1 && e2 不同,or 也是同理
bitwise
運算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 為下列何者
撰寫一程式用以判斷是否為閏年
輸入為年分,輸出 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
使用 for 迴圈撰寫一程式,輸出倒金字塔
輸入數字僅可以為奇數,若發現輸入非奇數需警告使用者重新輸入
2
not odd number!
5
*****
***
*
陣列基本宣告為,中括弧中間寫上大小 e.g. int myarray[5]
陣列從0開始,所以大小為 5 的陣列,其 index 為 0 ~ 4
考慮以下輸入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.
輸入的部分,一樣要記得加上 & 符號
課後練習
1D Arrays in C
讀取 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
注意到,hello
後面有一個 \0,C 語言以 \0(讀做 null character
) 這個符號作為判斷字串是否結尾
所以如果字串結尾沒有 \0 也可能會導致錯誤
字母的數值,以 Ascii 表示,對於電腦來說,萬物皆由數字形成,所以字母也是由一個數字組成,而字串則是一堆數字所組成,解析方式不同而已
所以若以數字的觀點來看,hello 的數值會是
課堂練習
讀取大小為 10 的字串,並輸出全大寫的結果(可假設使用者不會輸入超過大小為 10 的字串,須注意定義大小須包含 null character)
撰寫一程式,首先讀取測資數量(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]
#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 - 解答
使用二維陣列撰寫一程式,將數字以右旋轉的方式排列,達到如下圖效果
輸入大小介於
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 運算
意即在 bit level
做運算
考慮 a && 1
與 a & 1
的差別
假設
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
所以這個值是
<<
>>
另外還有 not,不過這個 not 很容易跟另外一個 not 搞混
差別在於 logical 是針對數值這層做操作
而 bitwise 是在 bit 層
考慮以下例子
a = 0x1010
!a = 0
~a = 0x0101 = 5
這邊有一個概念,邏輯算子與 bitwise 算子不一樣,對於位移的部分有以下差異
1
課堂練習
已知 ascii 字元的大小寫部分數字差異是 32
而數字 32 正好是,因此可以用 bitwise 算子將其改寫得很漂亮
撰寫一程式,輸入一字串,將大寫改小寫,小寫改大寫。輸入字串長度:
提示: 需要使用 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 = #
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;
}
試問
回顧陣列篇,以上我們講的都是固定大小的陣列,如果輸入的陣列長度不固定那該怎麼辦?
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 本身是一個指標,指向該陣列的開頭
記得一件事,如何完整描述一個陣列,你只需要
就可以了
課後練習
clang