維 護用 C/C++ 開發(fā)的遺留系統(tǒng)并添加新特性是一項艱難的任務(wù)。幸運的是,doxygen 可以幫助您完成這個任務(wù)。doxygen 是一種用于 C/C++、Java™、Python 和其他編程語言的文檔系統(tǒng)。本文在 C/C++ 項目的上下文中討論 doxygen 的特性,以及如何用 doxygen 定義的標記生成代碼文檔。

維護用 C/C++ 開發(fā)的遺留系統(tǒng)并添加新特性是一項艱難的任務(wù)。這涉及幾方面的問題:理解現(xiàn)有的類層次結(jié)構(gòu)和全局變量,不同的用戶定義類型,以及函數(shù)調(diào)用圖分析等等。本文 在 C/C++ 項目的上下文中通過示例討論 doxygen 的幾個特性。但是,doxygen 非常靈活,也可用于用 Python、Java、PHP 和其他語言開發(fā)的軟件項目。本文的主要目的是幫助您從 C/C++ 源代碼提取出信息,但也簡要描述了如何用 doxygen 定義的標記生成代碼文檔。

安裝 doxygen

有兩種獲得 doxygen 的方法??梢韵螺d預(yù)編譯的可執(zhí)行文件,也可以從 SVN 存儲庫下載源代碼并自己編譯。清單 1 演示的是后一種方法。


清單 1. 安裝和構(gòu)建 doxygen 源代碼
            bash-2.05$ svn co https://doxygen.svn./svnroot/doxygen/trunk doxygen-svn
            bash-2.05$ cd doxygen-svn
            bash-2.05$ ./configure –prefix=/home/user1/bin
            bash-2.05$ make
            bash-2.05$ make install
            

注意,配置腳本把編譯的源代碼存儲在 /home/user1/bin 中(進行編譯后,會在 PATH 變量中添加這個目錄),因為并非每個 UNIX® 用戶都有寫 /usr 文件夾的權(quán)限。另外,需要用 svn 實用程序下載源代碼。





使用 doxygen 生成文檔

使用 doxygen 生成源代碼的文檔需要執(zhí)行三個步驟。

生成配置文件

在 shell 提示上,輸入命令 doxygen -g 。這個命令在當前目錄中生成一個可編輯的配置文件 Doxyfile??梢愿淖冞@個文件名,在這種情況下,應(yīng)該調(diào)用 doxygen -g <user-specified file name>,見 清單 2。


清單 2. 生成默認的配置文件
            bash-2.05b$ doxygen -g
            Configuration file 'Doxyfile' created.
            Now edit the configuration file and enter
            doxygen Doxyfile
            to generate the documentation for your project
            bash-2.05b$ ls Doxyfile
            Doxyfile
            

編輯配置文件

配置文件采用 <TAGNAME> = <VALUE> 這樣的結(jié)構(gòu),與 Make 文件格式相似。下面是最重要的標記:

  • <OUTPUT_DIRECTORY>:必須在這里提供一個目錄名,例如 /home/user1/documentation,這個目錄是放置生成的文檔文件的位置。如果提供一個不存在的目錄名,doxygen 會以這個名稱創(chuàng)建具有適當用戶權(quán)限的目錄。
  • <INPUT>:這個標記創(chuàng)建一個以空格分隔的所有目錄的列表,這個列表包含需要生成文檔的 C/C++ 源代碼文件和頭文件。例如,請考慮以下代碼片段:
    INPUT = /home/user1/project/kernel /home/user1/project/memory
                    

    在這里,doxygen 會從這兩個目錄讀取 C/C++ 源代碼。如果項目只有一個源代碼根目錄,其中有多個子目錄,那么只需指定根目錄并把 <RECURSIVE> 標記設(shè)置為 Yes。

  • <FILE_PATTERNS>: 在默認情況下,doxygen 會搜索具有典型 C/C++ 擴展名的文件,比如 .c、.cc、.cpp、.h 和 .hpp。如果 <FILE_PATTERNS> 標記沒有相關(guān)聯(lián)的值,doxygen 就會這樣做。如果源代碼文件采用不同的命名約定,就應(yīng)該相應(yīng)地更新這個標記。例如,如果項目使用 .c86 作為 C 文件擴展名,就應(yīng)該在 <FILE_PATTERNS> 標記中添加這個擴展名。
  • <RECURSIVE>:如果源代 碼層次結(jié)構(gòu)是嵌套的,而且需要為所有層次上的 C/C++ 文件生成文檔,就把這個標記設(shè)置為 Yes。例如,請考慮源代碼根目錄層次結(jié)構(gòu) /home/user1/project/kernel,其中有 /home/user1/project/kernel/vmm 和 /home/user1/project/kernel/asm 等子目錄。如果這個標記設(shè)置為 Yes,doxygen 就會遞歸地搜索整個層次結(jié)構(gòu)并提取信息。
  • <EXTRACT_ALL>:這個標記告訴 doxygen,即使各個類或函數(shù)沒有文檔,也要提取信息。必須把這個標記設(shè)置為 Yes。
  • <EXTRACT_PRIVATE>:把這個標記設(shè)置為 Yes。否則,文檔不包含類的私有數(shù)據(jù)成員。
  • <EXTRACT_STATIC>:把這個標記設(shè)置為 Yes。否則,文檔不包含文件的靜態(tài)成員(函數(shù)和變量)。

