将 NULL 指针转换为对象并调用其成员函数之一有实际好处吗?

Posted

技术标签:

【中文标题】将 NULL 指针转换为对象并调用其成员函数之一有实际好处吗?【英文标题】:Is there a practical benefit to casting a NULL pointer to an object and calling one of its member functions? 【发布时间】:2011-02-03 21:42:58 【问题描述】:

好的,所以我知道从技术上讲这是未定义的行为,但尽管如此,我在生产代码中不止一次看到过这种情况。如果我错了,请纠正我,但我也听说有些人使用这个“功能”作为当前 C++ 标准缺乏方面的某种合法替代品,即无法获取地址(嗯,成员函数的偏移量。例如,这是 PCRE(Perl 兼容的正则表达式)库的流行实现:

#ifndef offsetof
#define offsetof(p_type,field) ((size_t)&(((p_type *)0)->field))
#endif

人们可以争论在这种情况下利用这种语言微妙之处是否有效,甚至是否必要,但我也看到它是这样使用的:

struct Result

   void stat()
   
      if(this)
         // do something...
      else
         // do something else...
   
;

// ...somewhere else in the code...

((Result*)0)->stat();

这很好用!它通过测试this 的存在来避免空指针取消引用,并且它不会尝试访问else 块中的类成员。只要这些防护措施到位,它就是合法的代码,对吧?所以问题仍然存在:是否有一个实际的用例,可以从使用这种结构中受益?我特别关心第二种情况,因为第一种情况更多的是一种解决语言限制的方法。还是这样?

PS。对 C 风格的演员表感到抱歉,不幸的是,如果可以的话,人们仍然喜欢少打字。

【问题讨论】:

“只要有这些保护措施,它就是合法的代码,对吧?”绝对不。正如你在第一句话中所说,结果是未定义的。 ***.com/questions/2511921/…的可能重复 【参考方案1】:

第一种情况是不调用任何东西。它正在使用地址。这是一个定义的、允许的操作。它产生从对象开始到指定字段的字节偏移量。这是一种非常非常常见的做法,因为像这样的偏移量是非常常见的。毕竟,并不是所有的对象都可以在堆栈上创建。

第二种情况相当愚蠢。明智的做法是将该方法声明为静态的。

【讨论】:

我知道这一点。但这是常见的做法吗?为什么要这样做,而不仅仅是在堆栈上创建一个对象?真的值得花哨的语法吗?或者我们是否对我缺少的东西进行了超级优化? :) 为什么人们会写出糟糕的代码?只有原作者才能解释这种滥用语言背后的动机。 得到了定义和允许的参考? (不是我怀疑你,但有一个合适的来源总是很好) E1->E2 等价于(*(E1)).E2*E1 取消引用指针 E1。取消引用空指针会导致未定义的行为。因此,我看不到 OP 中的 offsetof 宏是如何由定义的、允许的行为组成的。有什么我想念的吗? (E1E2 的示例来自 C++ 标准)。 C99 确实提到了 & 运算符,“如果操作数是一元 * 运算符的结果,则该运算符和 & 运算符都不会被评估,结果就像两者都被省略了”(§6.5.3.2/3)。 C89、C++03 和 C++0x FCD 没有任何语言可以达到这种效果。无论如何,虽然这允许&*a,其中a 不指向对象,但我认为它不允许&((*a).b)。我不知道;我同意,鉴于offsetof 的这种常见实现,我可能错了;我只是找不到文件说我错的地方。希望其他人知道得更好。 :-/【参考方案2】:

未定义的行为是未定义的行为。这个技巧对您的特定编译器“有效”吗?好吧,可能。他们会为它的下一次迭代工作吗?或另一个编译器?可能不是。你付你的钱,你做出你的选择。我只能说,在近 25 年的 C++ 编程中,我从来没有觉得有必要做这些事情。

【讨论】:

它适用于 GCC 4.3.3 和 VS 2005。我知道这是未定义的行为,但由于我已经看过几次了,我开始怀疑这是否是人们经常做的事情,并且如果是,为什么?【参考方案3】:

尽管针对特定 C++ 实现的库可能会这样做,但这并不意味着它通常是“合法的”。

这很好用!它避免了一个空 通过测试指针取消引用 这个的存在,它不尝试 访问 else 中的类成员 堵塞。只要这些警卫在 地方,它是合法的代码,对吧?

不,因为尽管它可能在某些 C++ 实现上运行良好,但它在任何符合 C++ 实现的情况下都不能正常运行。

【讨论】:

【参考方案4】:

取消引用空指针是未定义的行为,如果您这样做,任何事情都可能发生。如果您想要一个有效的程序,请不要这样做。

仅仅因为它不会在一个特定的测试用例中立即崩溃并不意味着它不会给您带来各种麻烦。

【讨论】:

【参考方案5】:

我看不到 ((Result*)0)->stat(); 的任何好处 - 这是一个丑陋的 hack,很可能迟早会坏掉。正确的 C++ 方法是使用静态方法 Result::stat()

另一方面,offsetof() 是合法的,因为 offsetof() 宏从不实际调用方法或访问成员,而只是执行地址计算。

【讨论】:

【参考方案6】:

关于声明:

它通过测试 this 的存在来避免空指针取消引用,并且它不会尝试访问 else 块中的类成员。只要这些防护措施到位,它就是合法的代码,对吧?

代码不合法​​。当指针为 NULL 时,无法保证编译器和/或运行时会实际调用该方法。检查方法没有帮助,因为您不能假设该方法实际上最终会被 NULL this 指针调用。

【讨论】:

【参考方案7】:

其他人都很好地重申了行为是未定义的。但是让我们假装它不是,即使p 不是一个有效的指针,p->member 在某些情况下也可以以一致的方式运行。

您的第二个构造仍然几乎没有任何作用。从设计的角度来看,如果单个函数可以在访问成员和不访问成员的情况下完成其工作,并且如果它可以然后将代码的静态部分拆分为单独的,那么你可能做错了,静态函数比期望你的用户创建一个空指针来操作更合理。

从安全角度来看,您只保护了一小部分可以创建无效this 指针的方法。对于初学者来说,有未初始化的指针:

Result* p;
p->stat(); //Oops, 'this' is some random value

有指针已经初始化,但仍然无效:

Result* p = new Result;
delete p;
p->stat(); //'this' points to "safe" memory, but the data doesn't belong to you

即使你总是初始化你的指针,并且绝对不会意外重用空闲的内存:

struct Struct 
    int i;
    Result r;

int main() 
    ((Struct*)0)->r.stat(); //'this' is likely sizeof(int), not 0

所以说真的,即使它不是未定义的行为,也是毫无价值的行为。

【讨论】:

以上是关于将 NULL 指针转换为对象并调用其成员函数之一有实际好处吗?的主要内容,如果未能解决你的问题,请参考以下文章

当我在 NULL 对象指针上调用成员函数时会发生啥? [复制]

当我在 NULL 对象指针上调用成员函数时会发生啥? [复制]

使用指针访问数组中的对象数据成员

向下转换指向成员函数的指针

Swig:将成员变量(指向值的指针)转换为 python 列表

对象指针