说明:如果看不懂的童鞋,可以直接跳到最后看总结,再回头看上文内容,如有不对,请指出~
环境:visual studio 2013(编译器优化关闭)

源代码

下面的源代码修改自http://blog.csdn.net/ljianhui/article/details/9245661
 #include <iostream>
#include <cstring>
using namespace std;
class ClassTest
{
public:
ClassTest()
{
c[] = '\0';
cout << "ClassTest()" << endl;
}
ClassTest& operator=(const ClassTest &ct)
{
strcpy(c, ct.c);
cout << "ClassTest& operator=(const ClassTest &ct)" << endl;
return *this;
}
ClassTest(ClassTest&& ct)
{
cout << "ClassTest(ClassTest&& ct)" << endl;
}
ClassTest & operator=(ClassTest&& ct)
{
strcpy(c, ct.c);
cout << "ClassTest & operator=(ClassTest&& ct)" << endl;
return *this;
}
ClassTest(const char *pc)
{
strcpy(c, pc);
cout << "ClassTest (const char *pc)" << endl;
}
//private:
ClassTest(const ClassTest& ct)
{
strcpy(c, ct.c);
cout << "ClassTest(const ClassTest& ct)" << endl;
}
virtual int ff()
{
return ;
}
private:
char c[];
};
ClassTest f1()
{
ClassTest c;
return c;
}
void f2(ClassTest ct)
{
;
}
int main()
{
ClassTest ct1("ab");//直接初始化
ClassTest ct2 = "ab";//复制初始化
ClassTest ct3 = ct1;//复制初始化
ClassTest ct4(ct1);//直接初始化
ClassTest ct5 = ClassTest("ab");//复制初始化
ClassTest ct6 = f1();
f1();
f2(ct1);
return ;
}

初始化1:ClassTest ct1("ab")

    ClassTest ct1("ab");//直接初始化
00B09518 push 0B0DCB8h //"ab"字符串地址
00B0951D lea ecx,[ct1]
00B09523 call ClassTest::ClassTest (0DC101Eh)
上面初始化汇编代码中,首先将“ab”字符串的地址压栈,并且取得ct1对象的地址存入寄存器ecx,即通过栈和寄存器传入两个参数,调用了ClassTest(const char *pc)构造函数。在ClassTest(const char *pc)函数中利用ct1对象的地址(即this指针)初始化ct1对象。
 

初始化2:ClassTest ct2 = "ab"

    ClassTest ct2 = "ab";//复制初始化
00B09528 push 0B0DCB8h //"ab"字符串地址
00B0952D lea ecx,[ct2]
00B09533 call ClassTest::ClassTest (0DC101Eh)
这是一个拷贝初始化式,底层的汇编有点出乎意料。本来赋值表达式右边会利用形参为const char*的构造函数生成一个临时对象,然后再利用这个临时对象拷贝或移动到ct2,但是经过visual studio编译器的处理,使得赋值表达式右边的字符串作为构造函数的实参直接对ct2进行初始化,和初始化1一样,这样可以省略了一步,加快运行速度,并且达到同样的效果。注意:在上面的汇编中,已经关闭了visual studio编译器优化,说明这种方法已经作为了visual studio的普遍方法,而不是作为一种vs所认为的优化手段了。
 

初始化3:ClassTest ct3 = ct1

    ClassTest ct3 = ct1;//复制初始化
00B09538 lea eax,[ct1]
00B0953E push eax
00B0953F lea ecx,[ct3]
00B09545 call ClassTest::ClassTest (0DC14C4h)
初始化3中通过栈和寄存器ecx传入了赋值表达式左右两边的对象地址,然后调用了类的拷贝构造函数(注意:函数只有一个形参,但其实也传入了ct3对象的地址,this指针),假如用户没有定义拷贝构造函数,编译器会生成合成的拷贝构造函数。如下:

010B3EE0  push        ebp
010B3EE1 mov ebp,esp
010B3EE3 sub esp,0CCh
010B3EE9 push ebx
010B3EEA push esi
010B3EEB push edi
010B3EEC push ecx
010B3EED lea edi,[ebp-0CCh]
010B3EF3 mov ecx,33h
010B3EF8 mov eax,0CCCCCCCCh
010B3EFD rep stos dword ptr es:[edi]
010B3EFF pop ecx
010B3F00 mov dword ptr [this],ecx
010B3F03 mov eax,dword ptr [this] //eax指向ct3对象地址
010B3F06 mov dword ptr [eax],10BDC70h //虚表指针存储在对象偏移量为0的地方
010B3F0C mov esi,dword ptr [__that] //esi存储ct1对象地址
010B3F0F add esi, //将esi加4,跳过4个字节的虚表指针,指向ct1后面的成员变量c
010B3F12 mov edi,dword ptr [this]
010B3F15 add edi, //edi指向ct2后面成员变量c
010B3F18 mov ecx,40h
010B3F1D rep movs dword ptr es:[edi],dword ptr [esi] //将ct1中字符数组元素拷贝到ct3字符数组
010B3F1F mov eax,dword ptr [this] //通过eax返回ct3对象地址
010B3F22 pop edi
010B3F23 pop esi
010B3F24 pop ebx
010B3F25 mov esp,ebp
010B3F27 pop ebp
 