清單 3 給出一個 Doxyfile 示例。


清單 3. 包含用戶提供的標記值的 doxyfile 示例
            OUTPUT_DIRECTORY = /home/user1/docs
            EXTRACT_ALL = yes
            EXTRACT_PRIVATE = yes
            EXTRACT_STATIC = yes
            INPUT = /home/user1/project/kernel
            #Do not add anything here unless you need to. Doxygen already covers all
            #common formats like .c/.cc/.cxx/.c++/.cpp/.inl/.h/.hpp
            FILE_PATTERNS =
            RECURSIVE = yes
            

運行 doxygen

在 shell 提示下輸入 doxygen Doxyfile(或者已為配置文件選擇的其他文件名)運行 doxygen。在最終生成 Hypertext Markup Language(HTML)和 Latex 格式(默認)的文檔之前,doxygen 會顯示幾個消息。在生成文檔期間,在 <OUTPUT_DIRECTORY> 標記指定的文件夾中,會創(chuàng)建兩個子文件夾 html 和 latex。清單 4 是一個 doxygen 運行日志示例。


清單 4. doxygen 的日志輸出
            Searching for include files...
            Searching for example files...
            Searching for images...
            Searching for dot files...
            Searching for files to exclude
            Reading input files...
            Reading and parsing tag files
            Preprocessing /home/user1/project/kernel/kernel.h
            …
            Read 12489207 bytes
            Parsing input...
            Parsing file /project/user1/project/kernel/epico.cxx
            …
            Freeing input...
            Building group list...
            ..
            Generating docs for compound MemoryManager::ProcessSpec
            …
            Generating docs for namespace std
            Generating group index...
            Generating example index...
            Generating file member index...
            Generating namespace member index...
            Generating page index...
            Generating graph info page...
            Generating search index...
            Generating style sheet...
            





文檔輸出格式

