我正在用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”时才会调用哪些隐藏函数?
简而言之: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
中所需的内联函数(在类定义中实现的函数被视为内联函数,例如,没有复制构造函数或赋值运算符,它们由编译器自动定义)。我不确定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
-destructor-version,它依次调用D1
-destructor-version - 这意味着所有函数都被使用 - 再次100%覆盖。
最后一块拼图,为什么D0
-destructor是虚拟表的一部分?如果animal
是Cat
,main
know将如何处理析构函数(Cat
而不是Animal
)?通过查看animal
指向的对象的虚拟表,为此,D0
-destructor包含在vtable中。
但是,这一切都是g ++的实现细节,我认为标准中没有太多强制执行它的方式。尽管如此,clang ++完全相同,但必须检查MSVS和intel。
PS:关于deleting destructors的好文章。
他们是allocating constructor and deallocating destructor。
这是g++
的实现细节。