C++ 开发人员在日常工作中通常要完成多个任务:开发新软件、调试其他人的代码、制订测试计划、为每个计划开发测试、管理衰退软件(regression suite)等等。在多种角色之间频繁转换会消耗大量宝贵的时间。为了帮助缓解这个问题,本文提供 10 种能够有效提高生产力的方法。本文中的示例使用 tcsh version 6,但是提供的思想适用于所有 UNIX® shell 变体。本文还介绍可以在 UNIX 平台上使用的几种开放源码工具。
保证数据安全
在 shell 提示上使用 rm -rf * 或其变体可能是导致 UNIX 开发人员丢失数小时工作成果的最常见的原因。有几种方法可以解决这个问题:通过在 $HOME/.aliases 中使用 alias rm、alias cp 或 alias mv,把这些命令连接到它们的交互式版本,然后在系统启动时引用这个文件。根据登录 shell 的不同,这意味着把 source $HOME/.aliases 放在 .cshrc(对于 C/tcsh shell)或 .profile 或 .bash_profile(对于 Bourne shell)中,见清单 1。
清单 1. 为 rm、cp 和 mv 设置别名
alias rm 'rm –i'
alias cp 'cp –i'
alias mv 'mv –i'
对于 tcsh shell 的用户还有另一种方法,在启动脚本中添加以下行:
set rmstar on
在设置了 rmstar 变量的情况下,如果发出 rm * 命令,系统会提示您确认,见清单 2。
清单 2. 在 tcsh 中使用 rmstar shell 变量
但是,如果使用带 -f 选项的 rm、cp 或 mv 命令,那么会抑制交互模式。更有效的一种方法是,为这些 UNIX 命令创建自己的版本,并使用 $HOME/.recycle_bin 这样的预定义文件夹保存删除的数据。清单 3 给出一个称为 saferm 的示例脚本,它只接受文件和文件夹名。
清单 3. saferm 脚本
arpan@tintin# pwd
/home/arpan/IBM/documents
arpan@tintin# set rmstar on
arpan@tintin# rm *
Do you really want to delete all files? [n/y] n
自动备份数据
恢复数据需要全面的策略和措施。根据需求的不同,数据备份可以在每天夜间执行,也可以每隔几小时执行一次。在默认情况下,应该使用 cron 工具备份 $HOME 及其所有子目录,并把备份保存在预先指定的文件系统区域中。注意,应该只有系统管理员对备份数据有 读(Write) 或 执行(Execute)的权限。下面的 cron 脚本简要演示备份的设置:
#!/usr/bin/tcsh
if (! -d ~/.recycle_bin) then
mkdir ~/.recycle_bin
endif
mv $1 ~/.recycle_bin/$1.`date +%F`
这个脚本在每天 20:00 备份数据。数据备份脚本见清单 4。
清单 4. 数据备份脚本
另一种策略是在网络中维护一些名称简单明了的文件系统区域,比如 /backup_area1、/backup_area2 等等。希望备份数据的开发人员应该在这些区域中创建目录或文件。另外,一定要注意一点:与 /tmp 相似,这种区域必须打开 sticky 位。
浏览源代码
使用可免费下载的 cscope 实用程序(见 参考资料 中的链接)是发现和浏览现有源代码的好方法。cscope 需要一个文件列表(C 或 C++ 头文件、源代码文件、flex 和 bison 文件、内联源代码 [.inl] 文件等等),以便创建它自己的数据库。创建数据库之后,它会在一个简洁的界面中列出源代码。清单 5 演示如何构建并调用 cscope 数据库。
清单 5. 使用 cscope 构建并调用源代码数据库
0 20 * * * /home/tintin/bin/databackup.csh
cscope 的 -b 选项让它创建内部数据库;-q 选项让它创建索引文件以加快搜索;-k 选项让 cscope 在搜索时不考虑系统头文件(否则,即使是最简单的搜索,也会产生大量结果)。
使用 -d 选项调用 cscope 界面,见清单 6。
清单 6. cscope 界面
cd /home/tintin/database/src
tar cvzf database-src.tgz.`date +%F` database/ main/ sql-processor/
mv database-src.tgz.`date +%F` ~/.backup/
chmod 000 ~/.backup/database-src.tgz.`date +%F`
按 Ctrl-D 退出 cscope。使用 Tab 键在数据 cscope 列表和 cscope 选项(例如,Find C Symbol 和 Find file)之间切换。清单 7 给出在搜索名称包含 database 的文件时的屏幕快照。按 0、1 等分别查看各个文件。
清单 7. 在搜索名为 database 的文件时 cscope 的输出
用 doxygen 调试遗留代码
要想有效地调试别人开发的代码,就要花时间了解现有软件的总体结构 — 类及其层次结构,全局变量和静态变量,公共接口例程。在从现有的源代码中提取类层次结构方面,GNU 实用程序 doxygen(见 参考资料 中的链接)可能是最好的工具。
要想在项目上运行 doxygen,首先需要在 shell 提示上运行 doxygen -g。这个命令在当前工作目录中生成一个名为 Doxyfile 的文件,必须手工编辑此文件。编辑之后,对 Doxyfile 再次运行 doxygen。清单 8 给出一个运行示例。
清单 8. 运行 doxygen
arpan@tintin# find . –name “*.[chyli]*” > cscope.files
arpan@tintin# cscope –b –q –k
arpan@tintin# cscope –d
您需要理解 Doxyfile 中的几个字段。比较重要的字段是:
· OUTPUT_DIRECTORY。保存生成的文档文件的目录。
· INPUT。这是一个以空格分隔的列表,其中包含必须为其生成文档的所有源代码文件和文件夹。
· RECURSIVE。如果源代码列表是层次化的,就把这个字段设置为 YES。这样就不必在 INPUT 中指定所有文件夹,只需在 INPUT 中指定顶层文件夹并把这个字段设置为 YES。
· EXTRACT_ALL。这个字段必须设置为 YES,这告诉 doxygen 应该从没有文档的所有类和函数中提取文档。
· EXTRACT_PRIVATE。这个字段必须设置为 YES,这告诉 doxygen 应该在文档中包含类的私有数据成员。
· FILE_PATTERNS。除非项目没有采用一般的 C 或 C++ 源代码文件扩展名,比如 .c、.cpp、.cc、.cxx、.h 或 .hpp,否则不需要在这个字段中添加设置。
注意:Doxyfile 中必须研究的其他字段取决于项目需求和所需的文档细节。清单 9 给出一个示例 Doxyfile。
清单 9. 示例 Doxyfile
Cscope version 15.5 Press the ? key for help
Find this C symbol:
Find this global definition:
Find functions called by this function:
Find functions calling this function:
Find this text string:
Change this text string:
Find this egrep pattern:
Find this file:
Find files #including this file:
使用 STL 和 gdb
在使用 C++ 开发的软件中,最复杂的部分常常使用 C Standard Template Library (STL) 中的类。但糟糕的是,调试包含许多 STL 类的代码并不容易,GNU Debugger (gdb) 常常指出缺少信息,无法显示相关数据,甚至可能崩溃。为了解决这个问题,可以使用 gdb 的一个高级特性 — 添加用户定义的命令。例如,请考虑一下清单 10 中的代码片段,这段代码使用一个向量并显示信息。
清单 10. 在 C++ 代码中使用 STL 向量
现在,在调试这个程序时,如果希望查明向量的长度,可以在 gdb 提示上运行 V._M_finish – V._M_start,其中的 _M_finish 和 _M_start 分别是向量开头和末尾的指针。但是,这要求您了解 STL 的内部原理,而这不总是可行的。我推荐的替代方法是使用可免费下载的 gdb_stl_utils,它在 gdb 中定义几个用户定义命令,比如 p_stl_vector_size(显示向量的大小)或 p_stl_vector(显示向量的内容)。清单 11 说明 p_stl_vector 如何循环遍历由 _M_start 和 _M_finish 指针指定的数据。
清单 11. 使用 p_stl_vector 显示向量的内容
Cscope version 15.5 Press the ? key for help
File
0 database.cpp
1.database.h
2.databasecomponents.cpp
3.databasecomponents.h
Find this C symbol:
Find this global definition:
Find functions called by this function:
Find functions calling this function:
Find this text string:
Change this text string:
Find this egrep pattern:
Find this file:
Find files #including this file:
在 gdb 提示上运行 help user-defined,就可以看到使用 gdb_stl_utils 定义的命令列表。
加快编译
对于任何比较复杂的软件,编译源代码都会占用不少时间。在加快编译过程方面,最好的工具之一是 ccache(见 参考资料 中的链接)。ccache 是一种编译器缓存,这意味着如果在编译期间文件没有修改过,就从工具的缓存获取它。如果用户只修改了一个头文件并调用 make clean; make,ccache 会显著加快编译。因为 ccache 不仅仅使用时间戳决定文件是否需要重新编译,可以更好地节省宝贵的编译时间。下面是使用 ccache 的一个示例:
arpan@tintin# doxygen -g
arpan@tintin# ls
Doxyfile
… [after editing Doxyfile]
arpan@tintin# doxygen Doxyfile
ccache 在内部生成一个散列(hash),使用这个散列和其他东西考虑源代码文件的预处理版本(使用 g++ –E 获得)、调用编译器所用的选项等。编译的对象文件根据这个散列存储在缓存中。
ccache 定义了几个可以定制的环境变量:
· CCACHE_DIR。ccache 在这个目录中存储缓存的文件。在默认情况下,文件存储在 $HOME/.ccache 中。
· CCACHE_TEMPDIR。ccache 在这个目录中存储临时文件。这个文件夹应该位于与 $CCACHE_DIR 相同的文件系统中。
· CCACHE_READONLY。如果不断增大的缓存文件夹可能造成问题,那么设置这个环境变量会有帮助。如果启用这个变量,ccache 在编译期间不会在缓存中添加任何文件;但是,它使用现有的缓存搜索对象文件。
通过结合使用 gdb 与 Valgrind 和 Electric-Fence 解决内存错误
C++ 编程有几个缺陷 — 最显著的问题是内存错误。有两个 UNIX 开放源码工具 — Valgrind 和 Electric-Fence — 它们可以与 gdb 结合使用以解决内存错误。下面简要讨论如何使用这些工具。
Valgrind
对程序使用 Valgrind 最容易的方法是在 shell 上运行它,然后使用一般的程序选项。注意,为了获得最佳效果,应该运行程序的调试版本。
Valgrind 报告一些常见的内存错误,比如不正确地释放内存(应该使用 malloc 分配,使用 delete 释放)、使用尚未初始化的变量以及两次删除同一个指针。清单 12 中的示例代码中有一个明显的数组覆盖问题。
清单 12. C++ 内存错误示例
OUTPUT_DIRECTORY = /home/tintin/database/docs
INPUT = /home/tintin/project/database
FILE_PATTERNS =
RECURSIVE = yes
EXTRACT_ALL = yes
EXTRACT_PRIVATE = yes
EXTRACT_STATIC = yes
Valgrind 和 gdb 可以结合使用。通过在 Valgrind 中使用 -db-attach=yes 选项,可以在运行 Valgrind 时直接调用 gdb。例如,如果带 –db-attach 选项对清单 12 中的代码调用 Valgrind,在首次遇到内存问题时,它会调用 gdb,见清单 13。
清单 13. 在执行 Valgrind 期间连接 gdb
#include <vector>
#include <iostream>
using namespace std;
int main ()
{
vector<int> V;
V.push_back(9);
V.push_back(8);
for (int i=0; i < V.size(); i++)
cout << V[i] << "\n";
return 0;
}
Electric-Fence
Electric-Fence 是一组用于在基于 gdb 的环境中检测缓冲区上溢出或下溢出的库。在发生错误的内存访问时,这个工具(与 gdb 结合)会准确地指出源代码中导致问题的指令。例如,对于 清单 12 中的代码,清单 14 显示在启用 Electric-Fence 的情况下 gdb 的表现。
清单 14. Electric-Fence 准确地指出源代码中导致崩溃的部分
在安装 Electric-Fence 之后,在 .gdbinit 文件中添加以下行:
define p_stl_vector
set $vec = ($arg0)
set $vec_size = $vec->_M_finish - $vec->_M_start
if ($vec_size != 0)
set $i = 0
while ($i < $vec_size)
printf "Vector Element %d: ", $i
p *($vec->_M_start+$i)
set $i++
end
end
end
使用 gprof 进行代码覆盖
最常见的编程任务之一是提高代码性能。为了完成这个任务,一定要查明代码的哪些部分花费的执行时间最多。用技术术语来说,这称为剖析。GNU 剖析工具 gprof(见 参考资料 中的链接)很容易使用,提供了大量有用的特性。
为了收集程序的剖析信息,第一步是在调用编译器时指定 –pg 选项:
arpan@tintin# ccache g__ foo.cxx
接下来,像一般情况下一样运行程序。成功地运行程序之后(也就是,没有出现崩溃或对 _exit system call 的调用),剖析信息被写入 gmon.out 文件中。生成 gmon.out 文件之后,对可执行文件运行 gprof(如下所示)。注意,如果没有指定可执行文件名,默认文件为 a.out。同样,如果没有指定剖析数据文件名,默认文件为当前工作目录中的 gmon.out。
在默认情况下,gprof 在标准输出中显示输出,所以需要把输出重定向到一个文件。gprof 提供两组信息:平面剖析数据和调用图,它们共同组成输出文件。平面剖析数据显示每个函数花费的总时间。Cumulative seconds 表示一个函数花费的总时间加上从这个函数调用的其他函数花费的时间。Self seconds 只计算这个函数本身花费的时间。
在 gdb 中显示源代码清单
开发人员常常需要通过非常缓慢的远程连接调试代码,所以不支持对 gdb 使用 Data Display Debugger (DDD) 等图形化界面。在这种情况下,在 gdb 中使用 Ctrl-X-A 组合键可以节省时间,因为它会在调试期间显示源代码清单。按 Ctrl-W-A 组合键就能够返回到 gdb 提示。另一种方法是用 –tui 选项调用 gdb,这会直接启动文本模式的源代码清单。清单 15 显示以文本模式调用 gdb 的情况。
使用文本模式的 gdb 源代码清单
arpan@tintin# valgrind <valgrind options>
<program name> <program option1> <program option2> ..
使用 CVS 维护有秩序源代码清单
不 同的项目采用不同的编码风格。例如,一些开发人员喜欢在代码中使用制表符,而其他人不喜欢这样做。但重要的是,所有开发人员应该遵守相同的编码标准。但 是,现实情况常常不是这样的。通过使用 Concurrent Versions system (CVS) 等版本控制系统,可以针对一组编码标准检查要签入的文件,从而有效地实施统一的编码标准。为了完成这个任务,CVS 附带一组预定义的触发器脚本,当涉及特定的用户操作时会运行这些脚本。触发器脚本的格式很简单:
int main ()
{
int* p_arr = new int[10];
p_arr[10] = 5;
return 0;
}
预定义的触发器脚本之一是 $CVSROOT 文件夹中的 commitinfo 文件。为了检查要签入的文件是否包含制表符,使用以下 commitinfo 文件语法:
commitinfo 文件识别出 ALL 关键字(这意味着应该检查提交的每个文件;也可以指定要检查的文件集)。然后运行相关联的脚本,根据源代码标准检查文件。