C++程序员大挑战之二----令人疑惑的NULL指针!
请看下面的程序
class foo
{
public:
foo() { m_nVar1 = 100;};
~foo(){};
void Display()
{
printf( " m_nVar2 is %d ", m_nVar2 );
}
int m_nVar1;
static int m_nVar2;
};
int foo::m_nVar2 = 500;
int main(int argc, char* argv[])
{
foo *p = NULL;
p-> Display();
_getch();
return 0;
}
请问哪种说法是正确的:
A. 程序编译错误。
B. 程序编译正确,但是运行时发生异常
C. 程序编译正确,运行时有时正常,有时发生异常。
D. 程序编译正确,运行时正常
你的选择是哪一个?
=========================================================
[解决办法]
D. 程序编译正确,运行时正常
[解决办法]
D
[解决办法]
A, 没有必要的头文件
[解决办法]
D
[解决办法]
呵呵,楼上的真幽默。
其实关于这个问题我以前也说过不少。
p-> Display();
使得Display()方法中的this参量指向p,这里由于p指向空,所以,this也指空。
但是display()中并没有访问对象p的成员属性,只是访问了类属性——static int m_nVar2
它不在*p所处的存储空间中,而是在全局静态区域。所以这里没有访问空地址的内容,故程序运行时正常。
而如果在Display()方法中访问了m_nVar1,编译连接全通过,发生运行时异常(存储空间受保护情况下)。
[解决办法]
不得不说,楼上的分析得完全正确。
不过我还是实际验证了一下。
如果仅仅使用搂主的代码,在VC6中的确编译不过。
Deleting intermediate files and output files for project 'TC2 - Win32 Debug '.
--------------------Configuration: TC2 - Win32 Debug--------------------
Compiling...
StdAfx.cpp
Compiling...
TC2.cpp
tc2.cpp(13) : error C2065: 'printf ' : undeclared identifier
tc2.cpp(25) : error C2065: 'NULL ' : undeclared identifier
tc2.cpp(25) : error C2440: 'initializing ' : cannot convert from 'int ' to 'class foo * '
Conversion from integral type to pointer type requires reinterpret_cast, C-style cast or function-style cast
执行 cl.exe 时出错.
TC2.exe - 1 error(s), 0 warning(s)
加上#include <stdio.h> 就可以编译通过了。
当然,可能这个不是楼主的本意。
[解决办法]
俺选D
[解决办法]
1. 我测试了一下. 在 xp + vc-7_1 的环境下. 加上 #include <iostream> 后, 正常编译连接, 正常运行.
2. 但是我还是选 B. 我的理由是, Display 不是一个静态函数. 应该需要一个对象的实例的呀.
3. 我能想象的是, Display 方法被隐式的当作了 implicitly static member. 这点让我很迷惑. 我的记忆中, 只有 4 个方法可以作为隐式的静态成员方法. 分别是
T::operator new
T::operator delete
T::operator new[]
T::operator delete[]
4. 另外, NULL 不是 C++ 标准的东西, 它是编译器厂商的实现方案. 目前的标准采用的是 0. 如下:
foo *p = 0; // 这样比较标准
C++0x 似乎有意要将引入 nullptr 作为关键字, 专门针对指针. 不过那是后话了.
5. mark一下. lz 的题目出的挺有意思. 坚持下去哦. 希望能把 C++ 的边边角角都扫扫干净.
[解决办法]
zenny_chen(ACE Intercessor) 说得很不错
偶补充一个 :)
void Display()
{
if (this)
printf( " m_nVar1 is %d ", m_nVar1 );
}
这样,你或许可以看出是什么回事情了:)
这个函数访问了 m_nVar1 ,即使是 NULL
对象也不会出错
------解决方案--------------------
to mymtom() :
1. 抱歉, 可能我没有说清楚, 我可不想误导你的概念, 不然可就罪莫大焉了.
2. 你说的是对的.静态成员方法和静态数据成员属于类的. 非静态的属于实例的.
3. 我是这样想的.
foo *p = 0;
p-> Display();
给人一种错觉, 似乎没有实例化对象, 就可以使用实例的非静态成员函数. 这是违背直觉的.因为多数的C++大师都在一再强调使用对象前要先初始化. 然而这里显然不是如此, 确有了个 "良好 "的运行 :( 如此, 让我有点难以接受. 假设是静态成员方法, 我稍微好受点, 呵呵.
4. 静态成员方法的使用大致有 3 种写法, 如下, 其中第一种是被推荐的, 清晰, 直接是良好的编程风格:
class test {
static void foo() { }
};
test::foo(); // #1
test t;
t.foo(); // #2
test * p = &t;
p-> foo(); // #3
那么, 因为静态成员函数不需要一个实例, 从而我也就能接受 foo *p = 0; p-> Display(); 即便是没有实例化, 虽然是风格不良.
5. 贴完前面的帖子, 我又仔细想了一下. 觉得可能是这样的, foo 仍然是一个非静态成员方法.编译器在处理这个方法的时, 大致可以理解为合成了如下的实体:
void __foo_Display(foo * __this);
而在 __foo_Display 方法的实现中又确实的没有使用 __this 所指向的对象. 因此, 不会发觉 __this 是 0, 是个悬空指针. 所以, 运行也就正常.
[解决办法]
答复yutaooo(旦见散分需快进, 莫等分尽空遗恨! → 今天,你散了没有?!) :
在Java中,楼主的程序在运行时肯定会出现异常。
因为Java中的非静态方法全都作为虚方法。或者可以这么说:每个类都有一个虚方法列表。
如果在楼主的foo类中的Display()方法前加个virtual,那么运行时必定报错(除非在嵌入式平台上运行时,将0x0地址内容先赋值为虚函数表的地址,即手工初始化虚函数表指针)。
其实不管指针指向哪里,它都指向了一个地址,尽管NULL在概念上表示空指针,但是如果这个NULL为0,而且系统允许你访问0x0的话,那么就算改成m_nVar1也不会有问题。
[解决办法]
因此,楼主的程序对于访问成员方法还是访问类方法(静态方法),没什么区别。
只是,成员方法还有this,而类方法没有。由于访问成员属性必须通过this,因此所有类方法中只能访问类属性(静态属性变量);而成员方法中类属性与成员属性均可访问。
在C++中,对对象进行实例化,当该对象含有虚函数表指针时才有意义。
[解决办法]
我觉得从C++实现原理考虑,就好理解了。
C++编译器是先进行重命名,转换为C的机制,然后再进行编译的,也就是说,类的函数其实最后实现还是C函数,与实例无关,静态变量也类似,其实是全局变量(从编译角度,而非作用域);而类则可以认为是C中的数据结构,类内的成员变量就是数据结构的成员了。从此原理出发,不难发现,此程序结果是编译,运行正确。
这个题目的目的是考验大家对类的成员变量和成员函数,以及静态变量的理解,至于stdio.h,我觉得是小节,大家不必细究了。
请指正
[解决办法]
在大多数编译器上,应该都是没问题的。
原因在于Display虽然不是静态函数,但幸运的是,其函数体中也没有任何访问非静态成员的语句,也就是说,对于这个Display来说,它根本没有用到自己的那个默认参数“this”,因此,虽然this当前为NULL,但所幸没有大碍。
还有一点需要注意的是,这个Display幸好也不是virtual的,否则运行时可能需要顺着this去找vtable,也可能发生问题(编译器若优化成静态绑定就另当别论了)。
从上面的分析也可看出,虽然这个代码似乎没什么问题,但它毕竟比较脆弱,所以还是不要这样为好,写出这样的代码主要有两点危险:
(1)在实际的项目开发中,代码总是被不断维护着的,而并非一次写好后就绝不再变。因此很可能以后有一个维护程序的人(可能是你自己,也可能不是)在这个Display中加入了别人语句,导致this指针实际被用到。
(2)实际上,C++语言标准中并没有规定必需采用什么机制来实现对象模型,也就是说,即使现在你试过的所有编译器都没问题,也不代码在将来一种新的编译器上同样没问题。比如说,一种编译器,完全可以在其DEBUG版中加入一些增强型的调试功能,要求任何一个非静态函数在被调用之前,必段满足“this指针不空”这个前条件——我不觉得这有什么不合理。
[解决办法]
我再跟一个类似的
#define LIST_ENTRY(ptr, type, member) \
((type *) ((char *) (ptr) - (unsigned long) (&((type *) 0)-> member)))
这是Linux中链表管理用到的一个Macro,通过它可以实现一个类的成员的 指针(ptr)得到类对象指针。(可以在网上查到对这的诠释)
[解决办法]
在编译器的内部:类函数的存储空间分配与类无关 :
f;;Display()被转化成了Display(foo* this) 的形式:
foo *p = NULL;
p-> Display();
相当于:
foo *p=NULL;
Display(p);
p赋NULL 对访问静态成员不影响
[解决办法]
今天参加一个大公司的实习面试。基本都是类似的问题。大公司在新手的时候还是考虑基础的。我是菜鸟,不过我觉得理解这些问题不是浪费时间,个人观点。
[解决办法]
这个题应该是选D的
p-> Display()可以通过 this指针寻到Display函数的,该函数所访问的静态成员本身具有内存地址,所以并不会引发错误.