除了 HTML 之外,doxygen 還可以生成幾種輸出格式的文檔??梢宰?doxygen 生成以下格式的文檔:

  • UNIX 手冊頁:把 <GENERATE_MAN> 標記設(shè)置為 Yes。在默認情況下,會在 <OUTPUT_DIRECTORY> 指定的目錄中創(chuàng)建 man 子文件夾,生成的文檔放在這個文件夾中。必須把這個文件夾添加到 MANPATH 環(huán)境變量中。
  • Rich Text Format(RTF):把 <GENERATE_RTF> 標記設(shè)置為 Yes。把 <RTF_OUTPUT> 標記設(shè)置為希望放置 .rtf 文件的目錄;在默認情況下,文檔放在 OUTPUT_DIRECTORY 中的 rtf 子文件夾中。要想支持跨文檔瀏覽,應(yīng)該把 <RTF_HYPERLINKS> 標記設(shè)置為 Yes。如果設(shè)置這個標記,生成的 .rtf 文件會包含跨文檔鏈接。
  • Latex: 在默認情況下,doxygen 生成 Latex 和 HTML 格式的文檔。在默認的 Doxyfile 中,<GENERATE_LATEX> 標記設(shè)置為 Yes。另外,<LATEX_OUTPUT> 標記設(shè)置為 Latex,這意味著會在 OUTPUT_DIRECTORY 中創(chuàng)建 latex 子文件夾并在其中生成 Latex 文件。
  • Microsoft® Compiled HTML Help(CHM)格式:把 <GENERATE_HTMLHELP> 標記設(shè)置為 Yes。因為在 UNIX 平臺上不支持這種格式,doxygen 只在保存 HTML 文件的文件夾中生成一個 index.hhp 文件。您必須通過 HTML 幫助編譯器把這個文件轉(zhuǎn)換為 .chm 文件。
  • Extensible Markup Language(XML)格式:把 <GENERATE_XML> 標記設(shè)置為 Yes。(注意,doxygen 開發(fā)團隊還在開發(fā) XML 輸出)。

清單 5 提供的 Doxyfile 示例讓 doxygen 生成所有格式的文檔。


清單 5. 生成多種格式的文檔的 Doxyfile
            #for HTML
            GENERATE_HTML = YES
            HTML_FILE_EXTENSION = .htm
            #for CHM files
            GENERATE_HTMLHELP = YES
            #for Latex output
            GENERATE_LATEX = YES
            LATEX_OUTPUT = latex
            #for RTF
            GENERATE_RTF = YES
            RTF_OUTPUT = rtf
            RTF_HYPERLINKS = YES
            #for MAN pages
            GENERATE_MAN = YES
            MAN_OUTPUT = man
            #for XML
            GENERATE_XML = YES
            





doxygen 中的特殊標記

doxygen 包含幾個特殊標記。

C/C++ 代碼的預(yù)處理

