--- title: C語言的特性 description: 介紹C語言這個語言界的毒瘤(X tags: 語法 robots: noindex, nofollow langs: zh --- # 編譯 先不說了,上圖www   我個人比較喜歡下面那張圖,不過因為他是放在Medium上的,怕哪天就死圖了QQ 但基本上我們今天的重點會集中在 - Pre-Processor 然後我不想講Compiler,因為我也不會:( ## Pre-Processor 其實這東西我會選擇把他解讀成一個「自動的複製貼上機」 Pre-Processor工作的方式很簡單 就是看到這行開頭為"#"就開始解讀他該做什麼 主要常見的就是```#include```跟```#define``` ### include include 進來的東西都會在編譯時期加入到你的程式碼一起編譯 白話文就是複製貼上啊XDDD ### define 範例如下 ```cpp #include <cstdio> #define HI printf("Hello World\n") #define RESP(x) printf("%s\n", x) int main(){ HI; RESP("World is not going to say hello back.\n"); return 0; } ``` Pre-Processor此時的工作很簡單 看到有空格隔開的HI就把他替換成printf("Hello World\n") 看到有空格格開的RESP(?)且括號裡面有些什麼 就把x的地方替換成?的內容 白話文還是 複製貼上機 ## Linking 本來還想寫,還想講的,但後來... \_(:3」∠)\_ 我懶 : ) # 未定義行為(Undefined Behavior) 大家討厭C語言就在這裡了,未定義行為(簡稱UB)就是說當你做出不合C語言規範的行為時,編譯器與你的程式有權利做任何的行為,不管是讓你程式假裝跑出對的答案還是把你的電腦給燒了(? 都是可以的 舉幾個未定義行為 ## 函數該回傳不回傳 比如說 ```cpp #include <cstdio> int hello(int n){ printf("%d\n", n); } int main(){ printf("%d", hello(10)); return 0; } ``` 此時....就算是一種未定義行為了 曾經clang(某編譯器)就曾有遇到這種狀況發生時,整支程式會亂跳亂跑.... ## 存取未初始化得值 比如 ```cpp #include <cstdio> int main(){ int a; printf("%d", a); } ``` 此時a要是多少都可以,編譯器沒有責任幫你把a設為0 ## 溢位(Overflow/ Downflow) 如果你的變數出現溢位的話,編譯器可能再優化期間亂搞你的程式 ```cpp #include <cstdio> int main(){ int a = INT_MAX; if(a+1 > a){ printf("Oh No\n"); } else{ printf("Oh Yes\n"); } return 0; } ``` 即使a+1已經溢位了,也就是說現在的a+1應該是-2147483648 但因為編譯時期的優化(實際看優化狀況而定),編譯器可能會把```if(a+1>a)```優化成```if(true)```因為$a+1$在沒有溢位的狀況下必大於$a$ ## 有號與無號變數相比較 如例 ```cpp #include <cstdio> int main(){ int a = -1 unsigned b = 1; printf("%d", a>b); } ``` 這隻程式很有可能會輸出1 因為-1在記憶體中表示成0xffffffff 而無號的1在記憶體中表示成0x00000001 所以執行階段時很可能會是0xffffffff > 0x00000001 而編譯器也沒有義務救你QQ,可能只會看著你爛 總之寫C/C++要十分小心自己寫的每一行程式,清晰地知道自己在幹嘛,不然等等被橫空飛出來的不知道什麼東西陰了也找不到人算帳QQ ## 存取非法記憶體空間 比如說 ```cpp #include <cstdio> using namespace std; int main(){ int arr[10] = {0}; printf("%d\n", arr[10]); return 0; } ``` 此時程式很有可能輸出任意數字而不出錯,也可能出現Segment Fault並直接退出 # 執行 ## Memory Layout  Memory Layout是一支程式所能存取到的記憶體的編排方式,在多數現代作業系統與執行平台的架構中,是由虛擬記憶體的方式建構的,也就是說看似連續的記憶題,在實體記憶裝置中是離散式儲存的(甚至可能一半以壓縮形式儲存在RAM裡,另一塊在硬碟的置換空間中也說不定) - text 儲存程式位元碼的記憶體區段,一般而言是唯獨的(事實上看cpu架構而言,[維基百科連結](https://zh.wikipedia.org/wiki/NX位元)) - Initialized Data 這裡所儲存的就是宣告時已賦值的全域變數,這裡事實上不是唯獨的 - BSS(uninitialized data) 這裡所儲存的就是宣告時未賦值的全域變數,**他會被系統初始化為0x00** - Stack(棧) 這裡是所有程式執行時呼叫的函數所帶有的區域變數,詳細的在下個小節中會繼續說明 - Heap(堆) 整隻程式中動態分配的空間都會出現在這裡 ## 函數的執行 函數可以一層一層的呼叫,甚至可以遞迴執行,事實上是因為函數的執行建構在Stack這個資料結構上。在函數被呼叫時,這一層函數執行時所需的區域變數空間便會被放入Stack中,當函數執行完畢後,便會從Stack中移除(事實上,這些空間的「移除」並不是真的清空,而是視而不見,這也就是為什麼區域變數一定要初始化,不然你可能讀到剛剛使用這個空間的函數內的區域變數) 很有趣的是,程式的進入點main本身也是一個函數,他的回傳值應該要回傳給啟動這隻程式的程式,而多數系統的慣例就是只要回傳值為0(事實上也有系統的標準不是這樣子的,所以stdlib裡有個define叫EXIT_SUCCESS),就代表程式正常退出。有些Online Judge可能會因為你的main回傳了非0的值而不給你AC(比如Google Code Jam跟Kattis都會這樣),所以乖乖寫return是好習慣! # 變數的生命週期 ## BSS & Initialized Data 全域變數的生命週期從程式的開始到結束都會存在 ## Stack 當一個函數被呼叫時,其所含的區域變數會在呼叫時分配進Stack的空間,並在函數結束時解構。這會導致如果你回傳了區域變數的話,在這個指標在函數外解開時會導致未定義行為的發生。 例如 ```cpp #include <cstdio> #include <cstdlib> #include <ctime> using namespace std; int *hello(){ int a = rand(); return &a; } int main(){ srand(time(NULL)); int *a = hello(); printf("%d\n", *a); hello(); printf("%d\n", *a); return 0; } ``` 可以發現上面的程式會輸出兩行不一樣的數字! ## Heap 從你把它new出來,直到你把他delete或是程式結束自動由系統清除。其空間分配仰賴Heap這種資料結構,因而得名。
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up