原文:A ‘C’ Test: The 0x10 Best Questions for Would-be Embedded Programmers
紀錄閱讀這篇原文時做的筆記~
#define
宣告一個常數,這個常數代表一年有多少秒(不用考慮閏年)要注意的地方:
#define
的基本語法,例如:
;
做結尾( )
包起來,以確保運算順序_
分隔不同單字L
,告訴編譯器要將這個數字視為 Long
。
UL
以避免 signed 和 unsigned 的陷阱第五點原文:
As a bonus, if you modified the expression with a UL (indicating unsigned long), then you are off to a great start because you are showing that you are mindful of the perils of signed and unsigned types
我想作者的意思應該是說:
若一個 expression 內,同時有 signed 跟 unsigend,那 signed 其實會被 implicitly 解讀成 unsigned
以上擷取自 CMU 15-213 「Bits, Bytes, and Integers」課程投影片
若一個 expression 同時有 signed 和 unsigned 的數字,則 signed 數字會被解讀成 unsigned(bit pattern 不會變,只是被解讀成 unsigned)。
例如以下程式碼:
為什麼 return 的 x
是 0?因為問題出在 if
的判斷句: d <= MY_ARRAY_SIZE
(sizeof(my_arr)/sizeof(my_arr[0]))
得到的是無號的整數 size_t
。d
是有號數!d
就不可能小於等於 MY_ARRAY_SIZE
,所以 x
就會一直是 0。補充:使用 macro 時為什麼要多善用 ( )
考慮以下例子:
所以在使用 macro 時,良好習慣就是用 ( )
包起來,以確保運算順序是正確的
補充:32-bit 和 64-bit 電腦 Data Type Size
以上擷取自 CMU 15-213 「Bits, Bytes, and Integers」課程投影片
MIN
macro,也就是給兩個參數,return 比較小的那個要注意的地方:
#define
來定義 macro。Macro 可產生 inline code,而在嵌入式系統通常會滿常用到 inline code,因為可達到好的效能。if-then-else
更優化的程式碼。由於在嵌入式系統,「效能」是很重要的議題,因此懂得使用 ternary conditional operator 是很重要的。以上寫法,在經由 pre-processor 展開後會變成以下:
接下來,先來看 *p++
是在做什麼事~再看這樣的 macro 有可能會造成怎樣的問題。
首先,依據 C Operator Precedence 可以看到,postfix increment 的優先權是高於 *
dereference operator:
所以 *p++
其實是 *(p++)
,實際執行的順序是:
p
的值(也就是某個記憶體位址)取出來,然後做 dereference 以取得儲存在該記憶體位址的 datap
再遞增,指向下一個 data 所在的位址(也就是 p++
,會讓 p
增加一個 sizeof(<data_type>)
單位)而這項的 MIN
macro 的寫法會造成的問題是,假如 *p
所取得的 data 的值是 小於等於 b
,那 *p++
在 macro 展開後 會重複出現兩次,導致 p++
被執行兩次:
(*p++) <= (b)
(*p++) : (b)
執行所以最終 p
指向的記憶體位址會是:原本的記憶體位址 + 兩個 data type 的 size。
如以下例子:
可以看到 p
最後指向的記憶體位址是 0x16d0ef5e8
,也就是 0x16d0ef5e0
加上兩個 sizeof(int)
。
補充:使用 typeof
Operator
若要解決前述傳入 ++x
或 x++
而造成被執行兩次的問題,可以使用 typeof
Operator
在 C23 標準(ISO/IEC 9899:2024 (en) — N3220 working draft),typeof
成為了 C 語言的一個 operator(在 C23 之前,typeof
是 GNU GCC 的 Extension,但 C23 就成為 C 語言的標準了~)
typeof
更詳細的內容可以看 C23 標準 6.7.3.6 Typeof specifiers
此章節。
編譯並執行:
PS. 若要使用 C23 標準,可以將 GNU GCC 更新到 gcc-14
,並使用 -std=c23
這個 option 就能使用 C23 標準囉
若 GNU GCC 是版本是 gcc-13
,那就使用 -std=c2x
這個 option。
從以上結果就可以看到,*p++
其實就只有被執行一次~
補充:從組合語言來看 Ternary Conditional Operator
這篇原文作者有提到 ternary conditional operator 可以產生比 if-else 還好的程式碼,所以就觀察看看會產生怎樣的組合語言~
有以下兩個 .c
檔:
接下來,我在我的 ARM64 Ubuntu 24.04 虛擬機安裝 x86-64 的 cross-compiler:
用以下兩個指令產生 x86-64 的 intel 語法的 NASM 組合語言程式碼(因為我只有學過 NASM 所以才轉成 NASM 格式的 x86-64 組語 XD),且沒開編譯器最佳化(-O0
):
以下是 ternary_nasm_asm
的 main
:
以下是 if-else_nasm_asm
的 main
:
可以看到最主要的差異在於,這個例子的 ternary condition operator,使用了 x86-64 的 cmovge
(conditional move if greater or equal)指令,這種 condition move(CMOVcc)指令是 branchless 的。它會依據前面 cmp
指令的結果決定要不要將 edx
move 到 eax
。這種做法就不用跳來跳去,比較有效率。
而 if-else
的方式則會產生 conditional jump 的組合語言。如果預測錯誤,那 CPU pipeline 就要 flush 然後重抓指令,這樣會比較沒效率。而 conditional move 的指令就能避免這種事情發生,所以會更有效率。
這篇有對 conditional jump 和 conditional move 的一些說明
https://stackoverflow.com/questions/26154488/difference-between-conditional-instructions-cmov-and-jump-instructions
接下來來實際觀察看看執行時間的差異~
以下兩個程式碼,為了要看出比較顯著的差異,改成讓 ternary conditional operator 和 if-else 都各執行 100000000 次。
此外,每一個 iteration 都變更 n1
和 n2
的值,增加 misprediction 的機率。
用以下指令編譯:
結果如下:
可以看到執行時間差了一倍多,有顯著的差異存在~
#error
這個 Pre-processor 指令的目的為何?原文所提供的參考資料:In Praise of the
#error
Directive
Diagnostic directives - C
Diagnostic directives - C++
在 C/C++,#error
會終止編譯過程,並產生指定的錯誤訊息。使用此指令可以讓 programmer 在編譯早期就發現某些錯誤條件,讓 programmer 能即時發現和處理問題。
#error
指令的語法如下:
其中,error_message
是一個字串,表示要顯示的錯誤訊息。
ex.
有許多方法可以寫出無限迴圈,原文作者比較喜歡的是以下這個寫法:
另一個寫法如下:
第三個寫法如下:
a
寫出以下 definitions解答:
補充:CMU 15-213 課程提到如何解析 C 語言 Declaration 的技巧
這個解析的技巧就是:
*
、( )
、[ ]
這三種 operator( )
,再來是 [ ]
,最後是 *
( )
:代表是 function
( )
指的是 function 傳入參數的 ( )
ㄛ()
,就代表沒有指定這個 function 的參數(int, float)
則代表這個 function 的第一個參數是 integer,第二個參數是 float[ ]
:代表是 array*
:代表是 pointer知道這個技巧後~再複雜再變態的 declaration 都有辦法解析了 >///< 開心 🥳
真的大推 CMU 15-213 這門課!!!
補充:關於 f()
和 f(void)
在 C 和 C++ 的差別
這是 cppreference 關於 C++ Function declaration 的頁面:
可以看到在 C++,f()
和 f(void)
都是指 empty parameter list,就是指這個 function 沒有參數。
但在 C 語言:
這兩種 function 的宣告方是是不一樣的意思,f(void)
代表這個 function 沒有參數,而 f()
則代表這個 function 的參數未定。
因為我是先學 C++,所以我的認知一直是 C++ 的版本,直到最近才知道原來 f(void)
和 f()
在 C 語言是不等價的 😱🤯😵💫😵
!!!更新!!!
在 C23 有新的規範了。依據 C23 標準 § 6.7.7.4 (13),f()
這樣的宣告就相當於 f(void)
這樣的宣告,兩者的效力都是一樣的,都是指這個 function 是沒有參數的。
關於 C/C++
static
的用法,我覺得這篇講的超清楚的:https://shengyu7697.github.io/cpp-static/
static
這個 keyword 的作用是什麼?static
在 C 語言的三個主要用途:
.c
檔,只有該 .c
檔可以存取到該 global variable,其他 .c
檔無法存取到該 global variable.c
檔,只有該 .c
檔可以存取到該 function,其他 .c
檔無法存取到該 function所以 static
主要是可以延長 local variable 的生命週期,或是將 function/global variable 的作用範圍限制在該 .c
檔。
補充:C 語言的 Translation Unit、Scope、Static Storage Duration、Internal Linkage
Translation Unit
依據 C 語言標準 § 5.1.1.1 (1),一個 translation unit 是指一個 .c
檔(source file)加上經由其 #include
所包含的所有檔案,且是經過 preprocessor 處理展開後的整體內容。
Scope 作用域
依據 C 語言標準 § 6.2.1 (4):
if
block 或 while
block),或是在 function definition 的參數列表中被宣告,那這樣的 identifier 就具有 block scope。The static
Storage-Class Specifiers
依據 cppreference:
The
static
specifier specifies both static storage duration (unless combined with_Thread_local
)(since C11) and internal linkage (unless used at block scope).
It can be used with functions at file scope and with variables at both file and block scope, but NOT in function parameter lists.
使用 static
宣告的話,會被設定成 static storage duration
和 internal linkage
。
_Thread_local
關鍵字,那就會是 thread storage duration
,而不是 static storage duration
。static storage duration
。static
可以用在:
那什麼是 static storage duration
和 internal linkage
?
Static Storage Duration
每個物件都會有所謂的 storage duration
的特性,會用來決定物件的生命週期(lifetime)。C 語言有四種 storage duration,分別是 automatic
、static
、thread
、以及 allocated
。
依據 cppreference:
Static storage duration
. The storage duration is the entire execution of the program, and the value stored in the object is initialized only once, prior tomain
function. All objects declaredstatic
and all objects with either internal or external linkage that aren't declared_Thread_local
(until C23)thread_local
(since C23)(since C11) have this storage duration.
static storage duration
的物件,其生命週期就是程式執行的整個期間。main
function 被執行前就會被初始化。static storage duration
?
static
關鍵字宣告的物件_Thread_local
或 thread_local
的物件Internal Linkage
Linkage 是指一個 identifier(變數或 function)能否被其他 scope 存取到的能力。C 語言有三種 linkage,分別是 no linkage
、internal linkage
、以及 external linkage
。
依據 cppreference:
internal linkage
. The variable or function can be referred to from all scopes in the current translation unit.
All file scope variables which are declaredstatic
orconstexpr
(since C23) have this linkage, and all file scope functions declaredstatic
(static
function declarations are only allowed at file scope).
static
或 constexpr
的 file scope 變數static
的 file scope functions
static
function 的宣告只允許發生在 file scopeStatic Storage Duration 物件的初始化
依據 C 語言標準 § 6.7.11 (11),若 static storage duration
物件沒有被初始化,那他會被初始化成其 data type 的 0。
補充:static
變數會被放在哪
以上擷取自 CMU 15-213「Linking」課程的投影片
一個 Process 的 memory layout 包含 stack、heap、data section(data segment)、text section(text segment)。
其中 data section 又分成 .data
section 和 .bss
section。
.data
section 放的是有初始值的 global variable 和 static
variable.bss
section 放的則是無初始值的 global variable 和 static
variable,這些 variable 會被初始化成其 data type 的 0假設有以下程式碼:
用以下指令編譯:
執行的結果:
static
variable(無論是 global 或 local)都會被初始化成 0記憶體位址重新整理後如下:
記憶體位址 | 變數名稱 |
---|---|
0xffffdebe7b04 |
local_var_uninitialized |
0xffffdebe7b00 |
local_var_initialized |
0x420050 |
static_local_var_uninitialized |
0x42004c |
static_global_var_uninitialized |
0x420048 |
global_var_uninitialized |
0x420040 |
static_local_var_initialized |
0x42003c |
static_global_var_initialized |
0x420038 |
global_var_initialized |
static
variable 都被放在一起static
variable 也都被放在一起
objdump
去查看可執行檔的 symbol table,可看到有初始值的 global 和 static
變數都被放在 .data
sectionstatic
變數則都被放在 .bss
sectionconst
這個 keyword 的作用是什麼?用 const
修飾的變數,在初始化後就不能再修改他的值。編譯器負責在編譯時期檢查 const
變數是否有被修改。
以下變數的宣告,分別代表什麼意思:
前兩個都是 constant integer。
在這裡先提一下第五個 int const * a const;
,第五個是原文裡面出現的,作者想表達的是 constant pointer to constant,但是 const
在 C 語言不能寫在變數名稱後面,這樣在編譯時期會出現 error 喔,如以下截圖:
那 const
跟 pointer 名稱,到底該怎麼放?
補充:const
pointer 的語法
跟據 cppreference 的說明:
宣告 pointer 的語法是:
qualifiers
就是像 const
、volatile
這類的修飾詞declarator
則是我要宣告的 pointer 的名稱*
和 pointer 名稱之間的 qualifiers
,是用來修飾該 pointer* const <ptr_name>
就代表這是一個 constant pointer,也就是這個 pointer 只能指向固定的對象,不能讓他指向其他人
所以用來修飾 pointer 的 qualifiers
是出現在 *
和 pointer 名稱之間,且 qualifiers
不應該出現在 pointer 名稱的右側。
接下來~搭配前面在 Data declaration 提到的 CMU 15-213 課程提到的方法~
補充:const
的一些資訊
依據 C 語言標準 § 6.7.4.1 (7),如果嘗試用用一個 non-const lvalue 物件去修改一個 const
物件,這會是 undefined behavior。
依據 C 語言標準 § 6.7.4.1,compiler 可以選擇把 const
(且非 volatile
)的變數放進 read-only 的記憶體區域(像 .rodata
)。所以並不是 const
物件都一定會被放到 read only 記憶體區域。
此外,如果程式裡完全沒用到該變數的位址(&obj
),那 compiler 甚至有可能不幫你分配記憶體空間(直接在指令裡用常數就好)。
https://en.cppreference.com/w/c/language/const
如以下例子:
用一個 pointer 去指向一個 const
integer,然後再用這個 pointer 去修改它的值,這是 undefined behavior。雖然這段程式碼用 GNU GCC 可以編譯,但是會有 warning,且由於這是 undefined behavior,可能用不同編譯器的結果會不一樣。
volatile
這個 keword 的作用是什麼?舉三個不同的例子說明簡單來講~volatile
最主要的效果就是禁止編譯器對變數的存取做最佳化的動作,所以每次去存取 volatile
變數時都一定會去 memory 裡面存取該變數。
假設某個變數在程式碼裡面都沒被修改過,也沒有被宣告成 volatile
,那編譯器可能就會假設該變數的值都不會被更改,那編譯器在做最佳化的時候可能就會直接省略對該變數的存取,甚至完全移除對該變數的讀取動作。
(這是合理但錯誤的假設,因為如果變數會被其他外部因素更改,像是被 ISR 或其他執行緒修改,那這樣就會有問題)
或是說假如我剛剛已經存取過某個變數的值了,這個變數的值被放在 register,那我接下來要用他的話,編譯器就假設這個變數在 memory 裡面的值都是不會被修改,所以就沒必要再跑去 memory 裡面抓他的值,直接用放在 register 裡面的值就好了。
那怎樣的變數會需要被宣告成 volatile
哩?基本上,只要是「可能在程式碼看不到的情況下會被修改」的變數(像是會被 ISR 或其他執行緒修改),就應該宣告成 volatile
,這樣才能避免編譯器做出錯誤的最佳化。
需要設定成 volatile
的情境:
原文作者還問了以下幾個更深入的問題:
const
和 volatile
嗎?請解釋你的答案volatile
嗎?請解釋你的答案答案如下:
volatile
;且由於程式不應該試圖去修改它,所以他是 const
*ptr
指向的值的平方,但由於 *ptr
是 volatile
,所以編譯器會產生類似以下的程式碼:由於 *ptr
的值有可能會不預期地被改變,因此 a
和 b
的值有可能會是不一樣的,也因此有可能會 return 不是平方的數值。正確的寫法應該如下:
補充:從組合語言來觀察 volatile
的效果
a
和 b
這兩個變數後續都不會被操作,編譯器可能會假設這兩個變數都不會被修改和存取,所以若有開編譯器最佳化的話,就沒必要去存取那兩個變數的值然後 assign 給 a
/b
;但由於 global_volatile_var
有被設定為 volatile
,所以編譯器不會對這個變數做任何假設、也就不會做任何優化的動作,所以還是會去記憶體存取這個變數的值global_var
,僅會去記憶體存取 global_volatile_var
接下來,我在我的 ARM64 Ubuntu 24.04 虛擬機安裝 x86-64 的 cross-compiler:
用以下兩個指令產生 intel 語法的 NASM 組合語言程式碼,且分別是將編譯器最佳化開到最大(-O3
)和沒開編譯器最佳化(-O0
):
以下是沒開編譯器最佳化的 main
的內容:
global_var
和 global_volatile_var
的值以下是有開編譯器最佳化的 main
的內容:
main
裡面只有去記憶體存取 global_volatile_var
的值(line 5)假設有個 integer 變數 a
,請寫程式碼:
a
的 bit 3 設為 1
a
的 bit 3 設為 0
解法就是使用 #define
和 bit masks:
補充:其他 bit manipulation 考題
Toogle 第 k 個 bit,也就是若第 k 個 bit 是 1,那就把它變成 0;若是 0,則把它變成 1。
找出第 k 個 bit 是 0 或 1
將第 k 個 bit 設定為 b
mask
將第 k 個 bit 設定為 0b
左移 k 位,然後和前述得到的 n
做 |
operation完整程式碼:
原文作者也有提到有的人可能會用 bit-field 來達到這樣的效果,但 bit-field 會是 NON-PORTABLE,不同編譯器的實作會不一樣喔!所以不要用這種做法去做 bit manipulation
依據 C 語言標準 § 6.7.3.2 的內容,bit-field 是 high-order to low-order,或是 low-order to high-order,這是編譯器廠商決定的。
例如以下程式碼,我想用 bit-field 的方式將 num
的第三個 bit 設為 1:
我在 ARM64 的 Ubuntu 24.04 虛擬機用 GNU GCC 編譯:
接下來,測試用不同的編譯器是否會有差異,這裡用 PowerPC 編譯器來測試。首先要在 Ubuntu 安裝 PowerPC cross-compiler 和模擬器:
用 PowerPC cross-compiler 編譯:
執行結果:
test_ppc
是一個 32-bit 的 PowerPC 架構 ELF 可執行檔,且是靜態連結的版本。而 MSB
則是指 most significant byte first,也就是 Big Endiantest_arm64
則是一個 64-bit 的 ARM 架構可執行檔,且是 Little EndianPS. %b
是 C23 新加入的 conversion specifier,在使用 GNU GCC 編譯時記得要設定 -std=c2x
(for gcc-13)或 -std=c23
(for gcc-14)
0x67a9
存入一個 integer 0xaa55
有許多寫法可達到這效果,原文作者比較想看到的是類似以下的寫法:
另一種寫法如下:
原文作者建議在面試時寫第一個寫法比較好~
一般而言,編譯器廠商提供的 interrupt extension 的 keyword 最常見的會是 __interrupt
。
以下程式碼使用 __interrupt
定義了一個 interrupt service routine(ISR),對以下程式碼有何看法呢?
以上程式碼有許多錯誤
printf()
通常會有 reentrancy 和 效能的問題。補充:其他不該用在 ISR 的東西
Mutex 跟 Semaphore 也不建議用在 ISR 裡面,因為 ISR 要設計成執行時間很短、很快就結束。而 Mutex 跟 Semaphore 是 Blocking 機制,如果已經被鎖住,然後 ISR 又嘗試去鎖,那就會 block 了!
不過有些 RTOS 會提供可以在 ISR 裡面使用的版本,像是 FreeRTOS 就有 xSemaphoreGiveFromISR
這個專門用在 ISR 的版本,這是在 ISR 裡面「釋放」semaphore 的操作。
而在 ISR 裡面釋放 semaphore 大多都是為了要把另一個 task 叫醒,讓他去處理跟 interrupt 有關、但可能較複雜、會需要花比較多時間的事情,這種用法叫做 deferred interrupt handling。
補充:Interrupt Extension
原文用的 __interrupt
,我只有查到好像是 TMS320C6000 Optimizing Compiler
這個編譯器有提供這個 extension。他們的 user manual 也有提到:
You can only use the
__interrupt
keyword with a function that is defined to returnvoid
and that has NO parameters. The body of the interrupt function can have local variables and is free to use the stack or global variables. For example:
而 GNU GCC 用的是 __attribute__ ((interrupt ("IRQ")))
,使用方法是:
https://gcc.gnu.org/onlinedocs/gcc/ARM-Function-Attributes.html
補充:Thread Safety & Re-Entrancy
以上截自 CMU 15-213「Synchronization: Advanced」課程投影片
以上程式碼,a
是無號整數,b
是有號整數。
如前面所述,若一個 expression 同時有有號數和無號數,那有號數會被轉成無號數,所以 a+b
的 b
會是一個非常非常大的數,所以相加後一定是大於 6,所以會印出 > 6
。
原文作者說這在嵌入式系統非常重要,並建議在嵌入式系統應多使用 unsigned data types(原文作者推薦的文章:Efficient C Code for Eight-Bit MCUS)。
以上程式碼的 compzero
是想要取得 0 的 1 補數,但 0xFFFF
這寫法僅有在 int
是 16 bits 的系統才會得到正確的結果。
但現在大部分的 CPU,int
都是 32 bits,那以上寫法得到的 0 的 1 補數就會是錯誤的喔!
正確的寫法應該如下,這樣才能確保程式碼的可移植性:
原文作者說~好的嵌入式工程師是有需要對底層硬體有深入的瞭解和敏感度的!
在 64 bits 的電腦執行以下程式碼,可看到 ~zero
才能得到正確的結果:
原文作者期望聽到的回答包括像是:
malloc
的執行時間不是固定的,會根據 heap 的狀況而有差異。若 heap fragmentation 的狀況很嚴重,可能會需要找很久才能找到需要的 free memory block以下程式碼的 output 為何?以及為什麼?
依據 C 語言的標準,若做了 malloc(0)
這件事,那就是依據各家編譯器廠商自己實作而定,有可能會有兩種狀況:
NULL
而 GNU GCC 的實作是會回傳一個 non-NULL pointer。
In the GNU C Library, a successful
malloc(0)
returns a non-null pointer to a newly allocated size-zero block
https://www.gnu.org/software/libc/manual/html_node/Malloc-Examples.html
補充:有些嵌入式系統不會使用 std C lib 的 malloc
和 free
,因為
malloc
和 free
會佔用比較大的空間,在一些記憶體空間有限的嵌入式裝置並不適用補充:FreeRTOS 的 Dynamic Allocation 機制
FreeRTOS 提供五種 dynamic allocation 的機制,programmer 可以依照專案的需求選擇合適的方式。
heap_1
heap_1
現在已經比較少用了,大多是用在記憶體配置這件事都在初始化階段就完成、不需要做釋放記憶體這個操作的系統heap_2
heap_3
malloc
和 free
的 wrapper,但是在呼叫 malloc
和 free
之前,會先暫停 scheduler,確保是 thread safeheap_4
heap_2
很像,但是會將相鄰的 free memory block 做合併,以降低 fragmentation 的程度malloc
還有效率heap_5
heap_4
一樣,用 first fit,且會將相鄰的 free memory block 做合併Heap 1 示意圖
Heap 2 示意圖
Heap 2 是用 link list 去維護 free memory block,每個 free memory block 的起始位址都會放入 BlockLink_t
這個 struct。該 struct 的pxNextFreeBlock
是一個 pointer,會指向下一個 free memory block 的起始位址;然後 xBlockSize
是紀錄這個 free memory block 的大小。
xStart
是整個 link list 的開頭,xEnd
則是結尾,由這兩個串起維護 free memory block 的 link list,且這個 link list 會依照 free memory block 的 size 由小排到大。
這是 heap 剛初始化完的樣子,現在整個 heap 都是 free 的,是一個大的 free memory block,所以在他的起始位址會被放入一個 BlockLink_t
。
然後 xStart
的 pxNextFreeBlock
就會指向這個大的 free memory block 的起始位址;然後這個大的 free memory block 的 pxNectFreeBlock
就會指向 xEnd
。
這是分配了第一個 task 的狀況,第一個 task 用掉了圖中藍色那部分的 heap。剩下的 heap 還是一整塊 free 的 memory block,所以在剩下的 free memory block 的起始位址也會放入一個 BlockLink_t
,然後 xStart
的 pxNextFreeBlock
改成指向這個 free memory block 的起始位址,而這個 free memory block 的 pxNextFreeBlock
則指向 xEnd
。
橘色的部分則是一開始的 free memory block 放 BlockLink_t
的地方。
接下來又分配了第二個 task。
最後,假如釋放了那兩個 task 所佔用的記憶體區塊,由於 heap 2 沒有將相鄰 free memory block 合併的機制,所以會有一塊一塊的 free memory block,這樣就會有 fragmentation 的問題!
然後由於這個 free memory block list 會依據 free memory block 的大小由小排到大,所以可看到 xStart
的 pxNextFreeBlock
會先指向最小的那塊,而最小的那塊的 pxNextFreeBlock
則會指向第二小的那塊,第二小的那塊的 pxNextFreeBlock
則指向剩下最大的那塊,最大的那塊的 pxNextFreeBlock
就指向 xEnd
。
typedef
是用來為既有的 data type 建立別名。我們也可以用 #define
來達到類似的效果,譬如以下程式碼:以上兩種方式所宣告的 dPS
和 tPS
,都是 pointer,且都是指向 struct s
的 pointer,那哪一種方式比較好勒?以及為什麼哩?
考慮以下狀況:
使用 #define
的作法,在經由 preprocessor 展開後會變成:
這樣的話,只有 p1
是 pointer,而 p2
則是一個 struct s
。
所以使用 typedef
:+1: 會是比較好的做法,才能避免這種狀況發生。
這是合法的寫法,但關鍵在於編譯器該如何解析 a+++b
。該解析成 a++ + b
?或 a + ++b
勒?
編譯器在 Lexical analysis 階段,會依據 Maximum munch rule 來解析程式碼,也就是當編譯器遇到一串 characters 時,他會盡可能「吃掉」最多的 characters 來組成一個合法的 token。
所謂的「吃掉」就是指,盡量把相鄰的 characters 當成一個完整的語法單位,像是將 ++
視為一個 token,而不是視為 +
和 +
兩個 tokens。
所以當編譯器看到 a+++b
時,會先盡量吃掉最多的 characters 來組成一個合法的 token。所以一開始先吃掉 a++
,而這是合法的,剩下的 +b
也是合法的 token,那編譯器就會解析成 (a++) + b
。
那 c = (a++) + b
實際做的事情就是:
a
的值取出來,然後跟 b
相加,並 assign 給 c
,所以 c
是 12a++
,所以 a
的值變成 6所以各變數最後的 value 會是:
a
:6b
:7c
:12