---
title: 'C 概念 & 編譯'
disqus: kyleAlien
---
C 概念 & 編譯
===
## Overview of Content
C 是**關注程式** 的語言(命令範式),重順序性、演算法
:::success
* 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/)
本篇文章對應的是 [**編譯器的角度看程式 | 低階與高階、作業系統、編譯器、直譯器、預處理 | C語言函數探討**](https://devtechascendancy.com/compiler-programming-os-c-functions/)
:::
[TOC]
## 程式概念
解決問題的方法稱為**演算法 (`algorithm`)**,它是解決問題的一種「**思路、方案**」
表達問題的解決方法稱為 **程式 (`program`)**,它是將人類的思路與方案,透過電腦表達出來的一種「**方式**」
### 低階語言概述
* 以組合語言(也可稱為彙編語言)來說它必須符合兩個規定
1. **一對一性**:**不同程式皆是針對於不同的電腦設備**,因為`不同的處理器有不同的指令`,不可兼容於其它的處理器
2. **不可攜帶性**(`portable`):**基於`不可兼容`,攜帶到其他設備也沒有用**
3. 編譯是透過「組譯器」
### 高階語言概述
* `C`、`C++`、`Java` 或是 `Kotlin` 它不需依賴於處理器(CPU)的不同作編譯不同的程式,是一種可攜帶性語言
1. 不再關注特定電腦的體系結構,**不依賴於指令集**
2. **語法的標準化**,在不同電腦上很少需要修改即可運行
3. 編譯是透過「編譯器、直譯器」
## 作業系統 & 編譯器 & 直譯器
### 作業系統 OS
* 控制電腦系統的程式,所有給予電腦的命令,都需要透過作業系統`分配資源` and `引導`才能正常執行
* **Unix 主要就是用 C 語言編寫**,並**對電腦架構做了很少的假設**(抽象化作的很好),所以可以成功的移植到不同電腦系統中
### 編譯器
* 編譯器及是 **翻譯高階語法** 給處理器知道的程式
:::success
* 副檔名 ?
副檔名是`.c`,這只是一個協定(讓電腦知道它是 C 程式),並不是要求
副檔名是`.out`,,這是 Unix 系統下的執行檔
:::
### 直譯器 - intepreted
* 使用直譯器的語言有像是 `JavaScript`、`BASIC`、`Python` & `Unix's shell`… 等等,其特點如下:
* 不需經過編譯即可執行,**運行時++同時++ ++分析++ 與 ++執行++**
* 速度較慢,因為不會轉換成低階型式
* 直譯器運行的概念是將特定語言的文字形式的原始碼,在「運行時逐行」解釋成機器語言
### C 語言的編譯器
* 常見的 C 語言編譯器:
1. **`gcc` 編譯器**:使用GNU通用公共許可證(GPL)等自由軟件許可證發布
```shell=
sudo apt install -y gcc
```
2. **`clang` 編譯器**:用一個更寬鬆的許可證(`University of Illinois` / `NCSA Open Source License`)發布,並它基於 LLVM
```shell=
sudo apt install -y clang
```
:::success
* **LLVM** (`Low-Level Virtual Machine`)? 它是一個開源的編譯器基礎建設項目
LLVM 的主要目標是提供一個靈活、高效、模塊化的編譯器基礎架構,以便用於各種不同的編譯和代碼優化任務
> 一般來說 Clang 的編譯速度較快
**它的架構允許前端(負責解析源代碼並生成中間表示)和後端(負責將中間表示轉換為目標平台的機器代碼)能夠獨立地進行擴展和優化**
> 相較之下 GCC 則是前、後端共同開發
> 由於切開維護使得拓展變得容易並清晰,也使得在不同的語言和目標平台上進行編譯變得更加容易
:::
### 編譯的過程 - 概述
:::info
編譯指令 `gcc` 的選項可以透過 `man gcc` 查看

:::
* C **編譯的過程** (Building) 一般來說會經過幾個階段:預編譯、編譯、匯編、連結
```c=
// Hello.c
#include <stdio.h>
int main(void) {
printf("Hello C");
return 0;
}
```
1. **cpp 預編譯**:一般副檔名為 `.i`;編譯 #define、#if、#ifndef、#endif...等等預編譯指令
> cpp 並不是說 C++ 檔案,它的意思是 `C Preprocessor`
```shell=
## 指令
gcc -E Hello.c -o Hello.i
```
預編譯結果較長,這裡只擷取部分,其中可以看到 `#include <stdio.h>` 被替換為具體的 `stdio.h` 檔案
```c=
... 省略一大部分
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 902 "/usr/include/stdio.h" 3 4
# 2 "Hello.c" 2
# 3 "Hello.c"
int main(void) {
printf("Hello C");
return 0;
}
```
> 
2. **cc 編譯(組合)**:一般副檔名為 `.s`;^1.^ 檢查語法、語意是否正確、^2.^ 將高階語言翻譯成低階語言(指令集、機械指令)
```shell=
## 以下兩個指令都可以運行
gcc -S Hello.c -o Hello.s
gcc -S Hello.i -o Hello.s
```
編譯結果如下
```shell=
.file "Hello.c"
.text
.section .rodata
.LC0:
.string "Hello C"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 9.4.0-5ubuntu1) 9.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
```
> 
3. **as 匯編**:一般副檔名為 `.o`,將上一個步驟中的組合語言轉為轉為 CPU 可了解的二進制碼
```shell=
## 指令
gcc -c Hello.s -o Hello.o
```
由於編譯出來的是二進位文件,所以一般人是看不懂的
> 
:::info
* 目標檔案(`.o` 擴展名)通常都是以二進制形式表示的,而不是純文本;這些二進制檔案的內容對於人類而言可能不是可讀的字符
:::
4. **ld 連結** (Linked):一般副檔名為 `.out`;^1.^ 搜尋 Library 函式庫的程式,與程式碼連接、^2.^ 連接上其他相關被編譯的檔案、^3.^ 將編譯的程式碼編譯成可執行檔案
```shell=
## 指令
gcc Hello.o -o Hello.out
```
執行 `Hello.out` 檔案
```shell=
./Hello.out
```
> 
* 各家編譯器會有不同的編譯流程,下圖是**瑞薩 RL 系列 的 CCRL**
> 
### GCC 常見擴展名
| 擴展名 | 含意 |
| -------- | -------- |
| `.c` | C Source Code |
| `.C`/`.cpp` | C++ Source Code |
| `.m` | Objective-C Source Code |
| `.h` | C 或 C++ 的頭文件 |
| `.i` | C 已經預處理過的檔案 |
| `.ii` | C++ 已經預處理過的檔案 |
| `.s` | 編譯後的文件檔,**之後編譯不再進行預處理操作** |
| `.S` | 編譯後的文件檔,**之後編譯可以再進行預處理操作** |
| `.o` | 匯譯後的文件檔 |
| `out` | 最後鏈結,變成一個平台可執行檔案 |
| `a` | 靜態 Library |
| `.so` | 動態 Library |
## 預處理 cpp
> cpp: `C Preprocessor`
1. 頭文件替換 `#include`:`.h` 檔案的內容會被原封不動的替換進 `.c` 檔案
2. 宏定義替換 `#define`
3. 條件替換 `#if`、`#else`、`#elif`、`#endif`、`#ifndef`、`#ifdef` ... 等等
4. keep 特殊處理 `#pragma` ... 等等
5. **移除注釋**
### 預編譯 define & typedef
* 要證明 `define` & `typedef` 是否都在預編譯處理,使用以下程式進行預編譯,測試他們是否都是預編譯時會處理的關鍵字
```c=
// Typedef_Test.c
#define dChar_t char*
typedef char* tChar_t;
int main(void) {
dChar_t c1, c2;
tChar_t c3, c4;
return 0;
}
```
* 對上面程式進行 cc、選項 `-i` 進行預編譯:
```shell=
gcc -E Typedef_Test.c -o Typedef_Test.i
```
從結果來看 **可以知道 `typedef` 是在編譯時期處理**,**而不是在預編譯時期** ( `#define` 才在預編譯時處理 )
> 
### 預處理 - include
* 有關於 `include` 會使用到兩個符號
1. **尖括號 `<>`**:編譯器直接去系統指定目錄尋找;
> 像是 Unix 就會去 `/usr/include` 目錄尋找
```shell=
# 在編譯時也可以使用 `-I` 選項來指定目錄
cc -c -I <指定目錄> 源碼.c
```
2. **雙引號 `""`**:^1.^ 編譯器會去 **當前文件目錄下尋找**,^2.^ 找不到才去系統目錄找
* include 進來的頭文件會 **直接替換** 進源檔案中
1. `H_Test.h`:宣告三個變數
```c=
int apple;
short book;
char name;
```
2. `H_Test.c`:引用 `H_Test.h`
```c=
#include <stdio.h>
#include "H_Test.h"
int main(void) {
int c = a + b;
printf("%d", c);
return 0;
}
```
> 下圖省略 `stdio.h`
>
> 可以看到 `H_Test.h` 是直接被替換上 Source code
> 
### 宏定義 - 解析
:::info
宏也就是 `#define`,可以用來 **減少函數的開銷**
:::
* `#define` 只是 **原封不動的替換**;但要注意它是可以遞迴進行替換的,直到不末端不再是宏為止
* `#define` 宏 **可以帶參數**:**每個參數在宏中都必須括號**,最後整體再括號 (**==括號==相當重要**)
1. 無參數宏:
```c=
#define SEC_YEAR (365*24*60*60UL) // UL: unsigned long
```
2. 有參數宏:
```c=
#define MAX(a, b) (((a) > (b)) ? (a) : (b)) // 括號相當重要,不使用會容易出錯
```
* 以下範例就是一個有參宏缺少括號造成的問題,導致跟原來要表達的意思完全不同
```c=
// 缺少 括號
#define ADD_1(X, Y) X+Y
// 每個參數都有括號
#define ADD_2(X, Y) ((X)+(Y))
int main(void) {
int a = 10, b = 30;
int c = 3 * ADD_1(a, b);
int d = 3 * ADD_2(a, b);
return 0;
}
```
預編譯
```shell=
gcc -E Define_Test.c -o Define_Test.i
```
預編譯 結果
```shell=
# 1 "Define_Test.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "Define_Test.c"
int main(void) {
int a = 10, b = 30;
int c = 3 * a+b; ## 錯誤
int d = 3 * ((a)+(b)); ## 正確
return 0;
}
```
### 宏定義 v.s inline 內聯
| \ | 一般函數 | 宏定義 | inline 內聯函數 |
| - | -------- | -------- | -------- |
| 優點 | 編譯器會進行類型檢查 | 在預編譯時期就解決,不會消耗內存 | 使用時進行替換,沒有 Stack開銷 |
| 缺點 | **耗費 Stack 空間**,效率較低 | **不進行類型檢查**,並建議不要進行 `++`/`--` 操作 | **耗費內存**,如果有循環就更耗費時間 |
* inline 內聯特色
1. 必須定義時使用,加在函數宣告是沒有用的
2. 編譯時會進行參數類型檢查(靜態語言的特性)
```c=
// 在宣告使用時沒用
inline int add(int a, int b);
int add(int a, int b) {
return a + b;
}
// 必須在定義時使用
inline int subtraction(int a, int b) {
return a - b;
}
```
### 條件編譯
* 條件宏 `#if`、`#else`、`#elif`、`#endif`、`#ifndef`、`#ifdef` ... 等等
```c=
#define NUM_1
#define NUM_2 1
#define NUM_3 1
int main(void) {
int a = 0, b = 0;
#ifdef NUM_1
a = 100; // 有定義,預編譯後會顯示
#endif
#undef NUM_1 // 取消定義
#ifndef NUM_1
a = 200 // 預編譯後會顯示
#endif
#if NUM_1
a = 300;
#endif
#if (NUM_1 && NUM_2) // 多定義判斷,由於 NUM_1 沒有定義,所以條件不符合
b = 300;
#elif NUM_2
b = 301; // 預編譯後會顯示
#elif NUM_3
b = 302;
#else
b = 303;
#endif
return 0;
}
```
:::warning
* **`with no expression` 錯誤** !?
`#if`、`#elif` 後面不但會檢查是否有定義,**還會檢查 ++定義值++**,如果沒有定義值就會報這個錯誤
> 但像是 `#define` 就不要求需要定義值
:::
預編譯後結果,可以看到 **不符合宏判斷條的原始碼就會被忽略**,無法進入下一個編譯階段
```shell=
int main(void) {
int a = 0, b = 0;
a = 100;
a = 200;
b = 301;
return 0;
}
```
> 
### 編譯時加入定義
* 預編譯的條件不一定要寫在源碼內,可以透過編譯時指令指定要使用的巨集;格式如下
```shell=
# 添加以下選項,動態決定巨集
-D<聚集名>[=數值]
```
範例如下
```java=
#include <stdio.h>
int main(void) {
// 源碼內沒有定義 HELLO 巨集
#ifdef HELLO
printf("Hello~\n");
#else
printf("Hi~\n");
#endif
return 0;
}
```
1. 尚未添加 `-D` 選項去指定巨集
```shell=
cc main.c -o mainWithOutD.o
```
> 
2. 添加 `-D` 選項去指定巨集
```shell=
cc main.c -DHELLO=1 -o mainWithD.o
```
> 
## 函數
* 函數有幾個特點
1. 入參建議不要超過 4 個,超過建議使用 struct 包裹起來(或是使用指標)
> 否則可能會造成 stack 的負擔
2. 傳入參數大小也不建議過大,避免超過 Stack Size,較大參數建議使用 Pointer 傳遞
3. 編譯完後函數 **會存在 elf 中的 `.text` 段**
:::info
* 可以使用 `readelf` 指令查看
> `readelf -S Hello.out`
> 
:::
### 函數 - 聲明、定義、調用
* 函數宣告
* 首先要先知道 **編譯器在編譯程式時,是以 ++文件為單位++**,所以在哪個文件裡面調用,就要在哪個文件內聲明
編譯器在編譯時會按照文件中的先後順序進行編譯,所以 **如果沒有宣告該函數,就必須按照順序進行撰寫**
* 宣告主要是告訴編譯器函數的原型
> 大多數都聲明在 `.h` 標頭檔案內
:::success
* **函數聲明可以重複! 但函數定義不可重複**
:::
* 函數定義:
* 當函數定義出來後,就 **表明了該函數的地址在哪**
* 可以不用宣告就直接定義,但是要注意順序;函數定義在後面,前面的函數就無法使用
```c=
int main(void) {
int result = subtraction(3, 5); // Error 呼叫不到 subtraction 函數
}
int subtraction(int a, int b) {
return a - b;
}
```
* 函數呼叫:
```c=
// function.c
#include <stdio.h>
int add(int, int); // 函數可多次聲明
int add(int, int);
int add(int, int);
int main(void) {
int result = add(3, 5); // 使用函數名呼叫
printf("result: %d\n", result);
return 0;
}
int add(int a, int b) { // 但只能定義一次
return a + b;
}
```
編譯
```shell=
gcc function.c -o function.out
```
### 函數 - 入棧 Stack
* 函數的入棧是按照順序的,如下面程式的入棧順序就是
:::info
* Stack 棧是由核心棧存器 `SP` 來管控,相關的核心棧存器還有 鏈結 `LR`、計數器 `PC`
:::
1. main 函數返回地址
2. main 函數
3. 遇到 subtraction 函數,保存 subtraction 函數完成後的返回地址
4. subtraction 函數入棧,**這裡又細分為,參數由右到左入棧**
```c=
int subtraction(int a, int b) {
return a - b;
}
int main(void) {
int result = subtraction(3, 5); // Error 呼叫不到 subtraction 函數
}
```
Stack 概念圖如下
> 
## 更多的 C 語言相關文章
關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言
### C 語言基礎
* **C 語言基礎**:有關於到 C 語言的「語言基礎、細節」
:::info
* [**理解C語言中的位元操作:位元運算基礎與宏定義**](https://devtechascendancy.com/bitwise-operations-and-macros-in-c/)
* [**C 語言解析:void 意義、NULL 意義 | main 函數調用、函數返回值意義 | 臨時變量的產生**](https://devtechascendancy.com/meaning_void_null_return-value_temp-vars/)
* [**C 語言中的 Struct 定義、初始化 | 對齊、大小端 | Union、Enum**](https://devtechascendancy.com/c-struct_alignment_endianness_union_enum/)
* [**C 語言儲存類別、作用域 | 修飾語、生命週期 | 連結屬性**](https://devtechascendancy.com/c-storage-scope-modifiers-lifecycle-linkage/)
* [**指標 & Array & typedef | 指標應用的關鍵 9 點 | 指標應用、細節**](https://devtechascendancy.com/pointers-arrays-const-typedef-sizeof-null/)
:::
### 編譯器、系統開念
* **編譯器、系統開念**:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節
:::warning
* [**理解電腦記憶體管理 | 深入瞭解記憶體 | C 語言程式與記憶體**](https://devtechascendancy.com/computer-memory_manager-c-explained/)
* [**C 語言記憶體區塊規劃 | Segment 段 | 字符串特性**](https://devtechascendancy.com/c-memory-segmentation-string-properties/)
* [**編譯器的角度看程式 | 低階與高階、作業系統、編譯器、直譯器、預處理 | C語言函數探討**](https://devtechascendancy.com/compiler-programming-os-c-functions/)
:::
### C 語言與系統開發
* **C 語言與系統開發**:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用
:::danger
* [**了解 C 語言函式庫 | 靜態、動態函式庫 | 使用與編譯 | Library 庫知識**](https://devtechascendancy.com/understanding-c-library-static-dynamic/)
* [**Linux 宏拓展 | offsetof、container_of 宏、鏈表 | 使用與分析**](https://devtechascendancy.com/linux-macro_offsetof_containerof_list/)
:::
## Appendix & FAQ
:::info
:::
###### tags: `C`