通过NULL类指针调用类方法[重复]

Posted

技术标签:

【中文标题】通过NULL类指针调用类方法[重复]【英文标题】:Calling class method through NULL class pointer [duplicate] 【发布时间】:2010-03-24 04:24:40 【问题描述】:

我有以下代码 sn-p:

class ABC
public:
        int a;
        void print()cout<<"hello"<<endl;
;

int main()
        ABC *ptr = NULL:
        ptr->print();
        return 0;

运行成功。谁能解释一下?

【问题讨论】:

谁能提供有关此代码运行时的更多详细信息?它运行是因为类指针在打印函数中没有使用任何成员变量,但这意味着在运行时它能够执行成员函数(通过执行代码段?)。对不起,我不是一个有经验的 C++ 程序员 那个问题是关于取消引用一个不确定的指针;因此,尽管它似乎起作用的原因是相同的;不同的 C++ 规则涵盖了代码的(缺乏)有效性。 注意print是虚拟的,它会崩溃(分段错误)!!阅读vtables 【参考方案1】:

使用不指向有效对象的指针调用成员函数会导致未定义的行为。什么事情都可能发生。它可以运行;它可能会崩溃。

在这种情况下,它似乎可以工作,因为print 中没有使用未指向有效对象的this 指针。

【讨论】:

它的“未定义行为”但它是如何工作的?我的意思是说,当它运行时,运行时发生的事情使它工作。 @Adil - 该函数不使用对象中的任何数据,也不调用任何虚拟成员函数,因此编译器没有生成对错误指针的任何引用。你很幸运,另一个编译器(或你当前编译器的下一个版本)可能会在相同的代码上崩溃。 @MarkRansom - 这永远不会发生:D 不过,最好使用静态,其中 _cdecl 是正确的,而不是 _thiscall @ПетърПетров 您会惊讶于最近的优化进展在未定义行为方面造成的各种麻烦。最好完全避免它,即使你可以合理地解释它为什么会起作用。 我一直很惊讶,但是不太可能有人很快会在非虚拟 this 指针上更改 __thiscall。但是我只在调试代码中使用这个!=nullptr 检查【参考方案2】:

在底层,大多数编译器会将你的类转换成这样的:

struct _ABC_data  
    int a ;  
;  
// table of member functions 
void _ABC_print( _ABC_data* this );  

其中_ABC_data 是C-style struct 您的电话ptr-&gt;print(); 将转换为:

_ABC_print(nullptr)

执行时没问题,因为您不使用this arg。


更新:(感谢 Windows 程序员right comment) 此类代码仅适用于执行它的 CPU。 绝对没有合理的理由来利用这个实现特性。因为:

    标准规定它会产生未定义的行为 如果您确实需要在没有实例的情况下调用成员函数,使用 static 关键字可以为您提供所有可移植性和编译时检查

【讨论】:

依赖这种行为的问题在于它是特定于编译器的。不同的编译器可能会为每个实例存储一个 vtable(尽管这会执行得非常糟糕)或一个指向公共 vtable 的指针(稍微好一点),并通过实际取消引用指针来调用所有这样的函数。 AFAIK 这将是一个合法的编译器实现,导致此代码崩溃。 @wds: 为什么要通过 vtable 调用非虚函数? “因为标准规定它会产生未定义的行为(任何人都可以提供链接或至少参考(第 N 章,par M ...)吗?)”查看其他答案,这清楚地表明它是UB,IMO 是所有需要 - 并且应该 - 回答这些问题的人。 ***.com/a/2505559/2757035 根据实例化只使用this的模板类方法呢?有些可能是根据静态变量定义的,有些调用可能会以不同的方式分派/解析,等等。您可以知道一个方法将被(好像)静态定义,而不是如何定义,并且您仍然需要该值。这与您在decltype 下使用std::declval 的原因相同——您可以保证任何实例都会产生相同的结果,但不一定能做出这样的结果。哦,好吧,也许有一天会有办法。【参考方案3】:

大多数答案都说未定义的行为可以包括“看起来”在工作,他们是对的。

Alexander Malakhov 的回答给出了一些常见的实施细节,并解释了为什么你的情况似乎有效,但他做了一个轻微的错误陈述。他写道“执行时没问题,因为你不使用这个参数”,但意思是“执行时似乎没问题,因为你不使用这个参数”。

但请注意,您的代码仍然是未定义的行为。它打印了你想要的东西,并将你银行账户的余额转移到我的账户上。谢谢你。

(SO风格说这应该是一个评论,但它太长了。我把它写成CW了。)

【讨论】:

为什么要这样评论?我认为这是一个很好的答案。 因为这主要是个玩笑。笑话属于 cmets 而不是答案。如果其他回答者还没有给出真正的答案,那么我会解释真正的答案,然后是一个笑话。【参考方案4】:

它会导致未定义的行为。我用bit of work 解释原因。 :) 但这是一个更具技术性的答案。

基本上,未定义的行为意味着您不再有任何关于程序执行的保证; C++ 简直无话可说。它可以完全按照您的意愿工作,也可以惨遭崩溃,或者两者都随机发生。

