免费开源crm,西安网站优化培训,公众号怎么开通直播,老油条视频h5二十五、C移动语义、左值和右值、左值引用右值引用、移动构造函数、std::move、移动赋值操作符
本部分讨论一些更高级的C特性#xff1a;C移动语义。但是讲移动语义之前我们得先了解什么左值右值、左值引用和右值引用。
1、C的左值和右值、左值引用和右值引用左值是有地址的…二十五、C移动语义、左值和右值、左值引用右值引用、移动构造函数、std::move、移动赋值操作符
本部分讨论一些更高级的C特性C移动语义。但是讲移动语义之前我们得先了解什么左值右值、左值引用和右值引用。
1、C的左值和右值、左值引用和右值引用左值是有地址的值(located value),就是左值是有地址的。 左值大部分情况下是在等号的左边右值在右边。
右值是一些比如字面量、函数的一些返回结果等
如果我通过返回int,把上面的func函数的返回整成左值会是什么情况这里也引出什么是左值引用:
再看一个使用字符串的例子
那有什么方法来检测某个值是左值还是右值吗这里也引出什么是右值引用 所以此时我们写个重载函数
小结 左值引用用一个符号右值引用则是用两个符号。 左值是带地址的数据就是有存储支持的变量。右值是临时值可以用右值引用来检测。 左值引用只能引用(接受)左值除非加const就也可以引用(接受)右值了。 右值引用只能应用(接受)右值。
左值、右值有什么用处呢 尤其是在移动语义方面非常有用。移动语义我们后面还要讲。这里主要是想说分清左右值的目的在于优化。如果我们知道传入的是一个临时对象的话我们就不需要担心它们是否活着、是否完整、是否拷贝我们可以简单的偷用它的资源给到特定的对象或者在其他地方使用它们因为我们知道它是暂时的它不会存在很长时间比如上面的lnfn就是暂时的我们就可以从这个临时值中偷取资源这对优化有很大帮助。能用右值就别用左值。所以有很多代码使用时我们要知道这是右值引用。
2、C移动语义移动构造函数 其实移动语义底层逻辑也不复杂。但是你要非常清晰的说清什么是移动语义、用它做什么、实践中它是如何工作的等这些问题就比较困难因为牵扯到很多底层的、被包装了的、我们看不见的东西所以很难说清。这里我尽量往细了说吧。
移动语义本质上就是允许我们移动对象。而这在C11之前是不可能的因为C11才引入了右值引用右值引用是移动语义必需的底层逻辑。通过上面的小标题我们已经知道什么是右值以及右值引用是什么。基本思想是当我们写C代码时很多情况下我们不需要或者不想把一个对象从一个地方复制到另一个地方但又不得不复制因为底层的设置就是要复制的。
举个例子比如现在我要把一个对象当作参数传递给一个函数。那么这个函数要获得那个对象的所有权此时就只能copy这个对象。这里涉及到函数调用的相关知识点不懂的可以参考【C】数据类型、函数、头文件、断点调试、输入输出、条件与分支、VS项目设置_cprintf头文件-CSDN博客中的函数部分。
同理当我们想从一个函数返回一个对象时也是一样的。我仍然需要在函数中创建那个对象然后返回它此时又是得复制数据了。不过现在有一种叫返回值优化的东西可以对这部分进行优化所以这不再是个问题了。
就是说我把一个对象当参数传入某个函数时这个函数首先是需要得到这个对象的所有权或者其他所以编译器或者操作系统首先是在当前堆栈帧中构造一个一次性对象不管它在哪里将它复制到我正在调用的函数中。然后才是开始执行这个函数的函数体。 当然如果你的对象只是由一对整数或类似的东西组成那么复制也没什么大不了。但如果你的对象需要堆分配内存之类的就像下图的例子它是一个字符串需要复制它就需要创建一个全新的堆分配。这就是一个沉重的复制过程。此时就是移动语义的用武之地下下图。如果我们只是移动对象而不是复制它那么性能会更高。
写一个类作为例子来演示这个沉重的复制过程 从上图可见不管是代码K还是代码L都调用了一次myString类的复制构造函数E。而E函数中还有堆分配new是不是非常沉重。所以我们要用移动语义优化上面的代码。但是这里先不急着讲如何优化下面我先讲透上面的代码
1A和G处都是类的定义定义一个类是不会引起内存分配的。定义一个类实例系统才会分配存储区并把类实例名称引用到这块儿存储区。
2类实例对象的空间是在调用构造函数之前就分配好了的。调用构造函数是初始化这个实例的数据。
3实例化一个类会有一个this指针类实例之间的区分就是通过this指针区分的。this指针就是一个地址实例对象就是一些空间构造函数、析构函数以及其它的函数是一堆指令的集合。
4上图C处代码是myString类的无参构造函数。default就表示如果实例化myString类对象时没有参数那就使用编译器生成的、默认的、构造函数。这也是在C11标准中新引入的编译器可以直接生成内联构造函数代码。 如果你的代码是myString s ;那实例对象s的m_datanullptrm_size0。 此处再多说一句default只能用于特殊成员函数构造函数、析构函数、拷贝/移动构造函数和拷贝/移动赋值操作符。
5上图D处函数是myString类的有参构造函数。 6上图E处函数是myString类的复制构造函数。 7上图F处函数是myString类的析构函数。
8当我们实例化一个类实例时系统是先分配一块不用初始化的内存空间这块空间的大小是这个类的数据成员对齐后的大小。然后再执行这个类的构造函数。构造函数一般情况下就是初始化这块空间的当然也会有其他功能比如上面代码中还有new操作、mempy操作等。 当构造函数执行完毕后也就是这个类实例化完毕了。一个类实例化完毕也就是说在内存中的一块存储区里存储了这个类的数据成员(而且一般是初始化完毕的)。
9当我们复制一个类实例时如果这个类中有复制构造函数那就类中的复制构造函数就自动重载了复制的操作。如果这个类中没有复制构造函数那么就是底层的复制函数进行复制操作。
10不管是实例化一个类实例还是复制一个类实例(不管是用复制构造函数复制的还是用底层的复制函数复制的)都意味着创建新创建了一个类实例对象当然也会同时生成一个指向这个对象的this指针。当这个类实例对象所在的作用域结束时都会自动调用这个类的析构函数。
11有了上面的知识点铺垫我们现在来理解代码K 当操作系统开始执行上面的程序时入口是main函数所以代码K是程序执行的第一条指令。执行这条指令的过程是第一步是执行myString(liyuanyuan)程序执行指针从K处跳到A处。也就是先去实例化一个没有名称的myString类实例。 系统先分配两个未初始化的资源char* m_data(指针是4个字节)和uint32_t m_size(1个字节)。也就是B处的代码。 然后生成右值liyuanyuan当作构造函数D的参数开始执行构造函数D于是就打印了M、初始化了m_data和m_size uint32_t类型的m_size初始化的值是临时右值liyuanyuan的长度10 char指针m_data初始化的值是m_size为长度的、堆上的(因为是new嘛)、char数组的首地址。 并且同时把临时右值liyuanyuan也拷贝到堆上的char数组里面了。
这就是在main函数的栈上执行myString(liyuanyuan)的过程。执行完毕后的状态是m_data、m_size是存储在main函数的栈内存上这个.exe程序的进程堆上还有一个char数组数组的首地址就是main函数线程中的m_data的值。 为了方便表述这里生成的{m_data、m_size}这套数据暂时给个名字dataA吧。
第二步执行Entity e1(dataA)也是实例化一个名叫e1(这次是有名字的)的Entity类实例。于是执行指令跳到G处。 同理系统先分配一块大小等于myString(415个字节)的、未初始化的栈空间(假设叫空间E)。 然后是在main函数的堆栈上复制一套dataA的数据我们姑且将复制品称为dataB。为啥要复制dataA因为为构造函数I准备参数啊。就是让dataB作为构造函数I的参数开始调用I完成e1的实例化。但是这里的复制dataA操作就出现下面两种情况
情况1我在myString类中写了复制构造函数E所以当系统复制dataA赋值给dataB时会自动被E函数重载。那被E重载了就打印了N、组成dataB的m_dataB的值就是10组成dataB的m_sizeB就是E又在堆内存上new的另外一个字符串[liyuanyuan]的首地址。然后用dataB{m_dataB、m_sizeB}这套临时数据当作参数来实例化e1了。就是用{m_dataB、m_sizeB}初始化空间E(就是把{m_dataB、m_sizeB}拷贝到E中)并将名称e1引用到空间E上。实例对象e1就生成了。此时右值{m_dataB、m_sizeB}和堆上又new的数组就寿终正寝了所以打印了P。至此代码K就执行完毕了。然后是执行下一条代码S就是打印Q最后执行到作用域结束T处就释放第一步生成的、没名字的、数据是dataA{m_data、m_size}的那个myString类实例于是又打印了个R。
情况2如果我的myString类中没有写复制构造函数E呢那系统是怎么赋值dataA的那系统就用底层的复制函数把内存块dataA原原本本的拷贝到空间E里将名称e1引用到空间E上。那此时的dataB的m_dataB就还是第一步时生成的地址m_sizeB也是10。但是不管是调用E还是调用底层的复制函数这都是一次生成一个新的myString类实例的操作。所以当系统把这个dataB当参数传入I并执行完毕后释放dataB时就调用了F析构函数把第一步new的字符串数组也释放了。至此代码K算是执行完毕了然后执行代码S,但是执行到T处时第一步生成的myString类实例也该释放了于是再次调用F,但是此时F就发现指针m_data指向的那块堆内存已经不见了(被m_dataB给释放了)于是没法delete了就报崩溃了
其实情况2就是浅拷贝情况1是深拷贝。 情况2中dataB拷贝的是dataA中的m_data指针这样就有两个指针指向堆上的同一个字符串数组当dataB释放时就把堆上的字符串数组给释放了那到作用域结束释放dataA时m_data指向的内存就已经不存在了就没法释放了程序就崩溃了。也所以说上面的拷贝构造函数E是有必要写的不然就崩溃了。
说明 上面解释中看不懂类数据的内存分配的同学请参考【C】类、静态static、枚举、重载、多态、继承、重写、虚函数、纯需函数、虚析构函数_类 多态与重载-CSDN博客 中的类定义、类实例部分内容 看不懂复制、复制构造函数的请参考【C】理解C中的复制、复制构造函数_c 复制函数-CSDN博客 看不懂堆栈的请参考【C】如何用C创建对象理解作用域、堆栈、内存分配_c 作用域 堆 内存-CSDN博客 看不懂进程线程的请参考【C】C中的线程-CSDN博客 看不懂函数调用的请参考【C】数据类型、函数、头文件、断点调试、输入输出、条件与分支、VS项目设置_cprintf头文件-CSDN博客 中的函数部分 看不懂构造函数的请参考【C】类成员初始化列表、三元运算符、运算符及其重载、箭头操作符-CSDN博客 中的构造函数初始化列表部分
上面洋洋洒洒写了那么多其实就是想说上面的代码其实并不优秀因为拷贝过程太沉重了。如果说实例化时很沉重是无可奈何那我只是拷贝一个一次性的、用完即丢的复制品都也这么沉重就太无语了。下面用移动构造函数优化代码 加上上图中红框中的两个移动构造函数代码就不会进行沉重的深拷贝了就是进行了轻量的浅拷贝而且加上C处的代码程序也不会出现崩溃了。
上图A是对参数name进行了强制右值转换。这样初始化Entity实例对象时如果有右值参数就可以重载这个只接受右值参数的构造函数A了。如果没有代码A那就得使用A上面的构造函数这个构造函数即可接受左值也可接受右值。但是如果是右值参数传入那它是先隐式转换将右值转化为左值然后才开始指向函数体的。所以也还是会发生深拷贝。所以我们一定要在Entity类中写一个只接受右值的构造函数A。
上图D处是使用std::move这种写法等价于A。一般我们不建议使用A因为不是什么对象都可以强制转换的。建议使用move而这个下面一个小标题展开讲的内容。
3、移动语义std::move与移动赋值操作符 上个小标题只讲了移动构造函数。其实移动语义还涉及到另外两个关键部分std::move和move assignment operator(移动赋值操作符)。这两个小知识点是本不标题的讲解内容。例子还是我们的myString类和Entity类
1std::move
2移动赋值操作符
待续。。。。