我如何知道代码中的哪些部分从未使用过?

我有遗留的c++代码,我应该删除不使用的代码。问题是代码库很大。

我如何才能知道哪些代码从未被调用/从未被使用?

57630 次浏览

一种方法是使用调试器和编译器特性,在编译过程中消除未使用的机器代码。

一旦某些机器代码被删除,调试器就不会让你在相应的源代码行上添加断点。因此,您将断点放置在任何地方,并启动程序并检查断点——那些处于“此源未加载代码”状态的断点对应于已消除的代码——要么该代码从未被调用,要么已内联,您必须执行一些最小分析,以找出这两者中哪一个发生了。

至少这是它在Visual Studio中的工作方式,我猜其他工具集也可以做到这一点。

这需要做很多工作,但我认为比手动分析所有代码要快。

这取决于创建应用程序时使用的平台。

例如,如果你使用Visual Studio,你可以使用像.NET ANTS分析器这样的工具来解析和分析你的代码。通过这种方式,您应该很快知道实际使用了代码的哪一部分。Eclipse也有等效的插件。

否则,如果您需要知道最终用户实际使用了应用程序的哪些功能,并且您可以轻松地发布应用程序,则可以使用日志文件进行审计。

对于每个主要函数,您可以跟踪它的使用情况,并在几天/一周后获取日志文件,并查看它。

我认为你正在寻找一个代码覆盖率工具。代码覆盖工具将在代码运行时分析它,它将让您知道哪些代码行被执行了,执行了多少次,以及哪些代码没有执行。

你可以试着给这个开源代码覆盖工具一个机会:TestCocoon - C/ c++和c#的代码覆盖工具。

如果你使用g++,你可以使用这个标志-Wunused

根据文档:

当变量未使用时发出警告 除了它的声明,无论何时 函数被声明为静态的,但是 从来没有定义,每当一个标签 声明但未使用,并且每当a 语句计算的结果为

http://docs.freebsd.org/info/gcc/gcc.info.Warning_Options.html

下面是另一个有用的标志-Wunreachable-code 根据文档:< / p >

此选项用于在编译器检测到至少一整行源代码永远不会执行时发出警告,因为某些条件永远不会满足,或者因为它在一个永远不会返回的过程之后。

更新:我发现类似的主题遗留C/ c++项目中的死代码检测

我不认为它可以自动完成。

即使使用代码覆盖工具,也需要提供足够的输入数据来运行。

可能是非常复杂和昂贵的静态分析工具,如Coverity的LLVM编译器可能会有所帮助。

但我不确定,我更喜欢手动代码审查。

更新

嗯. .不过,仅删除未使用的变量和未使用的函数并不难。

更新

看了其他人的回答和评论后,我更加坚定地认为这是不可能的。

您必须了解代码以获得有意义的代码覆盖率度量,如果您知道大量的手动编辑将比准备/运行/检查覆盖率结果更快。

有两种未使用的代码:

  • 局部路径,即在某些函数中,某些路径或变量未使用(或使用但没有任何意义,如写入但从未读取)
  • 全局的:永远不会调用的函数,永远不会访问的全局对象

对于第一种类型,一个好的编译器可以帮助:

  • -Wunused (GCC, 铿锵声)应该警告未使用的变量,Clang unused analyzer甚至已经增加到警告从未读取的变量(即使使用)。
  • -Wunreachable-code(旧的GCC, 2010年删除)应该警告从未被访问的局部块(它发生在早期返回或条件总是求值为true时)
  • 据我所知,没有选项可以警告未使用的catch块,因为编译器通常不能证明不会抛出异常。

对于第二种,要困难得多。静态地,它需要整个程序的分析,即使链接时间优化实际上可以删除死代码,在实践中,程序在执行时已经进行了如此多的转换,以至于几乎不可能向用户传递有意义的信息。