所以似乎工作是未定义行为的完美结果,这就是您所看到的。实际原因是,在您的实现中(老实说,每个实现),this 指针(被调用的实例的地址)根本没有在您的函数中使用。也就是说,如果您尝试使用 this 指针(例如通过访问成员变量),您可能会崩溃。

请记住,上述段落是特定于您的实现的内容,并且是当前行为。这只是一个猜测,你不能依赖。

【讨论】:

它的“未定义行为”但它是如何工作的?我的意思是说,当它运行时,运行时发生的事情使它工作。 @Adil:未定义。 Alexander 解释得很好,但这保证有效。这只是你不能指望的东西。【参考方案5】:

表达式 ptr-&gt;print(); 将根据 C++ 标准 (5.2.5/3) 隐式转换为 (*ptr).print();。并且取消引用空指针会导致未定义的行为。幸运的是,在您的情况下,有问题的代码可以正常工作。你不应该依赖它。

5.2.5/3:

如果 E1 的类型为“指向类的指针” X,” 那么表达式 E1->E2 是 转换为等价形式 (*(E1)).E2; 5.2.5 的剩余部分 将仅解决第一个选项 (点)59)。缩写 对象表达式。 id 表达式为 E1.E2,然后是类型和左值 这个表达式的属性是 确定如下。在里面 5.2.5的余数,cq代表 const 或没有 const; vq 代表 volatile 或 没有挥发物。 cv 代表一个 任意一组 cv 限定符,如 在 3.9.3 中定义。

【讨论】:

隐式转换没有问题。此外,如果程序员编写了 (*ptr).print() 那么仍然不会有句法问题。问题是 ptr 没有指向一个实际的对象,并且 ptr 正在被取消引用。 ...这是基里尔所说的。【参考方案6】:

虽然我不确定这是否是确切的答案,但这是我的理解。 (另外,我的 CPP 术语不好 - 如果可能,请忽略它)

对于 C++,当声明任何类时(即尚未创建即时),函数将放置在正在创建的二进制文件的 .text 部分中。创建瞬间时,函数或方法重复。也就是说,当编译器解析 CPP 文件时,它会将 ptr-&gt;print() 的函数调用替换为 .text 部分中定义的适当地址。

因此,编译器所要做的就是根据函数printptr类型 替换适当的地址。 (这也意味着一些检查相关的公共/私有/继承等)

我为您的代码(名为test12.cpp)做了以下操作:

编辑:在下面添加一些 cmets 到 ASM(我真的_不_擅长 ASM,我几乎看不懂它 - 足以理解一些基本的东西) - 最好是阅读 this Wikibook link,我也是已经完成了:D 如果有人在 ASW 中发现错误,请发表评论 - 我很乐意修复它们并了解更多信息。

$ g++ test.cpp -S
$ cat test.s
...
         // Following snippet is part of main function call
         movl    $0, -8(%ebp)   //this is for creating the NULL pointer ABC* ptr=NULL
                                //It sets first 8 bytes on stack to '0'
         movl    -8(%ebp), %eax //Load the ptr pointer into eax register
         movl    %eax, (%esp)   //Push the ptr on stack for using in function being called below
                                //This is being done assuming that these elements would be used
                                //in the print() function being called
         call    _ZN3ABC5printE //Call to print function after pushing arguments (which are none) and 
                                //accesss pointer (ptr) on stack.
...

vWhereZN3ABC5printEv代表class ABC中定义的函数的全局定义:

  ...
  .LC0:                    //This declares a label named .LC0
          .string "hello"  // String "hello" which was passed in print()
          .section        .text._ZN3ABC5printEv,"axG",@progbits,_ZN3ABC5printEv,comdat
          .align 2
          .weak   _ZN3ABC5printEv             //Not sure, but something to do with name mangling
          .type   _ZN3ABC5printEv, @function
  _ZN3ABC5printEv:                   //Label for function print() with mangled name
  //following is the function definition for print() function
  .LFB1401:                          //One more lavbel
          pushl   %ebp               //Save the 'last' known working frame pointer
  .LCFI9:
          movl    %esp, %ebp         //Set frame (base pointer ebp) to current stack top (esp)
  .LCFI10:
          subl    $8, %esp           //Allocating 8 bytes space on stack
  .LCFI11:
          movl    $.LC0, 4(%esp)     //Pushing the string represented by label .LC0 in 
                                     //in first 4 bytes of stack
          movl    $_ZSt4cout, (%esp) //Something to do with "cout<<" statement
          call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
          movl    $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, 4(%esp)
          movl    %eax, (%esp)
          call    _ZNSolsEPFRSoS_E   //Probably call to some run time library for 'cout'
          leave                      //end of print() function
          ret                        //returning control back to main() 
  ...

因此,即使((ABC *)0)-&gt;print(); 也能正常工作。

【讨论】:

