近日在寫一個統計項目中C/C++文件(后綴名:C/CPP/CC/H/HPP文件)代碼行數的小程序。給定包含C/C++代碼的目錄,統計目錄里所有C/C++文件的總代碼行數、有效代碼行數、注釋行數、空白行數。
其中:總代碼行數 =(有效代碼行數+注釋行數+空白行數)
每找到一個目標代碼文件,就創建任務投進線程池。線程池的設計基於任務,基於任務相比基於線程的優勢,請參考Scott Meyers撰寫的Moderm Effective C++一書。
先給出程序運行的效果,見下圖:
近5萬的代碼文件,1183萬總代碼行數,不到5分鍾統計完成,速度還是很快的。有人問了,用5分鍾才統計完,怎么也不能說快吧。咱不耍嘴皮子,用結果說話。作為對比的上面那款源碼統計專家工具,在對同一個目錄里的文件進行分析統計時,半個小時過去還沒給出結果。這工具單文件分析都不准,面對5萬個文件,結果只怕會錯的離譜,運行又慢,我想也沒等下去的必要了,果斷給×掉了。
需要程序的,可以在文末評論留下郵箱O(∩_∩)O
對代碼文件的分析中,空白行好處理,關鍵在於識別有效代碼和注釋代碼。大的識別原則有:
1.注釋符合文法,不能讓編譯期報錯。這是最重要的原則。
2.注釋中的空行是空行,原始字符串中的空行不是空行
3.原始字符串里的注釋(行注釋/塊注釋)是有效代碼,不能當作注釋統計
4.注釋中的原始字符串是注釋,不能當作有效代碼統計
為了驗證程序邏輯的嚴密性和准確性,設計了如下的復雜的代碼片段:
1 // idxRawStringStart = curLine.find(R"(R"()"); 2 string comment_test = R"({ 3 // comment /* with nested comment */ 4 // comment 5 "a": "text", 6 /* multi 7 // line-comment-inside-multiline-comment 8 */ 9 10 })"; 11 12 idxRawStringStart = curLine.find(R"(R"()"); 13 idxRawStringStart = curLine.find(R"(R"(\ 14 )"); 15 /************************************************************************** 16 * 功能描述: 17 **************************************************************************/ 18 /* 19 20 * Since some memmove()'s erroneously fail to handle 21 * overlapping regions, we'll do the shift by hand. 22 */ 23 int a = /*b*/ 0; 24 int b = 0 // comment 25 /*a = b*/ 26 // int a = b; 27 } 28 ///////////////////////////////////////////////////////////////////////// 29 }
這29行代碼中,從程序員的角度看,綠色部分是注釋共11行,紅色部分是代碼共16行,line 9是空行但因為包含在原始字符串中,所以是有效代碼行。
真正的空行只有 line 11和19,共2行。
對於這么個復雜代碼段,市面上某號稱源碼統計專家的工具給出的結果如下:
可以看到,在面對注釋風格多變且復雜的代碼文件時,專家工具變“磚”家,結果毫不可信。
而本程序輸出結果,完全正確。
上述復雜代碼段中,值得關注的是原始字符串的處理,其語法為R"()",小括號內放原始字符串的內容
1 std::string comment_test = R"({ 2 // comment /* with nested comment */ 3 "a": 1, 4 // comment 5 // continued 6 "b": "text", 7 /* multi 8 line 9 comment 10 // line-comment-inside-multiline-comment 11 */ 12 // and single-line comment 13 // and single-line comment /* multiline inside single line */ 14 "c": [1, 2, 3] 15 // and single-line comment at end of object 16 })";
conmment_tes存放的內容是
就是說里面的// 和/* */注釋符號不能被認定為注釋符號,哪怕它們在其中混用,注釋嵌套着注釋,不管是多復雜的組合,也是原始字符串的一部分,是有效代碼。程序的目的就是利用原始字符串的語法構造規則,找出原始字符串的起始和結束位置。
在識別過程中,本程序做的工作其實是編譯期語法/詞法分析的一部分功能,這也讓我更加了解原始字符串和嵌套注釋的語法規則。特別地,發現了vs注釋一個有意思的地方。vs注釋的快捷鍵會優先選擇/**/風格注釋,在/**/不能勝任的地方,會使用//代替。
以前看過一篇文章,建議注釋只用//,而不要用/* */,更不要用嵌套的/* */,因為后兩種注釋加大了文本分析程序解析的難度。 如果注釋用//開頭,很容易就識別該行代碼就是注釋了。我在做這個代碼行統計小程序時,對這條建議有了更深的體會。用//代替/**/這個建議,應該作為代碼規范執行下去。
寫程序難免踩坑,在這里記下來,避免后面踩到同樣的坑。
1. 在控制台程序輸入文件夾路徑時,路徑如果包含空格,cmd會把空格前后內容當作不同的參數,程序處理參數會錯誤(路徑無效)。把路徑用雙引號包含起來后就正常了。
2.程序在控制台運行出現中文亂碼,工程設置是unicode,程序打印的文件路徑含中文,而控制台默認代碼頁是936,GBK,開發環境和輸出環境編碼不一致,所以會亂碼。
亂碼截圖一張,圖中的問號其實是中文。
為適應控制台的編碼,使用setlocale(LC_CTYPE, "")就好了;
3.標准庫里的ofstream的<<重載符,對寬字符的處理不好,比如寫入std::wstring內容會出錯,而使用std::string則不會有問題。但代碼路徑有中文的情況下,無法輸出。解決辦法就是把ofstream換成wofstream。同時調用file.imbue(locale("", locale::all ^ locale::numeric));設置中文輸出環境。
4.標准庫的向量容器push_back(插入元素)非線程安全,在多線程環境下往此容器里寫東西時需加鎖。當然標准庫里的大多數容器操作都是非線程安全的,使用時需謹慎。