C語言語法

extern關鍵字

變數使用前要先宣告(declaration),C 的 extern 關鍵字,用來表示此變數已經在別處定義(definition),告知程式到別的地方找尋此變數的定義(可能在同一個檔案或其他檔案)。

  • C/C++使用變數有二個基本的原則:

    • 對於變數或函數,可以宣告多次,但只能定義一次。

      • 當鍵入int a;時,代表此變數已經被定義了。我們如何宣告一個變數呢?加上extern 變成exern int a;,告訴comipler,此變數a只是一個宣告,它的定義在別處。
      • .函數規則分成宣告與定義二種。當一個函數只有傳回值、名稱、傳入值,沒有大括號{},compiler會把此種形式當成是函數宣告;
      • 如果加上了{}, 形成函數定義。
      • 所以你要宣告一個函數,只須 鍵入 intf();,extern可加可不加,compiler會自動把它示為函數宣告。
      • 函數或變量在聲明時,並沒有給它實際的記憶體空間,因此可以保證你的程序編譯通過,但是當函數或變量定義的時候,它就在記憶體中有了實際的物理空間,如果你在編譯模塊中引用的外部變量沒有在整個工程中任何一個地方定義的話,那麼即使它在編譯時可以通過,在連接時也會報錯,因為程序在內存中找不到這個變量。你也可以這樣理解,對同一個變量或函數的聲明可以有多次,而定義只能有一次。
    • 尋找變數或函數時,是採用Lookup(向上尋找定義或宣告)。

  • static與extern是不相容的關鍵字,即extern和static不能同時修飾一個變量

    • static修飾的全局變量聲明與定義同時進行,也就是說當你在頭文件中使用static聲明了全局變量後,它也同時被定義了。
    • static修飾全局變量的作用域只能是本身的編譯單元,也就是說它的“全局”只對本編譯單元有效,其他編譯單元則看不到它。
  • c語言有三種鏈接,外部鏈接(全域變數),內部鏈接(區域變數)和無鏈接(區塊變數)。

    • 外部鏈接:對構成程序的所有文件可用,如函數和全局變量具有外部鏈接。
    • 內部鏈接:僅在聲明他們的文件中是已知的。如聲明為static的文件域具有內部鏈接。
    • 無連接:僅在自己的塊中已知,其它地方沒有辦法訪問,如局部變量。
  • 全域變數:直接宣告在(主)函式之外的變數,這個變數在整個程式之中都「看」得它的存在,而可以呼叫使用。

    • PI 這個變數可以被主函式 main() 與函式 area() 來使用,通常全域變數是用來定義一些常數,初學者不應為了方便而將所有的變數都 設定為全域變數,否則將來一定會發生變數名稱管理上的問題,全域變數的生命週期始於程式開始之時,終止於程式結束之時。
const double PI = 3.14159;

double area(double r) { return r * r * PI; }

int main(void) {
    // ..... 
    return 0;
}
  • 區域變數: 指宣告在函式之內的變數,或是宣告在參數列之前的變數,它的可視範圍只在宣告它的函式區塊之中,其它的函式不可以使用該變數,例如在上例的主函式中,您不可以直接對 area() 函式中的變數 r 進行存取,區域變數的生命週期開始於函式被呼叫之後,終止於函式執行完畢之時。

  • 區塊變數:區塊變數是指宣告在某個陳述區塊之中的變數,例如 while 迴圈區塊中,或是 for 迴圈區塊,例如下面的變數 i 在迴圈結束之後,就會自動消失。

while(...) {
    int i = 0;
    // ....
}
  • 變數覆蓋:當可視範圍大的變數與可視範圍小的變數發生同名狀況時,可視範圍小的變數會暫時覆蓋可視範圍大的變數,稱之為「變數覆蓋]。
