Try   HackMD

軟/韌體工程師面試及資源和心得雜談(非本科雜魚)

在學及目前等兵單在各大論壇收集許多心得及面試準備方向和自學的資源,在此用hackmd整理一下並且當作學習進度的整理檢視自我的學習狀況也怕當兵後失智也方便自己複習,方便將來自己及後進及先進參考並且修改我自己對於某些CS和EE專業上的錯誤認知,還請各位多包涵。小弟的背景為地名理工學四大機械碩。

面試考題

C語言的0x10問題

預處理器(Preprocessor)

前置處理器或稱預處理器,會在程式編譯開始前先行作用,其目的是為了使程式碼更簡潔、可讀性更佳,參考Jserv老師的你所不知道的 C 語言:前置處理器應用篇中的一段話:

原來 C preprocessor 以獨立程式的形式存在,所以當我們用 gcc 或 cl (Microsoft 開發工具裡頭的 C 編譯器) 編譯給定的 C 程式時,會呼叫 cpp (伴隨在 gcc 專案的 C preprocessor) 一類的程式,先行展開巨集 (macro) 或施加條件編譯等操作,再來 才會出動真正的 C 語言編譯器 (在 gcc 中叫做 cc1)

可以讓我們更加了解一些其中的原理,主要的C語言程式執行過程的流程圖如下:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

由上可看出.cheader file會先經過preprocessor展開巨集後再經由compiler生成obj file再透過linker把多個obj file打包成我們熟悉的可執行檔(.exe)GNU gcc預設輸出檔名為a.out
常見的一些預處理器有:#define (macro)、#include等並且預處理器有分為兩種形式:

1. Using the #define statement, how would you declare a manifest constant that returns the number of seconds in a year? Disregard leap years in your answer.

使用 #define 定義一個常數來計算一年有幾秒?

#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL

透過上述巨集可釐清一些關於預處理器的一些基本概念:

  • (1) Basic knowledge of the #define syntax (i.e. no semi-colon at the end, the need to parenthesize etc.).

(1) 關於前置處理器語法上需注意的點(不能用分號約束,對括號的需求)

  • (2) A good choice of name, with capitalization and underscores.

(2)好的名子的選擇,有大寫以及底線(這樣命名較可與程式碼內建變數做區隔)

  • (3) An understanding that the pre-processor will evaluate constant expressions for you. Thus, it is clearer, and penalty free to spell out how you are calculating the number of seconds in a year, rather than actually doing the calculation yourself.

(3)了解到使用預處理去計算常數運算式。相較自己去運算一年有多少秒來說簡潔許多

  • (4) A realization that the expression will overflow an integer argument on a 16 bit machine – hence the need for the L, telling the compiler to treat the expression as a Long.

(4) 透過上述可了解到對16位元的變數型態進行計算可能會造成溢位(Overflow)所以改使用Long型態的變數進行計算

  • (5)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 — and remember, first impressions count!

(5)當一個bonus,如果你更改運算式為UL(意指unsigned long),你會有一個好的開始,因為你注意到了signd與unsigned型態的危險(可能會造成位元表示型態不同)。

以下為其實作考量 使用 WSL Ubuntu 22.04 LTS僅參考他人實作方式

#include<stdio.h>
#define SECONDS_PER_YEAR (60UL* 60UL* 24UL * 365UL)
int main()
{
        printf("%lu\n", SECONDS_PER_YEAR);
        return 0;
}

輸出結果為:31536000
若將LU這個型態改為用int這個型態去實作為:

#include<stdio.h>
#define SECONDS_PER_YEAR (60* 60* 24 * 365)
int main()
{
        printf("%lu\n", SECONDS_PER_YEAR);
        return 0;
}

gcc會提醒你print的資料型態跟#define計算的資料型態不同:

define.c: In function ‘main’:
define.c:7:19: warning: format ‘%lu’ expects argument of type ‘long unsigned int’, but argument 2 has type ‘int’ [-Wformat=]
    7 |         printf("%lu\n", SECONDS_PER_YEAR);
      |                 ~~^
      |                   |
      |                   long unsigned int
      |                 %u

故我們將%lu改為原本內建計算的%d來進行計算可得正常輸出為:31536000

