一、问题
在编可执行文件的时候,为了图方便,没有使用工程的Makefile,而是自己做了一些特殊的处理,选择性的编译了一部分代码,导致最后调试时发现一些文件访问singleton特定组件时错误,然后就想了下C++对于头文件中定义的static类成员中的static变量是如何实现及保证这个单件的唯一性的。
代码大致如下,注意两个单件的定义不同,此时没有编译和链接错误:
tsecer@harry :cat def1.h
struct Base
{
static Base & Instance()
{
static Base stInstance;
return stInstance;
}
Base():x(10000),y(20000)
{
}
int y, x;
};
tsecer@harry :cat def2.h
struct Base
{
static Base & Instance()
{
static Base stInstance;
return stInstance;
}
Base():x(10000)
{
}
int x;
};
tsecer@harry :cat use1.cpp
#include "def1.h"
#include <stdio.h>
void use1()
{
printf("%s Base::Instance().x:%d
", __FUNCTION__, Base::Instance().x);
}
tsecer@harry :cat use2.cpp
#include "def2.h"
#include <stdio.h>
void use2()
{
printf("%s Base::Instance().x:%d
", __FUNCTION__, Base::Instance().x);
}
tsecer@harry :cat main.cpp
void use1();
void use2();
int main()
{
use1() ;
use2();
}
tsecer@harry :g++ *.cpp
tsecer@harry :./a.out
use1 Base::Instance().x:10000
use2 Base::Instance().x:20000
tsecer@harry :
这里可以看到是,两个头文件中定义了一个相同的单件类实现,在链接的时候不会有任何错误。当然,工程中的代码不会定义两个相同的单件,这个例子是还原了问题的本质,如果在头文件中修改了类的结构(这里是增加了一个变量y),只有重新编译了一个文件,就相当于两个文件看到了两份不同的类结构定义。对于类成员x的访问,汇编中都是通过偏移量来实现的,当类结构调整之后,之前代码通过偏移量访问到的就可能是另一个变量的位置。
二、多份代码在链接时如何确定使用哪一个(唯一的那一个)
看下汇编代码:
tsecer@harry :g++ -S use1.cpp
tsecer@harry :cat use1.s |c++filt
.file "use1.cpp"
.section .text._ZN4BaseC1Ev,"axG",@progbits,Base::Base(),comdat
.align 2
.weak Base::Base()
.type Base::Base(), @function
Base::Base():
.LFB5:
pushq %rbp
.LCFI0:
movq %rsp, %rbp
.LCFI1:
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movl $20000, (%rax)
movq -8(%rbp), %rax
movl $10000, 4(%rax)
leave
ret
.LFE5:
.size Base::Base(), .-Base::Base()
.globl __gxx_personality_v0
.section .text._ZN4Base8InstanceEv,"axG",@progbits,Base::Instance(),comdat
.align 2
.weak Base::Instance()
.type Base::Instance(), @function
Base::Instance():
.LFB2:
pushq %rbp
.LCFI2:
movq %rsp, %rbp
.LCFI3:
movl guard variable for Base::Instance()::stInstance, %eax
movzbl (%rax), %eax
testb %al, %al
jne .L4
movl guard variable for Base::Instance()::stInstance, %edi
call __cxa_guard_acquire
testl %eax, %eax
setne %al
testb %al, %al
je .L4
movl Base::Instance()::stInstance, %edi
call Base::Base()
movl guard variable for Base::Instance()::stInstance, %edi
call __cxa_guard_release
.L4:
movl Base::Instance()::stInstance, %eax
leave
ret
.LFE2:
.size Base::Instance(), .-Base::Instance()
.section .rodata
.LC0:
.string "%s Base::Instance().x:%d
"
.text
.align 2
.globl use1()
.type use1(), @function
use1():
.LFB6:
pushq %rbp
.LCFI4:
movq %rsp, %rbp
.LCFI5:
call Base::Instance()
movl 4(%rax), %edx
movl use1()::__FUNCTION__, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
leave
ret
.LFE6:
.size use1(), .-use1()
.weak guard variable for Base::Instance()::stInstance
.section .bss._ZGVZN4Base8InstanceEvE10stInstance,"awG",@nobits,guard variable for Base::Instance()::stInstance,comdat
.align 8
.type guard variable for Base::Instance()::stInstance, @object
.size guard variable for Base::Instance()::stInstance, 8
guard variable for Base::Instance()::stInstance:
.zero 8
.section .rodata
.type use1()::__FUNCTION__, @object
.size use1()::__FUNCTION__, 5
use1()::__FUNCTION__:
.string "use1"
.weak Base::Instance()::stInstance
.section .bss._ZZN4Base8InstanceEvE10stInstance,"awG",@nobits,Base::Instance()::stInstance,comdat
.align 4
.type Base::Instance()::stInstance, @object
.size Base::Instance()::stInstance, 8
Base::Instance()::stInstance:
.zero 8
……
.LEFDE5:
.ident "GCC: (GNU) 4.1.2 20070115 (prerelease) (SUSE Linux)"
.section .note.GNU-stack,"",@progbits
tsecer@harry :
这里的实现和系统为类定义的默认构造函数、析构函数、头文件中类内定义的函数的实现方法相同,也就是在每个编译单元中,如果这个函数被使用到,就生成一份关于这个函数定义的comdat结构,在最后链接时,从这么多分定义中选择一份,其它的并不会进入链接器的输入,被直接丢弃掉。这是一个最为保险的做法:如果一个源文件使用了定义于某个头文件中的函数或者系统自动生成的函数,它必须在自己的目标文件中定义一份comdat属性的这个代码实现,因为它不能假设在这个源文件之外有一定有另外一个编译单元定义了这个实现;反过来说,如果这个源文件中没有用到这个头文件中的函数定义,对应的编译单元中就可以不包含这个函数的实现。
三、虚函数表的定义
和上面的情况相同,每个类的虚函数表也是只需要一份即可,这一份定义由谁来定义呢?根据C++标准的规定,一个虚函数必须属于下面三种情况,也就是任何一个虚函数必须被定义,这样就可以把虚函数表放在类的第一个虚函数所在的目标文件中,也是合情合理的。假设说一个类的非第一个虚函数没有定义,此时报错方式为特定虚函数没有实现;如果第一个虚函数没有定义,则错误为虚函数表没有定义。但是如果第一个虚函数定义在头文件中,更极端的来说所有的虚函数都在头文件里定义呢?下面是几种情况的展示说明:
1、非第一个虚函数未定义
虚函数未定义
tsecer@harry :cat novf.h
struct base
{
virtual int func1();
virtual int func2();
};
tsecer@harry :cat novf.cpp
#include "novf.h"
int base::func1()
{
return 1;
}
int main()
{
base b;
return b.func1();
}
tsecer@harry :g++ novf.cpp
/tmp/cc2jehHk.o:(.rodata._ZTV4base[vtable for base]+0x18): undefined reference to `base::func2()'
collect2: ld returned 1 exit status
2、第一个虚函数未定义
虚函数表未定义
tsecer@harry :cat novf1.h
struct base
{
virtual int func1();
virtual int func2();
};
tsecer@harry :cat novf1.cpp
#include "novf.h"
int base::func2()
{
return 1;
}
int main()
{
base b;
return b.func2();
}
tsecer@harry :g++ novf1.cpp
/tmp/ccubLrw4.o: In function `base::base()':
novf1.cpp:(.text._ZN4baseC1Ev[base::base()]+0x9): undefined reference to `vtable for base'
collect2: ld returned 1 exit status
tsecer@harry :
3、第一个虚函数定义在头文件
第一个未定义在头文件中的虚函数所在定义文件中包含虚函数表
tsecer@harry :cat sepdef1.cpp
#include "sepdef.h"
int base::fun1()
{
return 1;
}
tsecer@harry :cat sepdef2.cpp
#include "sepdef.h"
int base::fun2()
{
return 2;
}
tsecer@harry :g++ sepdef*.cpp -S
tsecer@harry :cat sepdef1.s |c++filt |grep vtable
.weak vtable for base
.section .rodata._ZTV4base,"aG",@progbits,vtable for base,comdat
.type vtable for base, @object
.size vtable for base, 40
vtable for base:
.quad vtable for __cxxabiv1::__class_type_info+16
tsecer@harry :cat sepdef2.s |c++filt |grep vtable
tsecer@harry :
4、所有虚函数定义在头文件
每份目标文件都有一个定义
tsecer@harry :cat alldef.h
struct base
{
virtual int fun0() {return 0;}
virtual int fun1() {return 1;}
virtual int fun2() {return 2;}
};
tsecer@harry :cat alldef1.cpp
#include "alldef.h"
int use()
{
base b;
return b.fun0();
}
tsecer@harry :cat alldef2.cpp
#include "alldef.h"
int use()
{
base b;
return b.fun0();
}
tsecer@harry :g++ -S alldef[12].cpp
tsecer@harry :cat alldef1.s | c++filt | grep vtable
movl vtable for base+16, %edx
.weak vtable for base
.section .rodata._ZTV4base,"aG",@progbits,vtable for base,comdat
.type vtable for base, @object
.size vtable for base, 40
vtable for base:
.quad vtable for __cxxabiv1::__class_type_info+16
tsecer@harry :cat alldef2.s | c++filt | grep vtable
movl vtable for base+16, %edx
.weak vtable for base
.section .rodata._ZTV4base,"aG",@progbits,vtable for base,comdat
.type vtable for base, @object
.size vtable for base, 40
vtable for base:
.quad vtable for __cxxabiv1::__class_type_info+16
tsecer@harry :