因此有两种方法:

  • 理论上的方法是使用静态分析仪。这是一种软件,它可以一次非常详细地检查整个代码,并找到所有的流程路径。实际上,我不知道有什么在这里有用。
  • 实用的方法是使用启发式方法:使用代码覆盖工具(在GNU链中是gcov。请注意,在编译期间应该传递特定的标志,以便它正常工作)。你用一组不同的输入(你的单元测试或非回归测试)运行代码覆盖工具,死代码必然在未达到的代码中……所以你可以从这里开始。

如果您对这个主题非常感兴趣,并且有时间和意愿自己开发一个工具,我建议您使用Clang库来构建这样一个工具。

  1. 使用Clang库获取AST(抽象语法树)
  2. 从入口点开始执行标记-清除分析

因为Clang将为您解析代码,并执行重载解析,所以您不必处理c++语言规则,并且您将能够集中精力处理手头的问题。

然而,这种技术不能识别未使用的虚拟覆盖,因为它们可能由您无法推理的第三方代码调用。

我自己没有使用过它,但是cppcheck声称可以找到未使用的函数。这也许不能完全解决问题,但可能是个开始。

你可以尝试使用PC-lint/FlexeLint来自Gimple Software。它声称

找到未使用的宏,typedef's, 类、成员、声明等。 贯穿整个项目

我曾用它进行静态分析,并发现它非常好,但我必须承认,我没有专门用它来查找死代码。

如果你使用g++,你可以使用这个标志-Wunused

根据文档:

Warn whenever a variable is unused aside from its declaration, whenever a function is declared static but never defined, whenever a label is declared but not used, and whenever a statement computes a result that is explicitly not used.

http://docs.freebsd.org/info/gcc/gcc.info.Warning_Options.html

编辑:这是另一个有用的标志-Wunreachable-code根据文档:

This option is intended to warn when the compiler detects that at least a whole line of source code will never be executed, because some condition is never satisfied or because it is after a procedure that never returns.

我通常找没用的东西的方法是

  1. 确保构建系统正确地处理依赖项跟踪
  2. 设置第二个监视器,使用全屏终端窗口,运行重复构建并显示第一个满屏的输出。watch "make 2>&1"倾向于在Unix上完成任务。
  3. 在整个源代码树上运行查找和替换操作,添加“//?”“在每一行的开头
  4. 通过删除相应行中的"//?"来修复编译器标记的第一个错误。
  5. 重复操作,直到没有错误。

这是一个有点漫长的过程,但确实能得到很好的结果。

我真的没有使用过任何工具做这样的事情…但是,就我所看到的所有答案,没有人说过这个问题是不可计算的。

这是什么意思呢?这个问题不能用计算机上的任何算法解决。这个定理(这样的算法不存在)是图灵停止问题的一个推论。

你将使用的所有工具都不是算法,而是启发式(即不是精确的算法)。他们不会给你所有没有使用的代码。

在不引起编译错误的情况下,尽可能多地将公共函数和变量标记为私有或受保护的,同时尝试重构代码。通过将函数设置为私有并在某种程度上受到保护,您可以减少搜索区域,因为私有函数只能从同一个类中调用(除非有愚蠢的宏或其他技巧来规避访问限制,如果是这种情况,我建议您找一份新工作)。确定不需要私有函数要容易得多,因为只有当前正在处理的类可以调用这个函数。如果您的代码库具有较小的类并且是松散耦合的,则此方法更容易。如果您的代码基没有小类,或者耦合非常紧密,我建议首先清理这些类。

接下来将标记所有剩余的公共函数,并制作一个调用图,以找出类之间的关系。从这棵树上,试着找出树枝的哪一部分看起来可以修剪。

这种方法的优点是你可以在每个模块的基础上进行测试,所以当你的代码库损坏时,你很容易通过单元测试,而不会有很长一段时间。

