《C++性能优化指南》 linux版代码及原理解读 第六章 优化动态分配的变量

概述

本章主要通过讲解C++中的变量(变量的生命周期、所有权等),以及变量相关的操作(分配、析构、智能指针托管)所产生的开销对性能的影响等,通过深入介绍相关的流程,解释其中所产生的开销。以及如何从多个方面减少变量所带来的开销,从而对性能进行优化。(通过代码解释为什么智能指针的运行效率可能比普通指针慢几百倍)

.1 C++变量回顾

每个C++变量(每个普通数据类型的变量;每个数组、结构体或类实例)在内存中的布局都是固定的,它们的大小在编译时就已经确定了。C++允许程序获得变量的字节单位的大小和指向该变量的指针,但并不允许指定变量的每一位的布局。C++的规则允许开发人员讨论结构体成员变量的顺序和内存布局,C++也提供了多个变量可以共享同一内存块的联合类型,但是程序所看到的联合是依赖于实现的。

变量的生命周期

每个变量都有它的存储期,也称为生命周期。只有在这段时间内,变量所占用的存储空间或者内存字节中的值才是有意义的。为变量分配内存的开销取决于存储期。

静态存储期
    静态存储期具有静态存储期的变量被分配在编译器预留的内存空间中。在程序编译时,编译器会为每个静态变量分配一个固定位置和固定大小的内存空间。静态变量的内存空间在程序的整个生命周期内都会被一直保留。所有的全局静态变量都会在程序执行进入main()前被构建,在退出main()之后被销毁。在函数内声明的静态变量则会在“程序执行第一次进入函数前”被构建,这表示它可能会和全局静态变量同时被构建,也可能直到第一次调用该函数时才会被构建。C++为全局静态变量指定了构建和销毁的顺序,因此开发人员可以准确地知道它们的生命周期。但是这些规则太复杂了,实际上在使用存储期时,这些规则更像是警告而非行为。
    我们既可以通过名字访问静态变量,也可以通过指针或是引用来访问该变量。静态变量的名字与指向该变量的指针和引用一样,可能会在这个名字变得有意义之前就出现在了其他静态变量的构造函数中,或是在这个名字被销毁后又出现了其他静态变量的析构函数中。
    为静态变量创建存储空间是没有运行时开销的。不过,我们无法再利用这段存储空间。因此,静态变量适用于那些在整个程序的生命周期内都会被使用数据。
    在命名空间作用域内定义的变量以及被声明为static或是extern的变量具有静态存储期。
线程局部存储期
    自C++11开始,程序可以声明具有线程局部存储期的变量。在C++11之前,有些编译器和框架也以一种非标准的形式提供了类似的机制。
    线程局部变量在进入线程时被构建,在退出线程时被析构。它们的生命周期与线程的生命周期一样。每个线程都包含一份这类变量的独立的副本。
    访问线程局部变量可能会比访问静态变量开销更高,这取决于操作系统和编译器。在某些系统中,线程局部存储空间是由线程分配的,所以访问线程局部变量的开销比访问全局变量的开销多一次指令。而在其他系统中,则必须通过线程ID索引一张全局表来访问线程局部变量。尽管这个操作的时间开销是常量时间,但是会发生一次函数调用和一些计算,导致访问线程局部变量的开销变得更大。
    自C++11开始,用thread_local存储类型指示符关键字声明的变量具有线程局部存储期。
