老師有沒有講?不要用 magic number!哼,摔鍵盤。

前陣子[1]碰到一個奇怪的 bug,程式死在不應該出錯的地方,找半天找不到原因。最後靈機一動,發覺到每次跑,都死在同一筆 test data (跑一輪要很久,以小時計)。可是,檢查該筆 test data,又找不到任何特殊的地方。最後只好把目標放在,該筆 test data 的「序號」,才發覺又是新同事幹的好事。

我們的程式,跑一筆 test data,需要先配一塊資料結構,使用這個資料結構進行運算,最後再釋放這塊資料結構。為了效率的考量,我們選擇由 caller 負責準備這塊資料結構的記憶體。

也就是說,不像 fopen() 那樣,回傳一個 FILE* 指向 library 所配置的記憶體,而是由呼叫端,也就是應用程式,負責準備這塊記憶體,最後也由呼叫端,負責釋放。這樣的壞處是,資料結構的大小與內容,可以被呼叫端看見,違反了資訊隱藏的原則。但相對地,應用程式可以視需要,改用如全域變數等方式,而迴避了動態配置記憶體的效能負擔[2]

舉例來說,針對 command-line 傳來的各個 test case,我們可能測試如下:

int main(int argc, char* argv[])
{
    for (int i = 1; i < argc; ++i) {
        FooStruct foo;
        memset(&foo, 0, sizeof(foo));
        InitFoo(&foo);
        CalcFoo(&foo, argv[i]);
        UninitFoo(&foo);
    }
    return 0;
}

因為程式可能會以 single-thread 跑 N 次,或是以 multi-thread 分散開來跑,所以,這樣的架構不可行,多個 thread 可能同時存取到同一個 foo。因此,新同事選擇將 FooStruct 放在全域變數裡。如下:

FooStruct foo[1789];

int main(int argc, char* argv[])
{
    for (int i = 0; i < (argc - 1); ++i) {
        InitFoo(&(foo[i]));
        CalcFoo(&(foo[i]), argv[i + 1]);
        UninitFoo(&(foo[i]));
    }
    return 0;
}

然後當我們換了一組更多筆數的 test data 時,程式就爆炸了。

據說當初在開這個測試用程式的規格時,有講到 test data 的來源、數量甚至測法,要能夠在 run-time 時指定。


  1. 值此發表之際,這已經是很久以前的故事了。
  2. 這不一定是好的設計,只是當時我們做了這麼一個決策。