2.Write the ‘standard’ MIN macro. That is, a macro that takes two arguments and returns the smaller of the two arguments.

寫一個"標準的" MIN macro。也就是說,一個macro接收兩個參數並且回傳這兩個參數中較小的那個。

#define MIN(A,B) ((A) <= (B) ? (A) : (B))

透過上述巨集我們可釐清一些觀念:

  • (1)Basic knowledge of the #define directive as used in macros. This is important, because until the inline operator becomes part of standard C, macros are the only portable way of generating inline code. Inline code is often necessary in embedded systems in order to achieve the required performance level.

在C99之前還沒有inline這個關鍵字可以將某些程式碼嵌入於程式本體在此時只有#define可以將程式碼內嵌至程式本體中,對於嵌入式系統來說內嵌程式碼提升運行速率及效能是必須的(因為嵌入式系統相較個人電腦其硬體資源很小)

inline code是使用同一種語言或是其他語言嵌入於程式碼本體使其效能提升 EX:使用Assembly嵌入在C中提升效能,但現代編譯器內建優化已經很好了,其提升效能沒這麼多。(可參考:RISC-V GUN tool Chain)

  • (2) Knowledge of the ternary conditional operator. This exists in C because it allows the compiler to potentially produce more optimal code than an if-then-else sequence. Given that performance is normally an issue in embedded systems, knowledge and use of this construct is important.

三元運算子,這個在C語言中的存在因為它允許編譯器有潛力的製作比if-then-eles更佳的程式。因為嵌入式系統十分要求性能,使用三元運算子提升性能是非常重要的。

  • (3) Understanding of the need to very carefully parenthesize arguments to macros.

括號的影響對於巨集十分重要。

  • (4) I also use this question to start a discussion on the side effects of macros, e.g. what happens when you write code such as :

討論巨集的副作用

least = MIN(*p++, b);

先參考本文章的標準答案:

#include<stdio.h>
#define MIN(A,B) ((A) <= (B) ? (A):(B))
int main(){
        printf("%d\n", MIN(4,7));
}

輸出為:4
若把括號移除為:

#define MIN(A,B) (A) <= (B) ? (A):(B)
#define MIN(A,B) A <= B ? A:B

其實並不影響輸出仍然為:4
但根據作者之提問least = MIN(*p++, b);有何影響?
在此參考他人文章,上述程式碼會被替換為least=( (*p++) <= (b) ?(*p++):(b) ),因此我參考他人程式碼進行實作:

#include<stdio.h>
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
int main()
{
        int a = 4;
        int *p = &a;
        printf("least value: %d\n", MIN(*p++, 7));
        return 0;
}

連續執行五次發現每次指標指出的記憶體位址不同:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

很像原本指向p++位址的指標指向到其他位置去然後再與7這個常數去比較?故在此修改原本測試用的程式碼,在此將p指向一個陣列,這個陣列為4、5、6這些元素所組成:

#include<stdio.h>
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
int main()
{
        int a[3] = {4, 5, 6};
        int *p = a;
        printf("least value: %d\n", MIN(*p++, 7));
        printf("current pos value of array:%d \n", *p);
        return 0;
}

然後輸出為:

least value: 5
current pos value of array:6

可以發現原本為指向4的位置的指標偏移成5來去與7這個數字進行比較,然後透過這行程式碼來去觀察 printf("current pos value of array:%d \n", *p);目前執行位置,可發現偏移兩個位置,綜合上面的觀察,當我們使用:

#define MIN(A,B) ((A) <= (B) ? (A) : (B))
    least = MIN(*p++, b);
    least=( (*p++) <= (b) ?(*p++):(b) )

會替換成下方的程式碼執行造成我們程式執行結果與我們設計程式的預期不同,如果是使用變數而不是陣列的話,會產生不可預期的錯誤,因為我們並沒有對偏移一個整數與兩個整數位置的地方初始化。

3.What is the purpose of the preprocessor directive #error?

預處理指令#error的目的是什麼?

  • #error就是生成編譯錯誤的訊息,然後會停止編譯,可以用在檢查程式是否是照自己所預想的執行。其語法格式為:
