gpt4 book ai didi

c++ - 在堆栈中创建对象时,即使代码覆盖率为 100%,函数覆盖率也较低

转载 作者:塔克拉玛干 更新时间:2023-11-02 23:20:44 25 4
gpt4 key购买 nike

我正在用 gcov 分析我的代码。它说在堆栈中创建对象时,我的代码少了 2 个函数。但是当我执行 new-delete 时,实现了 100% 的功能覆盖。

代码:

class Animal
{
public:
Animal()
{
}
virtual ~Animal()
{
}
};

int main()
{
Animal animal;
}

我为生成 gcov 报告而执行的命令。
rm -rf Main.g* out.txt a.out coverage;
g++ -fprofile-arcs -ftest-coverage -lgcov -coverage Main.cpp;
./a.out;
lcov --capture --directory . --output-file out.txt;
genhtml out.txt --output-directory coverage;

生成的 html 显示我的功能覆盖率为 3/4 - 75%。

但是一旦我将堆栈对象更改为堆,

代码:
class Animal
{
public:
Animal()
{
}
virtual ~Animal()
{
}
};

int main()
{
auto animal = new Animal;
delete animal;
}

我的功能覆盖率为 100%。

仅在调用“new”和“delete”时调用的隐藏函数是什么?

最佳答案

简而言之:g++ 为一个类创建了两个析构函数

  • 一种用于销毁对象。
  • 一种用于销毁在堆上分配的对象。

  • 在某些情况下,它们都保存在目标文件中,而在某些情况下仅使用。在您的 75%-coverage-example 中,您仅使用第一个析构函数,但两者都必须保存在目标文件中。

    @MSalters 答案中的链接显示了方向,但主要是关于 g++ 发出的多个构造函数/析构函数符号。

    至少对我来说,从这个链接的答案中并没有直接显而易见,这是怎么回事,因此我想详细说明。

    第一种情况(100% 覆盖率):

    让我们从 Animal 的稍微不同的定义开始-class,一个没有 virtual析构函数:
    class Animal
    {
    public:
    Animal(){}
    ~Animal(){}
    };

    int main(){Animal animal;}

    对于这个类定义,lcov 显示 100% 的代码覆盖率。

    让我们来看看目标文件中的符号(为了简单起见,我没有使用 gcov 构建它):
    nm main.o
    0000000000000000 T main
    U __stack_chk_fail
    0000000000000000 W _ZN6AnimalC1Ev
    0000000000000000 W _ZN6AnimalC2Ev
    0000000000000000 n _ZN6AnimalC5Ev
    0000000000000000 W _ZN6AnimalD1Ev
    0000000000000000 W _ZN6AnimalD2Ev
    0000000000000000 n _ZN6AnimalD5Ev

    编译器只保留那些在 main 中需要的内联函数(在类定义中实现的函数被视为内联函数,例如没有复制构造函数或赋值运算符,它们由编译器自动定义)。我不确定是什么 AnimalX5Ev是,但对于这个类, AnimalXC1Ev 没有区别。 (完整的对象构造函数)和 AnimalXC2Ev (基本对象构造函数)-它们甚至具有相同的地址。如 linked answer 中所述,这是 gcc 的一些怪癖(但 clang 也有)和多态支持的副产品。

    第二种情况(75% 覆盖率):

    让我们像原始示例中一样将析构函数设为虚拟,然后查看生成的目标文件中的符号:
     nm main.o
    0000000000000000 T main
    ...
    0000000000000000 W _ZN6AnimalD0Ev <----------- NEW
    ...
    0000000000000000 V _ZTV6Animal <----------- NEW

    我们看到了一些新符号: _ZTV6Animal是众所周知的 vtable,和 _ZN6AnimalD0Ev - 所谓的删除析构函数(继续阅读以了解为什么需要它)。然而,在 main再一次只有 _ZN6AnimalD1Ev使用,因为与第一种情况相比没有任何变化(用 g++ -S main.cpp -o main.s 编译以查看它)。

    但究竟为什么是 _ZN6AnimalD0Ev保存在目标文件中,如果不使用?因为用在虚拟表 _ZTV6Animal (参见程序集 main.s):
    _ZTV6Animal:
    .quad 0
    .quad _ZTI6Animal
    .quad _ZN6AnimalD1Ev
    .quad _ZN6AnimalD0Ev <---- HERE is the address of the function!
    .weak _ZTI6Animal

    但是为什么需要这个 vtable 呢?因为只要类中有虚方法,类的每个对象都会引用该类的 vtable,正如在构造函数中看到的(仍然是 main.s):
    ZN6AnimalC2Ev:
    ...
    // in register %rdi is the address of the newly created object
    movl $_ZTV6Animal+16, (%rdi) ;write the address of the vtable (why +16?) to the address pointed to by %rdi.
    ...

    我必须承认,我稍微简化了程序集,但很容易看出, Animal 的内存布局。 -object 以虚拟表的地址开头。

    这个解除分配的析构函数 _ZN6AnimalD0Ev是覆盖范围缺失的函数 - 因为它没有在您的程序中使用。

    第三种情况(再次100%覆盖):

    如果我们使用 new 会发生什么变化+ delete ?首先我们必须知道,在堆上析构一个对象与在堆栈上为对象调用析构函数有点不同,因为我们需要:
  • 销毁对象(它与堆栈上的相同,即 _ZN6AnimalD1Ev )
  • 释放/释放堆上对象占用的内存。

  • 这两个步骤在解除分配的析构函数中捆绑在一起 _ZN6AnimalD0Ev ,再次可以在程序集中看到:
    _ZN6AnimalD0Ev:
    call _ZN6AnimalD1Ev ; <---- call "Stack"-destructor
    ....
    call _ZdlPv ; free heap memory
    ....

    现在,在 main我们必须从堆中删除对象,因此 D0 -destructor-version 必须被调用,它调用 D1依次为 -destructor-version - 这意味着使用所有功能 - 再次 100% 覆盖。

    最后一块拼图,为什么 D0 -析构函数是虚拟表的一部分?如 animalCat ,怎么会 main知道要调用哪个释放析构函数( Cat 而不是 Animal )?通过查看 animal指向的对象的虚拟表为此, D0 -析构函数包含在 vtable 中。

    但是,这都是 g++ 的实现细节,我认为标准中没有太多强制要求以这种方式完成。尽管如此,clang++ 的功能完全相同,但必须检查 MSVS 和 intel。

    PS:关于 deleting destructors 的好文章.

    关于c++ - 在堆栈中创建对象时,即使代码覆盖率为 100%,函数覆盖率也较低,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46447674/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com