近日在写一个统计项目中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(插入元素)非线程安全,在多线程环境下往此容器里写东西时需加锁。当然标准库里的大多数容器操作都是非线程安全的,使用时需谨慎。