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

时间:2017-09-27 12:17:56

标签: c++ gcov lcov

我正在用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”时才会调用哪些隐藏函数?

2 个答案:

答案 0 :(得分:5)

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

  1. 一个用于破坏物体。
  2. 一个用于破坏在堆上分配的对象。
  3. 在某些情况下,它们都保存在目标文件中,而在某些情况下只保留在使用中。在你的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中所需的内联函数(类定义中实现的函数被视为内联函数,例如,没有copy-constructor或assignment-operator,它们由编译器)。我不确定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. 释放/释放堆中对象占用的内存。
    3. 这两个步骤在分解析构函数_ZN6AnimalD0Ev中捆绑在一起,再一次可以在程序集中看到:

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

      现在,在main中我们必须从堆中删除对象,因此必须调用D0 - 析构函数 - 版本,然后再调用D1 - 析构函数 - 版本 - 这意味着使用所有功能 - 再次100%覆盖。

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

      然而,这一切都是g ++的实现细节,我不认为标准中有很多强制要求以这种方式完成。尽管如此,clang ++完全相同,但必须检查MSVS和英特尔。

      PS:关于deleting destructors的精彩文章。

答案 1 :(得分:2)

他们是allocating constructor and deallocating destructor

这是g++

的实施细节