搞笑C++复习笔记

来源:互联网 发布:sqlserver无法启动 编辑:程序博客网 时间:2024/06/07 16:32

高质量软件开发

笔记2   static相当于是类所有对象的一块共用存储区

主要内容:

•  掌握函数使用堆栈的原理、作用

  掌握函数参数的使用规则、含义、实例

  理解不定参数函数的使用方法和原理

  掌握回调函数的原理与作用

I.  内存分配

  从静态存储区域分配

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

列入全局变量,static变量

  在栈上创建

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

  从堆上分配,也称为动态内存分配

程序在运行的时候用mallocnew申请多少的内存,程序员自己负责在何时用freedelete释放内存。

动态内存的生存期由程序猿决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。

 

程序的内存空间:

代码区(code area)

全局数据区(data area)

堆区(heap area)

栈区(stack area)

  栈区:由编译器自动分配释放

  由编译器自动分配释放,存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址等。其操作方式类似于数据结构中的栈

   堆区:一般由程序员分配释放

  一般由程序猿分配释放,若程序猿不释放,程序结束时可能由OS回收。分配方式类似于链表

 

   全局区(静态区):存储全局变量

  存放全局变量、静态数据、常量。程序结束后,系统释放它。

  文字常量区-

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

程序代码区-

  存放函数体(类成员函数和全局函数)的二进制代码。

II. 函数

  静态链接

  静态链接就是在生成可执行文件的时候,把所有需要的函数的二进制代码都包含到可执行文件中去。

  因此,连接器需要知道参与链接的目标文件需要哪些函数,

  同时也要知道每个目标文件都能提供什么函数,这样链接器才能知道是不是每个目标文件所需要的函数都能正确的链接。

  如果某个目标文件需要的函数在参与目标文件中都找不到的话,链接器就报错了。

 

缺点:

静态链接器很浪费磁盘空间和内存空间。

标准库中那些函数会被放到每个静态链接的可执行文件中,在运行的时候,这些重复的内容也会被不同的可执行文件加载到内存中去

同时,如果静态库有更新的话,所有可执行文件都得到重新链接才能用上新的静态库。

  动态链接

  从动态库的角度看,动态库像普通的可执行文件一样,有其代码段和数据段。

  为了使得动态库在内存中只有一份,需要做到不管动态库装载到什么位置,都不需要修改动态库中代码段的内容,从而实现动态库中代码段的共享。

  而数据段中的内容需要做到进程间的隔离,因此必须是私有的,也就是每个进程都有一份。

  因此,动态库的做法是把代码段中变化的内容放到数据段中去,这样代码段中剩下的就是不变的内容,就可以装载到虚拟内存的任何位置。

  那代码段中变化的内容是什么,主要包括了对外部函数和变量的引用

   

III.     函数的作用与实现

  函数静态链接与动态链接

  函数的堆栈:实际上使用的是程序的堆栈段内存空间,虽然程序内存是静态分配,但函数堆栈却是在调用时才动态分配的;

原因1事前无法在编译前决定所需大小

原因2函数在调用完后必须动态释放

 

IV. 函数的堆栈作用

  在进入函数前保存环境变量和返回地址

  进入函数时保存实参的拷贝

  在函数体内保存局部变量

V.  参数规则

  const& 方式传递对象

  stdarg.h是C语言中C标准函数库的头文件,主要目的为让函数能够接收可变参数

  VA_LIST在C语言中解决变参问题的一组宏,所在头文件#include<stdarg.h>用于获取不确定个数的参数。

  va_start 函数名称,读取可变参数的过程其实就是堆栈中,使用指针,遍历堆栈段中的参数列表,从低地址到高地址一个一个地把参数内容读出来的过程。

VI. 可变参数函数实现原理

  C语言用宏来处理这些可变参数。根据参数入栈的特点从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址

 

 

1.  函数调用规范

2.  参数规范

参数的书写要完整,如果函数没有参数要用void填充

参数命名要恰当,顺序要合理。

如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改

