隨手扎
淺嘗「你所不知道的C語言」系列講座 技巧篇 筆記
今天把 你所不知道的 C 語言:技巧篇 (2019-07-11)看完了,簡單寫下一些筆記。
系列講座單元目錄:
以下內容可以在 https://ethercalc.org/dykc 找到。或是在系列講座直接查看個單元的簡介。有很多很少學校會教的技巧和概念,這次看的是技巧篇,其他內容我應該也會慢慢看完。(可能也會去找影片來看)
你所不知道的 C 語言 | |
---|---|
https://hackmd.io/s/HJpiYaZfl | 系列講座 |
https://hackmd.io/s/HJFyt37Mx | 為何要深入學習 C 語言 |
https://hackmd.io/s/HyBPr9WGl | 指標篇 |
https://hackmd.io/s/SJ6hRj-zg | 函式呼叫篇 |
https://hackmd.io/s/BkuMDQ9K7 | 記憶體管理、對齊及硬體特性 |
https://hackmd.io/s/rJ8BOjGGl | 遞迴呼叫篇 |
https://hackmd.io/s/B1e2AUZeM | goto 和流程控制篇 |
https://hackmd.io/s/BkRKhQGae | 數值系統篇 |
https://hackmd.io/s/Hy72937Me | 編譯器和最佳化原理篇 |
https://hackmd.io/s/H1ZzeiCIQ | C編譯器原理和案例分析 |
https://hackmd.io/s/HJLyQaQMl | 物件導向程式設計篇 |
https://hackmd.io/s/S1maxCXMl | 前置處理器應用篇 |
https://hackmd.io/s/HkK7Uf4Ml | 動態連結器和執行時期篇 |
https://hackmd.io/s/SysiUkgUV | 連結器和執行檔資訊 |
https://hackmd.io/s/Hkcr5cn97 | 執行階段程式庫 (CRT) |
https://hackmd.io/s/Skr9vGiQm | 未定義行為篇 |
https://hackmd.io/s/HyIdoLnjl | 技巧篇 |
https://hackmd.io/s/SkE33UTHf | linked list 和非連續記憶體操作 |
https://hackmd.io/s/B1s8hX1yg | 從打造類似 Facebook 網路服務探討整合開發 |
https://hackmd.io/@sysprog/Sy8pJ0x9G | Stream I/O, EOF 和例外處理 |
允許陣列結尾逗點
C語言流行的標準版本包含:C89、C99,和增加與C++相容性的C11/C18。我不清楚哪些是在哪個版本增加的,基本上下面的特性,在支援C11/C18的gcc編譯器都可以使用。C89或是C99可能會報錯。
看個簡單了程式,其中注意到變數a
是整數陣列,但是在初始化階段,最後是寫下,};
。,
後一個空元素,在早期C語言編輯器應該是不被允許的,不過在GCC可以正常編譯。
#include <stdio.h>
#include <stdlib.h>
int main(){
auto a[] = {1,2,3,4,};
printf("a[0]:%d\n", a[0]);
return 0;
}
不過auto
這種C++11開始允許自動判斷變數類型的能力,仍然會在C丟出警告。但仍可以編譯成功與執行:
test.c: In function ‘main’:
test.c:5:8: warning: type defaults to ‘int’ in declaration of ‘a’ [-Wimplicit-int]
auto a[] = {1,2,3,4,};
有趣的是,EMCAScript在ES8(2017年)也才有類似的能力。你可以拿下面程式去JDOODLE試試,在Node.js v6 以前,還不支援。(Node.js v12 怎麼 EMCAScript Module的功能都還在實驗階段阿Orz)
let a = [1,2,3,4,]
function add(a,b){
return a+b
}
console.log(add(1,2,))
[1,2,3,4,]
大概不會有問題。但是js的函式傳遞其實有些特別,與C的不定長參數也很不一樣。Node.js v6會報add(1,2,)
錯。EMCAScript的函式可以用
arguments
取得所有傳入的參數。看看以下例子:function foo(){ console.dir(arguments); } foo(1,2,3,4,) // => Arguments { 0: 1, 1: 2, 2: 3, 3: 4, … }
do
寫程式,必須了解變數的可見域和生存域。這兩者通常是高度重疊,不是太需要注意,但是理解到,可以有一些特別的技巧。
最常見的莫過於for-loop。下面例子i
是屬於main
函式的區域變數,但是他的生存範圍只有在for迴圈裡。
#include <stdio.h>
#include <stdlib.h>
int main(){
for(int i; i<=10; i++){
printf("%d\n", i);
}
}
所以如果有不想要污染的環境,但又有比要暫時使用的話,可以用do{...}while(0)
來處理:
#include <stdio.h>
#include <stdlib.h>
int main(){
int a = 1;
do{
int a = 10;
int tmp = 11;
printf("a: %d\n", a);
printf("tmp: %d\n", tmp);
}while(0);
printf("a: %d\n", a);
// printf("tmp: %d\n", tmp); // will not found tmp variable
return 0;
}
上面程式會輸出:
a: 10
tmp: 11
a: 1
變數a
在do-while裡被覆寫了,但離開while後就回復元值。但是main裡找不到tmp
值。在新的C語言裡,可以直接用{}
包起來就好:
#include <stdio.h>
#include <stdlib.h>
int main(){
int a = 1;
{ // some comments
int a = 10;
int tmp = 11;
printf("a: %d\n", a);
printf("tmp: %d\n", tmp);
}
printf("a: %d\n", a);
// printf("tmp: %d\n", tmp); // will not found tmp variable
return 0;
}
這部份與講座後提到的Block
似乎也有關係。我不常寫巨集,目前不是那麼了解。
此外,我也喜歡在區塊開頭加上一些註解,對於折疊代碼來瀏覽很有幫助。(如果編輯器支援的話)
但是在js、Python行為很不一樣
EMCASCript
但是在目前最流行的兩個動態語言EMCAScript和Python中,有著不同的行為。 先看看與C語言語法接近的EMACScript:
do{
var a = 0;
}while(0)
console.log(a); // => 0
上例中a
在之後仍然可以被存取。在ES6後(應該是ES6)多了let
這個關鍵字,改成下面這樣,do-while
外圍就存取不到a
do{
let a = 0;
}while(0)
// console.log(a); // => not found a
Python
i = 1
while(i):
i = 0
a = 1
print(a)
同樣,while
外圍仍然可以存取a
。不過有趣的是[y for y in range(10)]
當中的y
無法在外圍被存取,但是下面例子的x
同樣在外圍可以被存取:
for x in range(10):
print(x)
print("Out for " + str(x))
小節
※ 以下可能有誤,或是不精確
EMCAScript、Python這樣的語言,記憶體是由語言本身進行管理。何時釋放了變數使用者並不清楚實際時間。同時為了更好理解與使用,life time可能會長一些。區域生存空間就要看語言有沒有支援。
GCC 支援 Plan 9 C Extension (「繼承」比你想像中簡單)
Plan 9是貝爾實驗室開發的作業系統,Unix的後繼者。他的C語言編譯器有些與標準不同(gcc、clang、vc都有些自己個擴充,不是只有標準的內容)。GCC支援Plan 9的寫法(-fplan9-extensions
),並可以OOP裡的寫出繼承:
typedef struct S {
int i;
} S;
typedef struct T {
S; // <- "inheritance"
} T;
void bar(S *s) { }
void foo(T *t) {
bar(t); // <- call with implict conversion to "base class"
bar(&t->S); // <- explicit access to "base class"
}
這方式和Golang做繼承方式有夠像…
有時不用 goto 會寫出更可怕的程式碼
本節程式碼參考有時不用 goto 會寫出更可怕的程式碼。
goto
是一個非常底成的作法,這有些相像組語裡的jmp
。所以使用goto
可以建構一些特別的流程,應該偶可能模擬lisp裡的condition。簡單點來看,如果使用goto
:
int foo(int bar)
{
int return_value = 0;
allocate_resources_1();
if (!do_something(bar))
goto error_1;
allocate_resources_2();
if (!init_stuff(bar))
goto error_2;
allocate_resources_3();
if (!prepare_stuff(bar))
goto error_3;
return_value = do_the_thing(bar);
error_3:
cleanup_3();
error_2:
cleanup_2();
error_1:
cleanup_1();
return return_value;
}
cleanup
對應著每一次goto
。越晚執行的程式,在之後會需要做越多清理工作(有點類似switch
的技巧)。如果不使用goto
,會變得很丑:
int foo(int bar)
{
int return_value = 0;
allocate_resources_1();
if (do_something(bar))
{
allocate_resources_2();
if (init_stuff(bar))
{
allocate_resources_3();
if (prepare_stuff(bar))
{
return_value = do_the_thing(bar);
}
cleanup_3();
}
cleanup_2();
}
cleanup_1();
return return_value;
}
高階的 C 語言「開發框架」(LibCello)
Cello 在 C 語言的基礎上,提供以下進階特徵:
- Generic Data Structures
- Polymorphic Functions
- Interfaces / Type Classes
- Constructors / Destructors
- Optional Garbage Collection
- Exceptions
- Reflection
Cello 意思是大提琴。libcello提供一個框架,很漂亮、優雅的處理一些C會需要解決的問題,不過他是C的擴充,讓C可以很優雅的撰寫,甚至一些物件導向的概念。
with open file? No problem
with(f in new(File, $S("test.txt"), $S("r"))) {
var k = new(String); resize(k, 100);
var v = new(Int, $I(0));
foreach (i in range($I(2))) {
scan_from(f, 0, "%$ is %$ ", k, v);
show(k); show(v);
}
}