如果你使用的是Linux,你可能想要查看callgrind,这是一个C/ c++程序分析工具,是valgrind套件的一部分,它还包含检查内存泄漏和其他内存错误的工具(你也应该使用它)。它分析程序的运行实例,并生成关于其调用图的数据,以及关于调用图上节点的性能成本的数据。它通常用于性能分析,但它也为应用程序生成一个调用图,以便您可以看到调用了哪些函数以及它们的调用者。

这显然是对页面其他地方提到的静态方法的补充,它只会有助于消除完全不使用的类、方法和函数——它不会帮助找到实际调用的方法内部的死代码。

真正的答案是:你永远无法确定。

至少,对于重要的情况,你不能确定你已经得到了全部。考虑下面来自维基百科关于不可访问代码的文章的语句:

double x = sqrt(2);
if (x > 5)
{
doStuff();
}

正如维基百科正确指出的那样,一个聪明的编译器也许能够捕捉到这样的东西。但是考虑一下修改:

int y;
cin >> y;
double x = sqrt((double)y);


if (x != 0 && x < 1)
{
doStuff();
}

编译器会捕获这个吗?也许吧。但要做到这一点,它需要做的不仅仅是对常量标量值运行sqrt。它必须弄清楚(double)y始终是一个整数(简单),然后理解整数集sqrt的数学范围(困难)。一个非常复杂的编译器可能能够为sqrt函数,或math.h中的每个函数,或任何固定输入函数,其域可以计算出来。这就变得非常非常复杂,而这种复杂性基本上是无限的。您可以不断地在编译器中添加复杂的层,但是总会有一种方法潜入一些对于任何给定输入集都无法访问的代码。

然后还有简单的永远不要被录取。 input输入集,在现实生活中没有任何意义,或者在其他地方被验证逻辑阻塞。编译器没有办法知道这些。

这样做的最终结果是,虽然其他人提到的软件工具非常有用,但您永远无法确定您捕获了所有内容,除非您随后手动检查代码。即便如此,你也无法确定自己是否错过了什么。

恕我直言,唯一真正的解决方案是尽可能保持警惕,使用自动化,尽可能地重构,并不断寻找改进代码的方法。当然,这样做是个好主意。

对于未使用的整个函数(和未使用的全局变量),GCC实际上可以为您完成大部分工作,前提是您使用GCC和GNU ld。

编译源代码时,使用-ffunction-sections-fdata-sections,然后链接时使用-Wl,--gc-sections,--print-gc-sections。链接器现在将列出所有可以删除的函数,因为它们从未被调用,以及所有从未被引用的全局函数。

(当然,你也可以跳过--print-gc-sections部分,让链接器无声地删除函数,但将它们保留在源代码中。)

注意:这只会发现未使用的完整函数,它不会对函数中的死代码做任何事情。在活函数中从死代码调用的函数也将被保留。

一些c++特有的特性也会导致问题,特别是:

  • 虚函数。如果不知道存在哪些子类以及在运行时实际实例化了哪些子类,就无法知道在最终程序中需要存在哪些虚函数。链接器没有足够的信息,所以它必须保持所有的它们。
  • 具有构造函数的全局变量,以及它们的构造函数。一般来说,链接器不能知道全局函数的构造函数没有副作用,所以它必须运行它。显然,这意味着全局变量本身也需要保留。

在这两种情况下,虚函数或全局变量构造函数的任何使用也必须保留。

另一个警告是,如果你正在构建一个共享库,GCC中的默认设置将在共享库中导出每一个函数,导致它在链接器中被“使用”。为了解决这个问题,你需要将默认值设置为隐藏符号而不是导出(例如使用-fvisibility=hidden),然后显式地选择你需要导出的导出函数。

CppDepend是一个商业工具,可以检测未使用的类型,方法和字段,并做更多的事情。它适用于Windows和Linux(但目前不支持64位),并有两周的试用期。

免责声明:我不在那里工作,但我拥有这个工具的许可证(以及NDepend对于,这是一个更强大的。net代码的替代方案)。

