本文转载自微信公众号《程序喵大师》,作者程序喵大师。转载本文请联系程序大师喵公众号。大家好,我是逐渐过时的程序喵喵。在这篇文章中,我们将继续分析C++中各种操作的效率,包括不同类型变量的存储效率,智能指针、循环、函数参数、虚函数、数组等的使用效率,以及如何做有针对性的优化,或者选择更有效的替代方案。详细目录见下图:类和结构体现了当今面向对象编程的流行,个人认为这是一种让代码更清晰、更模块化的方式。面向对象编程风格的优缺点是显而易见的。优点是:如果变量是同一个结构或类的成员,一起使用的变量也存储在一起,这样数据缓存更有效。类成员变量不需要作为参数传递给类成员函数,省去了参数传递的开销。缺点是:有些程序员把代码分成太多小类,没有必要,效率低下。非静态成员函数有一个this指针,会作为隐式参数传递给函数,会产生一些开销,尤其是在32位系统中,寄存器是稀缺资源,this指针会占用一个寄存器。虚函数效率较低。类和成员函数的开销不是特别大。如果面向对象的风格能够让程序结构更加清晰,只要我们避免在程序最关键的部分使用过多的函数调用,我们就不必担心它的开销。.类的数据成员创建类或结构的实例时,其数据成员按照声明它们的顺序连续存储。大多数编译器内存对齐结构,这会导致结构或具有混合成员大小的类中未使用字节的空洞。structS1{shortinta;//2bytes//6holesdoubleb;//8intd;//4//4holes};S1ArrayOfStructures[100];在这里,a和b部分之间有6个未使用的字,因为b必须从可被8整除的地址开始。末尾有4个未使用的字节。这样做的原因是数组中S1的下一个实例必须从可被8整除的地址开始,以便将其b成员按8对齐。通过将最小的成员放在最后,未使用的字节数可以减少到2:structS1{doubleb;//8intd;//4shortinta;//2//2孔};S1ArrayOfStructures[100];这样重新排序,使结构体变小了8个字节,整个数组变小了800个字节。结构对象和类对象通常可以通过重新排序数据成员来变小。如果不确定结构或它的每个成员有多大,可以使用sizeof,其返回值包括对象末尾的任何未使用字节。如果成员距结构或类开头的偏移量小于128,则访问数据成员的代码会更紧凑,因为偏移量可以表示为8位有符号数。如果距结构或类开头的偏移量为128字节或更大,则偏移量必须表示为32位数字(指令集在8到32位之间没有偏移量)。例如:structS2{inta[100];//400intb;//4intReadB(){returnb;}};b的偏移量为400。任何通过指针或成员函数(如ReadB)访问b的代码都需要将偏移量编码为32位数字。如果交换a和b,两者都可以通过编码为8位有符号数的偏移量访问,或者根本没有偏移量。这使得代码更紧凑,可以更有效地使用缓存。因此,建议在结构或类声明中,大数组和其他大对象放在最后,最常用的数据成员放在前面。如果前128个字节不能包含所有数据成员,则将最常用的成员放在前128个字节中。每次声明或创建类的新对象时,类的成员函数都会生成数据成员的新实例。但是无论类有多少个实例,成员函数都只有一个。调用成员函数与使用结构指针或引用调用简单函数一样快。structS3{inta;intb;intSum1(){returna+b;}};intSum2(S3*p){returnp->a+p->b;}intSum3(S3&r){returnr.a+r.b;}这三个函数Sum1、Sum2和Sum3做完全相同的事情,而且它们同样高效。一些编译器会为所有三个函数生成完全相同的代码。Sum1有一个隐含的this指针,它与Sum2和Sum3中的p和r做同样的事情。无论您是使函数成为类的成员,还是为它提供指向类或结构的指针或引用,都只是编程风格的问题。某些编译器通过在寄存器中传输this指针,使Sum1在32位Windows上比Sum2和Sum3更高效。静态成员函数不能访问任何非静态数据成员或非静态成员函数。静态成员函数比非静态成员函数更快,因为它不需要this指针。如果不需要任何非静态成员访问的成员函数可以通过将它们设为静态来加速。虚函数虚函数用于实现运行时多态性,这是面向对象编程效率低于非面向对象编程的主要原因之一。如果可以避免使用虚函数,则可以提高程序的运行效率。一般来说,可以考虑编译时多态而不是运行时多态。关于虚函数为什么会导致程序效率低下,可以看我之前的文章:《》RTTIRTTI,运行时类型识别,会给所有类对象添加额外的信息,所以效率不高,可以考虑关掉RTTI提高程序效率的选项。继承派生类的对象的实现方式与包含父类和子类成员的简单类的对象相同。以相同的速度访问父类和子类的成员。一般来说,我们可以认为使用继承几乎没有性能损失。但是,在这些情况下,效率会稍微降低:子类包含父类的所有数据成员,即使子类不需要父类的数据成员。父类数据成员的大小被添加到子类成员的偏移量。访问总偏移量大于127字节的数据成员的代码稍微不够紧凑。继承多个父类可能会导致通过指向基类指针来更复杂地访问派生类的对象。我们可以通过在派生类中创建对象来避免多重继承,即组合而不是继承:classB1;;intc;};一般来说,只有在有利于程序的逻辑结构时才应该使用继承。位域位可以使数据更紧凑。访问位成员的效率低于访问结构的普通成员。如果可以使用大数组来节省代码空间,牺牲一点效率也未尝不可。使用<<和|组合位域操作比单独编写成员更快。例如:structBitfield{inta:4;intb:2;intc:2;};Bitfieldx;intA,B,C;x.a=A;x.b=B;x.c=C;假设A、B、C的值太小,不会造成溢出,这段代码可以改进为:unionBitfield{struct{inta:4;intb:2;intc:2;};charabc;};Bitfieldx;intA,B,C;x.abc=A|(B<<4)|(C<<6);如果你需要防止溢出,你可以这样做:x.abc=(A&0x0F)|((B&3)<<4)|((C&3)<<6);functionoverload加载的函数只是作为不同的函数处理,和普通函数一样,没有任何性能开销,可以放心使用。运算符重载重载运算符等同于函数。使用重载运算符与使用普通函数一样高效。具有多个重载运算符的表达式会导致为中间结果创建临时对象,这是低效的。例如:classvector{public:floatx,y;vector(){}vector(floata,floatb){x=a;y=b;}vectoroperator+(vectorconst&a){returnvector(x+a.x,y+a.y);}};vectora,b,c,d;a=b+c+d;//可以添加中间对象(b+c),避免为中间结果(b+c)创建临时对象:a.x=b。x+c.x+d.x;a.y=b.y+c.y+d.y;在简单的情况下,大多数编译器可能会自动执行此优化。模板类似于宏,它们的参数在模板编译之前被替换为它们的值。下面的例子说明了函数参数和模板参数的区别://Example7.46intMultiply(intx,intm){returnx*m;}template
