2012年9月1日 星期六

[轉錄] static 修飾詞

本課程到這裡才介紹 static 是因為它用在不同的地方有不同的意義,所以到了介紹類別以後才一併介紹,在 C++ 語言當中,static 可以用來修是以下三種對象,並產生三種效果。

修飾檔案中的全域變數或函式

令全域變數與函式只限於在該檔案中被使用,效果之一是保護內部函式不被使用,效果之是避免同名衝突,例如前例 qsort( ) 所用的 compare 函式,它只是用於 qsort( ) 中比較兩陣列元素的大小,不應該能夠開放在其他地方使用,所以把它宣告為 static,在別的 .cpp 檔案就算擁有它的原型宣告也無法呼叫它。

修飾函式中的區域變數

通常函式中的區域變數在程式流程每次進入函式的時候才被產生於堆疊區,並且初始化,程式流程一旦離開了函式,它就會被消滅,所以該變數的數值是無法保留的,每次進入函式都會由初始值重新開始,但如果將這些區域變數宣告為 static,就會令它保持現值,等於是讓區域變數有全域變數的效果。

修飾類別中的資料或函式成員

如前篇所述,要使用類別中的資料成員與函式成員,必須以該類別宣告變數(new instance)之後,才能夠透過 . 運算子或 -> 運算子取用,但是如果把類別中的資料或函式宣告為 static,它們就變成不依賴實體(instance)運作的全域變數與全域函式,可以直接依據類別明撐以 ::  運算子取用,等於是有了全域變數與全域函式的效果。
修飾檔案中的全域變數或函式
在全域變數或函式的宣告之前加上 static,就讓它們不能跨 .cpp 檔使用,如下例中的 compare_grade( )它的目的僅在於提供給下面的 qsort( ) 使用,所以在它函式宣告之前加了 static,這麼一來它就只能在這個 .cpp 檔案中有效,在其他的 .cpp 檔案中就算以 extern 來援用它也不可以,另一方面,在其他的 .cpp 檔案中就算有同名的 compare_grade( ) 函式也沒關係。
#include 
#include 
#include "GradeList.h"

static int compare_grade(const void *ptr1, const void *ptr2)
{
    float f1 = *((float *)ptr1);  // 把 ptr 轉型成 (float *)再取值
    float f2 = *((float *)ptr2);

    if (f1 > f2) { return 1; }    // 如果 f1 比較大就傳回正數
    else if (f1 < f2) {  return -1;  }

    return 0;
}

// 用 compare_grade( ) 比較 grade 中每個 float,由小到大排序:
void GradeList::sort_grade_list(void)
{   qsort(grade, students, sizeof(float), compare_grade);  }
另一個與這樣的 static 相對的修飾詞是 extern,這個修飾詞用來讓你使用其他檔案的全域變數與函式,在變數宣告或函式原型宣告前加上 extern,則編譯器不會產生新變數或函式,而是在連結(linking)階段才使用其他檔案中的變數或函式,例如:
A.cpp
int curUsers = 5;

B.cpp
extern int curUsers; // 不可以再給初始值
修飾函式中的區域變數
以下這個範例是 StaticVariables.cpp,使用 static 來修飾區域變數很簡單地就是在區域變數的宣告之前加上 static,如下例右邊所示,這個例子的 func( ) 函式會把整數變數 x 以 printf( ) 顯示後加一,然後在 main( ) 當中呼叫了 func( ) 函式 5 次。在左邊的例子裡,區域變數 x 未用 static 修飾,所以它的行為就是典型的區域變數一般,每一次呼叫 func( ) 都會重新產生一個區域變數 x 並初始化為 1,所以在執行結果當中,呼叫了 5 次 func( ) 都是顯示 x = 1,而在右邊的例子裡,由於 x 用了 static 去修飾它,所以它會保留上一次執行 func( ) 實的原值,而 func( ) 的內容是顯示後加一,所以 main( ) 中呼叫了 func( ) 函式 5 次的執行結果是 x 由 1 遞增到 5 了。
未使用static 修飾詞 使用 static 修飾區域變數 x
#include 

void func(void)
{
    int x = 1;
    printf("x = %d\n", x);
    ++x;
}