初始化4:ClassTest ct4(ct1)

    ClassTest ct4(ct1);//直接初始化
010B954A lea eax,[ct1]
010B9550 push eax
010B9551 lea ecx,[ct4]
010B9557 call ClassTest::ClassTest (0DC14C4h)
初始化4和初始化3汇编指令一样,底层都是传入了两个对象的地址,然后再调用拷贝构造函数。
 

初始化5:ClassTest ct5 = ClassTest()

    ClassTest ct5 = ClassTest();//复制初始化
010B955C lea ecx,[ct5]
010B9562 call ClassTest::ClassTest (0DC12ADh)
跟踪下去,发现它跳到了类的默认构造函数那里;

    ClassTest()
010B4C70 push ebp
010B4C71 mov ebp,esp
010B4C73 sub esp,0CCh
010B4C79 push ebx
010B4C7A push esi
010B4C7B push edi
010B4C7C push ecx
010B4C7D lea edi,[ebp-0CCh]
010B4C83 mov ecx,33h
010B4C88 mov eax,0CCCCCCCCh
010B4C8D rep stos dword ptr es:[edi]
010B4C8F pop ecx
010B4C90 mov dword ptr [this],ecx
010B4C93 mov eax,dword ptr [this]
010B4C96 mov dword ptr [eax],10BDC70h
{
c[] = '\0';
010B4C9C mov eax,
010B4CA1 imul ecx,eax,
010B4CA4 mov edx,dword ptr [this]
010B4CA7 mov byte ptr [edx+ecx+],
cout << "ClassTest()" << endl;
说好的生成一个临时对象,再将这个临时对象拷贝或移动到ct5中,其实不然。而是将ct5对象地址作为实参去调用默认构造函数,进而对ct5进行初始化。
 

初始化6:ClassTest ct6 = f1()

    ClassTest ct6 = f1();
010B9567 lea eax,[ct6]
010B956D push eax
010B956E call f1 (0DC14BFh)
010B9573 add esp,
这个初始化的底层实现也是比较出乎意料的一个。首先将已存在在main函数栈中的ct6对象地址压栈,此时根据函数调用规则,可以知道ct6对象地址其实作为了f1的实参。

ClassTest f1()
{
00DC5830 push ebp //栈帧开始
00DC5831 mov ebp,esp
00DC5833 sub esp,1D0h
00DC5839 push ebx
00DC583A push esi
00DC583B push edi
00DC583C lea edi,[ebp-1D0h]
00DC5842 mov ecx,74h
00DC5847 mov eax,0CCCCCCCCh
00DC584C rep stos dword ptr es:[edi]
00DC584E mov eax,dword ptr ds:[00DD0000h] //初始化栈
00DC5853 xor eax,ebp
00DC5855 mov dword ptr [ebp-],eax
ClassTest c;
00DC5858 lea ecx,[c] //c的值ebp+FFFFFEF4h即ebp-,说明c是一个栈内局部变量
00DC585E call ClassTest::ClassTest (0DC12ADh) //调用默认构造函数初始化c
return c;
00DC5863 lea eax,[c]
00DC5869 push eax //c对象地址
00DC586A mov ecx,dword ptr [ebp+] //ct6对象地址
00DC586D call ClassTest::ClassTest (0DC14BAh) //调用移动构造函数,初始化ct6
00DC5872 mov eax,dword ptr [ebp+] //返回ct6对象地址
}
00DC5875 push edx
00DC5876 mov ecx,ebp
00DC5878 push eax
00DC5879 lea edx,ds:[0DC58A4h]
00DC587F call @_RTC_CheckStackVars@8 (0DC1136h)
00DC5884 pop eax
//省略余下代码
从上面的汇编代码中可以看出,c是栈内的局部变量,并且调用了默认构造函数对c进行了初始化。但f1代码中return c语句,它就是返回一个和c一样的临时对象了吗?其实不然。在调用f1的时候,也传进了ct6对象的地址,在f1内部对c进行初始化后,直接通过c对象地址和ct6地址调用移动构造函数,对ct6进行了初始化,最后返回的是ct6对象地址。可以看出vs将ct6的初始化工作放在了函数内部进行!
 

临时对象:f1()

    f1();
00DC9576 lea eax,[ebp-814h]
00DC957C push eax
00DC957D call f1 (0DC14BFh)
00DC9582 add esp,
临时对象可以看成是无名的变量,在内部也是存在于栈中的一个对象。所以和初始化6一样,只不过这个时候传入的是临时对象的地址而已,最后返回的也是临时对象的地址,返回前也调用了移动构造函数
 

临时对象:f2(ct1)

    f2(ct1);
010F9392 sub esp,104h //开辟栈空间,生成一个临时对象,刚好是260个字节(+,即虚表指针和私有的char型数组的总大小)
010F9398 mov ecx,esp //将esp栈顶指针作为临时对象的起始地址
010F939A lea eax,[ct1] //传入ct1对象地址
010F93A0 push eax
010F93A1 call ClassTest::ClassTest (010F1078h)
010F93A6 call f2 (010F14BFh)
010F93AB add esp,104h
从上面的汇编代码中可以看出,编译器对于一个形参为类型的函数,不是直接传入ct1对象地址,而是在栈上生成一个临时对象并且用拷贝构造函数进行初始化,最后再传入临时对象的地址调用f2函数
 

总结

这么零散复杂的汇编,大部分人看了都有点头疼,最后再来个总结:
 
(1)什么是拷贝初始化(也称为复制初始化):将一个已有的对象拷贝到正在创建的对象,如果需要的话还需要进行类型转换。拷贝初始化发生在下列情况:
  1. 使用赋值运算符定义变量
  2. 将对象作为实参传递给一个非引用类型的形参
  3. 将一个返回类型为非引用类型的函数返回一个对象
  4. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
 
(2)什么是直接初始化:在对象初始化时,通过括号给对象提供一定的参数,并且要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
 
(3)在底层实现中,可以看出编译器的思想是能不用临时对象就不用临时对象。因此对于下面这些拷贝初始化,都不会生成临时对象再进行拷贝或移动到目标对象,而是直接通过函数匹配调用相应的构造函数。
 ClassTest ct2 ="ab"; //相当于ClassTest ct2("ab");
ClassTest ct5 =ClassTest("ab"); //相当于ClassTest ct5("ab")

下面的语句,visual studio才会生成一个无名的临时对象(位于main函数的栈中),注意:f1的返回值类型是非引用的,f2的形参类型是非引用的。

 f1(); //临时对象用于存储f1的返回值
f2(ct1); //临时对象用于拷贝实参,并传入函数

而下面则是直接传入赋值表达式左边对象地址,然后再对该对象进行移动拷贝,注意f1返回值类型是非引用的,如果是引用的,则会调用拷贝构造函数。

 ClassTest ct6 = f1();
 
(4)直接初始化和拷贝初始化效率基本一样,因为在底层的实现基本一样,所以将拷贝初始化改为直接初始化效率提高不大。
 
(5)拷贝初始化什么时候使用了移动构造函数:当你定义了移动构造函数,下列情况将调用移动构造函数
  1. 将一个返回类型为非引用类型的函数返回一个对象
 
(6)拷贝初始化什么时候使用拷贝构造函数:
  1. 赋值表达式右边是一个对象
  2. 直接初始化时,括号内的参数是一个对象
  3. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
  4. 将一个返回类型为引用类型的函数返回一个对象
  5. 形参为非引用类型的函数,其中是将实参拷贝到临时对象
 
(7)什么时候使用到拷贝赋值运算符:
  • 赋值表达式右边是一个左值对象(如果需要,可以调用构造函数类型转换,生成一个临时对象)
  • 当赋值表达式右边是一个右值对象,且没有定义移动赋值运算符函数
 
(8)什么时候使用移动赋值运算符:
  • 当赋值表达式右边是一个右值对象,且定义了移动赋值运算符函数
 
(9)即使编译器略过了拷贝/移动构造函数,但是在这个程序点上,拷贝/移动构造函数必须存在且是可访问的(例如:不能是private),如下:
ClassTest ct2 = "ab";//复制初始化

编译器会将其等同下面的语句,调用的是ClassTest的ClassTest(const char *pc)构造函数
ClassTest ct2("ab");//直接初始化

但是ClassTest的拷贝或移动构造函数需要定义至少其中一个,否则会报错
 

本文链接:【原创】c++拷贝初始化和直接初始化的底层区别 http://www.cnblogs.com/cposture/p/4925736.html

【原创】c++拷贝初始化和直接初始化的底层区别的更多相关文章

  1. 【c++】必须在类初始化列表中初始化的几种情况

    转自:http://www.cnblogs.com/kaituorensheng/p/3477630.html 1. 类成员为const类型 2. 类成员为引用类型 #include <iost ...

  2. 代码初始化 故事板初始化 xib初始化总结

    对象的初始化有三种方式   // 代码创建 - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { ...

  3. c++构造函数成员初始化中赋值和初始化列表两种方式的区别

    先总结下: 由于类成员初始化总在构造函数执行之前 1)从必要性: a. 成员是类或结构,且构造函数带参数:成员初始化时无法调用缺省(无参)构造函数 b. 成员是常量或引用:成员无法赋值,只能被初始化 ...

  4. Spark源码剖析 - SparkContext的初始化(八)_初始化管理器BlockManager

    8.初始化管理器BlockManager 无论是Spark的初始化阶段还是任务提交.执行阶段,始终离不开存储体系.Spark为了避免Hadoop读写磁盘的I/O操作成为性能瓶颈,优先将配置信息.计算结 ...

  5. Java静态初始化,实例初始化以及构造方法

    首先有三个概念需要了解: 一.静态初始化:是指执行静态初始化块里面的内容. 二.实例初始化:是指执行实例初始化块里面的内容. 三.构造方法:一个名称跟类的名称一样的方法,特殊在于不带返回值. 我们先来 ...

  6. C#中结构(struct)的部分初始化和完全初始化

    假设有这样一个值类型struct. public struct Size { public int Length; public int Width; public int Area() { retu ...

  7. java类的成员初始化顺序和初始化块知识

    java类的成员初始化顺序和初始化块知识 转自:http://blog.csdn.net/lgfeng218/article/details/7606735 属性.方法.构造方法和自由块都是类中的成员 ...

  8. C++的一大误区——深入解释直接初始化与复制初始化的区别

      转自:http://blog.csdn.net/ljianhui/article/details/9245661 不久前,在博客上发表了一篇文章——提高程序运行效率的10个简单方法,对于其中最后一 ...

  9. C++直接初始化和复制初始化1

    这篇文章主要介绍了C++直接初始化与复制初始化的区别深入解析,是很多C++初学者需要深入了解的重要概念,需要的朋友可以参考下   C++中直接初始化与复制初始化是很多初学者容易混淆的概念,本文就以实例 ...

随机推荐

  1. FTP模式简式:PORT/PASV/EPRT/EPSV

    简介 常见FTP有两种模式:PORT(主动模式).PASV(被动模式). 而EPRT/EPSV模式出现的原因是FTP仅仅提供了建立在IPv4上进行数据通信的能力,它基于网络地址是32位这一假设.但是, ...

  2. ES6使用fetch请求数据

    ie是完全不支持fetch的. fetch(url,{method:"get/post"}).then(res=>{   }) 如果请求返回的status是200,body是 ...

  3. (24)How generational stereotypes hold us back at work

    https://www.ted.com/talks/leah_georges_how_generational_stereotypes_hold_us_back_at_work/transcript ...

  4. centos7 Mycat/MySQL/MariaDB安装部署

    使用yum安装MySQL详细步骤 安装mysql源 centos系统中不包含mysql的源,需要先安装mysql源 1.官网下载源.使用图形界面操作系统进入mysql官网,进入以下界面. 2.在Cen ...

  5. linux (ubuntu) 命令学习笔记

    1, md5sum 输出字符串的MD5值 echo -n 123456 | md5sum //-n表示不打印回车符 2, ubuntu设置dock任务栏鼠标点击效果 16.04: 调整位置:gsett ...

  6. HTML之二 -特殊标签 与数据交互标签

    1.<img>  标签 © <h1> <img src="1.jpg" width="200" height="200& ...

  7. [译]使用explain API摆脱ElasticSearch集群RED苦恼(转)

    "哔...哔...哗",PagerDuty的报警通知又来了. 可能是因为你又遭遇了节点宕机, 或者服务器机架不可用, 或者整个ElasticSearch集群重启了. 不管哪种情况, ...

  8. Thread,ThreadPool,Task

    线程分为前台和后台.比如我们直接new一个Thread这就是前台线程. 前台线程一定会执行. 比如我们创建2个线程:1号,2号,同时执行,假设1号是主线程,1执行完了,依旧会等待2执行完成,整个程序才 ...

  9. 【MySql】启动/停止

    一.启动 1.查看启动命令所在目录 macdeMacBook-Pro:~ mac$ ps -ef|grep mysql 2.进入命令目录 macdeMacBook-Pro:~ mac$ cd /usr ...

  10. 可遇不可求的Question之MySql4.0前版本不支持union与批量SQL提交

    批量SQL提交 参考 21.2.6. Connector/NET Connection String Options Reference . Allow Batch true When true, m ...