文章程式碼顯示

2018年2月13日 星期二

《筆記》C語言 - 08:#define 巨集指令應用、頭文件、typedef、後綴修飾、inline

對於一個 C 程式在編譯之前, 編譯器會先處理程式內的 #define、#undef、#if、#ifdef、#ifndef、#endif、#elif、#else 及 #include 敘述, 將程式碼進行文字上的替換(或引入函式庫)

接著將之編譯為組合語言以及機器語言,最後再經由 linker 變成一執行檔。

與函數不同的地方在於,巨集(#define) 不需要另外在記憶體中建立暫存器等,它僅是一個文字替換器

我們在 《筆記》C語言 - 06:一維陣列、#define、函式與陣列、static 陣列 就有使用過 #define 來快速修改一個陣列的大小,在這章我們深入的講一下 #define 更廣泛的應用

首先我們複習一下最簡單的應用

#define macroname WYJ

如此一來在程式中出現 macroname 的地方都將被 WYJ 所取代。且這樣子的替換過程會在編譯之前的「前置處理階段」中完成。


含引數的巨集以及多行巨集

事實上 巨集(macro) 也可以宣告成含「引數」的形式,就像函數一樣,如

#define SUM(A,B) (A+B)
// 上方的程式碼代表用巨集產生一個類似函數的 SUM
// 當我們在後續的程式碼中鍵入 SUM(A,B) 時
// 會在前置處理階段藉由編譯器自動將其以 (A+B) 做替換

注意,在使用巨集定義時要特別注意空格、括號、分號的使用,此外還有多行巨集的寫法,其需要使用反斜線 \ 進行換行,如下

#include "stdio.h"

#define macroname 123
#define SUM(A,B) (A+B)
#define ABS(A)              \
       {                    \
        if(A > 0) A = A; \
          else A = A*(-1);  \
       }                    \

 int main(void) {

     printf("marconame = %d\n", macroname);
     printf("SUM(A+B) = %d\n", SUM(2,3));
     int C = SUM(5,6);
     printf("C = %d\n", C);

     int num = -2;
     printf("num(9) = %d\n", num);
     ABS(num);
     printf("ABS(9) = %d\n", num);

     return 0;
}


多行巨集的使用上有一些眉角,在此網站中有提到

例如某些特定的應用上,為了整個程式碼撰寫風格的一致性,我們會看到 do while(0) 的結構出現,該結構可以避免出現 if (a>b); 這種在 if 後面帶上一個分號導致條件判斷提早終結的情況發生

由上方網站我們得知,在定義巨集的時候其分號的使用要特別小心

原則上在定義巨集時不要使用分號,然後在撰寫主體程式碼的地方再把分號寫上去以保持程式碼的一致性。

當然,如果你在定義巨集的地方加上分號有可能不會造成編譯上的錯誤,但在撰寫的邏輯上可能會出現一些問題,盡量避免這樣的做法。

define 的條件編譯

條件編譯有 #ifdef、#ifndef、#elif、#else、#endif  等

舉一個簡單的例子,當我們在寫一個程式的時候難免會需要先測試一下結果是否符合期待,一個簡單的例子與驗證方法是我們藉由設定變數的 "值" 然後以結果來推論這之中的計算結果是否正確(也就是Debug),但要一個一個去更改值然後測試也很麻煩,所以我們可以利用條件編譯來一次對大量的值進行 define,例如

#include "stdio.h"

#define _DEBUG_MODE_

#ifdef _DEBUG_MODE_
#define a 1
#endif

#ifdef _RELEASE_MODE_
#define a 0
#endif

int main(void)
{
 printf("a = %d", a);
    return 0;
}



或是

#include "stdio.h"

//#define _DEBUG_MODE_
#define _RELEASE_MODE_

#ifdef _DEBUG_MODE_
#define MSG "Debug Mode"
#endif

#ifdef _RELEASE_MODE_
#define MSG "Release Mode"
#endif

int main(void)
{
 printf("%s\n",MSG);
    return 0;
}




藉由條件編譯,我們也可以區分程式目前是操作在開發人員模式還是客戶端模式


條件編譯對於頭文件的應用

我們在每個程式中幾乎都會在一開始進行 #include 的動作,這個動作會引入外部的函式庫

雖然截至目前為止我們幾乎都只引入 stdio.h 一個函式庫而已。但在大型程式開發團隊中每個人或是每個小組都會負責開發某一個小區塊然後將其寫成函式庫的形式以供其他團隊調用以及整合。一方面可以讓程式具有可重複使用的條件;另一方面則是可以將一個大型專案進行有組織的分工。

在前面的章節我們講到函數,並且我們有提到一般而言我們會將 "函數原型" 進行集中放置,而這個集中放置的文件我們就稱為 "頭文件(header)" ,並且會用 .h 做為文件名稱的結尾;至於函式實際上做了什麼事情我們會集中放在另一個文件,稱為 "源文件"  以 .cpp 做為文件名稱的結尾

在大型的程式中每個人負責不同的區塊,這樣的方式會出現一些問題。

比如你有兩個 cpp 文件分別由 A小組以及B小組 來撰寫,但這兩個小組都 include 了 C 小組所撰寫的頭文件。而最後大家統整編譯時,A小組 及 B小組 的成果又要合併編譯成一個大程式。

於是問題來了,「大量的聲明(宣告)衝突」

此時我們就需要有一個機制,來讓同一個頭文件只要被 include 一次就可以了

例如 A小組以及B小組 都在自己的文件裡加上下面的程式碼

#ifndef HEAD_H
#define HEAD_H
... 頭文件內容
#endif

我們擷取 Arduino 中的步進馬達 library 來做一個實際的例子,以下為 Stepper.h 的部分內容

#ifndef Stepper_h
#define Stepper_h

// library interface description
class Stepper {
  public:
    // constructors:
    Stepper(int number_of_steps, int motor_pin_1, int motor_pin_2);

 ... 略
 
    void setSpeed(long whatSpeed);
    void step(int number_of_steps);
    int version(void);

  private:
    void stepMotor(int this_step);

    int direction;            // Direction of rotation
    unsigned long step_delay; // delay between steps, in ms, based on speed
 
    ... 略
};

#endif

我們發現在程式一開始的地方使用了 #ifndef Stepper_h

意思就是「如果沒有 define Stepper_h」

假若我們在之前(可能是專案中別的文件)已經將 Stepper.h 做過 #include ,那麼肯定會執行過第二行的 #define Stepper_h  這樣的動作等於是建立了一個記號。

接著我們就將函式原型集中放置在 Stepper.h 之中(此處使用到了 "類(class)" 屬於 C++ 的範疇了,在此先看看就好),尾端用 #endif 包裹起來

而 Stepper.cpp 裡面自然就是放這些函式的標頭(主體), Stepper.cpp 部分內容如下

#include "Arduino.h"
#include "Stepper.h"

/*
 * two-wire constructor.
 * Sets which wires should control the motor.
 */
Stepper::Stepper(int number_of_steps, int motor_pin_1, int motor_pin_2)
{
  this->step_number = 0; 
  ... 略
}

void Stepper::setSpeed(long whatSpeed)
{
  this->step_delay = 60L * 1000L * 1000L / this->number_of_steps / whatSpeed;
}

void Stepper::step(int steps_to_move)
{
  int steps_left = abs(steps_to_move);  
 ... 略
}


void Stepper::stepMotor(int thisStep)
{
 ... 略
}

int Stepper::version(void)
{
  return 5;
}


#ifndef 另一個常用的狀況是用在一個嵌入式系統 "選版本"用途

如以下程式碼

//// The following define selects which electronics board you have. Please choose the one that matches your setup
// 10 = Gen7 custom (Alfons3 Version) "https://github.com/Alfons3/Generation_7_Electronics"
// 11 = Gen7 v1.1, v1.2 = 11
// 12 = Gen7 v1.3
// 13 = Gen7 v1.4
// 2  = Cheaptronic v1.0
// 20 = Sethi 3D_1
// 3  = MEGA/RAMPS up to 1.2 = 3
// 33 = RAMPS 1.3 / 1.4 (Power outputs: Extruder, Fan, Bed)
// 34 = RAMPS 1.3 / 1.4 (Power outputs: Extruder0, Extruder1, Bed)
// 35 = RAMPS 1.3 / 1.4 (Power outputs: Extruder, Fan, Fan)
// 4  = Duemilanove w/ ATMega328P pin assignment
// 5  = Gen6
// 51 = Gen6 deluxe
// 6  = Sanguinololu < 1.2
// 62 = Sanguinololu 1.2 and above
// 63 = Melzi
// 64 = STB V1.1
// 65 = Azteeg X1
// 66 = Melzi with ATmega1284 (MaKr3d version)
// 67 = Azteeg X3
// 68 = Azteeg X3 Pro
// 7  = Ultimaker
// 71 = Ultimaker (Older electronics. Pre 1.5.4. This is rare)
// 77 = 3Drag Controller
// 8  = Teensylu
// 80 = Rumba
// 81 = Printrboard (AT90USB1286)
// 82 = Brainwave (AT90USB646)
// 83 = SAV Mk-I (AT90USB1286)
// 9  = Gen3+
// 70 = Megatronics
// 701= Megatronics v2.0
// 702= Minitronics v1.0
// 90 = Alpha OMCA board
// 91 = Final OMCA board
// 301 = Rambo
// 21 = Elefu Ra Board (v3)
// 310 = Mega Controller

#ifndef MOTHERBOARD
#define MOTHERBOARD 33
#endif


上方程式碼是我從開源的 3D 印表機韌體 Marlin 中擷取出來的

在程式碼的最後表明如果在前面的文件中都沒有進行 #define MOTHERBOARD 的動作,就定義 MOTHERBOARD 為 33

如此一來就實現同一個程式碼可以用到不同的硬體配置 (在專案中的其它頭文件中會去抓到 MOTHERBOARD 為 33 的資訊,然後再去寫這個硬體的 I/O 腳位配置以相容這套韌體程式)

條件邊譯除了上面常見的用法外,還有其它常見的三種配對方式,如 if...else if...else 的用法

一、
#ifdef NAME
... 
 內容
...
#endif

二、
#ifdef NAME
...內容1
#else
...內容2
#endif

三、
#ifdef NAME1
...內容1
#elif NAME2
...內容2
#else
...內容3
#endif

typedef

typedef 很常被大家所使用,其容許我們將數據類型自行命別名,又或者說是定義一個新的資料型態。可參考此網站以及此處

首先我們先從簡單的說起,通常我們會將某個資料型態或者將常用的資料型態組合給予一個比較直觀而易懂的別名定義別名之後我們就可以像使用原有的資料型態(如 int, float ... )一樣,直接拿它來宣告或定義變數

指標也可以進行 typedef

typedef unsigned char boolean; /* 為 unsigned char 取了別名 boolean */ 

typedef unsigned long int uint32; /* Unsigned 32 bit value */ 

typedef unsigned short uint16; /* Unsigned 16 bit value */ 

typedef unsigned char uint8; /* Unsigned 8 bit value */ 


typedef signed long int int32; /* Signed 32 bit value */ 

typedef signed short int16; /* Signed 16 bit value */ 

typedef signed char int8; /* Signed 8 bit value */ 

typedef char* String;

String str = "This is a string !";


當我們透過 typedef 定義新的名稱時,相當於告訴編譯器,有一個新的資料型態!

定義一個新的型態,與利用 #define 進行文本取代還是有所區別的,請看下列例子

typedef char Mychar1; //定義了一個新的資料型態,從此多了一個型態
#define Mychar2 char; //請編譯器將文本內的 "Mychar2" 文字代換成 "char"

Mychar1 var1;//定義一個名為 var1 的 Mychar1型別 的變數
Mychar2 var2;//定義一個名為 var1 的 char 型別的變數

一個更困難的例子,typedef 配合指標函數使用

typedef int (*Myfunc)(int,int); //定義一個叫 Myfunc 的型態
//這型態是一個指標、有兩個數入 int型別的輸入引數、回傳值是 int型別

有了這個新的型態(型別)後,我們就能利用 Myfunc 來定義變數如

Myfunc A,B; //定義兩個變數 A 及 B,型態如上所述。

如果沒有 Myfunc 這個 typedef 的話,上述定義的寫法應為以下

int (*A)(int,int);
int (*B)(int,int);

如果上述這串是某個函式的引數或回傳值時,若有使用 typedef,則可以寫成

Myfunc NewFunc(int a,Myfunc b);//簡單易懂

若沒有使用,就算是高手也要看到眼花,例如

int (*NewFunc(int a,int (*b)(int,int)))(int,int); // ????????????????

#define 與 typedef 是不同的,如果你試著將上面那行用 #define 來做,你就看得出他們的分別了。

typedef 跟 define 兩者很相似,但其本意還是有些不同之處

#define 只是進行文件的替換,也就是在編譯以前的前處理器就進行了文字上的替換,編譯器看到的是展開後的程式碼(看不見 #define 這些語句),且 #define 會作用於整份檔案

typedef 不是文本的替換,其作用的範圍和宣告數據型別時的規則相符,且編譯器是看的見 typedef 這些語句的

#include "stdio.h"

int main() {
 typedef char Mychar;
 typedef int Myint;
 typedef double Mydouble;

 Mychar number_char = 10;
 Myint number_int = 1666;
 Mydouble number_double = 24.22;

 printf("number_char = %d, sizeof(number_char) = %d\n", number_char, sizeof(number_char));
 printf("number_int = %d, sizeof(number_int) = %d\n", number_int, sizeof(number_int));
 printf("number_double = %.2f, sizeof(number_double) = %d", number_double, sizeof(number_double));

}




後綴修飾

直接舉一個網路上常見的範例

#include 

#define SECONDS_PER_YEAR  60*60*24*365UL

int main(void)
{
    unsigned long int  a = SECONDS_PER_YEAR;
    printf("a = %ld\n",a);

    return 0;
}


#define SECONDS_PER_YEAR  60*60*24*365 後面加上 UL 後綴修飾,代表該值應該使用 unsigned long 整數來進行儲存,其精神在於相容不同位元的系統

因現在大多數的電腦為 32 位元或是 64 位元的作業系統, unsigned int 可以儲存 0~4294967295 的值 int 可以儲存 -2147483648 ~ 2147483647 的值,也就是說無論我宣告成這兩者之一,都可以儲存60*60*24*365 = 31536000 的值。

但是如果我們現在想要將同樣的程式碼移植到 16 位元甚至 8 位元的嵌入式系統中,若沒有加入後綴修飾進行提醒則很有可能會出現問題

以 16 位元舉例 unsigned int 只可以儲存 0~65535 而 unsigned long int 可以儲存 0~4294967295

所以我們在 #define SECONDS_PER_YEAR  60*60*24*365 加上 UL 可以有效的提醒編譯器或是程式人員要特別注意這個常數必須使用 unsigned long int 。

若我們平常在使用 #define 宣告整數常數的時候沒有使用後綴修飾詞,則型別就會自動用第一個放得下該數的型別來進行存放( 先是 int 接著 long int 接著 unsigned long int ... 等等)

浮點數的部分則是自動都設置為 double

後綴修飾(大小寫不分)

用於整數變量
u           表示 unsigned
l            表示 long
ul & lu 表示 unsigned long

用於浮點數
f             表示 float
l              表示 long double


最後巨集還可以跟指標連結在一起,附上連結網址

inline (僅 C++支援)

inline 使用在函數定義的最前方,可以讓函數產生如巨集那樣展開的效果

一般而言程式在進行函數調用時,會從 "函數呼叫" 的地方跳到函數標頭並且執行

如果我們在函數前方加入 inline ,則在前處理的過程中會自動將函數主體的內容與函式呼叫進行文本替代,就像巨集(#define)一樣。

首先我們先不要用 inline 來實現

#include "stdio.h"

int sum(int a,int b);

int main(void) {

 int A = 5;
 int B = 17;

 int C = sum(A,B);

 printf("C = %d\n", C);

 return 0;
}

int sum(int a,int b){

 int c = a + b;

 return c;
}


接著使用 inline

#include "stdio.h"

inline int sum(int a,int b);

int main(void) {

 int A = 5;
 int B = 17;

 int C = sum(A,B);

 printf("C = %d\n", C);

 return 0;
}

inline int sum(int a,int b){
 int c = a + b;
 return c;
}


inline 目前難產,待察明原因

後記補充 : inline 需在 C++ 的編譯環境才有支援(下圖);若編譯環境為 C 則會報錯(上圖)



↓↓↓ 連結到部落格方針與索引 ↓↓↓

Blog 使用方針與索引