自动存储期
    具有自动存储期的变量被分配在编译器在函数调用栈上预留的内存空间中。在编译时,编译器会计算出距离栈指针的偏移量,自动变量会以该偏移量为起点,占用一段固定大小的内存,但是自动变量的绝对地址直到程序执行进入变量的作用域内才会确定下来。
    在程序执行于大括号括起来的代码块内的这段时间,自动变量是一直存在的。当程序运行至声明自动变量的位置时,会构建自动变量;当程序离开大括号括起来的代码块时,自动变量将会被析构。
    与静态变量一样,我们可以通过名字访问自动变量。但是与静态变量不同的是,该名字只在变量被构建后至被析构前可见。当变量被析构后,指向该变量的指针和引用可能仍然存在,而解引它们会导致未定义的程序行为。
    与静态变量一样,为自动变量分配存储空间不会发生运行时开销。但与静态变量不同的是,自动变量每次可以占用的总的存储空间是有限的。当递归不收敛或是发生深度函数嵌套调用导致自动变量占用的存储空间大小超出这个最大值时,会发生栈溢出,导致程序会突然终止。自动变量适合于那些只在代码块附近被使用的对象。
    函数的形参变量具有自动存储期。除非使用了特殊的关键字,那些声明在可执行代码块内部的变量也具有自动存储期。
动态存储期
    具有动态存储期的变量被保存在程序请求的内存中。程序会调用内存管理器,即C++运行时系统函数和代表程序管理内存的数据结构的集合。程序会在new表达式(在13.1.3节进行详细讲解)中显式地为动态变量请求存储空间并构建动态变量,这可能会发生在程序中的任何一处地方。稍后,程序在delete表达式(在13.1.4节进行详细讲解)中显式地析构动态变量,并将变量所占用的内存返回给内存管理器。当程序不再需要该变量时,这可能会发生在程序的任何一处地方。

    与自动变量类似,但与静态变量不同的是,动态变量的地址是在运行时确定的。
    不同于静态变量、线程局部变量和自动变量的是,数组的声明语法被扩展了,这样可以在运行时通过一个(非常量)表达式来指定动态数组变量的最高维度。在C++中,这是唯一一种在编译时变量所占用的内存大小不固定的情况。
    动态变量没有自己的名字。当它被构建后,C++内存管理器会返回一个指向动态变量的指针。程序必须将这个指针赋值给一个变量,这样就可以在最后一个指向该变量的指针被析构之前,将动态变量返回给内存管理器,否则就会有因不断地创建动态变量而耗尽内存的危险。如果没有正确地返回动态变量,现代处理器可能会在数分钟内耗尽数吉字节的内存。
    不同于静态变量和线程局部变量的是,动态变量的数量和类型可以随着时间改变,而不受到它们所消耗的内存总量的限制。另外,与静态变量和自动变量不同的是,管理动态变量使用的内存时会发生显著的运行时开销。
    new表达式返回的变量具有动态存储期。

变量的所有权

C++变量的所有权和生命周期一样,都会决定一个变量什么时候被创建,什么时候被析构。
全局所有权
    具有静态存储期的变量整体上被程序所有。程序会在进入main()前构建它们,并在从main()返回后销毁它们。
词法作用域所有权
    具有自动存储期的变量被一段由大括号括起来的代码块构成的词法作用域所拥有。
    词法作用域可能是函数体,if、while、for或者do控制语句块,try或者catch子句,抑或是由大括号括起来的多条语句。这些变量在程序进入词法作用域时会被构建,在程序退出词法作用域时会被销毁。
    最外层的词法作用域,即最先进入和最后退出的词法作用域,是main()的函数体。也就是说,声明在main()中的自动变量的生命周期与静态变量相同。
成员所有权
    类和结构体的成员变量由定义它们的类实例所有。当类的实例被构建时,它们会被类的构造函数构建;当类的实例被销毁时,它们也会随之被销毁。
动态变量所有权
    动态变量没有预定义的所有者。取而代之,new表达式创建动态变量并返回一个必须由程序显式管理的指针。动态变量必须在最后一个指向它的指针被销毁之前,通过delete表达式返回给内存管理器销毁。因此,动态变量的生命周期是可以完全通过编程控制的,它是一个强大且危险的工具。如果在最后一个指向它的指针被销毁之前,动态变量没有通过delete表达式被返回给内存管理器,内存管理器将会在程序剩余的运行时间中丢失对变量的跟踪。
    动态变量的所有权必须由程序员执行并编写在程序逻辑中。它不受编译器控制,也不由C++定义。动态变量所有权对于性能优化非常重要。具有强定义所有权的程序会比所有权分散的程序更高效。