对于像我这样的人来说,很难一眼看出这段代码的作用。能不能说得通俗一点。比如说,添加像 .LCFI11 这样的 cmets: // 将 operator 的 args 移动到注册 ESP, movl $.LC0, 4(%esp) // put ESP* 上的“你好”。并将生成的名称替换为更易读的名称,例如 *$_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ ==> $_endl。这些都是纯粹的技术细节,真的无助于理解发生了什么 @Alexander,感谢您的建议,您是对的 - 抱歉有点无知。我已经添加了关于我所知道的任何内容的 cmets,这当然不多。【参考方案7】:

可能它运行是因为您的类指针没有使用打印函数中的任何成员变量...如果在打印函数中您尝试访问它不会运行...因为未初始化的类指针不能具有初始化的成员变量。 ..

【讨论】:

【参考方案8】:

正如其他人所说,这是未定义的行为。关于它似乎起作用的原因是您没有尝试访问print() 中的成员变量a。对于print() 的代码,该类的所有实例共享相同的内存,因此访问该方法不需要this 指针。但是,如果您尝试在方法内访问 a,您很可能会遇到访问冲突异常。

【讨论】:

您能否提供一个链接或草稿,其中提到它是未定义的行为。【参考方案9】:

这适用于我曾经尝试过的每个编译器(而且我已经尝试过很多)。是的,它是“未定义的”,但是当您调用非虚拟成员时,您并没有取消引用指针。您甚至可以使用此“功能”编写代码,尽管纯粹主义者会冲您大喊大叫并称您为讨厌的名字等。

编辑:这里关于调用成员函数似乎有些混乱。当您调用非虚拟成员时,您不会取消引用“this”指针。您只是使用花哨的语法将其作为参数传递。这是我见过的所有实现,但不能保证。如果没有以这种方式实现,您的代码会运行得更慢。成员函数只是一个带有额外半隐藏参数的函数。就是这样!故事结局。话虽这么说,Cletus 的 slack jam software Co. 编写的一些编译器可能对此有问题,但我还没有遇到过。

【讨论】:

***.com/questions/36893251/…【参考方案10】:

这个比我用简单的词更能解释你。尝试使用任何你想要的编译器来编译它:) 但请注意,根据标准,它是 UB!

#include <iostream>
using namespace std;

class Armor

public:
    void set(int data)
    
        cout << "set("<<data<<")\n";
        if(!this)
        
            cout << "I am called on NULL object! I prefer to not crash!\n";
            return;
        
        this->data = data;  //dereference it here
    
    void get()
    
        if(this) cout << "data = " << data << "\n";
        else cout << "Trying to dereference null pointer detected!\n";
    
    int data;
;

int main()

   cout << "Hello World" << endl; 
   Armor a;
   a.set(100);
   a.get();


   Armor* ptr1 = &a;
   Armor* ptr2 = 0;

   ptr1->set(111);
   ptr2->set(222);

   ptr1->get();
   ptr2->get();

   return 0;

然后阅读 __thiscall - 以及上面的所有 cmets。

Hello World
set(100)
data = 100
set(111)
set(222)
I am called on NULL object! I prefer to not crash!
data = 111
Trying to dereference null pointer detected!

【讨论】:

如果函数中不包含任何类变量,编译器是否将每个函数视为静态函数?如果是这样,这可能是调用 get 函数的原因? 每个。每个操作系统级别的函数都驻留在您的 RAM 中的某个位置并有它的地址。甚至是虚函数。在 C++ 中,在大多数编译器上,非虚拟类成员使用所谓的 thiscall 调用约定作为普通函数实现。阅读有关此内容以及堆栈实际上是什么的信息。以及编译器是如何工作的。然后你就可以随意滥用未定义的行为了:) 访问类成员是通过简单的指针添加实现的。如果您对数据成员 int32_t a、b 有严格的要求,那么“this”指针将准确指向 a,而 this+4 指向 b。如果你调用一个非虚函数来取消引用 this.a 并且 this 是一个空指针,this.b 的绝对地址将为 4,而 this.a 将为 0。只要代码尝试从这些无效地址读取或写入,操作系统本身会抛出访问冲突异常。您当然可以处理并继续执行。 还要注意,在指向类的空指针上调用虚函数将尝试取消引用对象的所谓 VTABLE,这将立即引发访问冲突异常并且不会跳转到代码中。但是,您可以通过某种方式获取指向虚函数的指针并尝试通过滥用函数指针和 void* 直接调用它 还有关于调用:链接器和共享库加载器负责加载和内存映射程序及其库中的所有函数。 C++ 类被奉承到任意函数。例如 Armor::set(int data) 可能会被链接器表示为 armour_set(intptr this, int data) ,因为它是类成员,所以第一个参数是“this”指针。编译器本身关心指针,当您键入 ptr->set(10) 时,它会硬编码为机器代码 armour_set(ptr, 10)。所以它的行为就像静摩擦。

以上是关于通过NULL类指针调用类方法[重复]的主要内容,如果未能解决你的问题,请参考以下文章

为什么通过空指针(NULL)能够正确调用类的部分成员函数

空指针异常与Optional类

在 C# 中的类构造函数中调用异步方法 [重复]

c++线程函数中如何调用该方法下的外部函数?

在ES6 JavaScript中调用类时,为什么我得到null值? [重复]

如何调用返回类方法指针的类方法?