int num = 10;
for(int i = 0; i < 100; i++)  {
    /* 當執行迴圈時,迴圈內的 num 變數作用將覆蓋迴圈外的 num 變數;
       同樣的作用發生於全域變數與區域變數發生同名的時候。
    */
    int num = 20;
    // ...
}
// 將會輸出10
printf("%d", num);
  • extern主要作用是:聲明在程序的其它地方使用外部鏈接聲明的對象。
    • 聲明:表述對象的名稱和類型。
    • 定義:為對象分配存儲空間。
    • extern最重要的用途是多文件程序,c允許程序分散在多個文件中,分別編譯,鏈接到一起。
//main.c
extern int a;  //extern一定要加
exter void f();//extern可加可不加

int main() {
    a=100;
    f();
    return 0;
}

//f.c
~~~~~~~~~~
int a;
void f()
{;}

變數定義在同一個檔案

  • 以下程式若無「extern int x;」,會編譯錯誤。
  • 若僅無「extern」,雖然可以編譯成功,但效果是main()裡面宣告了一個沒有初始值的 x,印出的 x 將是不可預期的,而不是x=10
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char** argv) {
    extern int x;
    printf("%d\n", x);
    return (EXIT_SUCCESS);
}
int x = 10;

變數定義在其他檔案

#include <stdio.h>
#include <stdlib.h>

extern int x;

void test1() {
    int x = 11;
    printf("test1():%d\n", x);
}

void test2() {
    x = 12;
    printf("test2():%d\n", x);
}

int main(int argc, char** argv) {
    printf("main():%d\n", x); //全域x=1
    aa_test(); //全域x=1
    test1(); //test1()區域x=11
    aa_test(); //全域x=1
    test2(); //全域x=12
    aa_test(); //全域x=12
    printf("main():%d\n", x); //全域x=12
    return (EXIT_SUCCESS);
}
// aa.c
#include <stdio.h>

int x = 1;

void aa_test() {
    printf("aa_test():%d\n", x);
}

C++ extern 關鍵字

  • extern "C" 是C++特有的組合關鍵字,在C裡並沒有這個的組合,僅有extern這個關鍵字‧

  • 為什麼C++會需要這樣的關鍵字組呢? 原因是C++它有一個複載(overloading)的功能,也就是說同樣的函式名稱可以有多個定義只要參數簽名不同即可。比如說C++裡可以有以下的二個宣告。

bar(int i, int j);
bar(double i, double j);
  • 這二個函式都是同樣的名字叫foo,僅參數型式不同。然而在C語言裡是不被允許的! C++是如何處理這同名的函式呢? 其實他在編譯時會偷偷的把這二個函式名變成不同的名字,舉例來說bar(int i, int j)可能會被改成_bar_int_int(每種compiler產生不太一樣),而另一個則被改成_bar_double_double。這技術稱mangling。

  • 當我們希望C++不要偷換函式名時該怎麼辦? 於是就有了extern "C" 這個關鍵字組出現了。這個字組就是請C++不要自己又偷天換日,請它保留原名。所以當我們宣告一個函式如下時,編譯器就不會把bar變成_bar_double_double。

    extern "C" bar(int i, int j);
    
  • 實際使用的注意事項:

    • 當C++使用C的函式庫(library)時,C++不能直接套用C的header檔。因為他會把header裡的宣告給mangleing了。必須使用如下:

      extern "C"  {
        #include "C_LIB.h"  //C_LIB 是C語言所產出。
      }
      
    • 相反的,在C語言的編譯器裡若要使用由C++所製告出來的C函式庫,那麼也不能直接的使用C++的header檔。因為此header檔必然存在extern "C" 這個關鍵字組,而這字組C語言是不認識的。所以必需要把C++的header檔裡的extern "C" { } 移除後才可以讓C編譯器使用。

static關鍵字

  • static 變數,當變數有宣告時加上 static 限定時,一但變數生成,它就會一直存在記憶體之中,即使函式執行完畢,變數也不會消失。
#include <stdio.h>

void count(void);