如果输入参数以值传递的方式传递对象,则宜改用“const&”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率

避免函数有太多的参数,参数个数尽量控制在5个以内。如果参数太多,在使用时容易将参数类型或顺序搞错

尽量不要使用类型和数目不确定的参数。

 

第二部分

内容:

  掌握返回值的规则与含义

  变量的类型与对应的作用域

  函数内部实现中易出现的问题及处理

  掌握使用const修改函数各部分作用

 

返回值规则

不要省略返回值的类型

•  C语言中,凡不加类型说明的函数,一律自动按整型int处理。这样做容易被误解为返回void类型

  C++语言有很严格的类型安全检查,不允许上诉情况发生。如果函数没有返回值,那么因声明为void类型

  函数名字与返回值类型在语义上补了冲突。 如 intgetchar(void)

  不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return语句返回。

  有时候函数原本不需要返回值,但为了增加灵活性如支持链接式表达,可以附加返回值。

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

  函数内部实现规则:根据经验,我们必须在函数体的“入口处”和“出口处”把关把严,从而提高函数质量(正确使用断言assert())

  在函数体的“出口处”,对return语句的正确性和效率进行检查。

  (1)return语句不可返回“栈内存”的指针“或者”引用“

  (2)搞清楚返回的究竟是值”、“指针”还是“引用”

  (3)如果函数返回值是一个对象,要考虑return语句的效率

  函数的功能要单一,不要设计多用途函数

  函数体的规模要小,一般不超过50行。

  不仅要检查输入参数的有效性,还要检查例如全局变量、文件句柄的有效性

  用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解为错误情况

 

存储类型及作用域

  标准C语言定义了4中存储类型:永久和临时

  externstatic:永久

  auto,register:临时的,只能声明局部变量或常量

默认情况下的规则:

  全局常量的默认为static类型,仅能同一个编译单元内的函数所调用,如果加上extern,则可以在其它编译单元中调用

  局部变量默认为auto类型

 

使用断言

  断言assert是仅在Debug版本起作用的宏,它用于检查不应该发生的情况。如果assert的参数为假,那么程序就会中止

void *memcpy(void *pvTo, const void *pvFrom, size_t size)

{

       assert((pvTo!= NULL) && (pvFrom != NULL));

 }

 

■  使用CONST修饰函数输入参数

❑   输入参数采用指针传递,可以防止意外地改变

 

■  使用CONST修饰函数输入参数(续)

❑   输入参数采用值传递,无需使用CONST修饰。因为即使在函数内部改变了该参数,也仅是在堆栈上改变了拷贝的值。

❑   虽然形参在语义上等价于局部变量,但与普通局部变量还不能完全等同,因为该参数包含了特殊的初值。

❑   如果在函数体内多次使用该变量,加上CONST也可防止在函数体内无意地改变。

■  使用CONST修饰返回值

❑   返回值是一种契约性常量,不能被直接修改,并且也只能被赋值给加CONST修饰的同类型变量。

 

■  CONST修饰函数

❑   int Stack::GetCount(void) const

❑   任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误

 

答:正确,用“引用传递”替换“值传递”可以提高效率。为什么?因为值传递时可能会去创建对象调用构造函数,而引用传递不会。所以提高了效率。

而有些场合只能用“值传递”而不能用“引用传递”。有时候只需要传递一个值过去就行了,而不要求改变原值。这样就只能用“值传递”。如果在函数中改变了不能改变的值,就可能报错。而值传递相当于是一个拷贝,改变拷贝的对象,并不会引起原对象的改变。

 

内存管理

 

 

1.  内存分配未成功却使用了它。

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

3.  内存分配成功并且已经初始化,但操作越过了内存边界

4.  忘记释放内存

5.   

常见内存错误及其对策

 

 

 

指针与数组对比

数组名是一个地址,地址的本质是个常量  

 

指针参数是如何传递内存的

如果函数的参数是一个指针,不要指望用该指针去申请动态内存

 

杜绝野指针

 

 

常量

1、字面型常量:字面常量只能引用不能修改,且无法取出地址。

