• 欢迎访问少将全栈,学会感恩,乐于付出,珍惜缘份,成就彼此、推荐使用最新版火狐浏览器和Chrome浏览器访问本网站。
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏少将全栈吧
  • 欢迎加博主微信:jiang_shaobo

从一道面试题来阐释一个普遍的认知误区

点滴 admin 6年前 (2014-03-14) 289次浏览 已收录 扫描二维码

上午一个师弟在QQ上问我一道笔试题,是他前两天去KONAMI面试时做的,这道题大致是这样的:
解释以下语句的含义:
1、new A;
2、new A();
也许很多人包括我自己,都可以马上给出第一种情况的答案:在堆上为A类分配内存,然后调用A的构造函数。这种说法被大家所熟知,因为包括《STL源码剖析》等大作在内也都是这么写的(但是你认为这种说法完全正确吗?其实不尽然,答案后面揭晓)
第二种情况,对象构造的时候初始化列表为空会和第一种有什么不同呢?对于这种在实际工程中很少使用的情况,我一时还真给不出确切的答案。
网上搜了一下,看到CSDN里面还有专门针对这个问题的一个帖子(原帖链接http://bbs.csdn.net/topics/320161716)。
好像最终也没有可以信服的答案,认同度比较高的是这样的说法:“加括号调用没有参数的构造函数,不加括号调用默认构造函数或唯一的构造函数,看需求” (peakflys注:这种说法是错误的,答案后面揭晓)
既然没有特别靠谱的答案,不如自己动手找出答案。
构造以下示例:

/**
*rief example1 differencebetweennewandnew()
*authorpeakflys
*data12:10:24Monday,April08,2013
*/

classA
{
public:
inta;
};

intmain()
{
A*pa=newA;
A*paa=newA();
return0;
}

查看main函数的汇编代码(编译器:gcc (GCC) 4.4.6 20120305 (Red Hat 4.4.6-4))

intmain()
{
4005c4:55push%rbp
4005c5:4889e5mov%rsp,%rbp
4005c8:4883ec10sub$0x10,%rsp
A*pa=newA;
4005cc:bf04000000mov$0x4,%edi
4005d1:e8f2feffffcallq4004c8_Znwm@plt //调用new
4005d6:488945f0mov%rax,-0x10(%rbp) //rax寄存器内容赋给指针pa(rax寄存器里是new调用产生的A对象堆内存地址)
A*paa=newA();
4005da:bf04000000mov$0x4,%edi
4005df:e8e4feffffcallq4004c8_Znwm@plt //调用new
4005e4:4889c2mov%rax,%rdx //rax的内容放入rdx,执行之后,rdx里存放的即是通过new A()产生的内存地址
4005e7:c70200000000movl$0x0,(%rdx) //把rdx内存指向的内容赋为0值,即把A::a赋值为0
4005ed:488945f8mov%rax,-0x8(%rbp) //rax寄存器内容赋给指针paa(rax寄存器里是new()调用产生的A对象堆内存地址)
return0;
4005f1:b800000000mov$0x0,%eax
}
4005f6:c9leaveq
4005f7:c3retq

通过上面产生的汇编代码(对ATT汇编不熟悉的可以看注释)可以很容易看出,new A()的执行,在调用完operator new分配内存后,马上对新分配内存中的对象使用0值初始化,而new A 仅仅是调用了operator new分配内存!
是不是这样就可以下结论 new A()比new A多了一步,即初始化对象的步骤呢?
我们再看看下面这种情况:

/**
*rief example2 differencebetweennewandnew()
*authorpeakflys
*data12:23:20Monday,April08,2013
*/

classA
{
public:
A(){a=10;}
inta;
};

intmain()
{
A*pa=newA;
A*paa=newA();
return0;
}

这种情况是类显示提供含默认值的构造函数。
查看汇编实现如下:

intmain()
{
4005c4:55push%rbp
4005c5:4889e5mov%rsp,%rbp
4005c8:53push%rbx
4005c9:4883ec18sub$0x18,%rsp
A*pa=newA;
4005cd:bf04000000mov$0x4,%edi
4005d2:e8f1feffffcallq4004c8_Znwm@plt
4005d7:4889c3mov%rax,%rbx
4005da:4889d8mov%rbx,%rax
4005dd:4889c7mov%rax,%rdi
4005e0:e82d000000callq400612_ZN1AC1Ev
4005e5:48895de0mov%rbx,-0x20(%rbp)
A*paa=newA();
4005e9:bf04000000mov$0x4,%edi
4005ee:e8d5feffffcallq4004c8_Znwm@plt
4005f3:4889c3mov%rax,%rbx
4005f6:4889d8mov%rbx,%rax
4005f9:4889c7mov%rax,%rdi
4005fc:e811000000callq400612_ZN1AC1Ev
400601:48895de8mov%rbx,-0x18(%rbp)
return0;
400605:b800000000mov$0x0,%eax
}
40060a:4883c418add$0x18,%rsp
40060e:5bpop%rbx
40060f:c9leaveq
400610:c3retq

上面的汇编代码就不在添加注释了,因为两种操作产生的汇编代码是一样的,都是先调用operator new分配内存,然后调用构造函数。
上面的情况在VS2010下验证是一样的情况,有兴趣的朋友可以自己去看,这里就不再贴出VS2010下的汇编代码了。
通过上面的分析,对于new A和 new A() 的区别,我们可以得出下面的结论:
1、类体含有显示适合地默认构造函数时,new A和new A()的作用一致,都是首先调用operator new分配内存,然后调用默认构造函数初始化对象。
2、类体无显示构造函数时,new A()首先调用operator new来为对象分配内存,然后使用空值初始化对象成员变量,而new A仅仅是调用operator new分配内存,对象的成员变量是无意义的随机值! (peakflys注:对于基本数据类型,如int等 适用此条)
注意到,现在很多书籍对new操作符的说明都存在纰漏,例如《STL源码剖析》中2.2.2节中有以下的描述:

事实证明,new Foo的操作是否有构造函数的调用是不确定的,具体要看Foo类体里是否有显示构造函数的出现。

by peakflys13:40:00Monday, April 08, 2013

/*****************************************华丽分割线**************************************
补充:刚才发现,在C++Primer第四版5.11节中,已经有了对于new A()的说明:

We indicate that we want to value-initialize the newly allocated object by following the type nameby a pair of empty parentheses. The empty parentheses signal that we want initialization but arenot supplying a specific initial value. In the case of class types (such as string) that define their own constructors, requesting value-initialization is of no consequence: The object is initialized by running the default constructor whether we leave it apparently uninitialized orask for value-initialization. In the case of built-in types or types that do not define any constructors, the difference is significant:

int *pi = new int; // pi points to an uninitialized int
int *pi = new int(); // pi points to an int value-initialized to 0

In the first case, the int is uninitialized; in the second case, the int is initialized to zero.
这里给出的解释和上面自己分析的new A()的行为是一致的。
/***************************************再次华丽分割线************************************
鉴于上面的结论是通过GCC和VS2010得出的,而且有朋友也提出同样的质疑,为了确定这种结果是否是编译器相关的,刚才特意查看了一下C++的标准化文档。
摘自:ISO/IEC 14882:2003(E) 5.3.4 – 15
? If the new-initializer is omitted:
? If T is a (possibly cv-qualified) non-POD class type (or array thereof), the object is default-initialized(8.5). If T is a const-qualified type, the underlying class type shall have a user-declared default constructor.
? Otherwise, the object created has indeterminate value. If T is a const-qualified type, or a (possibly cv-qualified) POD class type (or array thereof) containing (directly or indirectly) a member of const-qualified type, the program is ill-formed;
? If the new-initializer is of the form (), the item is value-initialized (8.5);
所以可以确定,这种情况完全是编译器无关的(当然那些不完全按照标准实现的编译器除外)。
但是通过上面标准化文档的描述,我们可以看出文中对new A在无显示构造函数时的总结并不是特别全面,鉴于很多公司都有这道面试题(撇去这些题目的实际考察意义不说),我们有必要再补充一下: 对于new A: 这样的语句,再调用完operator new分配内存之后,如果A类体内含有POD类型,则POD类型的成员变量处于未定义状态,如果含有非POD类型则调用该类型的默认构造函数。而 new A()在这些情况下都会初始化。
PS:估计很多公司的“正确答案“ 也不一定正确吧。

喜欢 (0)
[🍬谢谢你请我吃糖果🍬🍬~]
分享 (0)
关于作者:
少将,关注Web全栈开发、项目管理,持续不断的学习、努力成为一个更棒的开发,做最好的自己,让世界因你不同。