通过NULL类指针调用类方法
我有以下代码片段:
class ABC{ public: int a; void print(){cout<<"hello"<<endl;} }; int main(){ ABC *ptr = NULL: ptr->print(); return 0; }
它运行成功。 有人可以解释吗?
(我不记得我有什么知识,所以我可能是完全错误的)
在这个引擎之下,大多数编译器会把你的类转换成这样的东西:
struct _ABC_data{ int a ; }; // table of member functions void _abc_print( _ABC_data* this );
其中_ABC_data是一个C风格的结构
和你的调用ptr->print();
将转变为:
_abc_print( NULL)
因为你不使用this
参数,所以在执行的时候是可以的。
更新:(感谢Windows程序员的正确评论 )
这样的代码只适用于执行它的CPU。
绝对没有理由利用这个实现功能。 这是为什么:
- 因为标准状态它产生未定义的行为(任何人都可以提供一个链接或至少参考(第N章,标准M …)?)
- 如果你真的需要能力调用成员函数没有实例,使用static关键字给你的所有可移植性和编译时检查
使用不指向有效对象的指针调用成员函数会导致未定义的行为 。 什么事情都可能发生。 它可以运行; 它可能会崩溃。
在这种情况下,它似乎工作,因为this
指针,不指向一个有效的对象,不print
。
大多数答案都表示,未定义的行为可能包括“出现”工作,他们是正确的。
亚历山大·马拉霍夫的回答给出了实施细节,这是常见的,并解释了为什么你的情况似乎有效,但他做了一个轻微的错误陈述。 他写道:“执行以来,没有使用这个参数,这是可以的”,但是意思是“因为不使用这个参数,所以在执行的时候似乎没有问题”。
但是要注意,你的代码仍然是未定义的行为。 它打印出你想要的东西,并将你银行账户的余额转移给我。 我谢谢你。
(SO风格说这应该是一个评论,但它太长了,我做了CW)。
它导致未定义的行为。 我花了一些工作来解释为什么。 :)但这是一个更技术性的答案。
基本上,未定义的行为意味着你不再保证程序的执行。 C ++根本无话可说。 它可以按照你想要的方式工作,也可以很悲惨地崩溃,或者它可以随机地进行。
所以看起来工作是未定义的行为,这是你所看到的一个非常好的结果。 实际的原因是,在你的实现(在老实说,每个实现), this
指针(被调用的实例的地址)在你的函数中根本没有被使用。 也就是说,如果你试图使用this
指针(例如通过访问一个成员变量),你可能会崩溃。
请记住,上面的段落是特定于您的实现的东西,它是当前的行为。 这只是一个猜测,你不能依赖。
表达式ptr->print();
将被隐式转换为(*ptr).print();
根据C ++标准(5.2.5 / 3)。 而解除引用空指针会导致未定义的行为。 在你的情况下,有问题的代码没有错误是偶然的。 你不应该依赖它。
5.2.5 / 3:
如果E1的类型为“指向类X的指针”,则表达式E1-> E2被转换为等价形式(*(E1))。 5.2.5的其余部分将只涉及第一个选项(点)59)。 缩写对象表达式。 id-expression作为E1.E2,则该表达式的类型和左值属性确定如下。 在5.2.5的剩余部分中,cq表示const或const的缺失; vq代表挥发性或无挥发性。 cv表示3.9.3中定义的任意一组cv-限定符。
虽然我不确定这是否是确切的答案,但这是我的理解。 (另外,我的CPP术语是不好的,如果可能的话,
对于C ++,当任何类被声明时(即没有即时创建),函数被放置在被创建的二进制文件的.text部分。 当创建一个瞬间,函数或方法不重复。 也就是说,当编译器解析CPP文件时,它将用在.text部分中定义的相应地址替换对ptr->print()
的函数调用。
因此,所有的编译器都会根据ptr
的类型替换适当的地址来进行函数print
。 (这也意味着一些检查相关的公共/私人/继承等)
我为您的代码(名为test12.cpp
)做了以下操作:
编辑:添加一些意见,以下面的ASM(我真的是不擅长ASM,我几乎不能读取它 – 只是了解一些基本的东西) – 最好是阅读这个Wikibook链接 ,我也做了: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. ...
v ZN3ABC5printEv
表示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)->print();
完美地工作。
可能它运行,因为你的类指针没有在打印函数中使用任何成员变量…如果在打印功能,你尝试访问它将不会运行…因为未初始化的类指针不能有初始化的成员变量…
正如其他人所说,这是不明确的行为。 关于它似乎工作的原因是你不想访问print()
的成员变量a
。 所有的类的实例共享print()
的代码相同的内存,因此this
指针不需要访问该方法。 但是,如果尝试访问方法内部,则很可能会出现访问冲突异常。
这对我曾经尝试过的每个编译器都有效(而且我已经尝试了很多)。 是的,它是“未定义的”,但是当你调用一个非虚拟成员时,你并不是取消引用指针。 你甚至可以使用这个“功能”来编写代码,尽管纯粹主义者会骂你,并称你为恶名。
编辑:这里似乎有一些关于调用成员函数的困惑。 当您调用非虚拟成员时,您不会取消引用“this”指针。 你只是使用花哨的语法作为参数传递它。 这是所有我见过的实现,但不能保证。 如果没有以这种方式实现,你的代码将会运行得更慢。 成员函数只是一个带有额外的半隐藏参数的函数。 而已! 故事结局。 也就是说,可能有一些由Cletus的松弛下巴软件公司编写的编译器,这有一个问题,但我还没有遇到它。
这个比你用纯文字更能解释你。 尝试编译你想要的任何编译器:)但请注意,这是根据标准的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和上面的所有评论。
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!