2020年7月6日 星期一

工程師的C語言筆記

編譯 C 程式分為四個步驟:

前處理 (Preprocessing):將含巨集 (macro) 的 C 程式碼轉換成沒有巨集的 C 程式碼
編譯 (Compilation):將 C 原始碼轉換成等效的組合語言原始碼
組譯 (Assembly):組譯會將組合語言原始碼轉換成機械碼,轉換後的檔案為目的檔 (object files)。
連結 (Linking):將目的檔轉為執行檔 (executable)。

【註解】

單行註解以 // 開頭,多行註解用 /* 和 */ 把註解文字包起來。
利用巨集來註解掉整段程式碼的手法:
#if 0
    printf("It won't compile\n");
#endif

這樣操作的原理在於巨集會在編譯前就執行,相關程式碼會被抹去,等同於這段程式碼不存在。


【引入函式庫】

引入函式庫的語法是 #include。

使用標準函式庫或外部函式庫時,使用角括號:#include <stdlib.h>

使用內部函式庫時,則使用分號:#include "mylib.h"

當使用分號引用函式庫時,C 編譯器會優先從 C 程式碼所在的路徑找標頭檔,故可用來引用內部函式庫。

【主函式 (Main Function)】

 C 程式的起始點,一般的 C 語言皆有主函式。但在嵌入式系統或是作業系統本身的 C 程式碼中,主函式則不是必需的。

不需要接收命令列參數,則使用以下方式來寫:
int main(void)
{
    /* Implement your code here. */
}

若該 C 程式需要接收命令列參數,則改用以下方式來寫:
int main(int argc, char *argv[])
{
    /* Implement your code here. */
}

除了命令列參數外,如果想要接收環境變數,可以用以下方式改寫:
int main(int argc, char *argv[], char *env[])
{
    /* Implement your code here. */
}
標準 C 可用 getenv() 函式取得環境變數,因此沒有必要再用這種寫法。


C 語言的程式碼會區分大小寫,單行敘述會用 ; 結尾。
C 程式在結束時,會向系統回傳一個整數,用來代表程式的狀態。

【資料型態】

C 的基本資料型態主要區分為整數(Integer)、浮點數(Float)、字元(Character),而這幾種還可以細分,如下所示:

整數

用來表示整數值,可以區分為 short、int、long 與 long long(C99),配置的記憶體長度在不同編譯器上各不相同,可容納的大小各不相同,例如,在 64 位元 Ubuntu 16.04 中的 gcc 編譯器下,int 與 long 為 8 位元組,在 Windows 10 使用 MinGW-w64,GNU 編譯器版本是 8.1.0 型態的話,int 與 long 為 4 位元組,長度越長,表示可表示的整數值範圍越大。

浮點數

用來表示小數值,可以區分為 float、double 與 long double,越後面的型態使用的記憶體空間越大,精度也就越高。

字元

char 的 sizeof(char) 結果要是 1,基本上用來儲存字元資料,但沒有規定什麼是字元資料,也可用來儲存較小範圍的整數。

與字元相關的型態,其實還有來自 C89 的 wchar_t,以及 C11 標準規範的 char16_t、char32_t(定義在 unchar.h 標頭檔)。

在 C11 標準中,建議包括 stdint.h 程式庫,使用 int8_t、int16_t、int32_t、int64_t uint8_t、uint16_t、uint32_t、uint64_t 等作為整數型態的宣告,以避免平台相依性的問題。

'\t' 是跳格字元,它相當於在主控台中按下 Tab 鍵的效果,可以用來對齊下一個顯示位置。
%lu 為格式指定碼,表示該位置將放置一個 long unsigned 型態的整數。

字元是以單引號來表示,例如 'A'、'1' ;而有一些字元例如 “、'、\ 等,要在程式中表現這些字元則要使用轉義序列(escape sequence)來表示,即 \"、\'、\\,其他還有一些不可見字元,也要以轉義序列來表示,下表列出常用的轉義序列:

\n:換行、新行(newline)
\t:水平定位點(horizontal tab)
\v:垂直定位點(vertical tab)
\b:退回一格(backspace)
\r:返回(carriage return)
\f:換頁(formfeed)
\a:嗶聲(alert bell)
\\:倒斜線(backslash)
\?:問號
\':單引號
\": 雙引號
\nnn:nnn 為一到三個 8 進位數字,表示字元編碼,例如 \115
\xnn.:nn 為多個 16 進位數字,表示字元編碼,通常用兩個數字,例如 \xb4\xfa,
\unnnn:Unicode 碼點 U+nnnn 表示(C99)
\Unnnnnnnn:Unicode 碼點 U+nnnnnnnn 表示(C99)
八進位 ASCII 碼如 '\062' 則是字元 '2',十六進位 ASCII 碼如 "\x48" 為字元 'H'。

【變數】

宣告變數時使用 const 關鍵字來限定,一但將數值指定給變數之後,就不允許再重新指定給同一變數。