cha


r c=‘a’;

char *c=‘a’;

2、符号常量define定义的宏常量和const定义的常量如果是一个const符号常量的地址可以在运行时修改其值。

 

 

 

 

3、契约型常量:契约型常量:并未使用const关键字,但被作为一个const对象使用。

 

4、枚举型常量:使用enum来定义的相关常量的集合。

 

正确使用常量:(建议)

公开的放在头文件中头部,不公开的放在原文件头部,不同模块之间的常量建议统一存放

如果一些常量与其他常量密切相关,应在定义中包含这个关系,而不应给出孤立的值。

 

Const与define比较

相同点:

1、两者都可以用来定义常量,而const具有更多的优点

2、const有数据类型,而宏常量没有


,编译器无法检查,直接替换将会产生意料不到的效果

不同点:

3、编译器的处理方式不同

  define宏是在预处理阶段展开

  const常量是在编译运行阶段使用

4、类型和安全检查不同

  define宏没有类型,不做任何类型检查,仅仅是展开

  const常量有具体的类型,在编译阶段会执行类型检查

5、存储方式不同

  define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。

  const常量会在内存中分配,可以在堆中也可以在栈中

  const可以节省空间,避免不必要的内存分配

  const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像define一样给出立即数,所以const定义的常量在程序运行过程中只有一份拷贝,而define定义的常量在内存中有多份拷贝

 

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

2、不能在类声明中初始化const数据成员。因为类的对象未被创建时,类只相当于一个模板,除非使用static。

3、const数据成员的初始化只能在类构造函数的初始化表中进行;

4、使用枚举常量来是想在整个类中都恒定的常量

 

Const和Static的区别

const定义的常量在超出其作用域之后其空间会被释放,而static定义的静态常量在函数执行后不会释放其存储空间。

     static表示的是静态的。类的静态成员函数、静态成员变量是和类相关的,而不是和类的具体对象相关的。即使没有具体对象,也能调用类的静态成员函数和成员变量。一般类的静态函数几乎就是一个全局函数,只不过它的作用域限于包含它的文件中。

      在C++中,static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化,如:double Account::Rate=2.25;static关键字只能用于类定义体内部的声明中,定义时不能标示为static

      在C++中,const成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。

     const数据成员 只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类的声明中初始化const数据成员,因为类的对象没被创建时,编译器不知道const数据成员的值是什么。

      const数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static cosnt

 

指针与数组对比:

  数组要么在静态存储区被创建(如全局数组),要么在栈上创建。数组名对应着一块内存,其地址和容量在生命期内保持不变,只有数组的内容可以改变。

  指针可以随时指向任意类型的内存块,特征是“可变”,所以我们常用指针来操作动态内存。指针比数组更灵活但也更危险。

动态内存会被自动释放吗?

(1)  指针消亡了,并不表示它所指的内存会被自动释放。

(2)  内存被释放了,并不表示指针会消亡或者成了NULL指针

 

野指针:不是NULL指针,而是指“垃圾”内存的指针。一般不会错用NULL指针,因为if语句很容易判断某个指针是否为NULL。因为if语句不能对野指针起作用,所以野指针是非常危险的。

 

野指针的成因:

(1)  指针变量没有被初始化,如果没有被初始化,编译器会分配给这个指针一个随机的地址。

(2)  指针Pfree或者delete之后没有被置为NULL,如果没有被置为NULL,指针P很有可能会只想一个不确定的地址

 

 

 

对比于C语言的函数,C++增加了重载(overloaded)、内联(inline)、constvirtual四种新机制。

 

其中重载和内联机制既可用于全局函数也可用于类的成员函数,constvirtual机制仅用于类的成员函数。

 

用内联取代宏代码:C++语言支持函数内联,其目的是为了提高函数的执行效率(速度)

5.9 慎用内联

1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。类的构造函数和析构函数容易让人误解成使用内联更有效。

类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。

所以不要随便地将构造函数和析构函数的实现体放在类声明中。

一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了 inline不应该出现在函数的声明中)