GNU 的 gperf 工具是一種 「完美的」 散列函數,能夠爲用戶提供的一組特定字符串生成散列表、散列函數和查找函數的 C/C++ 代碼。經過本文學習如何使用 gperf 實現 C/C++ 代碼中高效的命令行處理。ios
命令行處理一直以來都是軟件開發中最容易被忽視的領域。幾乎全部比較複雜的軟件都具備一些可用的命令行選項。事實上,大量 if-else語句常常被用來處理用戶輸入,所以維護這種遺留代碼至關費時,對資深程序員亦是如此。這種情形下,不少 C 開發人員一般使用冗長(一般都嵌套使用)的 if-else 語句,以及 ANSI C 庫函數,例如 strcmp、strcasecmp 和 strtok 做爲補充,如清單 1 所示。程序員
清單 1. C 語言樣式的命令行處理數據庫
if (strtok(cmdstring, "+dumpdirectory")) { // code for printing help messages goes here } else if (strtok(cmdstring, "+dumpfile")) { // code for printing version info goes here }
C++ 開發人員並無使用基於 ANSI C 的應用程序編程接口,而是使用標準模板庫(Standard Template Library,STL)中的字符串。儘管如此,仍然沒法避免使用嵌套的 if-else 序列語句。很明顯,隨着命令行選項不斷增長,這種方法缺少可伸縮性。對於具備 N 個選項的典型程序調用,代碼最終執行 0(N2)比較。爲了生成運行更加快捷並易於維護的代碼,使用散列表存儲命令行選項並使用散列驗證用戶指定的輸入,這種方法很是有幫助。編程
這就是 gperf 扮演的角色。它將從預約的有效命令行選項列表和時間複雜度爲 O(1) 的查找函數中生成一個散列表。所以,對於具備 N 個選項的典型程序調用,代碼只需執行 O(N) [N*O(1)] 比較 — 這是對遺留代碼的巨大改進。數組
Gperf 將從用戶提供的文件中(一般使用 .gperf 做爲擴展名,但不作強制要求)— 例如,commandoptions.gperf — 並針對散列表、散列和查找方法生成 C/C++ 源代碼。全部代碼被定向到標準輸出,而後必須重定向到相似下面的文件:網絡
gperf -L C++ command_line_options.gperf > perfecthash.hpp
注意:-L 選項將指示 gperf 生成 C++ 代碼。數據結構
清單 2 展現了 gperf 輸入文件的典型格式。app
%{ /* C code that goes verbatim in output */ %} declarations %% keywords %% functions
文件格式由若干元素組成:C 代碼內容、聲明、關鍵字和函數。函數
C 代碼內容是可選的,使用 %{ 和 %} 括起來。其中的 C 代碼和註釋將被所有複製到 gperf 生成的輸出文件中。(注意,此處相似於 GNU flex 和 bison 實用程序)。工具
聲明部分也是可選的;若是沒有使用 -t 選項調用 gperf,則徹底能夠忽略聲明部分。可是,若是啓用了這個選項,聲明部分中最後一個元素的第一個字段必須是使用 char* 或 const char* 標識符調用的名稱。
可是,經過使用 gperf 中的 -K 選項能夠改寫第一個字段的名稱。例如,若是但願將該字段命名爲 command_option,執行如下 gperf 調用:
gperf -t -K command_option
清單 3 展現了 C 代碼內容和聲明部分。
清單 3. C 代碼內容和聲明部分
%{ struct CommandOptionCode { enum { HELPVERBOSE = 1, ..., // more option codes here _64BIT = 5 }; }; typedef struct CommandOptionCode CommandOptionCode; %} struct CommandOption { const char* command_option; int OptionCode; }; %%
關鍵字部分包含關鍵字— 在本例中指預約義的命令行參數。在該部分中,若是每行第一列以數字標誌 (#) 開頭,那麼該行屬於註釋行。關鍵字應該是每個非註釋行的第一個字段;一般與 char* 相關聯的字符串引號是可選內容。此外,字段能夠放在前面的關鍵字以後,可是必須使用逗號隔開並截止到行末。這些字段直接對應於聲明部分中最後一部分結構,如清單 4 所示。
清單 4. 關鍵字部分
%% +helpverbose, CommandOptionCode::HELPVERBOSE +append_log, CommandOptionCode::APPEND_LOG +compile, CommandOptionCode::COMPILE
第一個條目指 CommandOption 結構的 const char* command_option 字段,如 清單 3所示;第二個條目指同一個結構中的 int OptionCode 字段。那麼這裏究竟有什麼含義呢?事實上,這就是 gperf 初始化散列表的方式,其中存儲了命令行選項及其相關屬性。
函數也是可選的部分。函數部分中全部以 %% 開頭並延伸到文件末尾的文本將所有複製到生成的文件中。和聲明部分同樣,用戶須要爲函數部分提供有效的 C/C++ 代碼。
Gperf 混編了一組預約義的關鍵字,而後對這些關鍵字執行快速查找。與此類似,gperf 輸出兩個函數:hash() 和 in_word_set()。前者是一個散列例程,然後者用於執行查找。Gperf 輸出能夠是 C 語言,也能夠是 C++ 語言 — 您能夠指定爲其中一種。若是將輸出指定爲 C 語言,將生成兩個具備上述名稱的 C 函數。若是指定爲 C++ 語言,gperf 將生成名爲 Perfect_Hash 的類,該類包含兩種方法。
注意:可使用 -Z 選項修改生成的類名。
散列函數的原型爲:
unsigned int hash (const char *str, unsigned int len);
其中 str 表示命令行選項,而 len 表示其長度。例如,若是命令行參數爲 +helpverbose,則 str 爲 +helpverbose,len 爲 12。
在 gperf 生成的散列內,in_word_set() 爲查找函數。該例程的原型取決於用戶指定的 -t 選項。若是尚未指定該選項,那麼僅處理特定於用戶的命令字符串(做爲數據存儲在 gperf 生成的散列中),而不是與命令字符串相關的結構。
例如,在 清單 3 中,將 CommandOption 結構與用戶命令參數關聯起來,該參數將由 in_word_set() 例程返回。您可使用 -N 選項改變這個例程的名稱。該例程的參數相似於前面解釋的 hash() 函數:
const struct CommandOption* in_word_set (const char *str, unsigned int len);
Gperf 是能夠接受不一樣選項的高度可定製工具。gperf 在線手冊(參閱 參考資料小節 中的連接)說明了 gperf 中全部可用的選項,包括:
-L language-name:指示 gperf 使用指定的語言生成輸出。目前支持如下幾個選項:
KR-C:這種老式的 K&R C 能夠獲得新舊 C 編譯器的支持,可是新的符合 ANSI C 標準的編譯器可能會生成警告,或者,某些狀況下甚至會生成標誌錯誤。
C:該選項將生成 C 代碼,可是若是不對已有源代碼進行調整,則可能沒法使用某些舊的 C 編譯器進行編譯。
ANSI-C:該選項生成符合 ANSI C 標準的代碼,只能使用符合 ANSI C 標準的編譯器或 C++ 編譯器進行編譯。
C++:該選項生成 C++ 代碼。
-N:該選項容許用戶修改查找函數的名稱。默認名爲 in_word_set()。
-H:該選項容許用戶修改散列例程的名稱。默認名爲 hash()。
-Z:該選項在提供了 -L C++ 選項時使用。它容許用戶指定所生成的 C++ 類的名稱,該類包含 in_word_set() 和 hash() 函數。默認名爲 Perfect_Hash。
-G:該選項將生成查找表並將其做爲靜態全局變量,而不是在查找函數內生成以隱藏該表(默認行爲)。
-C:前面討論了 Gperf 將生成查找表。-C 選項將建立使用 const 關鍵字聲明的查找表。全部生成的查找表中的內容都是常量 — 即只讀形式。不少編譯器經過將表放入只讀內存中能夠生成更高效的代碼。
-D:該選項將處理散列爲重複值的關鍵字。
-t:該選項容許包含關鍵字結構。
-K:該選項容許用戶選擇關鍵字結構中的關鍵字組件的名稱。
-p:該選項能夠與較早版本的 gperf 兼容。在早期版本中,它將生成的函數 in_word_set() 返回的默認布爾值(即 0 或 1 )修改成pointer to wordlist array 類型。這個選項很是有用,尤爲是在使用 -t(容許使用用戶定義的 structs)選項時。在最新版的 gperf 中並不要求使用該選項而且能夠將其刪除。
靜態搜索集 是一種抽象數據類型,包含的操做包括 initialize、insert 和 retrieve。完美散列函數是一種在時間和空間方面都十分高效的靜態搜索集實現。Gperf 是一種完美散列函數生成器,它使用用戶提供的關鍵字列表構建完美散列函數。Gperf 將 n 個用戶提供的關鍵字元素列表轉換爲包含 k 個元素查找表和兩個函數的源代碼:
hash:該例程將關鍵字唯一地映射到範圍 0 .. k - 1 中,其中 k = n。若是 k = n,hash() 被認爲是最小完美 hash() 函數。這種 hash()函數具備兩個屬性:
perfect property:查找時間複雜度爲 O(1) 的表條目 — 就是說,至多須要一個字符串比較執行靜態搜索集中的關鍵字識別。
minimal property:爲存儲關鍵字而分配的最小內存。
in_word_set:該例程使用 hash() 肯定某個字符串是否屬於用戶提供的列表,大多數狀況下只使用一個字符串比較。
Gperf 的內部實現以兩個內部數據結構爲核心: 關鍵字簽名(keyword signatures)列表(Key_List)和 關聯值(associated values)數組(asso_values)。全部用戶指定的關鍵字及其屬性將從用戶指定的文件中讀取,並存儲爲連接列表中的一個節點(稱爲 Key_List)。在搜索完美 hash() 函數時,gperf 只將每一個關鍵字字符中的一部分做爲搜索鍵。這部分字符被稱爲關鍵字簽名 或 keysig。
關聯值數組在 hash() 函數內部生成,並使用 keysig 字符進行索引。Gperf 反覆搜索某種關聯值配置,該配置將全部 nkeysig 映射到非重複的散列值。當 gperf 找到某種配置,而且該配置將每一個 keysig 分配到生成的查找表中唯一位置時,將生成一個完美 hash() 函數。產生的完美 hash() 函數返回一個無符號的 int 值,範圍爲 0..(k-1),其中 k 值爲最大關鍵字散列值加 1。
當 k = n 時,將生成最小完美 hash() 函數。關鍵字散列值一般這樣計算:將關鍵字的 keysig 關聯值和關鍵字長度結合。默認狀況下,hash() 函數將關鍵字的第一個索引位置的關聯值和最後一個索引位置的關聯值添加到長度中;例如:
hash_value = length + asso_values[(unsigned char)keyword[1]];
下面使用一個簡單項目解釋目前爲止所討論的概念。考慮如清單 5 所示的 gperf 文件。
清單 5. command_options.gperf
%{ #include "command_options.h" typedef struct CommandOptionCode CommandOptionCode; %} struct CommandOption { const char *Option; int OptionCode; }; %% +helpverbose, CommandOptionCode::HELPVERBOSE +password, CommandOptionCode::PASSWORD +nocopyright, CommandOptionCode::NOCOPYRIGHT +nolog, CommandOptionCode::NOLOG +_64bit, CommandOptionCode::_64BIT
清單 6 展現了包含在 gperf 文件中的 command_options.h 頭文件。
清單 6. command_options.h 頭文件
#ifndef __COMMANDOPTIONS_H #define __COMMANDOPTIONS_H struct CommandOptionCode { enum { HELPVERBOSE = 1, PASSWORD = 2, NOCOPYRIGHT = 3, NOLOG = 4, _64BIT = 5 }; }; #endif
gperf 命令行以下所示:
gperf -CGD -N IsValidCommandLineOption -K Option -L C++ -t command_line_options.gperf > perfecthash.hpp
散列表做爲 perfecthash.hpp 文件一部分生成。因爲命令行中指定了 -G 選項,將在全局範圍內生成散列表。由於使用 -C 選項進行 gperf 調用,將使用 const 屬性定義散列表。清單 7 展現了所生成的源代碼的詳細內容。
清單 7. 生成的 perfecthash.hpp
/* C++ code produced by gperf version 3.0.3 */ /* Command-line: 'C:\\gperf\\gperf.exe' -CGD -N IsValidCommandLineOption -K Option -L C++ -t command_line_options.gperf */ /* Computed positions: -k'2' */ #if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \ && ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \ && (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \ && ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \ && ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \ && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \ && ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \ && ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \ && ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \ && ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \ && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \ && ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \ && ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \ && ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \ && ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \ && ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \ && ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \ && ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \ && ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \ && ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \ && ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \ && ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \ && ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126)) /* The character set is not based on ISO-646. */ #error "gperf generated tables don't work with this execution character set. \ Please report a bug to <bug-gnu-gperf@gnu.org>." #endif #line 1 "command_line_options.gperf" #include "command_options.h" typedef struct CommandOptionCode CommandOptionCode; #line 6 "command_line_options.gperf" struct CommandOption { const char *Option; int OptionCode; }; #define TOTAL_KEYWORDS 5 #define MIN_WORD_LENGTH 6 #define MAX_WORD_LENGTH 12 #define MIN_HASH_VALUE 6 #define MAX_HASH_VALUE 17 /* maximum key range = 12, duplicates = 0 */ class Perfect_Hash { private: static inline unsigned int hash (const char *str, unsigned int len); public: static const struct CommandOption *IsValidCommandLineOption (const char *str, unsigned int len); }; inline unsigned int Perfect_Hash::hash (register const char *str, register unsigned int len) { static const unsigned char asso_values[] = { 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 0, 18, 18, 18, 18, 18, 18, 18, 18, 5, 18, 18, 18, 18, 18, 0, 18, 0, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18 }; return len + asso_values[(unsigned char)str[1]]; } static const struct CommandOption wordlist[] = { #line 15 "command_line_options.gperf" {"+nolog", CommandOptionCode::NOLOG}, #line 16 "command_line_options.gperf" {"+_64bit", CommandOptionCode::_64BIT}, #line 13 "command_line_options.gperf" {"+password", CommandOptionCode::PASSWORD}, #line 14 "command_line_options.gperf" {"+nocopyright", CommandOptionCode::NOCOPYRIGHT}, #line 12 "command_line_options.gperf" {"+helpverbose", CommandOptionCode::HELPVERBOSE} }; static const signed char lookup[] = { -1, -1, -1, -1, -1, -1, 0, 1, -1, 2, -1, -1, 3, -1, -1, -1, -1, 4 }; const struct CommandOption * Perfect_Hash::IsValidCommandLineOption (register const char *str, register unsigned int len) { if (len <= MAX_WORD_LENGTH && len >= MIN_WORD_LENGTH) { register int key = hash (str, len); if (key <= MAX_HASH_VALUE && key >= 0) { register int index = lookup[key]; if (index >= 0) { register const char *s = wordlist[index].Option; if (*str == *s && !strcmp (str + 1, s + 1)) return &wordlist[index]; } } } return 0; }
最後,清單 8 展現了主要的源代碼清單。
注意:清單 8 演示了用戶能夠在常量時間內從給定的命令行選項關鍵字中查找命令行選項,並隨後使用相應的步驟處理該選項。IsValidCommandLineOption 的查找時間複雜度爲 O(1)。
清單 8. 定義應用程序入口點的 gperf.cpp
#include "command_options.h" #include "perfecthash.hpp" #include <iostream> #include <string> using namespace std; int main(int argc, char* argv[]) { string cmdLineOption = argv[1]; // First command line argument const CommandOption* option = Perfect_Hash::IsValidCommandLineOption(cmdLineOption.c_str(), cmdLineOption.length()); switch (option->OptionCode) { case CommandOptionCode::HELPVERBOSE : cout << "Application specific detailed help goes here"; break; default: break; } return 0; }
注意:本文中的全部示例都使用 gperf 版本 3.0.3 進行了測試。若是您使用的是早期的版本,則可能須要在命令行調用中使用 -p 選項。
gperf 實用程序能夠爲中小型數據庫快速生成完美散列。可是,gperf 還可用於其餘目的。事實上,能夠在 GUN 編譯器中使用它維護語言關鍵字的完美散列,其最新的功能使您可以操做更大的數據庫。所以,能夠考慮在您的下一個開發項目中使用 gperf。
您能夠參閱本文在 developerWorks 全球站點上的 英文原文。
gperf 在線手冊 提供了有關 gperf 使用的更多信息。
在 developerWorks Linux 專區 中查找面向 Linux 開發人員的更多資源,包括 Linux 教程 以及上月 讀者最喜好的 Linux 文章和教程。
隨時關注 developerWorks 技術事件和網絡廣播。
從 GNU Web 站點 下載 gperf。
Cygwin 用戶 能夠從 SourceForge 下載 gperf 包。
使用 IBM 試用軟件 構建您的下一個 Linux 開發項目,可直接從 developerWorks 下載。
經過參與新的 developerWorks 空間 中的開發人員博客、論壇、podcasts 和社區主題,加入 developerWorks 社區 。