318

我有遗留的 C++ 代码,我应该从中删除未使用的代码。问题是代码库很大。

如何找出从未调用/从未使用过的代码?

4

19 回答 19

199

有两种未使用的代码:

  • 本地的,也就是说,在某些函数中,某些路径或变量未使用(或使用但没有意义的方式,例如写入但从未读取)
  • 全局的:永远不会被调用的函数,永远不会被访问的全局对象

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

  • -Wunused(GCC,Clang)应该警告未使用的变量,Clang 未使用的分析器甚至已经增加以警告从未读取的变量(即使使用过)。
  • -Wunreachable-code(旧 GCC,于 2010 年删除)应该警告从未访问过的本地块(它发生在早期返回或总是评估为 true 的条件下)
  • 我知道没有选项可以警告未使用catch的块,因为编译器通常无法证明不会引发异常。

对于第二种,难度要大得多。静态地,它需要对整个程序进行分析,即使链接时间优化实际上可能会删除死代码,但实际上程序在执行时已经发生了很大的变化,几乎不可能向用户传达有意义的信息。

因此有两种方法:

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

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

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

因为 Clang 将为您解析代码并执行重载解析,所以您不必处理 C++ 语言规则,您将能够专注于手头的问题。

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

于 2011-01-27T09:25:04.817 回答
38

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

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

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

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

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

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

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

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

于 2011-02-01T01:07:00.920 回答
26

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

根据文件:

每当一个变量在其声明之外未被使用时,每当一个函数被声明为静态但从未定义时,每当一个标签被声明但未使用时,以及每当一个语句计算一个明确未使用的结果时发出警告。

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

编辑:这是其他有用的标志-Wunreachable-code 根据文档:

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

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

于 2011-01-27T08:36:48.583 回答
18

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

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

于 2011-01-27T08:27:11.593 回答
16

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

至少,对于非平凡的案例,你不能确定你已经得到了所有这些。考虑Wikipedia 关于无法访问代码的文章中的以下内容:

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整数集的数学范围(困难)。一个非常复杂的编译器可能能够为函数执行此操作,或为math.hsqrt中的每个函数,或任何它可以找出其域的固定输入函数。这变得非常非常复杂,而且复杂性基本上是无限的。您可以继续向编译器添加复杂的层次,但总会有一种方法可以潜入一些对于任何给定输入集都无法访问的代码。

还有一些输入集永远不会被输入。在现实生活中没有意义的输入,或者被其他地方的验证逻辑阻塞。编译器无法知道这些。

这样做的最终结果是,虽然其他人提到的软件工具非常有用,但除非您事后手动检查代码,否则您永远无法确定您是否掌握了所有内容。即使那样,你也永远无法确定你没有错过任何东西。

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

于 2011-01-31T23:13:19.230 回答
14

我自己没有使用过它,但是cppcheck声称可以找到未使用的功能。它可能无法解决完整的问题,但它可能是一个开始。

于 2011-01-27T10:00:33.920 回答
9

您可以尝试使用Gimple Software 的 PC-lint/FlexeLint。它声称

在整个项目中查找未使用的宏、typedef、类、成员、声明等

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

于 2011-01-27T10:08:25.533 回答
6

我寻找未使用的东西的正常方法是

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

这是一个有点漫长的过程,但确实会产生良好的效果。

于 2011-01-27T12:56:07.740 回答
4

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

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

这种方法的优点是您可以在每个模块的基础上进行,因此当您的代码库损坏时,很容易保持通过您的单元测试,而无需花费大量时间。

于 2011-01-27T15:26:30.817 回答
3

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

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

于 2011-01-31T22:15:56.453 回答
3

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

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

您将使用的所有工具都不是算法,而是启发式(即不是精确算法)。他们不会为您提供所有未使用的代码。

于 2011-01-27T14:44:30.713 回答
2

一种方法是使用调试器和编译器功能,在编译期间消除未使用的机器代码。

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

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

这是很多工作,但我想比手动分析所有代码要快。

于 2011-01-27T08:01:28.243 回答
2

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

免责声明:我不在那里工作,但我拥有该工具的许可证(以及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] }
于 2012-11-05T00:00:44.063 回答
1

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

例如,如果您使用 Visual Studio,则可以使用.NET ANTS Profiler 之类的工具,该工具能够解析和分析您的代码。这样,您应该很快知道实际使用了代码的哪一部分。Eclipse 也有等效的插件。

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

对于每个主要功能,您可以跟踪其使用情况,几天/一周后,只需获取该日志文件并查看它。

于 2011-01-27T08:26:35.743 回答
0

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

https://blog.flameeyes.eu/2008/01/today-how-to-identify-unused-exported-functions-and-variables

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

于 2013-07-11T03:43:00.170 回答
0

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

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

可能是非常复杂且价格昂贵的静态分析工具,例如来自CoverityLLVM 编译器的工具可能会有所帮助。

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

更新

好吧..只删除未使用的变量,未使用的函数并不难。

更新

在阅读了其他答案和评论后,我更加坚信这是做不到的。

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

于 2011-01-27T08:44:11.960 回答
0

是否会调用某个函数的一般问题是 NP-Complete。您无法以一般方式提前知道是否会调用某个函数,因为您不知道图灵机是否会停止。如果有一些路径(静态)从 main() 到您编写的函数,您可以获得,但这并不保证您会调用它。

于 2014-09-12T04:22:37.847 回答
0

GNU 链接器有一个--cref产生交叉引用信息的选项。您可以gcc从命令行通过-Wl,--cref.

例如,假设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. 现在,对于真正未使用的任何符号,编译器将能够发出警告,为您精确定位它们;你可以删除那些。

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

于 2022-01-24T02:24:01.647 回答
-3

好吧,如果你使用 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.
于 2011-01-27T10:51:41.187 回答