int main(void) {
    for(int i = 0; i < 10; i++) {
        count();
    }

    return 0;
}

void count(void) {
     /* 雖然變數 c 是在 count() 函式中宣告的,但是函式結束後,變數仍然存在,
        它會直到程式執行結束時才消失,
        雖然變數一直存在,但由於它是被宣告在函式之中,所以函式之外仍無法
        存取 static 變數。
    */
    static int c = 1;
    printf("%d ", c);
    c++;
}

// output
1 2 3 4 5 6 7 8 9 10
  • 一個 static函式表示,其可以呼叫的範圍限於該原始碼文件之中,如果有些函式僅想在該原始程式文件之中使用,則可以宣告為 static,這也可以避免與其他人寫的函式名稱衝突的問題。

標頭檔定義分析

// 以下兩行避免標頭檔被重覆引用
#ifndef __INCvxWorksh
#define __INCvxWorksh 

/* _cplusplus的定義是若該標頭檔被c++引用時,需加上extern "C"避免mangling,
   否則給C使用時不須加上此定義。
*/
#ifdef __cplusplus
extern "C" {
#endif 
/*...*/ 
#ifdef __cplusplus
}
#endif 
#endif /* __INCvxWorksh */

volatile 關鍵字

  • Volatile是一個變數聲明限定詞。它告訴編譯器,它所修飾的變數的值可能會在任何時刻被意外的更新,即便與該變數相關的上下文沒有任何對其進行修改的語句。造成這種意外更新的原因相當複雜。

  • volatile變數代表其所儲存的內容會不定時地被改變,宣告volatile變數用來告訴編譯器不要對該變數做任何最佳化操作,凡牽涉讀取該volatile變數的操作,保證會到該變數的實體位址讀取,而不會讀取CPU暫存器的內容 (提升效能) 。

  • Volatile陷阱: 其問題在於square函式的平方算式,var *var,此指令代表到var位址讀取其內容。然而,var位址可能儲存硬體暫存器,這些暫存器內容會隨時間而改變 (例如: 狀態暫存器),有可能第一次讀取的時候為4, 下一次讀取為5, 導致計算出來的值不正確。

#include <stdio.h>

int square(volatile int *var) {
    // *var指向的位址可能在計算到一半時,在程式外部被改變了
    return *var **var;
}

int main(void) {
    int var = 5;
    printf("result: %d\n", square(&var));
    return 0;
}
  • 要想給一個變數加上volatile限定,只需要在變數類型聲明附之前/後加入一個volatile關鍵字就可以了。下面的兩個實例是等效的,它們都是將foo聲明為一個“需要被即時更新”的int型變數。同樣,聲明一個指向volatile型變數的pointer也是非常類似的。下面的兩個聲明都是將foo定義為一個指向volatile integer型變數的pointer。
volatile int foo;
int volatile foo; 

volatile int * foo; 
int volatile * foo;
  • 一個Volatile型的pointer指向一個非volatile型變數的情況非常少見,儘管如此,其語法如下:
int * volatile foo;
  • 一個volatile型的指標指向一個volatile型的情形定義如下:
int volatile * volatile foo;
  • 最後,如果你將volatile應用在struct或者是union上,那麼該struct/union內的所有內容就都帶有volatile屬性了。如果你並不想這樣(牽一髮而動全身),你可以僅僅在struct/union中的某一個成員上單獨使用該限定。

  • 當一個變數的內容可能會被意想不到的更新時(by hardware),一定要使用volatile來聲明該變數。通常,只有三種類型的變數會發生這種意外:

    • 在記憶體中進行位址映射的周邊暫存器(peripheral registers);
    • 在中斷處理程式中(interrupt service routine)可能被修改的全域變數;
    • 多線程應用程式中(multi-threaded application) 的全域變數

const 關鍵字

