现在的位置: 首页 > 综合 > 正文

<高质量C++编程指南>学习笔记(一)

2013年09月06日 ⁄ 综合 ⁄ 共 4583字 ⁄ 字号 评论关闭

一、注意for循环执行效率的问题

1、原则是尽量不要打断循环“流水线”作业

流水线的定义:流水线(pipeline)Intel首次在486芯片中开始使用的。流水线的工作方式就象工业生产上的装配流水线。在CPU中由5~6个不同功能的电路单元组成一条指令处理流水线,然后将一条X86指令分成5~6步后再由这些电路单元分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高了CPU的运算速度。

2、对于多重循环,如果有可能应将循环最长的放最内层,最短的放最外层

问题:a)改成递减,代码优化后岂不是效率更高?

      b)原文所举例子,说是为减少CPU跨切循环层的次数,从汇编代码来分析:

00401D9A      xor     eax, eax                         

00401D9C      mov     dword ptr [ebp-8], eax            ===>col=0row=0

00401D9F  __  xor     edx, edx                          ===>外循环开始

00401DA1 |    mov     dword ptr [ebp-4], edx            ===>row=0col=0

00401DA4 | __ mov     ecx, dword ptr [ebp-4]            ===>内循环开始

00401DA7 | |  lea     ecx, dword ptr [ecx+ecx*4]

00401DAA | |  lea     eax, dword ptr [ebp+ecx*4-7DC]

00401DB1 | |  mov     edx, dword ptr [ebp-8]

00401DB4 | |  mov     ecx, dword ptr [eax+edx*4]

00401DB7 | |  add     dword ptr [ebp-C], ecx            ===>sum = sum+a[row][col]

00401DBA | |  inc     dword ptr [ebp-4]                 ===>col++row++

00401DBD | |  cmp     dword ptr [ebp-4], 64             ===>row=?col=?

00401DC1 | |_ jl      short 00401DA4                    ===>if(row<100)(col<5)continue   

00401DC3 |    inc     dword ptr [ebp-8]                 ===>col++row++

00401DC6 |    cmp     dword ptr [ebp-8], 5              ===>col=?row=?

00401DCA |__  jl      short 00401D9F                    ===>if(col<5)(row<100)continue

00401DCC      mov     eax, dword ptr [ebp-C]            ===>循环结束

我的理解:不管是哪种方式,内循环中的指令共要执行500次;而进入内循环前和从内循环返回后所执行的共五条指令在第一种方式下要执行100次,在第二种方式下只要执行5次。假设按流水线每条指令在一个CPU时钟周期内完成,这样后者能比前者快(100-5* 5个时钟周期完成。

发现:1、常量数组存放于.data段,函数调用的时候由.data拷贝到栈中,就是说编译的时候已经分配

2、形如char *b=”dffff”b在栈上,字符串在.data上,函数调用的时候将字符串地址赋予b

 

二、关于类常量

1const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。

2、不能在类声明中初始化const数据成员,只能在类构造函数的初始化表中进行

3、通过枚举常量来建立在整个类中都恒定的常量。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数

 

三、函数

1、注意_cdecl_stdcall_fastcall之间的区别:主要是入栈出栈方式和参数传递方式。知识点

2、如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。是不是对于复杂数据类型都可以借鉴?

引申:引用的内部原理:引用是寄存器间接寻址,堆栈中是变量所在地址的拷贝,相关代码如下

mov ax,[ebp+0x08]

mov [ax],value

值传递是基址变址寻址,堆栈中仅仅是变量的拷贝,相关代码如下

mov [ebp+0x08],value

发现:CodeGuard在代码中插入了CG32.__cg_newCG32.__cg_delete等函数

试验:通过看汇编代码,在未显示约定函数调用方式的情况下,在传递对象引用的时候是直接把对象的地址入栈的,_stdcall方式;在传对象的时候,函数调用前先构造了临时对象,_cdecl方式,在函数返回前调用了析构函数。值得注意的是并没有调用构造函数

3、关于形参: 如func()C会认为是类型和数目不确定的参数;C++认为是void参数。----这个有待确定

4、不要省略返回值的类型。

C语言中,凡不加类型说明的函数,一律自动按整型处理。这样做不会有什么好处,却容易被误解为void类型。

C++语言有很严格的类型安全检查,不允许上述情况发生。由于C++程序可以调用C函数,为了避免混乱,规定任何C++/C函数都必须有类型。如果函数没有返回值,那么应声明为void类型。

5、如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。

String类,重载的“operator=”用引用传递,return语句直接返回*this的引用,而不是把*this拷贝到保存返回值的外部存储单元之中,增加了不必要的开销,降低了赋值函数的效率。String& operate=(const String &other);

重载的“operator+”,如果改用“引用传递”,那么函数返回值是一个指向局部对象temp的“引用”。由于temp在函数结束时被自动销毁,将导致返回的“引用”无效。friend  String operate+( const String &s1, const String &s2);

是不是对于复杂数据类型都可以借鉴?

疑问:为什么没有friend修饰则只许有一个右侧参数?引申:friend的用法

6、如果返值是对象,必须要考虑return的效率,如创建一个临时对象并返回

      return String(s1+s2);

编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了效率。然而:

String temp(s1 + s2);

return temp;

a)首先,temp对象被创建,同时完成初始化;