void main(void)
{
    for (int i=0; i<5 func="func" i="i" pre="pre">
     
#include 

void func(void)
{
    static int x = 1; // 只會初始化一次
    printf("x = %d\n", x);
    ++x;
}

void main(void)
{
    for (int i=0; i<5 func="func" i="i" pre="pre">
  
執行結果 執行結果
x = 1
x = 1
x = 1
x = 1
x = 1
x = 1
x = 2
x = 3
x = 4
x = 5
在函式當中,我們經常會需要用到上一次執行的結果,如果不使用 static,就必須使用全域變數或是增加類別中的成員來紀錄它們,當然我們也可以這麼做,但會使得程式中冒出許多莫名其妙的計數器之類用途的全域變數或類別成員,這樣的程式碼看起來就不太清爽了。
修飾類別中的資料或函式成員
在前篇已經提到 class 的前身是 struct,故以資料成員的原理和使用機制來說都和 struct 差不多,在類別中的一個資料成員也是以此類別宣告出的變數(稱為 instance)之記憶體布局(memory layout)中的一欄,而函式成員也必須由 instance 透過 . 運算子或 -> 運算子使用,才能夠在函式當中使用類別的資料成員,換言之,未以 static 修飾的變數或函式成員,必須有實體(instance)才能被使用,例如:
GradeList list;
list.adjust_grade_list();
而類別中以 static 修飾的變數或函式成員,則不受這個限制,變成像是命名空間中的全域變數與函式呼叫,例如:
GradeList::get_sqrt_grade(x);
但也因為如此,static 函式成員中不可以使用任何非 static 變數成員,這是因為沒有 instance,那些成員當然不存在,所以,在類別中以 static 修飾的資料與函式成員,他們的性質和非 static 成員截然不同,前面說到 class 宣告會自成命名空間,他們是此命名空間中的全域變數與函數。除此之外要注意的是 static 變數成員必須另外在類別外再做一次變數宣告,這是和 struct 與命名空間都不一樣的地方,請見下面經過節錄的 Grade Class 2 例子:
GradeList.h
class GradeList
{
    public:
    char teacher[16];        // 教師的名稱
    GRADE * grade;           // 以動態陣列儲存
    int students;            // grade 陣列中的學生數

    static char school[16]; // 學校名稱

    private:
    int size;                // grade 目前已配置的空間數量

    public:
    GradeList();             // 建構函式: 初始化grade, students, size
    ~GradeList();            // 解構函式: 釋放grade 所佔用的記憶體

    // 增加分數:
    void add_grade(float value, int id = -1);

    // 只是提供一個計算公式:
    static float get_sqrt_grade(float x);
};
在 GradeList.h 這個例子當中我們看到,把變數成員 school 和函式成員 get_sqrt_grade( ) 宣告為 static,所以在下面的 GradeListAlloc.cpp 當中,首先 school 要再宣告一次並且初始化之,這點是和命名空間不一樣的,另外在靜態函式 get_sqrt_grade( ) 當中,你可以看到只是進行單純的運算,當中絕不可使用 GradeList 類別的任何非 static 的資料成員。
GradeListAlloc.cpp
#include 
#include 
#include "GradeList.h"

// 靜態變數成員必須另外宣告過才有實體:
char GradeList::school[16] = "ITRC";

// 只是提供一個計算公式供所有 instance 共用:
float GradeList::get_sqrt_grade(float x)
{
    return (float)(sqrt(x) * 10.0f); 
}
然後在 UserApplication.cpp 中使用 GradeList 類別的時候,非static 函式必須依賴 instance list 而使用,透過 list.add_grade( ) 的方式去使用之,而 static 的資料成員 school 與函式成員 get_sqrt_grade( ) 可以直接透過 GradeList:: 使用之。
UserApplication.cpp
#include 
#include 
#include "GradeList.h"

void main(void)
{
    // 宣告一個 GradeList 變數並且初始化之:
    GradeList list;

    // 類似命名空間的方式使用類別內的靜態變數成員:
    printf("School: %s\n", GradeList::school);

    // 透過 add_grade 在 list 裡面增加資料,並自動增長空間:
    list.add_grade(78.0f, 1); list.add_grade(44.0f, 2);
    list.add_grade(89.0f, 3); list.add_grade(55.0f, 4);
    list.add_grade(45.0f, 5); list.add_grade(75.0f, 6);
    list.add_grade(94.0f, 7); list.add_grade(74.0f, 8);
    list.add_grade(83.0f, 9); list.add_grade(65.0f, 10);

    // 試用一下靜態函式成員:
    float adjustedGrade = GradeList::get_sqrt_grade(36.0f);
    printf("Adjusted grade of 36 is %f\n", adjustedGrade);
}
執行結果如下:
School: ITRC
Adjusted grade of 36 is 60.000000
類別中的 static 資料成員還有一個很重要的性質,就是它們其實是所有 instance 共用的(因為它其實就是個全域變數),因此在該類別的任何一個 instance 中存取它,都會改變它的值,所以通常會用來保存所有 instance 中共同的數值,例如在上例當中,所有成績表的班級都是屬於同一間學校,所以 school 資料成員便宣告為靜態(static),如此一來就不會在不同的 GradeList 當中有不同的值。除此之外,基於這個特性,類別中的 static 資料成員還有一個很典型的應用就是計數(counting),也就是計算目前 new 出了幾個 instance,如下例所示,原理很單純地就是宣告一個 static 的計數變數成員 classes 並且初始化為零,接著在 GradeList 類別的建構函式當中將它遞增、在解構函式中將它遞減。
class GradeList
{
    public:
    static int classes;     // 紀錄目前有幾個 instances

    GradeList();
    ~ GradeList();
};

int GradeList::classes = 0; // 把計數初始化為零

GradeList::GradeList()      // 在建構式中遞增計數
{   ++classes;  }

GradeList::~GradeList()     // 在解構式中遞減計數
{   --classes;  }
由於建構函式是每當一個 instance 被宣告或 new 就會自動地執行到的,所以每當一個 new instance 產生了,classes 就會加一,同理、解構函式是每當一個 instance 被 delete 自動地執行到的,所以每當一個 instance 被消滅或 delete 了,classes 就會減一,因此 classes 這個變數等於是紀錄了 GradeList 類別有 instace 被 new 了幾次、delete 了幾次,所以它隨時呈現出 GradeList 類別目前的 instance 數量。

沒有留言: