提问人:Ingo 提问时间:5/30/2023 更新时间:5/31/2023 访问量:136
为什么我在 C++ 上得到指向具有虚拟构造函数的基类的错误指针?
Why do I get a wrong pointer to a base class with a virtual constructor on C++?
问:
我想从普通的普通 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数据时,它仅在不使用虚拟析构函数时才起作用。如果声明虚拟析构函数,我会得到一个随机的未定义地址系列号,该地址系列号因调用而异。显然,指针没有指向正确的位置。ss
sockaddr_storage
sa_in6
为什么在声明虚拟析构函数时,强制转换的指针不指向继承结构的开头?sa_in6
sockaddr_storage
答:
那是因为这是未定义的行为。
sockaddr_in6* sa_in6 = (sockaddr_in6*)&ss;
C++不是C(显然)。类似的强制转换在 C 中有效,但在 C++ 中无效。在 C++ 中,这是未定义的行为,因为 C++ 的类型检查更严格。 是 ,它与 没有任何关系。ss
Ssockaddr_storage2
sockaddr_in6
sockaddr_storage *sas= &ss;
此转换不需要强制转换,因为 只是 的超类。sockaddr_storage
Ssockaddr_storage2
现在你已经有了 POD C 结构,你可以闭上眼睛,假装它真的是一个:sockaddr_in6
sockaddr_in6* sa_in6 = (sockaddr_in6*)sas;
现在我们已经解决了这个问题,您观察到的行为的原因是,一旦您引入了虚拟继承,大多数 C++ 实现都会添加一个额外的隐藏指针,指向具有虚拟继承的对象,以处理所有实现细节。这个隐藏的指针通常放在类实例的开头,这个无效的强制转换现在最终不是指向 ,而是指向虚拟表指针。随之而来的是希拉里。sockaddr_storage
评论
sockaddr_in6* sa_in6 = (sockaddr_in6*)sas;
&
正如 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";
其他答案(由 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 建议的
评论
sockaddr_in6
Ssockaddr_storage2
sockaddr_in6*
ss
ss
Ssockaddr_storage1() : ::sockaddr_storage() {}
sockaddr_in6
::sockaddr_storage()