我正在用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”时才会调用哪些隐藏函数?
答案 0 :(得分:5)
简而言之:g ++为类创建了两个析构函数
在某些情况下,它们都保存在目标文件中,而在某些情况下只保留在使用中。在你的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
,会有哪些变化?首先我们必须知道,破坏堆上的对象与调用堆栈上的对象的析构函数有点不同,因为我们需要:
_ZN6AnimalD1Ev
)这两个步骤在分解析构函数_ZN6AnimalD0Ev
中捆绑在一起,再一次可以在程序集中看到:
_ZN6AnimalD0Ev:
call _ZN6AnimalD1Ev ; <---- call "Stack"-destructor
....
call _ZdlPv ; free heap memory
....
现在,在main
中我们必须从堆中删除对象,因此必须调用D0
- 析构函数 - 版本,然后再调用D1
- 析构函数 - 版本 - 这意味着使用所有功能 - 再次100%覆盖。
最后一块拼图,为什么D0
- 析构函数是虚拟表的一部分?如果animal
是Cat
,那么main
如何知道要调用哪个析构函数(Cat
而不是Animal
)?通过查看animal
指向的对象的虚拟表,为此,D0
- 析构函数包含在vtable中。
然而,这一切都是g ++的实现细节,我不认为标准中有很多强制要求以这种方式完成。尽管如此,clang ++完全相同,但必须检查MSVS和英特尔。
PS:关于deleting destructors的精彩文章。
答案 1 :(得分:2)
他们是allocating constructor and deallocating destructor。
这是g++
。