目錄表

Common concept of C/C++

0x00 前言

最近在找面試資料時從 Mr.Opengate 的 Blog 中看到這篇整理得很棒的文章

因為怕哪天原鏈結消失,所以我習慣整理消化成自己的筆記放過來

這邊附上 source C/C++ - 常見 C 語言觀念題目總整理(適合考試和面試)

Blog 內也有其他很棒的文章,有興趣的讀者不妨過去看看

以及這篇 小小科學實驗室 作者以工作面試的角度點出了 C 語言中的諸多小細節,也很受用


0x01 Outline


0x02 指標

指標 (pointer) 是代表記憶體位置的變數,換句話說,指標也是一個變數,只是這個變數存放的值是其他變數的記憶體位置

int x = 5;
int *xPtr = &x;

int *yPtr, zPtr;
/*
equals 
  int *yPtr;
  int zPtr;
  
not equals
  int *yPtr;
  int *zPtr;
*/


陣列與指標

指標與陣列關係非常密切

int array[5] = {1, 2, 3, 4, 5};
int *aPtr = array;
// equals: int *aPtr = &array[0];


指標的算術運算

承上,當指標指向陣列時,由於陣列中的元素在記憶體中的位置是連續的,所以我們可以對指標做一些算數運算

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
*/


指標陣列

像 C 語言中並沒有 String 型別,因此會使用 字元指標 char* str 或字元陣列 char str[] 來存放字串

而陣列存放的物件也可以是指標

char* str1 = "kshuang";
char str2[] = {"kshuang"};
char *strArr[3] = {"wiki", "kshuang", "xyz"};


函式指標(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]))
    {
    
    }
    ...
}

接下來這個範例要把前面的觀念都綜合起來了:

void function1(int);
void function2(int);
void function3(int);

int main(int choice)
{
    void (*f[3])(int) = {function1, function2, function3};
    
    (*f[choice])(choice);
}

網路上看到的瑞昱面試考題,就是函式指標陣列的概念

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 duration

可以理解為變數的生命週期,也就是這個變數會在記憶體中的時間

分為兩類

範圍 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;
}


0x06 延伸性資料型態:struct、typedef、union 和 enum

struct

結構 structure 可以將一些彼此相關的變數聚合 (aggregate) 在一起,概念有點像 java 的物件

struct employee {
    char *name;
    int age;
    char gender;
};

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


typedef

關鍵字 typedef 可以為之前定義的資料型別建立別名

通常會用在結構上,縮短型別名稱

typedef struct pokercard Card;

typedef struct {
    char *face;
    char *suit;
} Card;

Card deck[52];

typedef 也成用於建立基本資料型別的別名,例如某程式需要使用 4 bytes 的整數,這在有些系統上可能是 int,有些則是 long,此使可以透過 typedef 建立別名 Integer,後續用此宣告,則在不同平台只需要修改 Integer 這個別名定義就可以了


union

電腦架構早期記憶體空間比較不足,因此需要使用共用結構讓各變數共用一塊記憶體

union number {
    int x;
    double y;
};

union number value = {10};


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;
}


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;


0x09 bit operation

資料在電腦內部都是以一連串位元 (bit) 來加以表示,每個位元可以為 0 or 1

大部分系統中,8 個連續的位元構成一個位元組 (byte)

char 型別變數的標準儲存單位就是 1 byte

位元運算

bit 1 bit 2 bit 1 & bit 2
0 0 0
0 1 0
1 0 0
1 1 1
bit 1 bit 2 bit 1 | bit 2
0 0 0
0 1 1
1 0 1
1 1 1
bit 1 bit 2 bit 1 ^ bit 2
0 0 0
0 1 1
1 0 1
1 1 0

1 << 31
// (10000000 00000000 00000000 00000000)

左移一個位元在數學運算上相當於 *2

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

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; 
}


0x0a 進制轉換

其他進位轉十進位

在數字系統中我們使用位數標記法 (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

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))

#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


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 的差別

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";

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 參考資料