.2 C++动态变量API回顾

    new 和 delete 都是操作符,它们和 malloc / free 不同的是, 前一个组合可以调用对应类型的构造和析构函数,将类型自动进行初始化,但是后者不能。
    使用new 和 delete 对单个对象进行分配与数组类型的进行分配的方式是不同的。以下通过代码进行讲解:

{     
	int n = 100;     
	char* cp;             // 没有指定cp的值
	Book* bp = nullptr;   // bp指向无效地址//     
	...     
	cp = new char[n];                // cp指向一个新的动态数组    
	bp = new Book("Optimized C++"); // 新的动态类的实例//     
	...     
	char array[sizeof(Book)];     
	Book* bp2 = new(array) Book("Moby Dick"); // placement new操作符//     
	...     
	delete[] cp;   // 在改变指针之前删除动态数组    
	cp = new char; // cp现在指向一个动态char //     
	...     
	delete bp;      // 类实例使用完毕    
	delete cp;      // 动态分配的char使用完毕    
	bp2->~Book();  // placement new操作符创建出的类实例使用完毕
} // 在指针超出作用域前删除动态变量

内存管理函数
    new和delete表达式会调用C++标准库的内存管理函数,在C++标准中称为“自由存储区”的内存池中分配和归还内存(也就是堆中)。new操作符是new() 和new运算符的重载,delete 则是delete() 和 deletec操作符的重载。

使用智能指针实现动态变量所有权的自动化

    动态变量(动态分配的变量),即通过New 方法创建的变量,这种变量它的生命周期(何时析构)不被编译器管控,同时C++的规则里面也没有说这种New出来的变量会在什么时候失效或者消失。我们可以在程序的A文件中声明一个指针变量 T * pointer ,然后在B文件中将pointer进行赋值,我们可以在C文件中将pointer 赋值给 T* pointer_B ,然后在D文件中通过调用delete pointer_B删除实际的动态变量。这很灵活,但是这也是问题,过于分散的所有权导致问题很难定位,当我们在使用pointer的时候,怎么确定动态变量还在?怎么保证不会出现未定义行为呢?答案就是自查。这是很头铁的办法。
    还有另一种办法,能够管理这种动态分配的变量,我们可以设计一个类,这个类可以管理我们通过New 产生的动态变量,同时它自身能够在某些时刻自动的析构掉动态变量,这种就是我们常说的智能指针。关于智能指针的一些实现细节,可以查看我之前的博客c++ shared_ptr:从循环引用导致内存泄漏到shared_ptr介绍再到析构流程分析

    智能指针是从C++11 开始被正式引入的概念,虽然它可以在某种程度上减少内存的泄漏问题,但是通过上一篇的博客,我们可以认识到智能指针的问题:
        1. shared_ptr内部的结构是 ‘ 对象指针+智能指针的结构体 ’ , 所以在实际使用动态变量的时候,需要将智能指针解引用。
        2. 由于 ‘ shared_ptr的结构体 ’当中包含了引用计数,但是在改变引用计数的时候,会产生原子操作,不论原子操作的性能如何,它都是一种昂贵的操作(相对于CPU的计算频率而言)。
所以shared_ptr的开销是昂贵的,但是unique_ptr由于没有问题2的问题,所以性能会优于shared_ptr,但是他们都是比直接的指针要慢的。可以在我的开源项目中查看第六章的代码,我会附上具体的性能指标。

