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

                                                                                                  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:估计很多公司的“正确答案“ 也不一定正确吧。

posted on 2013-04-08 13:43 peakflys 阅读(6776) 评论(21)  编辑 收藏 引用 所属分类: C++

评论

# re: 从一道面试题来阐释一个普遍的认知误区 2013-04-08 18:09 Richard Wei

分析的不错,支持下.  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-04-08 18:34 zgpxgame

分析的不错,但就这道题目来说,感觉这题考的意义不大,面试过程用这种细枝末节的语法题考察不出面试者的思维。  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-04-09 01:29 Wilbur

Default initialization和value initialiaztion,http://en.cppreference.com/w/cpp/language/default_initialization
http://en.cppreference.com/w/cpp/language/value_initialization  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-04-09 09:51 zuhd

int *pi = new int; // pi points to an uninitialized int
int *pi = new int(); // pi points to an int value-initialized to 0
这样也行?
AT&T的汇编看的好别扭啊  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-04-09 09:54 tangfu

http://en.cppreference.com/w/cpp/language/default_initialization语言标准提到了几点

1. when an object with dynamic storage duration is created by a new-expression without an initializer
2. If T is a class type, the default constructor is called to provide the initial value for the new object.

感觉你这个是属于语言灰色地带,不同编译器的实现有关系啊。。。。  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-04-09 10:17 peakflys

其实现在国内很多公司的笔试题都很学究,真正考察在工程项目中使用的比较多的那些语法、算法或者一些编程技巧很少、很浅……@zgpxgame
  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-04-09 10:22 peakflys

恩,这个网站提供了很好的C++参考文档@Wilbur
  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-04-09 10:58 peakflys

@tangfu
其实这个不是编译器相关的,标准化委员会细化规则的程度大大超过你的想象。
摘自: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);  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-04-09 11:20 溪流

@peakflys
学习了
  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区[未登录] 2013-04-14 09:50 peakflys

看多了就很自然了@zuhd
  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-04-14 09:57 天马星空

@peakflys
学习了,我们公司笔试题也有这一题,刚才看了一下标准答案,确实没有像楼主分析的这么细,只要写出初始化的区别就行了。  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区[未登录] 2013-05-03 11:21 noname

你这完全是因为debug才有的问题, vs2012 release证明,所谓的初始化代码不会产生,其它版本应该也是一样的  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区[未登录] 2013-05-03 11:24 noname

class A{
int a;
};
#pragma optimize=2

int _tmain(int argc, _TCHAR* argv[])
{
auto x = new A;
x = new A();

return 0;
}




auto x = new A;
003A1000 push 4
003A1002 call dword ptr ds:[3A2090h]
x = new A();
003A1008 push 4
003A100A call dword ptr ds:[3A2090h]
003A1010 add esp,8
003A1013 test eax,eax
003A1015 je wmain+1Dh (03A101Dh)
003A1017 mov dword ptr [eax],0

return 0;
003A101D xor eax,eax
}
003A101F ret   回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区[未登录] 2013-05-10 09:25 路人甲

2012产生的汇编这么奇怪?不过如果上面汇编是release版本的话,不正好和楼主的观点一样吗? 看 003A1017,明显的0赋值操作啊。搬起石头砸自己的脚了,哈哈 @noname
  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-05-10 14:30 jxhgzs

我编程的时候从来没有使用过 new A 这种形式,都是 new A() ;但是也经常碰到数据取随机数值的问题,我得到的解释是 类成员变量不会赋初始值,如果是静态的 基本类型 会有初始值,求解!  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-05-10 17:29 syd

由编译器构造出来的构造函数是trivial的,不会对member初始为0的  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-05-10 17:52 peakflys

@noname
文中的结论是基于C++98标准的编译器验证结果,而VS2012是根据最新的C++11标准实现的,新标准对于这种情况的规定有没有变动尚未验证。
PS:你提供的汇编代码应该是和文中的结论吻合的吧?  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-05-10 17:54 peakflys

@jxhgzs
请提供你的编译器版本,因为这是C++98标准规定的操作,如果你的编译器产生的代码行为同文中描述有出入,只能说明你的编译器没按照98标准来实现。  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-05-10 17:58 peakflys

@syd
其实不然,编译器产生的构造函数很多时候是nontrivial的,
例如:成员变量是含有构造函数的类类型,含有virtual function等等  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2013-12-12 15:18 挑灯看剑

表达式 POD类型T non-POD类型T
new T 不初始化 缺省初始化
new T() 总是缺省初始化  回复  更多评论   

# re: 从一道面试题来阐释一个普遍的认知误区 2014-04-02 14:06 allen

赞 ! 态度很认真 !  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2013年5月>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

导航

统计

公告

人不淡定的时候,就爱表现出来,敲代码如此,偶尔的灵感亦如此……

常用链接

留言簿(4)

随笔分类

随笔档案

文章档案

搜索

最新评论

阅读排行榜

评论排行榜