#error error-message

例如說我們可以可以我們可以在程式碼加些#ifndef,如果偵測到沒有被define,我們就可以出現使用#error訊息中止程式,參考他人實作:

#include<stdio.h>
int main()
{
#ifndef NUM1
#error No define NUM1
#endif
        printf("%d\n", NUM1);
        return 0;
}

可得輸出為:

error.c: In functionmain’:
error.c:5:2: error: #error No define NUM1
    5 | #error No define NUM1
      |  ^~~~~
error.c:7:24: error: ‘NUM1undeclared (first use in this function)
    7 |         printf("%d\n", NUM1);
      |                        ^~~~
error.c:7:24: note: each undeclared identifier is reported only once for each function it appears in

我們可以透過#error確認NUM1並沒有被define並列印出來,接著我們把NUM1定義#define NUM1 6可得輸出為:

image

無窮迴圈(Infinite loops)

無窮迴圈常見於韌體及嵌入式系統領域,故了解其性質及背後原因對於韌體工程師是非常重要的

4. Infinite loops often arise in embedded systems. How does one code an infinite loop in C?

無窮迴圈在嵌入式系統是很常見的,如何使用C語言實作無窮迴圈?
有下列的實作方式:

while(1)

{

…

}

因為while loop是檢查是否為true or false來執行迴圈,若寫成這樣,程式永遠為true會持續執行迴圈。

for(;;)

{

…

}

這寫法的for loop沒有初始化也沒有檢查條件也沒有更新其實相等於while(1)可以提及為C語言的老爸們K&R推薦的無窮迴圈的寫法。

goto Loop;

剛學習C的時候常常被說goto不要常用,但其實此方法在linux kernel是很常用的,寫法跟Assembly function call是類似的。

5.Using the variable a, write down definitions for the following:

