命名空间和静态类成员链接

11

假设我有两个文件:

/**
 * class.cpp
 */ 
#include <stdio.h>
class foo 
{
private:
        int func();
};

int foo::func(void)
{
        printf("[%s:%d]: %s\n", __FILE__, __LINE__, __FUNCTION__);
        return -1; 
}

并且
/**
 * main.cpp
 */ 
#include <stdio.h>
namespace foo 
{
        int func(void);
}
int main(void)
{
        int ret = foo::func();
        printf("[%s:%d]: ret=%d\n", __FILE__, __LINE__, ret);
        return 0;
}

编译过程如下:

g++ -o a.out main.cpp class.cpp 

有一个可执行文件的输出:

[class.cpp:15]: func
[main.cpp:14]: ret=-1

最后我的问题是:
为什么这个示例代码能够编译通过,我们可以调用类foo的私有方法?
使用gcc 4.6.3编译,但不仅限于此版本。 我知道编译器无法区分这两个符号(命名空间foo中的func函数和class foo中的私有函数foo)。nm的输出:
nm class.o
00000000 T _ZN3foo4funcEv
00000017 r _ZZN3foo4funcEvE12__FUNCTION__
         U printf

nm main.o
         U _ZN3foo4funcEv
00000000 T main
         U printf

我想问这种行为是否正确?在我看来,这不是正确的行为,而且根本不安全(破坏了封装性)。

我想提一下,Visual Studio 2008的编译器不会链接这两个符号。

2个回答

3
为什么编译器没有抱怨?
需要注意的是,就编译器而言,“class”,“struct”和“namespace”都定义了一个命名空间。因此,编译器相应地装饰符号。如果您在同一文件中同时定义类和命名空间,则编译器会出现错误,但这里并非如此。
为什么链接器不抱怨?
您编写代码的方式使得在class foo中定义的func()比在namespace foo中定义的func()更弱。基本上,namespace foo中定义的func()只是一个没有实现的标记。您可以看到,在main.cpp中没有实现,因此需要在运行时由链接器解析符号。
nm main.o
       U _ZN3foo4funcEv
//Here^^^^

由于命名空间和类名恰巧相同(导致foo::func的符号相同),因此链接器在链接时解决符号,找到具有相同符号的强定义,并与之链接。

如果您还要在namespace foo中实现func()

/**
 * main.cpp
 */
#include <stdio.h>

namespace foo
{
    int func(void) {
        printf("NM_FOO [%s:%d]: %s\n", __FILE__, __LINE__, __FUNCTION__);
        return -1;
    };
}
int main(void)
{
    int ret = foo::func();
    printf("[%s:%d]: ret=%d\n", __FILE__, __LINE__, ret);
    return 0;
}

你可能会看到链接器报错:

duplicate symbol foo::func()     in:
/var/folders/.../class.o
/var/folders/.../main.o
ld: 1 duplicate symbol for architecture x86_64

如果您这次查看main.o文件,您会看到:

0000000000000064 T __ZN3foo4funcEv
0000000000000158 S __ZN3foo4funcEv.eh
00000000000000e0 s __ZZN3foo4funcEvE12__FUNCTION__
0000000000000000 T _main
0000000000000128 S _main.eh
                 U _printf

以及 class.o:

0000000000000000 T __ZN3foo4funcEv
00000000000000a0 S __ZN3foo4funcEv.eh
0000000000000080 s __ZZN3foo4funcEvE12__FUNCTION__
                 U _printf

这两种方法都定义了同样强大的函数符号,导致链接错误。

记住,链接器不知道命名空间和类之间的区别。它解析在目标代码中出现的符号。只有当出现强重新定义时,它才会发出警告。在链接器世界中,一个或多个较弱的定义与一个强定义完全没有问题。


好的,我理解了一切,但问题是为什么gcc编译器会有这样的行为。Windows编译器能够区分这些符号。命名空间和类不是同一个东西,我说得对吗?编译器应该知道命名空间和类之间的所有差异,不是吗? - pako
编译器知道,但链接器不知道。然而,编译器将每个文件转换为单独的目标文件。你定义这两个文件的方式是它们对编译器完全分离。VS所做的很好,但不是必须要做的。因此,gcc不会这样做。顺便说一下,Clang也可以编译这个而不会抱怨。 - meyumer
我知道编译器知道,但我的意思是,即使这些函数完全不同,编译器也会为它们生成完全相同的符号。因此,或许gcc应该像VS编译器一样做?这可以避免一些潜在的错误。 - pako
1
“class”、“struct”和“namespace”在编译器看来都定义了一个命名空间。您无法在同一文件中定义namespace foo { }class foo { },因此编译器没有真正需要以不同的方式装饰符号名称。 - Jonathan Potter

2
因为你在main.cpp中将foo()定义为命名空间的成员,所以编译器会按照此方式处理。类/结构体/命名空间等的公共/私有区别取决于编译器知道函数的定义 - 在这里,你故意设置了一个欺骗它的情形。
链接器不知道这种区别,它只解析符号名称,在你的编译器的情况下,函数名称的修饰最终相同。C++中未指定符号名称的修饰方式,因此这是完全有效的行为。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接