当在堆栈中创建对象时,即使具有100%的代码覆盖率,功能覆盖也较少

问题描述 投票:5回答:2

我正在用gcov分析我的代码。它说在堆栈中创建对象时,我的代码是2个函数。但是,当我做新删除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;

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

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

码:

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

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

我的功能覆盖率是100%。

只有在调用“new”和“delete”时才会调用哪些隐藏函数?

c++ gcov lcov
2个回答
6
投票

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

  1. 一个用于破坏物体。
  2. 一个用于破坏在堆上分配的对象。

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


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

至少对我而言,从这个相关的答案中得到的不是直接明显的,正在发生的事情,因此我想详细说明。

第一个案例(100%覆盖率):

让我们从Animal类的一个略有不同的定义开始,一个没有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对象的内存布局以虚拟表的地址开头。

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

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

如果我们使用new + delete会有什么变化?首先我们必须知道,破坏堆上的对象与调用堆栈上的对象的析构函数有点不同,因为我们需要:

  1. 销毁对象(它与堆栈上的相同,即_ZN6AnimalD1Ev
  2. 释放/释放堆上的对象占用的内存。

这两个步骤在分解析构函数_ZN6AnimalD0Ev中捆绑在一起,再次可以在程序集中看到:

_ZN6AnimalD0Ev:
    call    _ZN6AnimalD1Ev    ; <---- call "Stack"-destructor
    ....
    call    _ZdlPv            ; free heap memory
    ....

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

最后一块拼图,为什么D0-destructor是虚拟表的一部分?如果animalCatmainknow将如何处理析构函数(Cat而不是Animal)?通过查看animal指向的对象的虚拟表,为此,D0-destructor包含在vtable中。

但是,这一切都是g ++的实现细节,我认为标准中没有太多强制执行它的方式。尽管如此,clang ++完全相同,但必须检查MSVS和intel。


PS:关于deleting destructors的好文章。


2
投票

他们是allocating constructor and deallocating destructor

这是g++的实现细节。

© www.soinside.com 2019 - 2024. All rights reserved.