peakflys原创作品,转载请保留原作者和源链接!
上午一个师弟在QQ上问我一道笔试题,是他前两天去KONAMI面试时做的,这道题大致是这样的:
解释以下语句的含义:
1、new A;
2、new A(); 也许很多人包括我自己,都可以马上给出第一种情况的答案:在堆上为A类分配内存,然后调用A的构造函数。这种说法被大家所熟知,因为包括《STL源码剖析》等大作在内也都是这么写的(但是你认为这种说法完全正确吗?
其实不尽然,答案后面揭晓) 第二种情况,对象构造的时候初始化列表为空会和第一种有什么不同呢?对于这种在实际工程中很少使用的情况,我一时还真给不出确切的答案。
网上搜了一下,看到CSDN里面还有专门针对这个问题的一个帖子(原帖链接 http://bbs.csdn.net/topics/320161716)。
好像最终也没有可以信服的答案,认同度比较高的是这样的说法:“
加括号调用没有参数的构造函数,不加括号调用默认构造函数或唯一的构造函数,看需求” (peakflys注:这种说法是错误的,答案后面揭晓)
既然没有特别靠谱的答案,不如自己动手找出答案。
构造以下示例:
/**
*\brief example1 difference between new and new()
*\author peakflys
*\data 12:10:24 Monday, April 08, 2013
*/
class A
{
public:
int a;
};
int main()
{
A *pa = new A;
A *paa = new A();
return 0;
}
查看main函数的汇编代码(编译器:gcc (GCC) 4.4.6 20120305 (Red Hat 4.4.6-4) )
int main()
{
4005c4: 55 push %rbp
4005c5: 48 89 e5 mov %rsp,%rbp
4005c8: 48 83 ec 10 sub $0x10,%rsp
A *pa = new A;
4005cc: bf 04 00 00 00 mov $0x4,%edi
4005d1: e8 f2 fe ff ff callq 4004c8 <_Znwm@plt> //调用new
4005d6: 48 89 45 f0 mov %rax,-0x10(%rbp) //rax寄存器内容赋给指针pa(rax寄存器里是new调用产生的A对象堆内存地址)
A *paa = new A();
4005da: bf 04 00 00 00 mov $0x4,%edi
4005df: e8 e4 fe ff ff callq 4004c8 <_Znwm@plt> //调用new
4005e4: 48 89 c2 mov %rax,%rdx //rax的内容放入rdx,执行之后,rdx里存放的即是通过new A()产生的内存地址
4005e7: c7 02 00 00 00 00 movl $0x0,(%rdx) //把rdx内存指向的内容赋为0值,即把A::a赋值为0
4005ed: 48 89 45 f8 mov %rax,-0x8(%rbp) //rax寄存器内容赋给指针paa(rax寄存器里是new()调用产生的A对象堆内存地址)
return 0;
4005f1: b8 00 00 00 00 mov $0x0,%eax
}
4005f6: c9 leaveq
4005f7: c3 retq
通过上面产生的汇编代码(对AT&T汇编不熟悉的可以看注释)可以很容易看出,
new A()的执行,在调用完operator new分配内存后,马上对新分配内存中的对象使用0值初始化,而new A 仅仅是调用了operator new分配内存!
是不是这样就可以下结论 new A()比new A多了一步,即初始化对象的步骤呢?
我们再看看下面这种情况:
/**
*\brief example2 difference between new and new()
*\author peakflys
*\data 12:23:20 Monday, April 08, 2013
*/
class A
{
public:
A(){a = 10;}
int a;
};
int main()
{
A *pa = new A;
A *paa = new A();
return 0;
}
这种情况是类显示提供含默认值的构造函数。
查看汇编实现如下:
int main()
{
4005c4: 55 push %rbp
4005c5: 48 89 e5 mov %rsp,%rbp
4005c8: 53 push %rbx
4005c9: 48 83 ec 18 sub $0x18,%rsp
A *pa = new A;
4005cd: bf 04 00 00 00 mov $0x4,%edi
4005d2: e8 f1 fe ff ff callq 4004c8 <_Znwm@plt>
4005d7: 48 89 c3 mov %rax,%rbx
4005da: 48 89 d8 mov %rbx,%rax
4005dd: 48 89 c7 mov %rax,%rdi
4005e0: e8 2d 00 00 00 callq 400612 <_ZN1AC1Ev>
4005e5: 48 89 5d e0 mov %rbx,-0x20(%rbp)
A *paa = new A();
4005e9: bf 04 00 00 00 mov $0x4,%edi
4005ee: e8 d5 fe ff ff callq 4004c8 <_Znwm@plt>
4005f3: 48 89 c3 mov %rax,%rbx
4005f6: 48 89 d8 mov %rbx,%rax
4005f9: 48 89 c7 mov %rax,%rdi
4005fc: e8 11 00 00 00 callq 400612 <_ZN1AC1Ev>
400601: 48 89 5d e8 mov %rbx,-0x18(%rbp)
return 0;
400605: b8 00 00 00 00 mov $0x0,%eax
}
40060a: 48 83 c4 18 add $0x18,%rsp
40060e: 5b pop %rbx
40060f: c9 leaveq
400610: c3 retq
上面的汇编代码就不在添加注释了,因为两种操作产生的汇编代码是一样的,都是先调用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 peakflys 13:40:00 Monday, 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:估计很多公司的“正确答案“ 也不一定正确吧。