在學及目前等兵單在各大論壇收集許多心得及面試準備方向和自學的資源,在此用hackmd整理一下並且當作學習進度的整理檢視自我的學習狀況也怕當兵後失智也方便自己複習,方便將來自己及後進及先進參考並且修改我自己對於某些CS和EE專業上的錯誤認知,還請各位多包涵。小弟的背景為地名理工學四大機械碩。
前置處理器或稱預處理器,會在程式編譯開始前先行作用,其目的是為了使程式碼更簡潔、可讀性更佳,參考Jserv老師的你所不知道的 C 語言:前置處理器應用篇中的一段話:
原來 C preprocessor 以獨立程式的形式存在,所以當我們用 gcc 或 cl (Microsoft 開發工具裡頭的 C 編譯器) 編譯給定的 C 程式時,會呼叫 cpp (伴隨在 gcc 專案的 C preprocessor) 一類的程式,先行展開巨集 (macro) 或施加條件編譯等操作,再來 才會出動真正的 C 語言編譯器 (在 gcc 中叫做 cc1)
可以讓我們更加了解一些其中的原理,主要的C語言程式執行過程的流程圖如下:
由上可看出.c
和 header file
會先經過preprocessor
展開巨集後再經由compiler
生成obj file
再透過linker
把多個obj file
打包成我們熟悉的可執行檔(.exe)
但GNU gcc
預設輸出檔名為a.out
。
常見的一些預處理器有:#define (macro)、#include
等並且預處理器有分為兩種形式:
#
:Stringification/Stringizing (字串化):讓一個表示式變成字串,在 assert 巨集用到。##
:concatenation (連結,接續)使用 #define 定義一個常數來計算一年有幾秒?
透過上述巨集可釐清一些關於預處理器的一些基本概念:
(1) 關於前置處理器語法上需注意的點(不能用分號約束,對括號的需求)
(2)好的名子的選擇,有大寫以及底線(這樣命名較可與程式碼內建變數做區隔)
(3)了解到使用預處理去計算常數運算式。相較自己去運算一年有多少秒來說簡潔許多
(4) 透過上述可了解到對16位元的變數型態進行計算可能會造成溢位(Overflow)所以改使用Long型態的變數進行計算
(5)當一個bonus,如果你更改運算式為UL(意指unsigned long),你會有一個好的開始,因為你注意到了signd與unsigned型態的危險(可能會造成位元表示型態不同)。
以下為其實作考量 使用 WSL Ubuntu 22.04 LTS
僅參考他人實作方式
輸出結果為:31536000
若將LU這個型態改為用int這個型態去實作為:
gcc
會提醒你print
的資料型態跟#define
計算的資料型態不同:
故我們將%lu
改為原本內建計算的%d
來進行計算可得正常輸出為:31536000
寫一個"標準的" MIN macro。也就是說,一個macro接收兩個參數並且回傳這兩個參數中較小的那個。
透過上述巨集我們可釐清一些觀念:
在C99之前還沒有inline這個關鍵字可以將某些程式碼嵌入於程式本體在此時只有#define可以將程式碼內嵌至程式本體中,對於嵌入式系統來說內嵌程式碼提升運行速率及效能是必須的(因為嵌入式系統相較個人電腦其硬體資源很小)
inline code是使用同一種語言或是其他語言嵌入於程式碼本體使其效能提升 EX:使用Assembly嵌入在C中提升效能,但現代編譯器內建優化已經很好了,其提升效能沒這麼多。(可參考:RISC-V GUN tool Chain)
三元運算子,這個在C語言中的存在因為它允許編譯器有潛力的製作比if-then-eles更佳的程式。因為嵌入式系統十分要求性能,使用三元運算子提升性能是非常重要的。
括號的影響對於巨集十分重要。
討論巨集的副作用
先參考本文章的標準答案:
輸出為:4
若把括號移除為:
其實並不影響輸出仍然為:4
但根據作者之提問least = MIN(*p++, b);
有何影響?
在此參考他人文章,上述程式碼會被替換為least=( (*p++) <= (b) ?(*p++):(b) )
,因此我參考他人程式碼進行實作:
連續執行五次發現每次指標指出的記憶體位址不同:
很像原本指向p++
位址的指標指向到其他位置去然後再與7
這個常數去比較?故在此修改原本測試用的程式碼,在此將p
指向一個陣列,這個陣列為4、5、6
這些元素所組成:
然後輸出為:
可以發現原本為指向4的位置的指標偏移成5來去與7這個數字進行比較,然後透過這行程式碼來去觀察 printf("current pos value of array:%d \n", *p);
目前執行位置,可發現偏移兩個位置,綜合上面的觀察,當我們使用:
會替換成下方的程式碼執行造成我們程式執行結果與我們設計程式的預期不同,如果是使用變數而不是陣列的話,會產生不可預期的錯誤,因為我們並沒有對偏移一個整數與兩個整數位置的地方初始化。
預處理指令#error
的目的是什麼?
#error
就是生成編譯錯誤的訊息,然後會停止編譯,可以用在檢查程式是否是照自己所預想的執行。其語法格式為:例如說我們可以可以我們可以在程式碼加些#ifndef
,如果偵測到沒有被define
,我們就可以出現使用#error
訊息中止程式,參考他人實作:
可得輸出為:
我們可以透過#error
確認NUM1
並沒有被define
並列印出來,接著我們把NUM1定義#define NUM1 6
可得輸出為:
無窮迴圈常見於韌體及嵌入式系統領域,故了解其性質及背後原因對於韌體工程師是非常重要的
無窮迴圈在嵌入式系統是很常見的,如何使用C
語言實作無窮迴圈?
有下列的實作方式:
因為
while loo
p是檢查是否為true or false
來執行迴圈,若寫成這樣,程式永遠為true
會持續執行迴圈。
這寫法的
for loop
沒有初始化也沒有檢查條件也沒有更新其實相等於while(1)
可以提及為C
語言的老爸們K&R
推薦的無窮迴圈的寫法。
剛學習
C
的時候常常被說goto
不要常用,但其實此方法在linux kernel
是很常用的,寫法跟Assembly function call
是類似的。
(a) An integer
(b) A pointer to an integer
© A pointer to a pointer to an integer
(d) An array of ten integers
(e) An array of ten pointers to integers
(f) A pointer to an array of ten integers
(g) A pointer to a function that takes an integer as an argument and returns an integer
(h) An array of ten pointers to functions that take an integer argument and return an integer.
Solution:
在此提及一下function pointer
的基本語法為:返回類型 (*指針名稱)(參數類型列表);
這代表它指向的為函數而不是單純記憶體位址,這是我一直困惑的地方或許未來要看一下jserv
老師的C語言講座。
實作以下指標操作 int *a[10]、int (*a)[10]、int (*a)(int)、int (*a[10])(int)
:
int *a[10]
在此簡化成四個指標的int *a[4]
的陣列,另外宣告了b=20、c=30、d=40、e=50這五個變數讓我們將a[0]指向b,a[1]指向c,a[2]指向d,a[3]指向e。可看出輸出為:
嘗試將每個指標+1觀察記憶體位址是否有聯動
觀察出指標及變數是有聯動的,確實有指向該變數的位址。
int(*a)[10]
簡化為 int(*a)[3]
然後使其指向二維陣列b[2][3]={{2,2,3},{3,5,6}}
;,並且讓a
指到b
輸出為:
函式指標(function pointer)
:int (*a)(int)、int (*a[10])(int)->int(*a[3])(int)
static有三種不同的用法:(好處)
在函數區間內(in funtion block),一個被宣告為靜態的變數,在函數被呼叫的過程中其值維持不變
在一個Block(ie. {…} )內 (但在函數外),一個被宣告為靜態的變數可以被Block內所有的函數存取,但不能被其他Block中的函數存取。它是一個本地的全局變數。(local的global變數)
在一個block內宣告為static的函數只可以被其他同一個block內的其他函數呼叫。也就是說,這個函數的範圍對它所宣告在的block而言是區域性的。
補充:在C語言中,static的三種作用:
- 1.隱藏功能,利用這一特性可以在不同的檔案中定義同名函式和同名變數,而不必擔心命名衝突
- 2.保持變數內容的持久,儲存在靜態資料區的變數會在程式剛開始執行時就完成初始化,也是唯一的一次初始化。
- 3.預設初始化為0,其實全域性變數也具備這一屬性,因為全域性變數也儲存在靜態資料區。在靜態資料區,記憶體中所有的位元組預設值都是0x00。
當應試者回答說 ‘const就是常數’,我知道我會認為他們是業餘的。Dan Saks去年已經辛苦的概括const,因此每一個ESP(Embedded System Programming)的讀者應該要很熟悉 const 對你而言可以做什麼以及不能做什麼。如果你還沒有讀到這個專欄,只要說 const 代表 “read-only” 就夠了。雖然這個答案並不是完全,但我接受它是一個正確的答案。(如果你想要知道更詳細的答案,那仔細的就去讀Saks的專欄)
含意:前兩個代表同一件事情,也就是說 a 是個 const (read-only) 整數。第三個代表 a 是個指向 const int 的指標(意即,整數無法修改,但是指標可以)。第四個宣告 a 是個指向整數的 const 指標(意即,被指到的整數可以修改,但是指標沒辦法)。最後一個宣告 a 是代表 const 指標指到 const integer(意即,被 a 指到的整數,或者是指標本身都無法修改)。
- 使用 const 概括一些非常有用的資訊對那些正在讀你程式的人。實際上,宣告一個 parameter const 會告訴使用者關於它的預期使用。如果你付了很多時間在清理其他人留下的混亂,你將會更快的學到感謝這個多餘的訊息。(當然,程式設計師使用 const ,很少會留下混亂給其他人清理)
Const有潛力產生更緊湊的程式碼藉由給予優化器一些附加資訊
使用 const 的程式碼自然的是固有的被編譯器保護來對抗不注意的程式結構導致不該改變的參數被改變。簡而言之,它們傾向擁有更少的bug
一個 volatile 變數會被不可預期的改變。因此,編譯器可以使這個變數沒有假設。具體來說,編譯器會小心的重載這個變數當這個變數每次被使用時,而不是保存一份拷貝在編譯器中。Volatile變數的範例如下:
( a ) 周邊設備的硬體暫存器,如狀態暫存器(state Register
)
( b ) 中斷服務函式(ISR)中會訪問到的非自動變數(Non-automatic variables
)
( c ) 多執行緒應用中,被多個任務共享的變數
volatile變數代表其所儲存的內容會不定時地被改變,宣告volatile變數用來告訴編譯器 (Compiler) 不要對該變數做任何最佳化操作,凡牽涉讀取該volatile變數的操作,保證會到該變數的實體位址讀取,而不會讀取CPU暫存器的內容 (提升效能) 。舉個例子,某一硬體狀態暫存器 (Status Register)就必須宣告volatile關鍵字,因為該狀態暫存器會隨時改變,宣告volatile便可確保每次讀取的內容都是最新的。
( a )一個參數可以同時是const也是volatile嗎?解釋為什麼。
( b )一個指標可以是volatile 嗎?解釋為什麼。
( c )下面的函數有什麼錯誤︰
答案如下:
( a )是的,舉例說明像是"read only的狀態暫存器"。它是volatile,因為它可能會被非預期的改變;它是const,因為程式不應該試圖修改它。
( b )是的,儘管這並不常見。一個例子是中斷服務函式修改一個指向buffer的指標時。
( c )這段程式碼的目的是用來返指標*ptr指向值的平方,但是,由於 *ptr指向一個volatile型參數,編譯器將產生類似下面的程式碼︰
因為*ptr的值可能會被不預期的改變,因此a和b可能是不同的。所以,這段程式碼可能返回不是你所期望的平方值!正確寫法的程式碼如下︰
Question
:如果沒有用volatile keyword
用在重複執行的變數上會發生什麼事?對於哪種instruction
會有影響?Ans:在重複運用變數時,
complier
會將重複執行pop/push等操作的變數優化掉使得程式不符合預期行為,在load/store instructions
會被其影響。
在 C 中提供的位元運算子,分別是AND
、OR
、NOT
、XOR
與補數等bitwise
或bytewise
的操作。
用 #defines
和 bit masks
操作。解決方法如下:
或使用巨集(macro)的寫法
重點是要看到明白的常數,以及使用 |= 和 &= ~結構。在韌體常用的位元操作通常都是bitwise。
嵌入式系統常有一個特點是要求程式設計失去存取特定的記憶體位置。在某個專案中被要求設定一個絕對位址在0x67a9的整數變數為數值0xaa55。編譯器是一個純ANSI編譯器。寫下程式碼來完成這個任務。
這個問題測試你是否知道為了存取一個絕對位置,去型別轉換一個整數成一個指標這是合法的。確切的語法根據每個人的風格因人而異。典型的程式碼如下:
當CPU在執行程式時,遇到外部或內部的緊急事件須優先處理,因此暫停執行當前的程式,轉而服務突發的事件。直到服務完畢,再回到原先的暫停處(記憶體地址)繼續執行原本尚未完成的程式。
這個函數有太多錯誤了,問題如下:
1.ISR不能返回一個值
2.ISR不能傳遞參數
3.在許多編譯器/處理器中,浮點數操作是不可重入的(re-entrant)。有些處理器/編譯器需要讓多餘的暫存器入棧(PUSH入堆疊),有些處理器/編譯器就是不允許在ISR中做浮點運算。此外,ISR應該是短而有效率的,在ISR中做浮點運算是不明智的。
4.與第三點類似,printf通常會有可重入和效能的問題。
這個問題測試你是否懂得C語言中的整數自動轉型原則。
這題的答案會輸出“> 6”。因為當表達式中存在singed與unsinged型態的時候,所有的運算元都會自動轉換為無符號類型(unsigned)。
因此–20變成了一個非常大的正整數,並且這個表達式計算出的結果大於6。這是個在嵌入式系統非常重要的點,因為unsigned的資料型態應該會被頻繁的使用。
對於一個int型不是16位元的機器來說,它將會導致錯誤。
正確的程式碼如下:
這個問題真的可以知道應試者是否了解字長在處理器的重要性。好的嵌入式程式設計師會清楚的知道硬體的細節與它的限制,然後電腦程式設計師傾向忽視硬體,並把它視為一個無法避免的煩惱。
會發生的問題像是記憶體碎片,碎片收集(垃圾回收)的問題,變量的生命週期(變數的執行時間)等等。
以上兩種情況都是要定義dPS和tPS為一個指向結構s的指標。哪個方法比較好,並解釋為什麼?
typedef更好。思考下面的例子:
第一個式子會被擴展成struct s * p1, p2
;
上面程式碼定義p1為一個指向結構的指標,p2為一個實際的結構變數,這並不是我們原本想要的。
第二個式子正確定義了p3和p4兩個指標。
根據“maximum munch”原則,編譯器應當能儘可能處理所有合法的用法。因此,上面的程式碼被處理成︰
c = a++ + b;
因此,在這個程式碼執行之後,a = 6, b = 7 & c = 12;
(其實a++ 就是後做,先運算完之後再++)
兩者的差異主要在於記憶體空間的佔用,struct佔用的記憶體空間至少為成員的總和,union佔用的記憶體空間為所有成員裡佔用最大空間的資料型態的size
(union變數裡面的成員會共用一個記憶體位址)
struct: 自定義的一種型別, 可以包含多個不同型別的變數, 每個成員都會配置一塊空間
union: 跟struct有點像, 主要差別是裡面的成員共用一塊記憶體, 所需記憶體由型別最大的成員決定
enum: 可以用來定義常數, 主要是可以提升程式可讀性, 裡面的值從值指定的值開始遞增, 預設為0
1.在儲存多個成員訊息時,編譯器會自動給struct的成員分配儲存空間,struct可以儲存多個成員訊息。而union每個成員會共用同一個儲存空間,只能儲存最後一個成員訊息。
2.都是由多個不同數據類型的成員組成,但在任何同一時刻,union只存放一個被先選中的成員,而struct的所有成員都存在。
3.對於union的不同成員賦值,將會對其他成員重寫,原來成員的值就不存在了;而對於struct的不同成員賦值,是互不影響的。
靜態區域變數:生命週期貫穿整個運行期間,直到程式結束。
靜態全域變數:生命週期貫穿整個運行期間,直到程式結束。
補充:一般宣告的區域變數,都是自動變數,即隨著宣告區域決定生命週期的變數。
1.extern:不同文件中想要互相使用的變量。當我們在某一個文件中定義了一個global variable,使用extern修飾變數即可在多個檔案中使用該變數。
2.static:包含同一個include檔的文件間想要互相使用的變量,但又不希望其他文件的操作改變本文件的變量。static 的意義就是 “被修飾的東西,會從程式一開始執行就存在,且不會因為離開 scope 就消失,會一直存在到程式結束”。
lvalue 定義為 "locator value",亦即 lvalue 是個物件的表示式 (an expression referring to an object),該物件的型態可以是一般的 object type 或 incomplete type,但不可為 void。換句話說,運算式的結果會是個有名稱的物件。
1.封裝(Encapsulation)的概念就是在程式碼中設置權限,讓不同的物件之間有不同的存取限制,而不是把所有資料都攤在陽光下讓大家使用,「封裝」可防止程式的原始碼被竄改,保障了資料的隱密性,並提高了程式的穩定性和安全性,最常用的三種:public、private和protected。
2.繼承性(Inheritance)的概念很簡單,可用日常生活的比喻來理解。例如,兒子繼承了爸爸的家業(子類別會繼承父類別的屬性和方法),所以兒子會有父親已經做過的東西,而不必再重新做一次。而在程式語言中,繼承最大的好處是可以不必一再撰寫重複的程式碼,不只節省心力和時間,更重要的是可以提高程式的可讀性,增加程式的結構化程度,並讓維護和新增功能時更加容易方便、減少錯誤。
3.多型性(Polymorphism)的概念,又可分為多載(Overloading)和複寫(Overriding),以下分成兩個子項目來解說。
一、多載(overloading):
多載的概念簡單來說,就是相同名稱的方法(Method),藉由傳給它不同的參數(函數的輸入值),它就會執行不同的敘述,以產生不同的輸出。就像是同一台果汁機,丟進去蘿蔔就會輸出蘿蔔汁,丟進去蘋果就會輸出蘋果汁。而且,由於蘋果比較硬,所以這台優秀的果汁機會自動把刀片旋轉的力道和轉速調強一點。
以上比喻,用程式語言的術語表達就是:「同一個方法(Method)會依據它的參數值(輸入值)的「型態」、「數量」,甚至「順序」的不同,自動選擇對應的定義,執行不同的敘述,輸出不同的結果」。
二、上面計算矩形面積的例子已經示範過何謂「多載」,而複寫(Overriding)是指子類別對其父類別的方法(method)做改寫、並取而代之。複寫的觀念也很容易理解,打個比方,兒子繼承了父親的公司,並且對公司制度做了多項改革,例如行政流程全面電腦化、導入ERP系統管理庫存、汰換過時的設備、甚至於人事變動…等,這就是複寫。
韌體工程師的0x10個問題-by chienyu
韌體工程師的0x10個問題-by Yunnie
面試整理-by 陳家錡
工程師應知道的0x10個問題 -by MuLong PuYang
A ‘C’ Test: The 0x10 Best Questions for Would-be Embedded Programmers
物件導向的三大特性 : 封裝,繼承,多型 -by metal35x
你所不知道的C語言:指標篇
你所不知道的 C 語言:前置處理器應用篇