const修飾普通變數和指標

  • 常數與指標的讀法(由右內往左讀)
    • const double *ptr; // 記憶體的位置可以變動,但是記憶體的內容不能變動
    • double *const ptr; // 記憶體的內容可以變動,但是記憶體的位址不能變動
    • double const* ptr; // // 記憶體的位置可以變動,但是記憶體的內容不能變動
    • const double *const ptr; //記憶體的位址與內容都不能變動
const char ch = 'a';
const int a[5] = {1, 2, 3, 4, 5};  

// a是一個陣列的首位址.p是指向常量的指標
const int *p = a;           

//a是一個陣列的首位址.p是指標常量;
int * const p = a;          

//a是一個陣列的首位址。p是指向常量的指標常量
const int * const p = a;    


// p是指向常量的指標,因此不可以通過給指標賦值來改變陣列中的資料
const int *p = a;      
// *p = 10;       /*錯誤*/
// *(p + 2) = 1;  /*錯誤*/

// 指標常量只是它的位址不可以改變,並不是它指向的內容一定不可以改變
int * const p = a;      
//  *p = 2;          /*可以*/
//  *(p+1) = 10;     /*可以*/
//  p++;             /*不可以,因改變了p的位址*/

const修飾函數參數

  • const修飾函數參數是它最廣泛的一種用途,它表示函數體中不能修改參數的值(包括參數本身的值或者參數其中包含的值)。

    • void function(const int Var); //傳遞過來的參數在函數內不可以改變(無意義,因為Var本身就是call by value)
    • void function(const char* Var); //參數指標所指內容為常量不可變
    • void function(char const Var); //參數指標本身為常量不可變(也無意義, 因為char Var也是定值)
  • 參數為引用,為了增加效率同時防止修改。修飾傳址參數時:

    • void function(const Class& Var);//傳址參數在函數內不可以改變
    • void function(const TYPE& Var); //傳址參數在函數內為常量不可變

const修飾函數返回值

  • const修飾函數返回值其實用的並不是很多,它的含義和const修飾普通變數以及指標的含義基本相同。
    • const int fun1() 這個其實無意義,因為參數返回本身就是賦值。
    • const int fun2(): 調用時 const int pValue = fun2();我們可以把fun2()看作成一個變數,即指標內容不可變。
    • int const fun3(): 調用時 int const pValue = fun2();我們可以把fun2()看作成一個變數,即指針本身不可變。

const修飾類物件/物件指標/物件引用

*const修飾類物件表示該物件為常量物件,其中的任何成員都不能被修改。對於物件指標和物件引用也是一樣。

  • const修飾的物件,該物件的任何非const成員函數都不能被調用,因為任何非const成員函數會有修改成員變數的企圖。
class AAA
{
   void func1();
void func2() const;
}

const AAA aObj;
aObj.func1(); ×
aObj.func2(); 正確

const修飾成員變數

  • const修飾類的成員函數,表示成員常量,不能被修改,同時它只能在初始化列表中賦值。
class A
{
   ...
   //成員常量不能被修改
   const int nValue;      
   ...
   //只能在初始化列表中賦值
   A(int x): nValue(x) {};
}

const修飾成員函數

  • const修飾類的成員函數,則該成員函數不能修改類中任何非const成員函數。一般寫在函數的最後來修飾。
class A{
   ...
//常成員函數, 它不改變物件的成員變數. 也不能調用類中任何非const成員函數。
void function() const; 
}

const常量與define巨集定義的區別

  • 編譯器處理方式不同
    • define巨集是在預處理階段展開。
    • const常量是編譯運行階段使用。
  • 類型和安全檢查不同
    • define巨集沒有類型,不做任何類型檢查,僅僅是展開。
    • const常量有具體的類型,在編譯階段會執行類型檢查。
  • 存儲方式不同
    • define巨集僅僅是展開,有多少地方使用,就展開多少次,不會分配記憶體。
    • const常量會在記憶體中分配(可以是堆中也可以是棧中)。

results matching ""

    No results matching ""