内存相关的运行时开销

    在第四章《C++性能优化指南》 linux版代码及原理解读 第四章中,通过对代码的优化,我们可以发现,哪怕在代码中移除一次内存分配操作,也会对程序的性能有一些提升。这是因为一般的C++语言,在被编译成机器码之后,会由CPU直接执行,在这个过程序可能只有几次的内存访问操作,比如加载指令、加载指令对应的地址数据、寄存器操作、然后是写回,但是如果是要分配内存的话,它的开销可就不一样了,会有很多种不同的情况,甚至最慢的话能达到几千次的内存访问!
    抛开系统之间的差异不谈,单纯的从逻辑概念上,描述一个内存分配的操作。
    首先,函数申请一块内存,它会先从内存块中寻找一块符合条件的内存,如果找到了一块大小相同的,那这部分内存就会从 [ 可用内存集合 ] 中被移除,然后返回地址 ;如果找到了一块比较大的,它会先拆分内存,然后更改 [ 可用内存集合 ] 中的数据,然后返回地址 ; 如果没有找到可用的内存 , 那么分配函数会调用操作系统内存, 从系统整体的内存池中给程序增加内存大小,当然,这种调用的开销更大。内核返回的内存,可能会缓存在物理Ram中,但是由于虚拟内存的缘故,也有可能是在硬盘空间中,在首次遍历的时候,系统可能会需要遍历一遍可用的内存块列表,从而加载这部分空间到实际的物理Ram中,当然这一步操作肯定也是开销巨大的。
    刚才提到的 [ 可用内存集合 ] 这个是指定的程序所有线程共享的资源,所以对这一个结构进行的任何操作都必须是线程安全的。但是一旦多线程并且涉及到了共有的数据会发生改变这种情况,就不可避免的会发生资源竞争的情况。这样也会拖慢程序的性能。
    同样地,当一部分的内存被释放的时候,从概念上说,这部分内存会被重新放入到 [ 可用内存集合 ] 中 , 实际的实现中,不同的操作系统,不管是分配或者是释放内存,都会有不同的策略,不过绝大多数的释放内存的操作都会尝试和临近的未使用内存块合并。无论是分配或者释放内存,都有相同的问题,降低缓存效率以及数据竞争问题。

.3 减少动态变量的使用

    通常,动态分配的变量可以在很多方面被更换掉,改用静态变量。

静态创建类实例

--- // Method 1 
MyClass* myInstance = new MyClass(***);	//创建一个动态变量
--- // Method 2
MyClass myInstance = MyClass ( *** ) ; // 静态创建
--- // Method 3
MyClass  myInstance( *** ) ;//静态创建

    上面的方法中,方法3要比方法二好一点,具体的可以从构造函数角度进行分析(复制构造 – 移动构造);

二段初始化

    假设在一个类的初始化阶段当中包含了象文件操作、内存分配等有可能执行失败的操作,那可以将这一部分危险的操作和安全操作分开执行,当危险操作执行完成执行检查执行的状态,如果失败了,那就认为类的初始化失败了,从而在上层返回null。后续会有博文介绍,以及具体的代码实例。

使用静态数据结构

    C++中常用的几种数据结构,vector \ list \ string \ map 等,他们都会有分配内存插入的操作,是否有其他的容器可以进行替代呢?
    1. std::array 替代 std::vector
        因为array 与 c风格的数组的性能差不多,所以在某些情况下,可以直接使用std::array 进行替换。
    2. 事先在栈上申请大块缓冲区
        之前的第四章string的性能优化中我们有看到,提前将缓冲区分配好,然后对字符的操作放到已经分配的缓冲区中进行,这样对于性能的优化是非常巨大的。
    3. 静态创建链式数据结构
        可以使用静态初始化的方式构建具有链式数据结构的数据。例如,可以静态地创建树形结构。
    4. 在数组中创建二叉树
    5. 用环形缓冲区替代双端队列
        环形缓冲区的数据是一个数组的结构,队列的首尾通过数组的索引对数组的长度取余来给定。环形缓冲区和队列他们对push_back()和 pop_front()以及随机访问的时间开销都是常量级的。但是list的大小是容易改变的,而对于环形缓冲区来说,只要生产者生产出来的数据能够及时被消费者处理掉,那缓冲区就不需要进行扩展。所以环形缓冲区的大小,就是生产者生产的速度领先消费者的速度的大小,我们可以利用这个特性来限制使用者特化某种通用的数据队列,而同样地这种操作也使得性能优化有了可行的地方。