(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:

(a) int a; // An integer
(b) int *a; // A pointer to an integer
(c) int **a; // A pointer to a pointer to an integer
(d) int a[10]; // An array of 10 integers
(e) int *a[10]; // An array of 10 pointers to integers
(f) int (*a)[10]; // A pointer to an array of 10 integers
(g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer
(h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer   

在此提及一下function pointer的基本語法為:返回類型 (*指針名稱)(參數類型列表);這代表它指向的為函數而不是單純記憶體位址,這是我一直困惑的地方或許未來要看一下jserv老師的C語言講座。
實作以下指標操作 int *a[10]、int (*a)[10]、int (*a)(int)、int (*a[10])(int):

  • 1.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。
#include<stdio.h>
int main(){
 int *a[4];
 int b=20,c=30,d=40,e=50;
 a[0] = &b;
 a[1] = &c;
 a[2] = &d;
 a[3] = &e;

 printf("*a[0]:%d\n", *a[0]);
 printf("*a[1]:%d\n", *a[1]);
 printf("*a[2]:%d\n", *a[2]);
 printf("*a[3]:%d\n", *a[3]);

 return 0;
}

可看出輸出為:

*a[0]:20
*a[1]:30
*a[2]:40
*a[3]:50

嘗試將每個指標+1觀察記憶體位址是否有聯動

*a[0]:20
*a[1]:30
*a[2]:40
*a[3]:50
*a[0]:21 and b is 21
*a[1]:31 and c is 31
*a[2]:41 and d is 41
*a[3]:51 and e is 51

觀察出指標及變數是有聯動的,確實有指向該變數的位址。

    1. int(*a)[10] 簡化為 int(*a)[3]然後使其指向二維陣列b[2][3]={{2,2,3},{3,5,6}};,並且讓a指到b
#include<stdio.h>
int main(){
int (*a)[3];
int b[2][3]={{2,2,3},{3,5,6}};
a=b;
printf("%d\n", *(*a));
printf("%d\n", *(*a+1));
printf("%d\n", *(*a+2));

printf("%d\n", *(*(a+1)));
printf("%d\n", *(*(a+1)+1));
printf("%d\n", *(*(a+1)+2));

return 0;
}

輸出為:

2
2
3
3
5
6
  • 3.函式指標(function pointer):int (*a)(int)、int (*a[10])(int)->int(*a[3])(int)
#include <stdio.h>

int func(int num) {
    return num;
};

int main(int argc, char* argv[])
{
    int (*a)(int) = func;
    printf("%d\n", a(10)); // use function pointer
    return 0;
}
#include <stdio.h>
int b(int num)
{
    return 2*num;
}
int c(int num)
{
    return 3*num;
}
int d(int num)
{
    return 4*num;
}
int main(int argc, char* argv[])
{
    int (*a[3])(int);
    a[0] = b;
    a[1] = c;
    a[2] = d;
    printf("%d\n", a[0](1));//也可以寫成(*a[0])(1)
    printf("%d\n", a[1](1));
    printf("%d\n", a[2](1));
    return 0; //   a => &a function ptr
}

6. What are the uses of the keyword static?

static有三種不同的用法:(好處)

    1. A variable declared static within the body of a function maintains its value between function invocations.

在函數區間內(in funtion block),一個被宣告為靜態的變數,在函數被呼叫的過程中其值維持不變

  • 2.A variable declared static within a module [1], (but outside the body of a function) is accessible by all functions within that module. It is not accessible by functions within any other module. That is, it is a localized global.

在一個Block(ie. {…} )內 (但在函數外),一個被宣告為靜態的變數可以被Block內所有的函數存取,但不能被其他Block中的函數存取。它是一個本地的全局變數。(local的global變數)

  • 3.Functions declared static within a module may only be called by other functions within that module. That is, the scope of the function is localized to the module within which it is declared.

在一個block內宣告為static的函數只可以被其他同一個block內的其他函數呼叫。也就是說,這個函數的範圍對它所宣告在的block而言是區域性的。

另一種答案
  • 1.function內做靜態變數宣告,在這一函數被呼叫的過程中其值維持不變
  • 2.function外做靜態變數宣告或靜態函數宣告,代表此變數或此函數只能被同file的函數做存取

補充:在C語言中,static的三種作用:

  • 1.隱藏功能,利用這一特性可以在不同的檔案中定義同名函式和同名變數,而不必擔心命名衝突
  • 2.保持變數內容的持久,儲存在靜態資料區的變數會在程式剛開始執行時就完成初始化,也是唯一的一次初始化。
  • 3.預設初始化為0,其實全域性變數也具備這一屬性,因為全域性變數也儲存在靜態資料區。在靜態資料區,記憶體中所有的位元組預設值都是0x00。
  • 待補實作

7.What does the keyword const mean?

當應試者回答說 ‘const就是常數’,我知道我會認為他們是業餘的。Dan Saks去年已經辛苦的概括const,因此每一個ESP(Embedded System Programming)的讀者應該要很熟悉 const 對你而言可以做什麼以及不能做什麼。如果你還沒有讀到這個專欄,只要說 const 代表 “read-only” 就夠了。雖然這個答案並不是完全,但我接受它是一個正確的答案。(如果你想要知道更詳細的答案,那仔細的就去讀Saks的專欄)

What do the following incomplete [2] declarations mean?
const int a;

int const a;

const int *a;

int * const a;

int const * a const;

含意:前兩個代表同一件事情,也就是說 a 是個 const (read-only) 整數。第三個代表 a 是個指向 const int 的指標(意即,整數無法修改,但是指標可以)。第四個宣告 a 是個指向整數的 const 指標(意即,被指到的整數可以修改,但是指標沒辦法)。最後一個宣告 a 是代表 const 指標指到 const integer(意即,被 a 指到的整數,或者是指標本身都無法修改)。

  • 1.The use of const conveys some very useful information to someone reading your code. In effect, declaring a parameter const tells the user about its intended usage. If you spend a lot of time cleaning up the mess left by other people, then you’ll quickly learn to appreciate this extra piece of information. (Of course, programmers that use const, rarely leave a mess for others to clean up…)
  1. 使用 const 概括一些非常有用的資訊對那些正在讀你程式的人。實際上,宣告一個 parameter const 會告訴使用者關於它的預期使用。如果你付了很多時間在清理其他人留下的混亂,你將會更快的學到感謝這個多餘的訊息。(當然,程式設計師使用 const ,很少會留下混亂給其他人清理)
  • 2.const has the potential for generating tighter code by giving the optimizer some additional information.

Const有潛力產生更緊湊的程式碼藉由給予優化器一些附加資訊

  • 3.Code that uses const liberally is inherently protected by the compiler against inadvertent coding constructs that result in parameters being changed that should not be. In short, they tend to have fewer bugs.

使用 const 的程式碼自然的是固有的被編譯器保護來對抗不注意的程式結構導致不該改變的參數被改變。簡而言之,它們傾向擁有更少的bug

  • 待補實作

8.What does the keyword volatile mean? Give three different examples of its use.

一個 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 )下面的函數有什麼錯誤︰

int square(volatile int *ptr){
    return *ptr * *ptr;
}

答案如下:
( a )是的,舉例說明像是"read only的狀態暫存器"。它是volatile,因為它可能會被非預期的改變;它是const,因為程式不應該試圖修改它。
( b )是的,儘管這並不常見。一個例子是中斷服務函式修改一個指向buffer的指標時。
( c )這段程式碼的目的是用來返指標*ptr指向值的平方,但是,由於 *ptr指向一個volatile型參數,編譯器將產生類似下面的程式碼︰

int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}

因為*ptr的值可能會被不預期的改變,因此a和b可能是不同的。所以,這段程式碼可能返回不是你所期望的平方值!正確寫法的程式碼如下︰

int square(volatile int *ptr){
    int a;
    a = *ptr;
    return a*a;
}
  • Question:如果沒有用volatile keyword用在重複執行的變數上會發生什麼事?對於哪種instruction會有影響?

Ans:在重複運用變數時,complier會將重複執行pop/push等操作的變數優化掉使得程式不符合預期行為,在load/store instructions會被其影響。

  • 待補實作

位元操作(Bit Manipulation)

在 C 中提供的位元運算子,分別是ANDORNOTXOR與補數等bitwisebytewise的操作。

9.Embedded systems always require the user to manipulate bits in registers or variables. Given an integer variable a, write two code fragments. The first should set bit 3 of a. The second should clear bit 3 of a. In both cases, the remaining bits should be unmodified.

#definesbit masks 操作。解決方法如下:

//寫法一
#define BIT3(0x1<<3)
static int a;

void set_bit3(void){
    a |= BIT3;
}

void clear_bit3(void){
    a &= ~BIT3;
}
--------------
//寫法二
#define BIT3 (1U << 3)
int main()
{
    int a = 0;
    /* set bit 3 */
    a |= BIT3;
    /* clear bit 3 */
    a &= ~BIT3;
}

或使用巨集(macro)的寫法

#define SET_BIT(p,n) ((p) |= (1 << (n)))
#define CLEAR_BIT(p,n) ((p) &= ~(1 << (n)))
#define FLIP_BIT(p,n) ((p) ^= (1 << (n)))
#define CHECK_BIT(p,n) ((p) & (1 << (n))

重點是要看到明白的常數,以及使用 |= 和 &= ~結構。在韌體常用的位元操作通常都是bitwise。

  • 待補實作

10.Embedded systems are often characterized by requiring the programmer to access a specific memory location. On a certain project it is required to set an integer variable at the absolute address 0x67a9 to the value 0xaa55. The compiler is a pure ANSI compiler. Write code to accomplish this task.

嵌入式系統常有一個特點是要求程式設計失去存取特定的記憶體位置。在某個專案中被要求設定一個絕對位址在0x67a9的整數變數為數值0xaa55。編譯器是一個純ANSI編譯器。寫下程式碼來完成這個任務。
這個問題測試你是否知道為了存取一個絕對位置,去型別轉換一個整數成一個指標這是合法的。確切的語法根據每個人的風格因人而異。典型的程式碼如下:

int main(){
    int *ptr = (int *)0x67a9;
    *ptr = 0xaa55;
}
  • 待補實作

中斷(interrupt)

當CPU在執行程式時,遇到外部或內部的緊急事件須優先處理,因此暫停執行當前的程式,轉而服務突發的事件。直到服務完畢,再回到原先的暫停處(記憶體地址)繼續執行原本尚未完成的程式。

11. Interrupts are an important part of embedded systems. Consequently, many compiler vendors offer an extension to standard C to support interrupts. Typically, this new key word is __interrupt. The following code uses __interrupt to define an interrupt service routine. Comment on the code.

__interrupt double compute_area(double radius)
{
    double area = PI*radius*radius;
    printf("\nArea=%f", area);
    return area;
}

這個函數有太多錯誤了,問題如下:
1.ISR不能返回一個值
2.ISR不能傳遞參數
3.在許多編譯器/處理器中,浮點數操作是不可重入的(re-entrant)。有些處理器/編譯器需要讓多餘的暫存器入棧(PUSH入堆疊),有些處理器/編譯器就是不允許在ISR中做浮點運算。此外,ISR應該是短而有效率的,在ISR中做浮點運算是不明智的。
4.與第三點類似,printf通常會有可重入和效能的問題。

  • 待補實作

12.What does the following code output and why?

void foo(void)
{
    unsigned int a = 6;
    int b = -20;
    (a + b > 6) ? puts(">6")puts("<=6");
}    

這個問題測試你是否懂得C語言中的整數自動轉型原則。

這題的答案會輸出“> 6”。因為當表達式中存在singed與unsinged型態的時候,所有的運算元都會自動轉換為無符號類型(unsigned)。
因此–20變成了一個非常大的正整數,並且這個表達式計算出的結果大於6。這是個在嵌入式系統非常重要的點,因為unsigned的資料型態應該會被頻繁的使用。

  • 待補實作

13.Comment on the following code fragment?

unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/* 1's complement of zero */

對於一個int型不是16位元的機器來說,它將會導致錯誤。
正確的程式碼如下:

unsigned int compzero = ~0;

這個問題真的可以知道應試者是否了解字長在處理器的重要性。好的嵌入式程式設計師會清楚的知道硬體的細節與它的限制,然後電腦程式設計師傾向忽視硬體,並把它視為一個無法避免的煩惱。

  • 待補實作

動態記憶體配置(Dynamic Memory Allocation)

14.Although not as common as in non-embedded computers, embedded systems still do dynamically allocate memory from the heap. What are the problems with dynamic memory allocation in embedded systems?

會發生的問題像是記憶體碎片,碎片收集(垃圾回收)的問題,變量的生命週期(變數的執行時間)等等。

  • 待補充

15.Typedef is frequently used in C to declare synonyms for pre-existing data types. It is also possible to use the preprocessor to do something similar. For instance, consider the following code fragment:

#define dPS struct s *
typedef struct s * tPS;

以上兩種情況都是要定義dPS和tPS為一個指向結構s的指標。哪個方法比較好,並解釋為什麼?
typedef更好。思考下面的例子:

dPS p1, p2;
tPS p3, p4;

第一個式子會被擴展成struct s * p1, p2;
上面程式碼定義p1為一個指向結構的指標,p2為一個實際的結構變數,這並不是我們原本想要的。
第二個式子正確定義了p3和p4兩個指標。

  • 待補實作
int a = 5, b = 7, c;
c = a+++b;

根據“maximum munch”原則,編譯器應當能儘可能處理所有合法的用法。因此,上面的程式碼被處理成︰
c = a++ + b;
因此,在這個程式碼執行之後,a = 6, b = 7 & c = 12;
(其實a++ 就是後做,先運算完之後再++)

  • 待補實作

其餘常見考題

1.struct 與 union之差異

兩者的差異主要在於記憶體空間的佔用,struct佔用的記憶體空間至少為成員的總和,union佔用的記憶體空間為所有成員裡佔用最大空間的資料型態的size
(union變數裡面的成員會共用一個記憶體位址)
struct: 自定義的一種型別, 可以包含多個不同型別的變數, 每個成員都會配置一塊空間
union: 跟struct有點像, 主要差別是裡面的成員共用一塊記憶體, 所需記憶體由型別最大的成員決定
enum: 可以用來定義常數, 主要是可以提升程式可讀性, 裡面的值從值指定的值開始遞增, 預設為0

1.在儲存多個成員訊息時,編譯器會自動給struct的成員分配儲存空間,struct可以儲存多個成員訊息。而union每個成員會共用同一個儲存空間,只能儲存最後一個成員訊息。
2.都是由多個不同數據類型的成員組成,但在任何同一時刻,union只存放一個被先選中的成員,而struct的所有成員都存在。
3.對於union的不同成員賦值,將會對其他成員重寫,原來成員的值就不存在了;而對於struct的不同成員賦值,是互不影響的。

2.Static函數的生命週期、使用時間點

靜態區域變數:生命週期貫穿整個運行期間,直到程式結束。
靜態全域變數:生命週期貫穿整個運行期間,直到程式結束。
補充:一般宣告的區域變數,都是自動變數,即隨著宣告區域決定生命週期的變數。

3.extern和static的差異

1.extern:不同文件中想要互相使用的變量。當我們在某一個文件中定義了一個global variable,使用extern修飾變數即可在多個檔案中使用該變數。
2.static:包含同一個include檔的文件間想要互相使用的變量,但又不希望其他文件的操作改變本文件的變量。static 的意義就是 “被修飾的東西,會從程式一開始執行就存在,且不會因為離開 scope 就消失,會一直存在到程式結束”。

4.lvalue & rvalue

lvalue 定義為 "locator value",亦即 lvalue 是個物件的表示式 (an expression referring to an object),該物件的型態可以是一般的 object type 或 incomplete type,但不可為 void。換句話說,運算式的結果會是個有名稱的物件。

5.解釋封裝,繼承,多型

1.封裝(Encapsulation)的概念就是在程式碼中設置權限,讓不同的物件之間有不同的存取限制,而不是把所有資料都攤在陽光下讓大家使用,「封裝」可防止程式的原始碼被竄改,保障了資料的隱密性,並提高了程式的穩定性和安全性,最常用的三種:public、private和protected。
2.繼承性(Inheritance)的概念很簡單,可用日常生活的比喻來理解。例如,兒子繼承了爸爸的家業(子類別會繼承父類別的屬性和方法),所以兒子會有父親已經做過的東西,而不必再重新做一次。而在程式語言中,繼承最大的好處是可以不必一再撰寫重複的程式碼,不只節省心力和時間,更重要的是可以提高程式的可讀性,增加程式的結構化程度,並讓維護和新增功能時更加容易方便、減少錯誤。
3.多型性(Polymorphism)的概念,又可分為多載(Overloading)和複寫(Overriding),以下分成兩個子項目來解說。
一、多載(overloading):
多載的概念簡單來說,就是相同名稱的方法(Method),藉由傳給它不同的參數(函數的輸入值),它就會執行不同的敘述,以產生不同的輸出。就像是同一台果汁機,丟進去蘿蔔就會輸出蘿蔔汁,丟進去蘋果就會輸出蘋果汁。而且,由於蘋果比較硬,所以這台優秀的果汁機會自動把刀片旋轉的力道和轉速調強一點。
以上比喻,用程式語言的術語表達就是:「同一個方法(Method)會依據它的參數值(輸入值)的「型態」、「數量」,甚至「順序」的不同,自動選擇對應的定義,執行不同的敘述,輸出不同的結果」。
二、上面計算矩形面積的例子已經示範過何謂「多載」,而複寫(Overriding)是指子類別對其父類別的方法(method)做改寫、並取而代之。複寫的觀念也很容易理解,打個比方,兒子繼承了父親的公司,並且對公司制度做了多項改革,例如行政流程全面電腦化、導入ERP系統管理庫存、汰換過時的設備、甚至於人事變動…等,這就是複寫。

專業科目

韌體相關開放式課程的資源(待補)

  1. 作業系統 清大周志遠 教授
  2. 自動控制系統 交大 吳炳飛 教授
  3. 數位訊號處理
  4. 計算機結構 (Jserv) 黃敬群 教授
  5. Linux核心實作 (Jserv) 黃敬群 教授

計算機結構及面試考題

資料結構及演算法及面試考題

作業系統及面試考題

Linux kernel(Jserv老師)及面試考題

控制系統及面試考題

數位邏輯及數位系統及面試考題

訊號與系統及數位訊號處理及面試考題

其他補充資料

C語言筆記及練習

參考資料

韌體工程師的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 語言:前置處理器應用篇