对于那些好奇的人,这里有一个用于检测死方法的内置(可定制)规则示例,用CQLinq编写:

// <Name>Potentially dead Methods</Name>
warnif count > 0
// Filter procedure for methods that should'nt be considered as dead
let canMethodBeConsideredAsDeadProc = new Func<IMethod, bool>(
m => !m.IsPublic &&       // Public methods might be used by client applications of your Projects.
!m.IsEntryPoint &&            // Main() method is not used by-design.
!m.IsClassConstructor &&
!m.IsVirtual &&               // Only check for non virtual method that are not seen as used in IL.
!(m.IsConstructor &&          // Don't take account of protected ctor that might be call by a derived ctors.
m.IsProtected) &&
!m.IsGeneratedByCompiler
)


// Get methods unused
let methodsUnused =
from m in JustMyCode.Methods where
m.NbMethodsCallingMe == 0 &&
canMethodBeConsideredAsDeadProc(m)
select m


// Dead methods = methods used only by unused methods (recursive)
let deadMethodsMetric = methodsUnused.FillIterative(
methods => // Unique loop, just to let a chance to build the hashset.
from o in new[] { new object() }
// Use a hashet to make Intersect calls much faster!
let hashset = methods.ToHashSet()
from m in codeBase.Application.Methods.UsedByAny(methods).Except(methods)
where canMethodBeConsideredAsDeadProc(m) &&
// Select methods called only by methods already considered as dead
hashset.Intersect(m.MethodsCallingMe).Count() == m.NbMethodsCallingMe
select m)


from m in JustMyCode.Methods.Intersect(deadMethodsMetric.DefinitionDomain)
select new { m, m.MethodsCallingMe, depth = deadMethodsMetric[m] }

今天我有一个朋友问我这个问题,我环顾了一些有前途的Clang开发,例如ASTMatchers和静态分析器,它们可能在编译期间有足够的可见性来确定死代码段,但后来我发现了这个:

< a href = " https://blog.flameeyes。欧盟/ 2008/01 / today-how-to-identify-unused-exported-functions-and-variables " rel = " https://blog.flameeyes.eu/2008/01/today-how-to-identify-unused-exported-functions-and-variables nofollow”> < / >

它几乎完整地描述了如何使用一些GCC标志,这些标志似乎是为了识别未引用的符号而设计的!

如果某个函数将被调用的一般问题是np完全的。一般来说,你无法提前知道某个函数是否会被调用,就像你不知道图灵机是否会停止一样。如果存在从main()到您所编写的函数的某个路径(静态),则可以获取,但这并不保证它将被调用。

GNU链接器有一个--cref选项,它产生交叉引用信息。你可以通过-Wl,--crefgcc命令行传递它。

例如,假设foo.o定义了一个符号foo_sym,该符号也在bar.o中使用。然后在输出中你会看到:

foo_sym                            foo.o
bar.o

如果foo_sym仅限于foo.o,那么你将不会看到任何额外的目标文件;后面会跟着另一个符号:

foo_sym                            foo.o
force_flag                         options.o

现在,从这里我们不知道foo_sym没有被使用。它只是一个候选:我们知道它是在一个文件中定义的,而没有在其他任何文件中使用。foo_sym可以在foo.o中定义并在那里使用。

那么,你该怎么处理这些信息呢

  1. 执行一些文本修改,以识别限制在一个目标文件中的这些符号,生成候选列表。
  2. 进入源代码,给每个候选对象提供与static的内部链接,就像它应该有的那样。
  3. 重新编译源代码。
  4. 现在,对于那些真正未使用的符号,编译器将能够警告,为你精确定位它们;你可以删除它们。

当然,我忽略了其中一些符号是故意不使用的可能性,因为它们是为动态链接而导出的(即使在链接可执行文件时也可能出现这种情况);这是一种更微妙的情况,你必须了解并明智地处理。