目錄表
Common concept of C/C++
0x00 前言
最近在找面試資料時從 Mr.Opengate 的 Blog 中看到這篇整理得很棒的文章
因為怕哪天原鏈結消失,所以我習慣整理消化成自己的筆記放過來
這邊附上 source C/C++ - 常見 C 語言觀念題目總整理(適合考試和面試)
Blog 內也有其他很棒的文章,有興趣的讀者不妨過去看看
以及這篇 小小科學實驗室 作者以工作面試的角度點出了 C 語言中的諸多小細節,也很受用
0x01 Outline
- 指標
- call by value, call by reference
- 變數的特性
- sizeof
- 延伸性資料型態:struct、typedef、union 和 enum
- 關鍵字 const
- 關鍵字 volatile
- bit operation
- 進制轉換
- 前處理器相關
- 關鍵字 inline
- 字串處理
0x02 指標
指標 (pointer) 是代表記憶體位置的變數,換句話說,指標也是一個變數,只是這個變數存放的值是其他變數的記憶體位置
int x = 5; int *xPtr = &x; int *yPtr, zPtr; /* equals int *yPtr; int zPtr; not equals int *yPtr; int *zPtr; */
- 這邊透過
int *
宣告了一個指向整數的指標 yPtr &
是取址運算子,會回傳運算元(變數)位址,這邊會取到整數變數 x 的記憶體位置並賦予給 xPtr- * 為間接運算子,或反參考運算子,會回傳運算元(指標)所指向物件的值,這邊 *xPtr 的值會等於 5
- 星號(*) 的效用不會作用到同一行宣告內的所有變數名稱,指標變數都需要個別宣告:
int *yPtr, *zPtr;
陣列與指標
指標與陣列關係非常密切
int array[5] = {1, 2, 3, 4, 5}; int *aPtr = array; // equals: int *aPtr = &array[0];
- 陣列的名稱可以想像成一個常數指標,他指向陣列中第一個元素的記憶體位置
- 在這邊
array
這個陣列名稱指向的就是array[0]
的記憶體位置,array[0]
是一個整數,值為 1 aPtr
這個整數指標也會跟 array 一樣指向array[0]
的記憶體位置- 這邊
aPtr
和array
的差別在於array
是常數指標,無法被賦值改變,而aPtr
是指標但不是常數指標,所以是可以透過 ++, – 等運算元做 offset 運算的
指標的算術運算
承上,當指標指向陣列時,由於陣列中的元素在記憶體中的位置是連續的,所以我們可以對指標做一些算數運算
int a[5] = {1, 2, 3, 4, 5}; int *aPtr = &a[0]; int *bPtr = aPtr + 3; /* 位置 3000 3004 3008 3012 3016 | | | | | |______|______|______|______|______ | | | | | | | a[0] | a[1] | a[2] | a[3] | a[4] | |______|______|______|______|______| ^ ^ ^ | | | | | | 指標變數 aPtr aPtr + 1 bPtr */
- 我們宣告了整數指標變數 aPtr 指向陣列開頭,也就是第一個元素的記憶體位置
- *(aPtr + 1) 為 a[1] = 1, *aPtr + 1 為 a[0] + 1,意義不同,* 的運算優先權高於 +,-
- 接著我們宣告了整數指標變數 bPtr,他的值是 aPtr 加上 3 個 offset
- 當指標變數做算數運算時,他會先將運算的數字乘上指標指向的物件之大小,亦即假設 int 在這邊佔 4byte 的話,bPtr = 3000 + (3 * 4)
- 對 bPtr 取值的話,*bPtr 會等於 a[3] 也就是 3
- 同理,bPtr - aPtr 的話 int 所佔的 4 byte 會被處理掉,因此會得到 3 而非 12
- 指標的算術運算只有對陣列這種連續記憶體位置才有意義
指標陣列
像 C 語言中並沒有 String 型別,因此會使用 字元指標 char* str
或字元陣列 char str[]
來存放字串
而陣列存放的物件也可以是指標
char* str1 = "kshuang"; char str2[] = {"kshuang"}; char *strArr[3] = {"wiki", "kshuang", "xyz"};
strArr[3]
代表了一個含有三個元素的陣列char *
則代表了每個元素都是指向 char 的指標- 雖然 StrArr 陣列的大小是固定的,但因裡面存放的是指標,所以他可以存取任何長度的字串
函式指標(function pointer)
指標也可以用於函式,一個指向函式的指標會記錄這個函式在記憶體中的位置
就如同陣列名稱一樣,函式名稱是此函式在記憶體中的位置(執行函式之程式碼起始位置)
void sort(int array[], int size, int (*compare)(int a, int b)); int asending(int a, int b); int descending(int a, int b); int array[] = {3, 6, 2, 5, 4}; sort(array, 5, ascending); sort(array, 5, descending); void sort(int array[], int size, int (*compare)(int a, int b)) { ... if((*compare)(array[count], array[count + 1])) { } ... }
- 範例中我們有一個 sort 函式,他的第三個參數
compare
是一個函式指標 - 我們在呼叫 sort function 時可以把 ascending, descending 這兩個函數名稱當成引數傳給 sort
- 透過對函式指標 compare 求值,亦即 (*compare),會得到一個函式名稱 ascending 或 descending,而函式名稱即為執行函式的記憶體位置
- sort function 中呼叫 compare 也可以不需先求值,但不建議這樣寫,因為程式碼閱讀上會讓 compare 看起來像一個函式名稱,而非函式指標
- sort function 的參數宣告中
int (*compare)
不可寫成int *compare
,後者表示 compare 是一個會回傳整數指標的函式
接下來這個範例要把前面的觀念都綜合起來了:
void function1(int); void function2(int); void function3(int); int main(int choice) { void (*f[3])(int) = {function1, function2, function3}; (*f[choice])(choice); }
- 變數 f 是存放三個函式指標的陣列,而這些指標指到的函式其引數為一個 int,且回傳值都是 void
(*f[choice])(choice);
,是一個函式指標的呼叫,會根據 choice 選定陣列中不同 function 執行
網路上看到的瑞昱面試考題,就是函式指標陣列的概念 Q: 不能用if 和 switch case , 請用你認為最快的方法實作main extern void func1(void); extern void func2(void); extern void func3(void); extern void func4(void); extern void func5(void); void main(int n) { if n==1 execute func1; if n==2 execute func2; if n==3 execute func3; if n==4 execute func4; if n==5 execute func5; }
基礎指標判讀
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 個指標的陣列,每個指標指向一個函數,該函數有一個整數型參數並返回一個整數
0x03 call by value, call by reference
call by value, 傳值呼叫
事實上 C 語言只有 call by value
在函式呼叫時,呼叫者 (caller) 和被呼叫者 (callee) 的變數各自佔有記憶體,參數傳入函式是複製一份傳入
在被呼叫的函式中,參數更改不會影響到原來呼叫函式的引數
call by reference, 傳參考呼叫
在 C 裡面,我們可以透過指標和間接運算子 (*) 來模擬傳參考呼叫
藉由將變數的記憶體位置作為引數傳入被呼叫函式
在被呼叫函式中利用間接運算子在該記憶體位置直接取值
由於呼叫與被呼叫函式在相同記憶體位置取到的是同一個變數值,因此任一方對變數做更動都會影響另一方
0x04 變數的特性
C 語言中有多個識別字來當著我們宣告變數或函式
這些識別字包含了一些特性:
- 儲存類別 storage class
- 儲存佔用期間 storage duration
- 範圍 scope
- 連結 linkage
儲存類別 storage class
有助於於判斷變數的 storage duration, scope, linkage
識別字包括:
- auto: 宣告變數為自動儲存佔用期間,區域變數若沒有其他識別字宣告,預設即為 auto,一般不需要寫
- register: 建議編譯器將頻繁改動的變數放入暫存加快存取速度,但目前編譯器功能強大,大部分可以自動識別,不需特別宣告
- static: static 區域變數在程式離開這個函式後,仍會保有他們的值,但在函式外無法參考到這個變數
- extern: 對於分佈在不同原始檔的變數,透過此宣告可取得同一變數值
儲存佔用期間 storage duration
可以理解為變數的生命週期,也就是這個變數會在記憶體中的時間
分為兩類
- 自動儲存佔用期間: auto, register,這類型的變數只有在程式進入宣告他們的區塊時才會產生出來,程式離開該區塊後變數就會被清除
- 靜態儲存佔用期間: static, extern,這類型變數具有靜態儲存空間,從程式開始執行就存在,直到程式執行結束才清除
範圍 scope
指變數能被參考的範圍
分為兩類
- 區域變數: 只有在變數宣告的區塊內可以參考到這個變數
- 全域變數: 在同個原始程式檔內都可以參考到這個變數
連結 linkage
在有多個原始檔的情況下,變數的宣告是否能被不同檔案的程式參考到
int flag;
extern int flag;
如此在 file2.c 中才能存取到 file.c 的 flag 值
記憶體配置
這部分可以參考我之前的筆記 Process Environment: Memory Layout
0x05 sizeof
sizeof 是一個一元運算子,他可以在編譯時期計算出資料型別的大小,單位為位元組 bytes
#include<stdio.h> int main(void) { int array[20]; int *ptr = array; printf("sizeof char = %lu\n", sizeof(char)); printf("sizeof short = %lu\n", sizeof(short)); printf("sizeof int = %lu\n", sizeof(int)); printf("sizeof long = %lu\n", sizeof(long)); printf("sizeof float = %lu\n", sizeof(float)); printf("sizeof double = %lu\n", sizeof(double)); printf("sizeof long double = %lu\n", sizeof(long double)); printf("sizeof array = %lu\n", sizeof(array)); printf("sizeof ptr = %lu\n", sizeof(ptr)); printf("length of array = %lu\n", sizeof(array) / sizeof(array[0])); return 0; }
- sizeof 運算子可以用在任何變數名稱、型別、或值上
- 對陣列名稱進行 sizeof 運算會得到整個陣列所佔的位元組數
- 我們常以 sizeof(array) / sizeof(array[0]) 來取得陣列元素個數
- sizeof 是運算子不是函式,所以對於單一運算元其實可以不加 (),但像 long double 這種包含兩個字以上的運算元就必須要有括號
0x06 延伸性資料型態:struct、typedef、union 和 enum
struct
結構 structure 可以將一些彼此相關的變數聚合 (aggregate) 在一起,概念有點像 java 的物件
struct employee { char *name; int age; char gender; };
- 結構的定義不佔用記憶體空間,它的功能是要建立一種新的資料型別
- struct 關鍵字後面的 employee 稱為結構標籤 (structure tag),即為這個結構的名稱
- 同一個結構內的變數名稱不能相同,但不同結構內的變數名稱則可以重複
- 結構定義必須以分號結尾
struct queueNode { char data; struct queueNode * nextPtr; // Wrong for: struct queueNode next; }; struct queueNode qNode, queue[10], *queuePtr; /* equals: struct queueNode { char data; struct queueNode * nextPtr; }qNode, queue[10], *queuePtr; */
- 結構中的變數不能包含結構自己,因此上面 struct queueNode 型別的變數不能宣告在 struct queueNode 這個結構中
- 指向結構本身的指標可以包含在結構中,C 語言中的許多資料結構都是以此為基礎建立的,這稱為自我參考結構 (self-referential structure)
- 結構宣告: qNode 是 struct queueNode 型別的變數,queue 是具有十個 struct queueNode 型別之元素的陣列,queuePtr 則是一個指向 struct queueNode 型別變數的指標
- 結構標籤名稱是可有可無的,只是結構若沒有標籤名稱,則該結構型別的變數宣告就只能跟著結構一起宣告,而無法單獨宣告
struct pokercard { char *face; char *suit; }; struct pokercard aCard = {"Three", "Hearts"}; struct pokercard *cardPtr = &aCard; printf("%s", aCard.suit); printf("%s", cardPtr->suit); // Both print "Hearts" // cardPtr->suit 等同 (*cardPtr).suit
- 結構可以向陣列一樣透過 {} 來宣告初始值
- 若 {} 內的初始值個數少於結構內的變數,剩下的變數會自動初始化成 0 或 NULL(指標)
- 對於結構型別的變數我們可以用 結構成員運算子 . 來存取其成員
- 對於指向結構變數的指標我們可以用 結構指標運算子 -> 來存取其成員,-> 前後不可有空格
- (*cardPtr).suit 的 () 不可省略,C 語言中,結構成員運算子 .、結構指標運算子 ->、陣列下標 []、括號 () 的優先全都是最高的,結合性皆由左而右
typedef
關鍵字 typedef 可以為之前定義的資料型別建立別名
通常會用在結構上,縮短型別名稱
typedef struct pokercard Card; typedef struct { char *face; char *suit; } Card; Card deck[52];
- typedef 不會產生新的型別,只是為現存的型別建立新的別名
- 透過 typedef 可以省略結構標籤,直接建立結構型別別名
- 增加程式可讀性
typedef 也成用於建立基本資料型別的別名,例如某程式需要使用 4 bytes 的整數,這在有些系統上可能是 int,有些則是 long,此使可以透過 typedef 建立別名 Integer,後續用此宣告,則在不同平台只需要修改 Integer 這個別名定義就可以了
union
電腦架構早期記憶體空間比較不足,因此需要使用共用結構讓各變數共用一塊記憶體
union number { int x; double y; }; union number value = {10};
- 一個 union 所需的位元數至少要能夠放的下 union 中所佔記憶體空間最大的元素
- 在同一個時刻只有一個 union 的成員可以被參考
- union 的初始值宣告其型別要與第一個成員型別相同
enum
enum 是一種常數定義方式,可以提升可讀性,enum 裡的識別字會以 int 的型態,從指定的值開始遞增排列 (預設為 0)
#include<stdio.h> enum months { JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC }; int main(void) { enum months month; const char *monthName[] = {"", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; for(month = JAN; month < DEC; month++) { printf("%2d%11s\n", month, monthName[month]); } return 0; }
- 列舉型別中第一個數值設定為 1,後面會依序遞增
- 在列舉型別定義時,我們也可以為每個常數都設定一個值
- 列舉型別內的識別字必須唯一
- 在列舉型別定義後,不該再將值設給列舉型別常數
0x07 關鍵字 const
const 宣告是用來告訴編譯器該變數不該被更改
若嘗試更改宣告成 const 的變數,則在編譯期間就會出現 error
通常是宣告只讀不寫的變數
const v.s #define
1. 編譯器處理方式: define 在預處理階段展開;const 在編譯階段使用
2. 類型和安全檢查: const 會在編譯階段會執行類型檢查,define 則不會
3. 存儲方式: define 直接展開不會分配記憶體,const 則會在記憶體中分配
若是指標宣告成 const 則該指標無法在程式運作中改指向其他記憶體位置 (但該記憶體位置上的資料可以更動)
int a = 5; int b = 6; int *foo = &a; /* 一個 pointer,指向 int 變數 foo = &b OK, foo 變為指向 b 的記憶體位置,*foo 變成 6 *foo = 6 OK, 指標指向的 int 值 a 變為 6 */ const int * foo = &a; /* 一個 pointer,指向 const int 變數 foo = &b OK, foo 變為指向 b 的記憶體位置,*foo 變成 6 *foo = 6 not OK, 指標指向的值為 const int 不能被更改 */ int const * foo = &a; /* 一個 pointer,指向 const int 變數 foo = &b OK, foo 變為指向 b 的記憶體位置,*foo 變成 6 *foo = 6 not OK, 指標指向的值為 const int 不能被更改 */ int * const foo = &a; /* 一個 const pointer,指向 int 變數 foo = &b not OK, foo 為 const pointer 指向的記憶體位置不能被更改 *foo = 6 OK, 指標指向的值 a 變為 6 */ int const * const foo = &a; /* 一個 const pointer,指向 const int 變數 foo = &b not OK, foo 為 const pointer 指向的記憶體位置不能被更改 *foo = 6 not OK, 指標指向的值為 const int 不能被更改 */
0x08 關鍵字 volatile
由於嵌入式系統常處理 I/O、中斷、即時操作系統 (RTOS) 相關的問題,因此在嵌入式系統開發中 volatile 尤為重要
被 volatile 修飾的變數代表它可能會任何時刻被意外的更新,即便與該變數相關的上下文沒有任何對其進行修改的語句
因此告知編譯器不對它涉及的地方做最佳化,並在每次操作它的時候都讀取該變數實體位址上最新的值,而不是讀取暫存器的值
編譯器的優化工作原理是一旦我們將變數讀入暫存器,如果從變數相關的上下文來看,變數值是沒有改變的,那後面就不會再從記憶體讀取這個變數,而是直接從暫存器取得,加速程式運作
但嵌入式系統中有些變數可能不是被程式上下文改變的,改變它可以是hardware 暫存器,Interrupt Service Routine,Multi-thread Share memory,所以從暫存器讀取可能是讀到過時的資料,因此變數需要宣告成 volatile ,每次都從記憶體讀入
volatile int foo; int volatile foo; // 宣告 foo 是一個需要被即時更新的整數變數 volatile int * foo; int volatile * foo; // 需告 foo 是一個指向 volatile integer 的指標 extern const volatile int real_time_clock;
- 這邊 real_time_clock 是一個嵌入式系統中的變數,通常是指系統時鐘
- const 和 volatile 是可以並用的,不衝突
- const 代表對程式來說這個變數是不可改變的,只能讀
- 但硬體上可能會去改變這個數值,所以宣告成 volatile,確認每次讀取到的都是最新數值
0x09 bit operation
資料在電腦內部都是以一連串位元 (bit) 來加以表示,每個位元可以為 0 or 1
大部分系統中,8 個連續的位元構成一個位元組 (byte)
char 型別變數的標準儲存單位就是 1 byte
位元運算
- AND & : 兩個 bit 都為 1 時才會為 1,否則為 0
bit 1 | bit 2 | bit 1 & bit 2 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
- inclusive OR | : 兩個 bit 其中一個為 1 時就會為 1,兩者都為 0 才為 0
bit 1 | bit 2 | bit 1 | bit 2 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
- exclusive OR ^ : 兩個 bit 只有一個為 1 時會為 1,也就是兩 bit 相同時為 0
bit 1 | bit 2 | bit 1 ^ bit 2 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
- left shift << : 將第一個運算元往左移動數個位元,左移位元數由第二個運算元指定,右邊以 0 填滿
1 << 31 // (10000000 00000000 00000000 00000000)
左移一個位元在數學運算上相當於 *2
- right shift >> : 將第一個運算元往右移動數個位元,右移位元數由第二個運算元指定,左邊填空方式隨機器而異
- 1's complement ~ : 1 的補數,將每個位元的 0,1 反轉
void displayBits(unsigned value) { unsinged c; insigned displayMask = 1 << 31; // 1000000 00000000 00000000 00000000 printf("%10u = ", value); for(c = 1; c <= 32; c++) { putchar(value & displayMask ? '1' : '0'); value <<= 1; if(c % 8 == 0) putchar(' '); } putchar('\n'); } // 65000 = 00000000 00000000 11111101 11101000
- 在 line 4: 我們將 1 左移 31 位,用來當遮罩,後面我們會用這個遮罩做運算保留下最左邊的 bit
- 在 line 11: 我們將 value 與遮罩做 AND 運算,由於遮罩後 31 bits 皆為 0,因此 AND 運算後這 31 位必定也為 0
- value & displayMask 運算後只會得到
1000000 00000000 00000000 00000000
或0000000 00000000 00000000 00000000
兩種結果 - 在 C 語言中 0 為 false,而所有非 0 數皆代表 true,所以上面運算,只要 value 最左方是 1 則條件為 true (231 != 0),印出 1,反之印出 0
- 在 line 12: 整個 value 左移一位,繼續運算
unsigned setBit(unsigned value, unsigned bit) { unsigned mask = 1 << bit; value |= mask; return value; } unsigned clearBit(unsigned value, unsigned bit) { unsigned mask = 1 << bit; value &= ~mask; return value; } unsigned toggleBit(unsigned value, unsigned bit) { unsigned mask = 1 << bit; value ^= mask; return value; }
- setBit function 中,bit 參數決定我們要設置的位元位置
- 將 value 與 mask 做 inclusive OR 後,因為 mask 中為 0 的位元並不會影響 value 本來的位元,而 1 的那個位置不管與 0 or 1 運算結果都為 1,達成設置位元的目的
- clearBit function 中,因為 1 不管跟什麼位元做 AND 都不會改變該位元,而 0 跟任何位元 AND 都為 0,所以我們將 mask 反轉,並對 value 做 AND 運算,達到清除位元的目的
- toggleBit function 中,因為 1 跟什麼位元做 xor 都能達到反轉效果
0x0a 進制轉換
- 二進位: 0, 1
- 八進位: 0, 1, 2, 3, 4, 5, 6, 7
- 十進位: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
- 十六進位: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
其他進位轉十進位
在數字系統中我們使用位數標記法 (positional notation) 來標記數字
我們可以透過位數值 (positional value) 與 符號值 (symbol value) 來計算十進位數字
/* 十六進位 a3b ||| 符號值 a, 位數值 16^2 -----|||----- 符號值 b, 位數值 16^0 | 符號值 3, 位數值 16^1 轉十進位: 10*(16^2) + 3*(16^1) + 12*(16^0) = 2560 + 48 + 12 = 2620 */
/* 十進位 937 ||| 符號值 9, 位數值 10^2 -----|||----- 符號值 7, 位數值 10^0 | 符號值 3, 位數值 10^1 */
/* 八進位 425 ||| 符號值 4, 位數值 8^2 ------|||------ 符號值 5, 位數值 8^0 | 符號值 2, 位數值 8^1 轉十進位: 4*(8^2) + 2*(8^1) + 5*(8^0) = 256 + 16 + 5 = 277 */
/* 二進位 101 ||| 符號值 1, 位數值 2^2 ------|||----- 符號值 1, 位數值 2^0 | 符號值 0, 位數值 2^1 轉十進位: 1*(2^2) + 0*(2^1) + 1*(2^0) = 4 + 0 + 1 = 5 */
二進位轉八進位
將二進位由左開始,三個位元計算成一個八進位數重新表示即可
/* 100 011 010 001 4 3 2 1 */
二進位轉十六進位
將二進位由左開始,四個位元計算成一個十六進位數重新表示即可
/* 1000 1101 0001 8 D 1 */
二進位負數: 二補數法
二補數 = 一補數 + 1
int value = 13; // 00000000 00000000 00000000 00001101 ~value // 11111111 11111111 11111111 11110010 ~value + 1 // 11111111 11111111 11111111 11110011 /* 這在二進位中表示為 -13 00000000 00000000 00000000 00001101 +) 11111111 11111111 11111111 11110011 --------------------------------------- 00000000 00000000 00000000 00000000 進位超過 32 bits 忽略,證明兩者互為正負數,相加為 0 */
整數與字元的十六進位宣告
int A = 65; int x = 0x61; char y = '\x61'; char z = ='\x80'; char cat[] = { 0x63, 0x61, 0x74, 0x00 }; printf("%x\n", A); // 十六進位 41 printf("%d\n", x); // 十進位 97 printf("%x\n", x); // 十六進位 61 printf("%c\n", x); // a printf("%d\n", y); // 十進位 97 printf("%x\n", y); // 十六進位 61 printf("%c\n", y); // a printf("%d\n", z); // 十進位 -128 printf("%x\n", z); // 十六進位 ffffff80 printf("%c\n", z); // a printf("%s\n", cat); // cat
- char 要看是 signed 還是 unsigned,前者可表達的數字範圍是 -128 ~ 127,後者為 0 ~ 255
- char 宣告值可以為數字,但不能再將數字以 ' ' 括起來,會出現編譯警告,print 的結果數字與預期不同
- 相反的,若要以十六進位宣告 char,必須採用 '\x61' 且不可省略 ' '
- %x 可以以十六進位印出數字
- 變數 char z 印出來可以發現十進位為 -128,十六進位為 ffffff80,這是因為我的 char 在 MacBook 上是 signed char,最大的數字表達是 128(10),也就是 7E(16),而 '0x80' 已經超出這個表達範圍
Printing hexadecimal characters in C
0x0b 前處理器相關
C preprocessor 會在程式編譯前執行,包含將其他檔案含入編譯檔中、定義符號常數和巨集、程式碼的條件編譯、條件式執行前置處理器命令
所有的前置處理器命令都是以 # 開頭,在同一行前方僅能有空白或註解
#include 前置處理器命令
載入標頭檔稍後一起編譯
#include <filename> #include "filename"
- 第一個寫法是載入標準函式庫標頭檔
- 第二個寫法通常是載入自定義的標頭檔,這種寫法會優先從編譯檔案所在的目錄底下去搜尋標頭檔位置
#define 前置處理器命令: 符號常數
符號常數 Symbolic Constant
#define PI 3.14159
#define 前置處理器命令: 巨集
巨集 Macros
#define CIRCLE_AREA(x) ((PI) * (x) * (x)) #define RECTANGLE_AREA(x, y) ((x) * (y))
- 巨集中小括號 () 不可省略,針對每個變數都要括
- x 有可能為 c + 2 這種變數,省略括號展開後的運算次序會出錯
- 若巨集文字太長,可使用
\
來換行接續
#undef 前置處理器命令
- 可取消常數符號或巨集的定義
- 常數符號和巨集的範圍是從定義開始之處到 #undef 之處或是檔案結尾
條件式編譯
#if !defined(MY_CONSTANT) #define MY_CONSTANT #endif // Equals: #ifndef MY_CONSTANT #define MY_CONSTANT #endif //---------------------- #if #elif #endif //---------------------- #ifdef DEBUG printf("Variable x = %d\n", x); #endif
#error 前置處理器命令
#error tokens
- 印出與實作環境相依的錯誤訊息
assertion 斷言
assert(x <= 10); #define NDEBUG
- assert 巨集定義在 <assert.h> 標頭檔
- 會測試某個運算式,若結果為偽 (false) 則 assert 印出一段錯誤訊息並呼叫 abort 函式 (定義在 <stdlib.h>)
- 在符號常數 NDEBUG 的定義之後的的 assert 都會忽略
- 用於測試邏輯錯誤,無法檢測到執行其的錯誤
0x0c 關鍵字 inline
行內函式是一種編譯最佳化的方式,對於一些內容較為簡短又常使用的函式,編譯器在程式設計師的建議 (使用關鍵字inline) 下,可以將指定的函式插入並取代每一處呼叫該函式的地方,做展開編譯,從而減少呼叫函式耗費的時間
#include <stdio.h> inline int f(int num) { return num-1; } int main(){ int i = f(4); } // ------Compile after inline may like------ int main(){ int i = 4-1; }
inline 與 macro 的差別
- 巨集 macro 是前置處理器 preprocessor 處理的,行內函式 inline 是編譯器 compiler 處理的
- 編譯器可以不接受原始碼給的 inline 建議,不用取代的方式而維持呼叫該指定函式。但是巨集一定得替換
- 巨集並不檢查輸入參數的類型,行內函式則會
- 巨集使用的是文本替換,很難發現編譯錯誤,也可能導致無法預料的後果
C99/C11 的 inline function
根據C99/C11 Standard1,當編譯器接受建議去 inline 某函式,該函式便會插入並取代在同一文件中每一處呼叫該函式的地方,此時,外部程式不能以 extern 的方式呼叫該函式,因為該函式已經被 inline 而沒有自己的位址。 但如果 inline 失敗,那該函式就變為普通函式,外部程式就可以 extern 的方式呼叫該函式。
那如果希望外部程式至少能以非內聯的方式呼叫該函式要怎麼辦? 有兩種辦法:一是定義一個含inline的函式和另一個不含inline的同名函式, 另一是用extern inline去定義該函式,這樣編譯器就會另外產生該函式的位址, 在能看得到該函式的地方編譯器便會嘗試用內聯,看不到的地方用函式呼叫指到此位址。
如果是用 static inline 宣告某函式,那不論內聯成功或失敗外部均不能呼叫該函式。
這邊觀念比較細一點,可以參考
0x0d 字串處理
char *str1 = "abcde"; // const char *str1 = "abcde"; char str2[] = "abcde";
- C 語言中的字串是以 空字元 \0 來結束字串的
- 字串指標與字串陣列都是指向此字串的第一個字元
- 字串指標在多數編譯器上是預設為 const 的,編譯器會將指標字串放在不能更改的記憶體上,嘗試在執行期更改這個字串值會造成 error
- 下面程式在在執行期便會 error,應將 line 14 改為 char str[] = “12345”;
What is the difference between char a[] = "string" and char *p = "string";?
#include <stdio.h> #include <string.h> void reverse(char* str) { int i, j; char c; for(i=0, j = strlen(str)-1; i<j; ++i, --j) c = str[i], str[i]=str[j], str[j]=c; } int main() { char *str = "12345"; // This is a const char reverse(str); puts(str); return 0; }