为什么我在 C++ 上得到指向具有虚拟构造函数的基类的错误指针?

Why do I get a wrong pointer to a base class with a virtual constructor on C++?

提问人:Ingo 提问时间:5/30/2023 更新时间:5/31/2023 访问量:136

问:

我想从普通的普通 C 结构中派生出一个结构,这样我就可以使用方法来扩展它来处理它自己的数据。我可以像往常一样对派生结构进行类型转换,以访问存储在结构中的不同套接字地址AF_INET6或AF_INET。但是我不明白使用虚拟析构函数时的问题,如下所示:::sockaddr_storage

#include <netinet/in.h>
#include <iostream>

namespace myns {

struct Ssockaddr_storage1 : public ::sockaddr_storage {
    Ssockaddr_storage1() : ::sockaddr_storage() {}
    ~Ssockaddr_storage1() {}
};

// This is only different by using a virtual destructor.
struct Ssockaddr_storage2 : public ::sockaddr_storage {
    Ssockaddr_storage2() : ::sockaddr_storage() {}
    virtual ~Ssockaddr_storage2() {}
};

} // namespace myns

int main() {
    {
        myns::Ssockaddr_storage1 ss;

        std::cout << "Ssockaddr_storage1:\n";
        std::cout << "ss_family   = " << ss.ss_family << "\n";
        sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;
        std::cout << "sin6_family = " << sa_in6->sin6_family << "\n";
    }
    {
        myns::Ssockaddr_storage2 ss;

        std::cout << "\nSsockaddr_storage2 with virtual destructor:\n";
        std::cout << "ss_family   = " << ss.ss_family << "\n";
        sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;
        std::cout << "sin6_family = " << sa_in6->sin6_family << "\n";
    }
}

我用以下命令编译它:

~$ g++ -std=c++11 -Wall -Wpedantic -Wextra -Werror -Wuninitialized -Wsuggest-override -Wdeprecated example.cpp

输出为:

Ssockaddr_storage1:
ss_family   = 0
sin6_family = 0

Ssockaddr_storage2 with virtual destructor:
ss_family   = 0
sin6_family = 15752

如使用对继承的引用时所示,它始终有效。但是,当使用类型强制转换指针访问AF_INET6数据时,它仅在不使用虚拟析构函数时才起作用。如果声明虚拟析构函数,我会得到一个随机的未定义地址系列号,该地址系列号因调用而异。显然,指针没有指向正确的位置。sssockaddr_storagesa_in6

为什么在声明虚拟析构函数时,强制转换的指针不指向继承结构的开头?sa_in6sockaddr_storage

C++ 指针 派生类

评论

0赞 chrysante 5/30/2023
什么?我假设如果您不使用 C 样式的指针强制转换,这个问题会在编译时被捕获。在第二种情况下,当您将对象的地址强制转换为 时,您可能正在读取该对象的 vtable 指针。vtable 指针通常位于对象内的内存偏移量 0 处sockaddr_in6Ssockaddr_storage2sockaddr_in6*
0赞 Chris Dodd 5/30/2023
由于从不初始化 的字段,因此在访问它们时会得到未指定的值。ss
0赞 Ingo 5/30/2023
@ChrisDodd 我通过调用基本结构默认构造函数进行初始化。ssSsockaddr_storage1() : ::sockaddr_storage() {}
0赞 Ingo 5/30/2023
@chrysante 是AF_INET6套接字地址结构,例如在 stackoverflow.com/a/16010670/5014688sockaddr_in6
0赞 Avi Berger 5/30/2023
sockaddr_in6 不是任一类的基类。 default 初始化其第一个成员,即 Unsigned int 类型 - 这是一个 no-op。(在某些实现中,它可能有其他成员。至于sockaddr_in6的其他成员 - 你没有一个,这样的成员在sockaddr_storage中不存在。由于 sockaddr_in6 和 sockaddr_storage 具有相同的初始元素,因此,如果您实际上有一个sockaddr_in6,而您没有,那么在 C 中,您只能通过指向 sockaddr_storage 的指针访问第一个元素。与您的帖子不同的情况。::sockaddr_storage()

答:

9赞 Sam Varshavchik 5/30/2023 #1

那是因为这是未定义的行为。

sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;

C++不是C(显然)。类似的强制转换在 C 中有效,但在 C++ 中无效。在 C++ 中,这是未定义的行为,因为 C++ 的类型检查更严格。 是 ,它与 没有任何关系。ssSsockaddr_storage2sockaddr_in6

sockaddr_storage *sas= &ss;

此转换不需要强制转换,因为 只是 的超类。sockaddr_storageSsockaddr_storage2

现在你已经有了 POD C 结构,你可以闭上眼睛,假装它真的是一个:sockaddr_in6

sockaddr_in6* sa_in6 = (sockaddr_in6*)sas;

现在我们已经解决了这个问题,您观察到的行为的原因是,一旦您引入了虚拟继承,大多数 C++ 实现都会添加一个额外的隐藏指针,指向具有虚拟继承的对象,以处理所有实现细节。这个隐藏的指针通常放在类实例的开头,这个无效的强制转换现在最终不是指向 ,而是指向虚拟表指针。随之而来的是希拉里。sockaddr_storage

评论

0赞 Ingo 5/30/2023
根据我的理解,它一定是,没有,不是吗?然后它起作用了。有没有办法使用 C++ 类型转换?sockaddr_in6* sa_in6 = (sockaddr_in6*)sas;&
0赞 Sam Varshavchik 5/30/2023
是的,解决了这个问题。
0赞 Ingo 5/31/2023 #2

正如 Sam Varshavchik 在他的回答中指出的那样,问题在于使用 C 编程语言完成的通常类型转换。这在 C++ 上具有未定义的行为。

sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;

总结一下 Sam 的解释,我只需要告诉编译器使用引用地址作为指针:&ss

myns::Ssockaddr_storage2 ss;
sockaddr_in6* sa_in6 = (sockaddr_in6*)(::sockaddr_storage*)&ss;
std::cout << "sin6_family = " << sa_in6->sin6_family << "\n";
1赞 Hans Olsson 5/31/2023 #3

其他答案(由 Ingo 和 Sam 提供)在 C++ 中有效,但为了清楚地记录强制转换之间的差异,您可以在 C++ 中使用命名强制转换:

sockaddr_in6* sa_in6 = reinterpret_cast<sockaddr_in6*>(static_cast<::sockaddr_storage*>(&ss));

这是C++核心准则 https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es49-if-you-must-use-a-cast-use-a-named-cast 建议的

评论

0赞 Ingo 6/5/2023
我不知道。非常有帮助,会使用它。谢谢。