使用std::make_shared 代替new T

    对于一段常见的代码,看看其中会有哪些开销:

std::shared_ptr<MyClass> p(new MyClass("hello", 123));

    首先这段代码会调用两次内存管理器,第一次用于创建MyClass的实例,第二次用来创建std::shared_ptr的内部结构。
这种繁琐的方式在被察觉之后,于是就出现了std::make_shared的方式,这个函数可以一次性分配内存同时保存以上两种数据结构。
使用方式很简单:

std::shared_ptr<MyClass> p = std::make_shared<MyClass>("hello", 123);
--- // C++11
auto p = std::make_shared<MyClass>("hello", 123);

不要无谓的共享所有权

    在C++11 中增加了所有权的概念,最常见的所有权的共享就是shared_ptr了,但是shared_ptr会有性能的损耗,参看之前的内容。在一个对象的所有权重叠的情况下,这会产生一些不必要的性能开销。

	extern void fiddle(std::shared_ptr<Foo> f);    
{
	... 
	shared_ptr<Foo> myFoo = make_shared<Foo>(); 
	... 
	fiddle(myFoo);//复制传参
}

    在函数调用的部分,会进行复制传参,在fiddle函数的作用域中,会首先将myFoo进行复制,其中的计数会增加,然后fiddle函数执行完毕,在fiddle中的局部变量myFoo就会被析构,然后引用计数减少,由于引用计数会涉及到原子操作,所以这样会导致不必要的性能浪费。解决办法是传递的可以是引用,也可以直接传递具体的指针。

//方法1
extern void fiddle(std::shared_ptr<Foo> f);    
//方法2
extern void fiddle(Foo* f);    
{
	... 
	shared_ptr<Foo> myFoo = make_shared<Foo>(); 
	... 
	fiddle(myFoo.get());//传递实际的指针
}

使用unique_ptr管理动态变量

    当一个动态变量的生命周期期间,有很多函数会使用这个函数或者赋值给变量等操作,这时候我们可以使用一个 ”主指针“ 来管理这个动态变量。

.4 减少动态变量的重新分配

这里可以参考第四章的项目代码 ’ 尝试优化字符串 ’ 部分,针对std::string 的操作来说,有很多方法可以帮助我们减少内存的操作次数。以下是几种常见的方式:
1 预先分配动态变量以防止重新分配
比如std::string 的reverse函数, 或者std::unordered_map的reverse , 它会指定链接到其他数据结构的骨干数组( 桶的链表 )。
2 在循环外创建动态变量

.5 移除无谓的复制

    对于内置的变量类型,实际的复制操作的时候,其实生成的指令码很简短,对内存的访问次数也是有限的,参看 ‘ 内存相关的运行时开销 ’ 部分,但是如果复制的对象是一个自定义的类型,而这个类的复制操作的开销可能会无限大。
    以下是几种复制会出现的情况:
    • 初始化(调用构造函数)
    • 赋值(调用赋值运算符)
    • 函数参数(每个参数表达式都会被移动构造函数或复制构造函数复制到形参中)
    • 函数返回(调用移动构造函数或复制构造函数,甚至可能会调用两次)
    • 插入一个元素到标准库容器中(会调用移动构造函数或复制构造函数复制元素)
    • 插入一个元素到vector中(如果需要重新为vector分配内存,那么所有的元素都会通过移动构造函数或复制构造函数复制到新的vector中)

在类定义中禁止不希望发生的复制

    如果可以确定一个类,它不需要面对复制的情况,那么就可以屏蔽掉复制构造和赋值运算符 ’ operator= ’ ,以下是代码部分:

class BigClass {
private:
    BigClass(BigClass  const & oth);
    BigClass & operator=( BigClass const &oth);
public:
    BigClass() = default;
};