為了提取信息,doxygen 必須對 C/C++ 代碼進行預(yù)處理。但是,在默認情況下,它只進行部分預(yù)處理 —— 計算條件編譯語句(#if…#endif),但是不執(zhí)行宏展開。請考慮 清單 6 中的代碼。


清單 6. 使用宏的 C++ 代碼示例
            #include <cstring>
            #include <rope>
            #define USE_ROPE
            #ifdef USE_ROPE
            #define STRING std::rope
            #else
            #define STRING std::string
            #endif
            static STRING name;
            

通過源代碼中定義的 <USE_ROPE>,doxygen 生成的文檔如下:

                Defines
            #define USE_ROPE
            #define STRING std::rope
            Variables
            static STRING name
            

在這里可以看到 doxygen 執(zhí)行了條件編譯,但是沒有對 STRING 執(zhí)行宏展開。Doxyfile 中的 <ENABLE_PREPROCESSING> 標記在默認情況下設(shè)置為 Yes。為了執(zhí)行宏展開,還應(yīng)該把 <MACRO_EXPANSION> 標記設(shè)置為 Yes。這會使 doxygen 產(chǎn)生以下輸出:

                Defines
            #define USE_ROPE
            #define STRING std::string
            Variables
            static std::rope name
            

如果把 <ENABLE_PREPROCESSING> 標記設(shè)置為 No,前面源代碼的 doxygen 輸出就是:

                Variables
            static STRING name
            

注意,文檔現(xiàn)在沒有定義,而且不可能推導(dǎo)出 STRING 的類型。因此,總是應(yīng)該把 <ENABLE_PREPROCESSING> 標記設(shè)置為 Yes。

在文檔中,可能希望只展開特定的宏。為此,除了把 <ENABLE_PREPROCESSING> 和 <MACRO_EXPANSION> 標記設(shè)置為 Yes 之外,還必須把 <EXPAND_ONLY_PREDEF> 標記設(shè)置為 Yes(這個標記在默認情況下設(shè)置為 No),并在 <PREDEFINED> 或 <EXPAND_AS_DEFINED> 標記中提供宏的細節(jié)。請考慮 清單 7 中的代碼,這里只希望展開宏 CONTAINER。


清單 7. 包含多個宏的 C++ 源代碼
            #ifdef USE_ROPE
            #define STRING std::rope
            #else
            #define STRING std::string
            #endif
            #if ALLOW_RANDOM_ACCESS == 1
            #define CONTAINER std::vector
            #else
            #define CONTAINER std::list
            #endif
            static STRING name;
            static CONTAINER gList;
            

清單 8 給出配置文件。


清單 8. 允許有選擇地展開宏的 Doxyfile
            ENABLE_PREPROCESSING = YES
            MACRO_EXPANSION = YES
            EXPAND_ONLY_PREDEF = YES
            EXPAND_AS_DEFINED = CONTAINER
            …
            

下面的 doxygen 輸出只展開了 CONTAINER:

                Defines
            #define STRING   std::string
            #define CONTAINER   std::list
            Variables
            static STRING name
            static std::list gList
            

注意,只有 CONTAINER 宏被展開了。在 <MACRO_EXPANSION> 和 <EXPAND_ONLY_PREDEF> 都設(shè)置為 Yes 的情況下,<EXPAND_AS_DEFINED> 標記只選擇展開等號操作符右邊列出的宏。

對于預(yù)處理過程,要注意的最后一個標記是 <PREDEFINED>。就像用 -D 開關(guān)向 C++ 編譯器傳遞預(yù)處理器定義一樣,使用這個標記定義宏。請考慮 清單 9 中的 Doxyfile。


清單 9. 定義了宏展開標記的 Doxyfile
            ENABLE_PREPROCESSING = YES
            MACRO_EXPANSION = YES
            EXPAND_ONLY_PREDEF = YES
            EXPAND_AS_DEFINED =
            PREDEFINED = USE_ROPE=             ALLOW_RANDOM_ACCESS=1
            

下面是 doxygen 生成的輸出:

                Defines
            #define USE_CROPE
            #define STRING   std::rope
            #define CONTAINER   std::vector
            Variables
            static std::rope name
            static std::vector gList
            

在使用 <PREDEFINED> 標記時,宏應(yīng)該定義為 <macro name>=<value> 形式。如果不提供值,比如簡單的 #define,那么只使用 <macro name>=<spaces> 即可。多個宏定義以空格或反斜杠(\)分隔。

從文檔生成過程中排除特定文件或目錄

在 Doxyfile 中的 <EXCLUDE> 標記中,添加不應(yīng)該為其生成文檔的文件或目錄(以空格分隔)。因此,如果提供了源代碼層次結(jié)構(gòu)的根,并要跳過某些子目錄,這將非常有用。例如,如果層次結(jié) 構(gòu)的根是 src_root,希望在文檔生成過程中跳過 examples/ 和 test/memoryleaks 文件夾,Doxyfile 應(yīng)該像 清單 10 這樣。


清單 10. 使用 EXCLUDE 標記的 Doxyfile
            INPUT = /home/user1/src_root
            EXCLUDE = /home/user1/src_root/examples /home/user1/src_root/test/memoryleaks
            …
            





生成圖形和圖表

在默認情況下,Doxyfile 把 <CLASS_DIAGRAMS> 標記設(shè)置為 Yes。這個標記用來生成類層次結(jié)構(gòu)圖。要想生成更好的視圖,可以從 Graphviz 下載站點 下載 dot 工具。Doxyfile 中的以下標記用來生成圖表:

  • <CLASS_DIAGRAMS>:在 Doxyfile 中這個標記默認設(shè)置為 Yes。如果這個標記設(shè)置為 No,就不生成繼承層次結(jié)構(gòu)圖。
  • <HAVE_DOT>:如果這個標記設(shè)置為 Yes,doxygen 就使用 dot 工具生成更強大的圖形,比如幫助理解類成員及其數(shù)據(jù)結(jié)構(gòu)的協(xié)作圖。注意,如果這個標記設(shè)置為 Yes,<CLASS_DIAGRAMS> 標記就無效了。
  • <CLASS_GRAPH>:如果 <HAVE_DOT> 標記和這個標記同時設(shè)置為 Yes,就使用 dot 生成繼承層次結(jié)構(gòu)圖,而且其外觀比只使用 <CLASS_DIAGRAMS> 時更豐富。
  • <COLLABORATION_GRAPH>:如果 <HAVE_DOT> 標記和這個標記同時設(shè)置為 Yes,doxygen 會生成協(xié)作圖(還有繼承圖),顯示各個類成員(即包含)及其繼承層次結(jié)構(gòu)。

清單 11 提供一個使用一些數(shù)據(jù)結(jié)構(gòu)的示例。注意,在配置文件中 <HAVE_DOT>、<CLASS_GRAPH> 和 <COLLABORATION_GRAPH> 標記都設(shè)置為 Yes。


清單 11. C++ 類和結(jié)構(gòu)示例
            struct D {
            int d;
            };
            class A {
            int a;
            };
            class B : public A {
            int b;
            };
            class C : public B {
            int c;
            D d;
            };
            

圖 1 給出 doxygen 的輸出。


圖 1. 使用 dot 工具生成的類繼承圖和協(xié)作圖
類繼承圖




代碼文檔樣式

到目前為止,我們都是使用 doxygen 從原本沒有文檔的代碼中提取信息。但是,doxygen 也鼓勵使用文檔樣式和語法,這有助于生成更詳細的文檔。本節(jié)討論 doxygen 鼓勵在 C/C++ 代碼中使用的一些常用標記。更多信息參見 參考資料。

每個代碼元素有兩種描述:簡短的和詳細的。簡短描述通常是單行的。函數(shù)和類方法還有第三種描述體內(nèi)描述(in-body description),這種描述把在函數(shù)體中找到的所有注釋塊集中在一起。比較常用的一些 doxygen 標記和注釋樣式如下:

  • 簡短描述:使用單行的 C++ 注釋,或使用 <\brief> 標記。
  • 詳細描述:使用 JavaDoc 式的注釋 /** … test … */(注意開頭的兩個星號 [*])或 Qt 式的注釋 /*! … text … */。
  • 體內(nèi)描述:類、結(jié)構(gòu)、聯(lián)合體和名稱空間等 C++ 元素都有自己的標記,比如 <\class>、<\struct>、<\union> 和 <\namespace>。

為了為全局函數(shù)、變量和枚舉類型生成文檔,必須先對對應(yīng)的文件使用 <\file> 標記。清單 12 給出的示例包含用于四種元素的標記:函數(shù)標記(<\fn>)、函數(shù)參數(shù)標記(<\param>)、變量名標記(< \var>)、用于 #define 的標記(<\def>)以及用來表示與一個代碼片段相關(guān)的問題的標記(<\warning>)。


清單 12. 典型的 doxygen 標記及其使用方法
            /*! \file globaldecls.h
            \brief Place to look for global variables, enums, functions
            and macro definitions
            */
            /** \var const int fileSize
            \brief Default size of the file on disk
            */
            const int fileSize = 1048576;
            /** \def SHIFT(value, length)
            \brief Left shift value by length in bits
            */
            #define SHIFT(value, length) ((value) << (length))
            /** \fn bool check_for_io_errors(FILE* fp)
            \brief Checks if a file is corrupted or not
            \param fp Pointer to an already opened file
            \warning Not thread safe!
            */
            bool check_for_io_errors(FILE* fp);
            

下面是生成的文檔:

                Defines
            #define SHIFT(value, length)   ((value) << (length))
            Left shift value by length in bits.
            Functions
            bool check_for_io_errors (FILE *fp)
            Checks if a file is corrupted or not.
            Variables
            const int fileSize = 1048576;
            Function Documentation
            bool check_for_io_errors (FILE* fp)
            Checks if a file is corrupted or not.
            Parameters
            fp: Pointer to an already opened file
            Warning
            Not thread safe!
            





結(jié)束語

本文討論如何用 doxygen 從遺留的 C/C++ 代碼提取出大量相關(guān)信息。如果用 doxygen 標記生成代碼文檔,doxygen 會以容易閱讀的格式生成輸出。只要以適當?shù)姆绞绞褂茫琩oxygen 就可以幫助任何開發(fā)人員維護和管理遺留系統(tǒng)。