提出问题
以下代码,能编译链接通过吗?
void f(const int &value)
{
}
class Test
{
public:
static const int a = 1;
};
int main()
{
f(Test::a);
return 0;
}
我的第一感觉是:应该没问题,吧。在VS 2013 实验了下,顺利通过,一切正常.然而gcc下是报错的:Undefined reference to ‘Test::a’这是为什么?
分析问题
写作本文时所用的环境是gcc 4.8.2 (Ubuntu 14.04 , X86平台)。注意,本文的讨论只针对类的static const成员,也就是所谓的class scope。namespace scope的情况不属于我们的讨论范围内。
把以上代码保存为test.cpp,然后用gcc编译它:
g++ -c -o test.o test.cpp
这个命令执行之后,我们会在目录下得到test.o文件。接着,我们通过objdump来查看符号表:
objdump -x test.o
我们可以看到类似如下的输出:
SYMBOL TABLE:
00000000 l df *ABS* 00000000 test.cpp
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000000 g F .text 00000005 _Z1fRKi
00000005 g F .text 00000019 main
00000000 *UND* 00000000 _ZN4Test1aE
上面的最后一行,_ZN4Test1aE就是对应我们程序中声明的Test::a。之所以长得这么复杂、奇怪,是因为编译器做了mangling处理。
注意到UND没?根据文档的解释:
UND : if the section is referenced in the file being dumped, but not defined there
也就是_ZN4Test1aE在本.o文件中引用,然而它却木有定义。因此,报Undefined reference to ‘Test::a’的错,也就情理之中了。
那么,我们的程序是否真的引用了_ZN4Test1aE呢?恩,我们接着往下看。
输入如下命令:
g++ -S test.cpp
我们可以得到类似这样的汇编代码(做了整理,节选):
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $_ZN4Test1aE, (%esp) ;看到没?_ZN4Test1aE !
call _Z1fRKi ;调用函数f
movl $0, %eax
leave
ret
虽然我们已经分析出为什么会报错,然而,我们还有一个疑问,就是,为什么下面的代码,是OK的?
void f(const int value) //这里没有 &
{
}
class Test
{
public:
static const int a = 1;
};
int main()
{
f(Test::a);//没问题
return 0;
}
恩,有了前面的基础,相信读者诸君已经知道怎么分析。我们可以用同样的方法,看看它的汇编代码:
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $1, (%esp) ;看到没?1,不是_ZN4Test1aE,也不是Test::a
call _Z1fi
movl $0, %eax
leave
ret
也就是说,在这里,编译器只是把Test::a认作一个占位符,实际使用之处用1代替了。
解决问题
知道原因了,那么怎么解决呢?恩,至少三种方法:
1.我们可以定义(而不是声明)Test::a。是的,上面的static const int a = 1;并不是它的定义式。如果要定义,那么我们应该这么做:
void f(const int &value)//还是传引用
{
}
class Test
{
public:
static const int a;
};
const int Test::a = 1;//定义a
int main()
{
f(Test::a);//现在没问题了
return 0;
}
有兴趣的读者可以看看这个程序对应的符号表,就会发现Test::a被放在了程序的rodata section,而不是UND了。
2.如果仅仅声明a,那么我们可以按值的方式使用它,这没问题。也就是只使用它的值;而不去获得它的地址(当然,也包括引用。引用本质上也是地址)。
3.使用枚举类型。是的,枚举!像这样:
void f(const int &value)//还是传引用
{
}
class Test
{
public:
enum HELLOWORLD {a = 1}; //枚举,而不是 static const
};
int main()
{
f(Test::a);//没问题
return 0;
}
那么,这种情况下,编译器是如何处理的呢?就留给读者诸君作为练习吧。
关于程序设计基石与实践更多讨论与交流,敬请关注本博客和新浪微博songzi_tea