在C++11中,可以将函数改成public属性并且显示的屏蔽掉;

class BigClass {
public:
    BigClass(BigClass  const & oth) = delete;
    BigClass & operator=( BigClass const &oth) = delete;
    BigClass() = default;
};

移除函数调用上的复制

通常情况下,在函数调用中将传参的方式改成引用传参会减少无谓的复制操作,能够相应的提升性能。但是实际上,解引用也会产生开销,如果在函数中多次的出现了解引用操作,那通过引用传参提升的性能有可能会递减。
同时要避免引用可能会导致的一些问题:

extern void func(Foo& a, Foo& b);
{
	Foo myFoo();
	func( myFoo, myFoo );
}

移除函数返回上的复制

一段简单的代码:

MyClass func( *** ){
	auto ret = MyClass(); // 第一次复制
	...
	return ret;
};

{
	MyClass gotIt = func( *** );	//第二次复制
}

通常情况下,通过调用函数返回类型的过程中,会产生两次复制操作,其实这中间有性能的浪费。目前来说,有的编译器会针对这种情况进行优化,通常成为复制省略(copy elision)或者返回值优化(return value optimization,RVO)。但是RVO的产生情况非常严格,有很多情况下不一定编译器会进行返回值优化。
另一种方法就是将返回值放到传入的参数中,通过传引用的方式将返回值传递出去。这种方法的性能要优于返回值的方法。

免复制库

在很多的库中,通过传递引用的方式在多层库当中进行传递的开销是很小的。

实现写时复制

通常来说,当一个带有动态变量的对象被复制时,也必须复制该动态变量。这种复制被称为深复制。通过复制指针,而不是复制指针指向的变量得到包含无主指针的对象的副本,这种复制被称为浅复制。
写时复制的核心思想是,在其中一个副本被修改之前,一个对象的两个副本一直都是相同的。因此,直到其中一个实例或另外一个实例被修改,两个实例能够共享那些指向复制开销昂贵的字段的指针。写时复制首先会进行一次“浅复制”,然后将深复制推迟至对象的某个元素发生改变时。

.6 移动语义

在很多情况下,如果编译器能将我们申请的变量从这里转移到另一个变量中,而不需要触发复制操作,这对于程序的性能来说会有很大的提升。
考虑以下几种情况:

{
	MyClass a ;
	MyClass ret = a;
	return ret ;
}
{
	std::vector < MyClass > V_A;
	std::vector < MyClass > V_B;
	std::swap(V_A,V_B);
}
...

移动语义的移动部分

    为了实现移动语义,编译器需要知道哪些变量是临时的,比如new出来的对象或者函数的返回值,这个临时变量可以被初始化、赋值给一个变量或是作为表达式或函数的参数。但是接下来它会立即被销毁。这种的变量称为右值
    与右值相对应的就是左值,他是通过名称定义的一个变量。比如 int a = 2 ;
一个很好用的方法判断左值或者右值,就是使用& , 如果能取出地址的就是左值,相反则是右值。
在C++11 中,增加了右值引用的类型,就是T&& 。同理,在类的构造函数以及赋值函数中,也增加了移动构造和移动赋值运算符。他们的定义如下:

Class MyClass{
public:
	...
	MyClass ( MyClass && rhs){...}		//通过右值构造函数
	Myclass ( MyClass const &rhs):...{};	//移动构造函数
}

移动语义的优势

1. 移动实例至std::vector

    在向std::vector中添加元素的时候,可以使用emplace_back进行添加,这会尽量减少不需要的复制。
    如果要自己在类中添加支持的话,除了要给类添加移动语义外,还要在移动函数上声明noexcept , 因为vector中有一个强异常安全保证(strong exception safety guarantee),就是说它希望当在执行某个操作的过程中出现了异常的时候,vector的状态和执行操作之前一样。复制构造函数不会改变原始的对象,但是移动构造却会销毁原来的对象。所以这里就会有一个冲突: 用户希望使用移动语义进行移动,但是vector却希望所有的操作都是强安全保证的,所以我们需要在自己的类中的移动语义上增加noexcept声明,就是说在进行这个类的移动构造的时候,不会有任何异常抛出。

