資訊人筆記

Work hard, Have fun, Make history!

使用者工具

網站工具


programming:c:common_concept_of_c

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] 的記憶體位置
  • 這邊 aPtrarray 的差別在於 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 000000000000000 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;
}


0x0 參考資料

programming/c/common_concept_of_c.txt · 上一次變更: 127.0.0.1