b)然后拷贝构造函数把temp拷贝到保存返回值的外部存储单元中;

c)最后,temp在函数结束时被销毁(调用析构函数)。

实验:

前者:operator+内临时对象构造、operator+返回局部临时对象拷贝构造、operator+内临时对象析构、全局临时对象拷贝构造、operator+返回局部临时对象析构、全局临时对象析构。共创建了三次临时对象,前面所说的“外部存储单元”应该是指接收函数返回的全局临时对象,如全局定义了一个对象地址是1245064,全局临时对象是1245060,很明显。

后者:operator+内临时对象构造、operator+返回局部临时对象拷贝构造、operator+内临时对象析构、temp对象拷贝构造、operator+返回局部临时对象析构、全局临时对象拷贝构造、temp对象析构、全局临时对象析构。共创建了四次临时对象

问题:1、不要忽视了operator+内部和返回时产生的临时对象,原文中语焉不详。

2、为什么不用return s1+s2String temp=s1+s2呢?这样可以省一次临时对象的拷贝构造和析构

 

对于int,float,double等变量,效率提升不大,但是可以使代码更简洁。

 

四、断言

1、断言的设计要保证其实现的正确性、易于理解,难以理解的断言常常被程序员忽略,甚至被删除。

2、使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。

 

五、引用与指针(可参阅前面几大条对引用的研究)

1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。

2)不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。

3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。

 

做为参数传递,引用和指针的实现类似,之所以要引用,是因为指针功能强大,对内存的访问很随意,这样也意味着危险性,通过引用可以避免类似的危险出现。

 

六、内存管理

1、内存分配方式有三种(参见第一大条关于内存分配的说明)

从全局/静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

补充说明:a) 静态存储区位于EXE文件的.data头,也就是说在编译的时候已经分配。

          b) 初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放

          c) 文字常量区:常量字符串就是放在这里的。 程序结束后由系统释放

          d) 程序代码区存放函数体的二进制代码。也就是.text

具体参见“堆和栈有什么区别“这篇文章,说得比较详细。

知识点:关于PE的说明,可以做深入了解。

在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

     从堆上分配,亦称动态内存分配。程序在运行的时候用mallocnew申请任意多少的内存,程序员自己负责在何时用freedelete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多,容易产生碎片。

补充说明:用VirtualAllocVirtualCopy是不错的选择,注意reservecommit参数的区别。

2常见的内存错误

要特别注意内存越界的情况,这种错误不容易发现。

注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。

内存分配虽然成功,但是尚未初始化就引用它。

使用freedelete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。

3、指针与数组比较

对于数组,sizeof包括’/0’strlen不包括’/0’

对于指针,sizeof4 ,并不是指针所指向的内存块容量

注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。

抱歉!评论已关闭.