2. 右值引用作为参数是一个左值

	std::string MoveExample(std::string&& s) {     
	std::string tmp(std::move(s)); //  注意!现在s是空的    
	return tmp; 
}    
	... 
	std::string s1 = "hello"; 
	std::string s2 = "everyone"; 
	std::string s3 = MoveExample(s1 + s2);

    这里解释一下,当用户在调用这个函数并且传进一个右值引用的时候,这个右值引用作为实参传入到MoveExample函数中,这时候这个右值就成了一个左值(因为右值是没有名字的),然后这个"s"变量被显示的通过std::move()函数转换成右值,然后通过std::string的移动构造函数进行创建tmp变量,然后这个tmp变量作为返回值被返回到调用处。
    当我们在执行 std::string s3 = MoveExample(s1 + s2); 这一步的时候,首先是s1+s2的和作为了一个临时变量,之后这个临时变量传入到MoveExample函数中,然后这个临时变量有了变量名称"s",成了左值,然后tmp通过移动构造的方式进行构造,之后这个tmp变量复制到一个临时的返回值中,这个返回值传递给s3,然后调用s3的复制构造进行构造。不过实际上这种情况下编译器会进行RVO,也就是说,参数s会被直接移动到s3的存储空间中。RVO通常比移动更高效。

3. 不要返回右值引用

    如果在函数中定义了返回一个右值引用的话,这样编译器便不会进行返回值优化,而返回值优化的实现方式是编译器向函数传递一个指向目标的引用作为隐藏参数,来移除从未命名的临时变量到目标的复制。这样只会执行一次移动操作。而如果我们使用返回右值引用的话,会调用两次移动操作。所以在能使用RVO的地方要尽量避免返回右值引用。

4. 有移动语义的父类以及类成员

    查看以下的一段代码:

class Base {
.	..
}; 
class Derived : Base {     
	...     
	std::unique_ptr<Foo> member_;     
	Bar* barmember_; 
}; 
Derived::Derived(Derived&& rhs)   
	: Base(std::move(rhs)),     
	member_(std::move(rhs.member_)),     
	barmember_(nullptr) {     
	std::swap(this->barmember_, rhs.barmember_); 
	}

    在之前有聊到过,要想为一个类实现移动语义,那就必须要为它的父类以及类成员都实现移动语义。
在这里需要注意的是,我们只有显示的将rhs转换成右值,才能触发父类的移动构造函数,即这段代码
"Base(std::move(rhs)) "
    以下是一段移动赋值函数

void Derived::operator=(Derived&& rhs) {     
	Base::operator=(std::move(rhs));     
	delete(this->barmember_);     
	this->barmember_ = rhs.barmember_;     
	rhs.barmember_ = nullptr; 
}

    这里有个地方需要注意,我们是先将this->barmember_通过delete手动删除掉,因为如果我们不主动删除这部分数据,那么这部分数据的生命周期就会进入到rhs里面去,这个时候barmember_的释放是需要在rhs释放的时候才会释放。如果barmember_里面的数据是一个很大内存的数据,这样会导致潜在的内存增多的问题。

.7 扁平数据结构

    当一个数据结构中的元素被存储在连续的存储空间中时,我们称这个数据结构为扁平的。相比于通过指针链接在一起的数据结构,扁平数据结构具有显著的性能优势。

  • 和通过指针链接起来的数据结构相比,这种扁平的数据结构调用内存管理器的开销更小。比如对于map\list\queue\unordered_map\等数据结构,他们在每次新增数据的时候都会增加一个新的动态变量,而vector则比较少。

  • array\vector的数据结构相对链式容器来说会更加简单,这样会减少指针的开销。同时,连续的存储空间具有更好地缓存局部性。

    
    
    
    
    
    

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值