C面試考題

2018.1.9 AndyLee

1. Function Pointer

void (*fptr)(type_a, type_b) = &func;

#include <stdio.h> int add(int a, int b) { return a+b; } int mult(int a, int b) { return a*b; } int main() { int (*op)(int a, int b); op = add; printf("op(3,5)=%d\n", op(3,5)); op = mult; printf("op(3,5)=%d\n", op(3,5)); }

2. Function Pointer Type

typedef void(F1)(void);

#include <stdio.h> typedef int(*OP)(int,int); int add(int a, int b) { return a+b; } int mult(int a, int b) { return a*b; } int main() { OP op = add; printf("op(3,5)=%d\n", op(3,5)); op = mult; printf("op(3,5)=%d\n", op(3,5)); }

3. 指標判讀

int a; // 一個整型數 int *a; // 一個指向整數的指標 int **a; // 一個指向指標的指標,它指向的指標是指向一個整型數 int a[10]; // 一個有10個整數型的陣列 int *a[10]; // 一個有10個指標的陣列,該指標是指向一個整數型的 int (*a)[10]; // 一個指向有10個整數型陣列的指標 int (*a)(int); // 一個指向函數的指標,該函數有一個整數型參數並返回一個整數 int (*a[10])(int); // 一個有10個指標的陣列,該指標指向一個函數,該函數有一個整數型參數並返回一個整數
const int *foo; // 一個 pointer,指向 const int 變數。 int const *foo; // 一個 pointer,指向 const int 變數。 int* const foo; // 一個 const pointer,指向 int 變數。 int const *const foo; // 一個 const pointer,指向 const int 變數。

4. Call by Value, Call by Reference, Call by Address

  1. call by value : 最常見的函式寫法,呼叫者和被呼叫者的變數各自佔有記憶體,將參數複製再傳給函式。

註:C 語言之父明確表示 C 語言只有 call by value。

void swap (int c , int d){ int temp=c; c=d; d=temp; } int main(){   int a=5,b=10;   swap(a,b);   printf(" %d %d ", a,b); }

Ans:5,10

  1. Call by reference : 呼叫者和被呼叫者的變數使用相同的記憶體位址,因此在被呼叫函式中改變變數時,變動結果會保留。

註:傳參考是C++才有的東西,C語言是沒有的唷!

void swap (int &c , int &d){    int temp=c;    c=d;    d=temp; } int main(){    int a=5,b=10;    swap(a,b);    printf(" %d %d ", a,b);    return 0; }

Ans:10,5

註:注意的是,一變數已經是別的變數的參考,就不可以再參考別的變數

int   a=5,b=10;
int   &c=a;
printf("c=%d \\n",c);
&c=b;  //錯誤,因為他已經參考了a
int   a=5;
int   &c;     //錯誤,傳參考必須在宣告的時候就一起給參考的對象,沒辦法之後再給。
  1. Call by address
    傳指標,仍然也是call by value,只不過複製的值Value剛好就是位址address

坊間有 call by address 的說法其實是方便教學,指的是對指標變數進行操作的 call by value或是call by value of pointer,具體的執行效果和 call by reference 一樣。