如果要宣告無號的整數變數,則可以加上 unsigned 關鍵字,例如:unsigned int i;

【printf 】

使用 printf 時要指定整數、浮點數、字元等進行顯示,要配合格式指定字(format specifier),以下列出幾個可用的格式指定碼:

%c:以字元方式輸出
%d:10 進位整數輸出
%o:以 8 進位整數方式輸出
%u:無號整數輸出
%x、%X:將整數以 16 進位方式輸出
%f:浮點數輸出
%e、%E:使用科學記號顯示浮點數
%g、%G:浮點數輸出,取 %f 或 %e(%f 或 %E),看哪個表示精簡
%%:顯示 %
%s:字串輸出
%lu:long unsigned 型態的整數
%p:指標型態

printf("example:%.2f\n", 19.234);
.2 指定小數點後取兩位,執行結果會輸出:example:19.23

可以指定輸出時,至少要預留的字元寬度,無論是數值或字串,例如:printf("example:%6.2f\n", 19.234);
整數 6 表示預留 6 個字元寬度,由於預留了 6 個字元寬度,不足的部份要由空白字元補上,執行結果會輸出如下(19.23只佔五個字元,所以補上一個空白在前端):example: 19.23

若在 % 之後指定負號,例如 %-6.2f,表示靠左對齊,沒有指定則靠右對齊

gets 函式無法知道字元陣列的大小,而是依賴換行符號或 EOF 才會結束輸入,因此有可能引發緩衝區溢位的安全問題。可以使用 fgets 來取代 get,使用時必須指定字元陣列、大小以及 stdin。

 C 中的條件運算子(Conditional operator),它的使用方式如下:


條件式 ? 成立傳回值 : 失敗傳回值
例:
printf("輸入學生分數:");
scanf("%d", &score);
printf("該生是否及格?%c\n", score >= 60 ? 'Y' : 'N');

int num = ++i;   // 相當於i = i + 1; num = i;
int num = i++;    // 相當於num = i; i = i + 1;


【冷門的用法】

C/C++中的volatile使用時機?


有2兩個場合(I/O & multithread program)請記得加上volatile:
1. I/O, 假設有一程式片斷如下

       U8   *pPort;
       U8   i, j, k;
   
       pPort = (U8 *)0x800000;
 
       i = *pPort;   
       j = *pPort;   
       k = *pPort;   

    以上的i, j, k很有可能被compiler最佳化而導致產生
       i = j = k = *pPort;
    的code, 也就是說只從pPort讀取一次, 而產生 i = j = k 的結果, 但是原本的程式的目
    的是要從同一個I/O port讀取3次的值給不同的變數, i, j, k的值很可能不同(例如從此
    I/O port 讀取溫度), 因此i = j = k的結果不是我們所要的

    怎麼辦 => 用volatile, 將
       U8   *pPort;
    改為
       volatile U8   *pPort;

    告訴compiler, pPort變數具有揮發性的特性, 所以與它有關的程式碼請不要作最佳化動作. 因而
       i = *pPort;   
       j = *pPort;   
       k = *pPort;   
    此三列程式所產生的code, 會真正地從pPort讀取三次, 從而產生正確的結果

2. Global variables in Multithread program
    => 這是在撰寫multithread program時最容易被忽略的一部份
    => 此原因所造成的bug通常相當難解決(因為不穩定)

    假設有以下程式片斷, thread 1 & thread 2共用一個global var: gData
        thread 1:                                thread 2:                             
                                                                                       
            ...                                      ....                             
            int  gData;                              extern int gData;                 
                                                                                       
            while (1)                                int  i, j, k;                     
            {                                                                         
                ....                                 for (i = 0; i < 1000; i++)
                gData = rand();                      {                                 
                .....                                    /* A */
            }                                            j = gData;                   
                                                         ....                         
            ....                                     }                                 

    在thread 2的for loop中, 聰明的compiler看到gData的值, 每次都重新從memory load到register,
    實在沒效率, 因此會產生如下的code(注意,tmp也可以更進一步的用register取代):
       tmp = gData;
       for (i = 0; i < 1000; i++         
       {                                 
           /* A */
           j = tmp;                   
           ....                         
       }                                 
    也就是gData只讀取一次, 這下子問題來了, 說明如下:
    .thread 2在執行for loop到j = gData的前一列(A)的時候(假設此時gData=tmp=5), 被切換到thread 1執行
    .在thread 1的while loop中透過gData = rand(), 對gData做了修改(假設改為1), 再切換回thread 2執行
    .繼續執行 j = gData, 產生j = 5的結果
    .但是正確的結果應該是 j = 1
    怎麼辦 => 也是用volatile,

    在thread 1中, 將
        int  gData;
    改為
        volatile int  gData;
 
    在thread 2中, 將
        extern int  gData;
    改為
        extern volatile int  gData;

沒有留言:

張貼留言