void swap (int *c , int *d){    int temp=*c;    *c=*d;    *d=temp; } int main(){    int a=5,b=10;    swap(&a,&b); //意思就是要把a跟b的位址給swap副程式c跟d使用    printf(" %d %d ", a,b); }

Ans:10,5


5. 變數範圍和生命周期

  1. local 變數 : local 變數僅活在該函式內,存放位置在 stack 記憶體中。

  2. static 變數 : static 變數生命周期(life time)跟程式一樣長,而範圍(scope)則維持不變,即在宣告的函式之外仍無法存取 static 變數。

  3. global 變數 : 所有區段皆可使用此變數

註:除了範圍不同,static 變數只有宣告的檔案可以使用;而 global 變數可加上 extern 關鍵字修飾,即可在其他檔案以 .h 標頭檔方式使用該變數 (也就是 internal linkage 和 external linkage 的不同)。


6. 關鍵字const

const 通常表示只可讀取不可寫入的變數,常用來宣告常數。使用const有以下好處:

  • 提升程式碼可讀性
  • 使編譯器保護那些不希望被改變的參數
  • 給優化器一些附加的資訊

編譯器處理方式 : define 在預處理階段展開;const 在編譯階段使用。
類型和安全檢查 : const 會在編譯階段會執行類型檢查,define 則不會。
存儲方式 : define 直接展開不會分配記憶體,const 則會在記憶體中分配。


7. 關鍵字 volatile

由於嵌入式系統常處理I/O、中斷、即時操作系統(RTOS)相關的問題,因此在嵌入式系統開發中 volatile 尤為重要。

被 volatile 修飾的變數代表它可能會被不預期的更新,因此告知編譯器不對它涉及的地方做最佳化,並在每次操作它的時候都讀取該變數實體位址上最新的值,而不是讀取暫存器的值。

volatile 常見的應用:

  • 修飾中斷處理程式中(ISR)中可能被修改的全域變數。
  • 修飾多執行緒(multi-threaded)的全域變數。
  • 設備的硬體暫存器(如狀態暫存器)

const 和 volatile 合用

extern const volatile unsigned int rt_clock;

這是在 RTOS kernel 常見的一種宣告:rt_clock通常是指系統時鐘,它經常被時鐘中斷進行更新。所以它是volatile。

因此在用的時候,要讓編譯器每次從記憶體裡面取值。而rt_clock通常只有一個寫者(時鐘中斷),其他地方對其的使用通常都是唯讀的。所以將其聲明為 const,表示這裏不應該修改這個變數。

所以volatile和const是兩個不矛盾的東西,並且一個物件同時具備這兩種屬性也是有實際意義的。


8. 關鍵字 inline

inline 可以將修飾的函式設為行內函式,即像巨集(#define)一樣將該函式展開編譯,用來加速執行速度。

inline 和 #define 的差別在於:

  • inline 函數只對參數進行一次計算,避免了部分巨集易產生的錯誤。
  • inline 函數的參數類型被檢查,並進行必要的型態轉換。
  • 巨集定義盡量不使用於複雜的函數
  • 用 inline 後編譯器不一定會實作,僅為建議

9. 巨集 #define

#define 是巨集,在前置處理器(preprocessor)執行時處理,將要替換的程式碼展開做文字替換。define 語法範例如下:``

#define PI 3.1415926 //常數巨集 #define A(x) x //函數巨集 #define MIN(A,B) ( (A) <= (B) ? (A) : (B))

注意把參數用括號括起來,不然容易發生以下錯誤:

#define SUM(a,b) a+b

當 SUM(2,5)*10 時,因為沒有括弧先乘除後加減,得輸出為 52,錯誤
正確寫法:

#define SUM(a,b) (a+b)

10. 未定義行為 (Undefined behavior)

常見的語法 i++ 和 ++i 具有以下性質:

  • i++ : 先用 i ,再將 i+1
  • ++i : 先 i+1,再用 i

因此會出現這種問題:

int i = 10
i = i++ + ++i;

這個問題的標準答案是

i = i++ + ++i;
i = 10 + ++i;
i = 10 + 12;
i = 22

語言規格在定義時為了編譯器實做上的彈性和效率考量,會刻意不去規定某些規格,因此如果寫出來的程式依賴或著錯用某些沒有在規格內所規定的特性時,我們就稱之為Undefined behavior,

reference: 萬惡的未定義行為


11. extern

  • 可以聲明變數會在其它的位置被定義,這個位置可能是在同一份文件之中,或是在其它文件之中
  • 不能在使用extern時同時指定其值,必須先聲明extern找到變數,再重新指定其值

12. Call Back Function

簡單的說,如果你使用了某個function,那麼你就是『call』了一個function。

如果系統或是函式是要求你給一個function pointer,這個function pointer指到一個實際的函式。然後它會在適當的時間呼叫此function,則此function就是所謂的 callback function。


13. Pointer

  • 指標空間釋放(delete)後,指標變數仍會記錄其位址,為避免誤用,釋放後應指向NULL
  • 有new就要有delete,若在副程式new又需回傳指標,則需在主程式接收指標後delete該空間,但不建議在副程式new記憶體且回傳指標,因若封裝給別人使用,別人不知道副程式裡面有new而沒delete掉,就會造成memory leak

14. const int* p 和 int* const q 兩者之差別:

  • 前指標指的內容不可變動,後指標不可變動
  • void dp()const; \我不會更改成員
  • const int* op() const{return &data;} //回傳const參考 保證不會更改

15. 寫出一個Macro求出兩數之最大值:

#define Max(x, y) ((x)>=(y)? (x): (y))

16. 指標與陣列的差別?

就記憶體方向來看,指標所用的記憶體位置不為連續,而"矩陣"所配置的空間為連續。


17. 給予10個任意整數,輸出其最小值、最大值、平均值

int Max = a[0], Min = a[0], Avg = 0; for(int i = 0 ; i < 10 ; i++) { if(Min > a[i]) Min = a[i]; if(Max < a[i]) Max = a[i]; Avg += a[i]; } cout << Min << Max << static_cast<float>(Avg)/10 << endl;

18. #define SUM(a,b) a+b

若是 #define SUM(2,5)*10 的答案是什麼?

Compiler展開後變成 2 + 5 * 10 = 52

Ans:52


19. 寫出一個字串拷貝程式: void StrCpy(char* dst , char* src) ;

// array version void strcpy(char *dst, char *src){ int i = 0; while((dst[i] = src[i]) != '\0') i++; }
// pointer version void strcpy(char *s, char *t) while((*s = *t) != '\0') { s++; t++; } }

19. 實作int strlen( const char *str )

int strlen( const char *str ){ int len; while( (*str++) != '\0' ){ len++; } return len; }

Ref :
http://www.voidcn.com/article/p-hfobsfai-bnz.html
http://www.cppblog.com/mydriverc/articles/35389.html


20. Interrupt Service Routine 之錯誤

__interrupt double isr(double r) //不能有參數
{
 double area = PI*r*r ;
 printf("%f\n",area) ; //改成printk
 return area ; // 不能有return 
}
  • ISR不能有返回值(不知道給誰)
  • ISR不能傳遞參數(不知道誰呼叫)
  • pintf改printk
  • ISR應短而高效

21. 設定一個絕對位址為0x67a9的整數型變數的值為0xaa55

int *ptr; ptr = (int *)0x67a9; // 設定指標變數的值 *ptr = 0xaa55; //設定指標變數指向的值

22. 一行程式碼 N是否為判斷2的次方

boolean isPowerof2(int n) { return n > 0 && (n & (n - 1)) == 0; }

二進位中,只要是 2 的次方,都剛好會是像 100 (4)或者 1000 (8)這種 1 開頭,尾巴都是 0 的狀況

8 的二進位 1000
7 的二進位 0111

1000
&
0111
-----
0000 = 0 (return true)

Reference: http://davidhsu666.com/410/ispowerby2


23. 下列程式碼輸出什麼

int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p = &(a + 1)[3]; printf("%d\n", *p);
  • 輸出 5
  • 因為a+1指向a的第二個元素,[3]表示再向後移動3個元素
  • a+1是跳一個int的大小(a想成指標)
  • &a+1是跳一整個array的大小

24. 連續呼叫 func 10 次,印出的值為何?

void func(void){ static int i = 0 ; i++ ; printf("%d" , i ) ; }

ANS: 12345678910


25. C 檢查錯誤

void GetMemory(char *p) { p=(char *)malloc(100); } void test() { char *str=NULL; GetMemory(str); strcpy(str,"hello world"); printf(str); }

传入GetMemory(char *p)函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值去执行完。


void GetMemory(void) { char p[]="hello world"; return p; } void test() { char *str=NULL; str=GetMemory(); printf(str); }

这段代码的p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。


void GetMemory(char **p,int num) { *p=(char *)malloc(num);//这段代码后面未进行内存申明成功的判断 } void test() { char *str=NULL; GetMemory(&str,100); strcpy(str,"hello"); printf(str); }

传入GetMemory的参数为字符串指针的指针,但是在GetMemory中执行申请内存及赋值语句。

*p=(char *)malloc(num);

后未判断内存是否申请成功,应加上:

if(*p == NULL) {
//进行申请内存失败处理
}


swap(int *p1,int *p2) { int *p; *p=*p1; *p1=*p2; *p2=*p; }

在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃。

在VC++中DEBUG运行时提示错误“ACCESS  Violation”。程序应该改写为:

swap(int * p1,int *p2) { int p=*p1; *p1=*p2; *p2=p; }

26. 下列印出什麼?

http://dummyh.pixnet.net/blog/post/9902458-【c語言】面試考古題-經驗分享-%E4%B8%AD%E7%B4%9A

A:
第一行字串為 : 0113234
第二行字串為 : 0123456

首先,*p = s 所代表的含意為 pointer p 指向字串s的第一個位址 0,而接下來一連串的運算可分成四類:

Operations Meaning
*p++ = *(p++) 先取值,後指標下移
*++p = *(++p) 指標下移,後取值
++*p = ++(*p) 先把該值+1,後取值
(*p)++ 先取值,後該值+1