1. 高质量软件开发之道
1.提高软件质量的基本方法
- 在开发过程中防止产生缺陷
- 当刚刚完成工作成果时马上进行质量检查
- 当软件交付后出现缺陷,赶紧补救
2.软件质量属性
- 正确性:软件按照需求正确执行任务的能力
- 健壮性:在异常情况下,软件能够正常运行的能力(容错能力、恢复能力)
- 可靠性:在一定环境下,在一定时间内,程序不出现故障的概率
- 性能:软件的“时间-空间”效率
- 易用性:用户使用软件的容易程度
- 清晰性:工作成果易读、易理解
- 安全性:防止系统被非法入侵的能力
- 可扩展性:软件适应“变化”的能力
- 兼容性:两个或两个以上的软件交换信息的能力
- 可移植性:软件不经修改或稍加修改就可以运行于不同的软硬件环境的能力
3.质量、生成率和成本
- 软件质量与生成率相辅相成,软件质量提高了可促进生产率提高
- 软件过程比较低的企业,应该将质量放在第一,生成率放在第二
- 将高质量和高生成率内建与开发过程之中,就能自然的降低开发成本
4.软件过程改进
- 经典软件过程:需求分析、系统设计、编程、测试、维护
- 过程基本要素:人、工具和技术、方法和规程
- 提供软件过程管理能力:
- 制定合适于本企业的软件过程规范
- 培训员工依据规范来开发产品
- 购买或开发一些软件工程和项目管理工具,提高员工工作效率
5.高质量软件开发的基本方法
- 建立软件过程规范:SPP-精简并行过程
- 软件复用:把大部分时间用在小比例的创新上,把小部分时间用在大比例的成熟工作中
- 分而治之:把一个复杂的问题分解成若干个简单的问题,然后逐个解决
- 优化与折中:优化软件的各个质量属性,但要相互协调,实现整体质量最优
- 技术评审:尽早发现工作成果中的缺陷,帮助开发人员及时消除缺陷
- 测试:发现尽可能多的缺陷,不要把麻烦留给将来
- 质量保证:提供一种有效的人员组织形式和管理方法,通过客观地检查和监控“过程质量”与“产品质量”,从而实现持续地改进质量
- 改错:粗分细找、归纳、推理、回归测试
2. 程序的基本概念
1.程序设计语言
一套规范的集合,主要包括语言使用字符集、数据类型集合、运算符集合、关键字集合、指令集合、语法规则、对特定构造的支持(函数、继承、模板...)
2.语言实现
语言标准规定了标准库的标准接口,但是没有提供其实现。语言实现就是具体地实现一种语言的各种特征并支持特定编程模式的技术和工具(编译器和连接器的实现)。
3.程序库
由具体的语言实现提供,它使用语言本身的基本构造开发而成。
4.开发环境
支持软件开发的一切工具(操作系统、代码编译器、连接器、调试器...)
5.程序的工作原理
程序工程包含:编译单元、资源文件、静态库、配置文件...。一个可执行文件至少包含代码段(可执行语句序列)、静态数据段(全局变量、静态对象、符号表)、堆栈段(线程和函数使用)。
3. 程序设计入门
1.C++/C程序的基本概念
- mian函数:不能重载、不能内联、不能定义为静态、不能取其地址,不能由用户自己调用
- 内部名称:编译器会按照特定的规则把用户定义的标识符转换为相应的内部名称(例:_main)
- 连接规范:关系到编译器采样什么样的内部命名方案(例:extren "C")
- 变量:用来保存数据的程序元素,它是内存单元的别名(全局变量、static变量、自动变量)。
- 一个编译单元中的全局变量初始化不要依赖于另一个编译单元中的全局变量。
- C运行时库:有单线程和多线程版本(例:glibc)
- 编译时/运行时:语言中有些构造仅在“编译时”起作用,有些则在“运行时”起作用
- 编译时:预编译伪指令、类定义、外部对象声明、函数原型、标识符、修饰符、内成员访问说明符、连接规范
- 运行时:容器越界访问、虚函数动态决议、函数动态链接、动态内存分配、异常处理、RTTI
2.基本数据类型和内存映像
- 数据类型用来定义变量的值得类型,每种类型对应特定的字节数
- 变量在内存中的存放位置必须自然对齐:起始地址必须能被它们的大小整除(intel的CPU除外)
3.类型转换
- 类型转换:并不是改变原来的值,而是生成目标类型的临时变量
- 安全性:内存扩张、内存截断、尾数截断、值的改变、溢出
- 任何类型指针可以直接转换为viod*,反过来则必须进行强制转换
- 指针转换会改变编译器对指针所指内存单元的解释方式
4.标识符
- 标识符具有的属性:值、值类型、名字、存储类型、作用域范围、连接类型、生存期
- 避免使用前导"_"和"__"来定义标识符,编译器要使用它定义内部名称或预定义宏
- 使用长的标识符名字,并不会增加可执行代码的体积,因为标识符只在编译时起作用
5.运算符
- 常量:字面常量、符号常量、枚举常量、布尔常量
- 常量表达式:字符串常量、const常量要分配运行时存储空间,其他常量及常量表达式不需要分配运行时存储空间
- 尽可能把结果为false的表达式放在&&的左边,把结果为true的表达式放在||的左边
6.选择/判断结构
- 在if/else结构中,尽量把为true概率较高的条件判断置于前面
- 与零值比较:(!bool)、(0!=int)、(EPSILON>=abs(float))、(NULL!=piont)
7.循环/重复结构
- 对多维数组遍历应该"先行后列",影响序列的是内存页面的交换次数(大数组才存在)和cache命中率
- 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体外(这样编译器可以优化循环)
- 少用、慎用goto,而不是禁用
4. 常量
1.认识常量
- 字面常量:数字、字符、字符串(一般保存在程序的符号表里(有访问保护),通常相同的常量会合并成一个)
- 符号常量:#define定义、const定义(const实为不能改变的变量,c++中基本数据类型const放在符号表中,取地址时编译器会在内存中新建一个拷贝,const符号只是编译时强类型安全检查机制的一种手段;在C中const常量默认是外连接的,在C++中const常量默认是内连接的)
- 契约性常量:被看做const对象(例:void foo(const int&n))
- 枚举常量:标准C/C++规定枚举常量是可以扩展的,并非受限于一般的整型数的范围
2.const与#define的比较
- const常量有数据类型,而宏常量没有数据类型(编译器对前者可以进行静态类型检查)
- 可以对const常量进行调试,但是不能对宏常量进行调试
3.类中的常量
- 类中的const是局部的,宏常量是全局的,非static的const常量只能在构造函数中初始化
- 定义类共享常量:static const或类中的枚举常量(枚举在编译时会全部求值)
5. 函数设计基础
1.认识函数
- 函数实际上是“输入-处理-输出”模型的一种具体表现
- 在需要某种功能的函数时,应优先使用库函数
- 静态链接库使用:连接器只会把你调用的库函数相关的代码链接进你的应用程序
- 动态链接库使用:运行时必须将所有DLL复制到运行环境的相应目录下
- 未调用自己编写的函数,编译器不会为其产生可执行代码
2.函数的原型和定义
- 函数原型(声明):[作用域] [连接规范] 返回值类型 [调用规范] 函数名 (类型 [形参名], ...)
- 函数调用参数传递本质:用实参来初始化形参而不是替换形参
3.函数调用方式
- 调用方式:过程调用、嵌套调用、递归调用
- 回调函数:并不使用普通的函数堆栈,而是使用线程自己的堆栈
4.认识函数堆栈
- 函数堆栈实际上使用的是程序的堆栈段空间
- 堆栈是自动管理的,局部变量的创建和销毁、堆栈的释放都是函数自动完成的
- 用途:进入函数前保存环境变量和返回地址、进入函数时保存实参的拷贝、在函数体内保存局部变量
5.函数调用规范
- 函数调用规范决定了函数调用的实参压栈、退栈及堆栈释放的方式,及编译器函数重命名方案
- __cdecl、__stdcall、_thiscall、__fastcall
- 凡是接口函数都必须显示地指定调用规范、除非接口函数是类的非静态成员函数
6.函数连接规范
- 连接规范主要影响到名字的改编方案
- 全局数据类、全局函数、全局变量、全局常量的连接规范必须在库和调用端保持一致
- 常用连接规范:extern “C”
7.参数传递规则
- 传递方式:值传递、地址传递、引用传递
- C把空的参数列表解释为可以接受任何类型和个数的的参数,而C++则解释为不能接受任何参数
- 一般输出参数放在前面、输入参数放在后面,并且不要交叉
- 如果参数是指针或引用,且只做输入用,则应在类型前加上const
- 应避免函数有太多的参数(5个以内),尽量不要使用类型和参数个数不确定的参数列表
8.返回值得规则
- 返回方式:使用return,使用输出参数
- C函数默认返回int,但最后指定返回类型
- 建议正常值使用输出参数获得,错误标志用return语句返回
- 有时为了增加灵活性,如支持链式表达,可以在不需要返回值的函数中附加返回值
9.函数内部实现的规则
- 在函数的入口处,对参数的有效性进行检查、对要使用的全局变量进行检查(使用断言来检测)
- 在函数的出口处,对return语句的正确性和效率进行检查
- 不可返回堆栈内存的指针或引用
- 要搞清楚返回的是对象的值、指针还是引用
- 若返回值是一个对象,要考虑return语句的效率(return会初始化临时对象,"return x+y;"比"int temp=x+y; return temp;"效率更高)
- 函数功能要单一,函数体规模要小(50行内)
- 用于出错处理的返回值一定要清楚,尽量避免函数有记忆功能
10.存储类型及作用域规则
- 存储类型:extern、static、auto、register
- 默认为extern:全局变量、全局函数
- 默认为static:全局常量
- 默认为auto:局部变量、局部符号常量、函数形参
- 作用域范围:文件、函数、程序块、函数原型、类、名字空间
- 标号具有函数作用域、局部变量只有程序块作用域
- 当局部变量屏蔽了全局变量,可以用作用域解析符::来引用全局变量
- 类的非静态成员函数可以直接访问类的其他任何成员(默认包含this指针)
- 连接类型:表明一个标识符的可见性,包括:外连接、内连接、无连接
-
11.递归函数
-
- 递归必须是有条件的递归,递归必须保证最终能够收敛于基本条件
- 递归函数的实现首先必须检测基本条件:如果基本条件通过,则返回基本值,或者修改规模并进入下一次递归
- 任何能够用递归函数实现的解决方案都可以用迭代来实现
- 递归使用了函数的反复调用并占用了大量堆栈空间,所以其运行时的开销比较大
- 不要使用间接递归:int a(){ b(); } int b(){ a(); }
12.使用断言
- 断言:如果表达式为假,则输出错误消息并终止程序
- assert()宏只在Debug版本中有效
- 要用assert检测非法情况,而不是错误情况
13.使用const
- 被const修饰的东西都受到语言实现的静态类型安全机制检查
- 使用const修饰指针或引用类型的输入参数
- 使用const修饰非指针或引用的形参,可防止函数内部修改其值
- 对ADT/UDT的输入参数,应将值传递改为const&传递,基本数据类型则不需要
- 当函数返回指针或引用时用const修饰才有意义
6. 指针、数组和字符串
1.指针
- 指针:指针是变量,只是和其他变量的解释方式不一样
- 只要掌握了对象的内存地址,我可以在任何地方对其进行任何操作,除非其受到操作系统的保护
- 指针类型与语言实现对其内容的解释相关
- 无论在哪里定义指针变量都要赋一个初值
- 指针的加减i,其含义是在其值上加减i*指针所指对象的字节数(引出void*不能参与加减)
- 指针传递:传递的是一个地址而不是该地址所指的对象
- 使用指针时,必须保证它所指向的内存单元有效
2.数组
- 任何数组在内存中都是连续存放的
- 使用下标来引用数组元素时,编译器必须转换为同类型的指针表示形式
- 编译器在给数组分配内存空间时总是以指定的元素个数为准
- 初始化个数不能大于指定的元素个数,如果初始化个数小于元素个数,则后面的元素初始化为0
3.二维数组
- 二维数组定义必须指定其列数,因为对其的访问要转换为对应指针类型的访问
- 数组名和指针等价关系:一维int *const a、二维int (*const a)[4]、三维int (*const a)[4][5]
4.数组传递
- 数组传递会被改为指针传递
- 数组传递时不需说明第一维的长度,但必须说明其他维的长度(因为行优先规则)
5.动态操作数组
- p=new char[1024]; delete []p; (按p类型解释只是指向一个char对象,delete p只会释放其对应的char对象,delete []则会去寻找p所对应的数组大小)
- 多维数组分配:char (*p)[5] = new [3][5]; delete []p;
6.字符数组、字符指针和字符串
- 用字符数组存储字符串时,数组长度必须至少比strlen(str)大1用于存放'\0'
- 使用字符指针时要特别小心,char*p; p代表字符串指针,*p代表字符
- 对字符串进行复制时,要保证目标字符串结尾有'\0',strcpy、strcat不会自动添加'\0'
7.函数指针
- 函数地址就是一个编译时的常量,函数连接就是把函数地址绑定到函数的调用语句上
- C++中vtable就是用来保存虚成员函数地址的函数指针数组
- 类的成员函数:inline、virtual、static、normal
- inline函数在运行时展开,其地址没有太大意义
- virtual成员函数地址指的是其在vtable中的位置
- static成员函数与普通全局函数的地址一样
- 普通成员函数指针使用上比较特别(virtual也一样):void (A::*fun)(void); fun = &A::foo; A a; (a.*fun)();
8.引用与指针的比较
- 引用在创建时必须初始化,而指针则不必
- 不存在NULL的引用,引用必须与合法的存储单元关联
- 引用一旦初始化后,它就不能改为对另一个对象的引用
- 引用的创建和销毁并不会调用类的拷贝构造函数和析构函数
- 在语言层面,引用和对象的用法一样,在二进制层面,引用一般都是通过指针来实现
- 用引用代替指针,体现了最小特权原则
7. 高级数据类型
1.结构体
- 就C++本身而言,struct和class除了"默认的成员访问权限"不同外,没有任何区别
- 若使用struct/class当做参数传递给函数,默认为值传递,其中的数组将全部复制到函数堆栈中
- C风格的struct可以像数组一样的初始化,students s= {0}; 后面自动补0
- 由于存在字节对齐,补齐的部分为"脏值",所以不能使用逐位比较的方式比较结构体
2.结构体位域
- C位域成员类型必须是int,unsigned int等类型,C++还允许char、long类型
- 非具名的位域成员:unsigned int :3; 定义长度为0的位域成员:该类型剩余空间补0
- 操作位域成员的值时一定要防止可能出现的上溢
- 不能取位域对象成员的地址
- 用位域节省存储空间会导致程序运行速度的下降
3.结构体成员对齐
- 使用#pragma pack()指定对齐方式的编译器指令可能是不可移植的
- 按照从大到小的顺序从前到后依次声明每一个数据成员(这样只会在对象末尾填充)
- 对象布局关系到复合类型的可移植性和二进制兼容性,这对于模块间的接口很重要
4.联合体
- 联合体可以实现不同类型数据成员之间的自动类型转换
- 联合体的大小取决于其中字节数最多的成员
- 联合指定初始值:只能指定与第一个成员类型相匹配的初始值
- 不能比较两个联合变量的大小
- C++中的联合体可以定义:成员访问说明符、非静态非虚拟成员函数、构造函数、析构函数
5.枚举
- C中枚举类型为int,C++则是动态的且可以支持很大的值
8. 预编译处理
1.头文件包含
- 内部包含卫哨:避免同一个编译单元里包含同一个头文件超过一次
- 外部包含卫哨:避免多次查找和打开头文件的操作,提高编译速度
- 包含顺序:
- 头文件中:自定义头文件 => 第三方库头文件 => 标准头文件
- 源文件中:源文件对应头文件 => 自定义头文件 => 第三方库头文件 => 标准头文件
2.宏定义
- 无论宏在文件的哪里定义,宏定义都具有文件作用域
- 带参数的宏体和各个形参应该分别用括号括起来
- 不要在引用宏定义的参数列表中使用自增和自减运算符,可能导致变量多次求值
- 用宏来构造一些重复的、数据和函数混合的、功能较特殊的代码段
- 可以使用#undef来取消其定义,宏的注释应使用/**/
- 尽量使用const来代替宏来定义符号常量
3.预编译
- 使用#if 0 ... #endif来屏蔽一段代码
- #error:用于输出与平台、环境有关的信息,它会停止继续编译
- #pragma:用于执行语言实现所定义的动作(参考所使用编译器的帮助文档)
- #:构串操作符(例:#define STR(x) "A" #x "C" STR(B)展开为"ABC")
- ##:合并操作符(例:#define MG(x) A##xC MG(B)展开为ABC)
4.预定义符号
- __LINE__、__FILE__、__FUNCTION__、__DATA__、__TIME__、__TIMESTAMP__、__STDC__
9. C++面向对象程序设计方法概述
1.类的继承
- 若在逻辑上B是A的一种,并且A的所有功能和属性对B而言都有意义,才让B继承A
- 纯虚函数实质是把函数地址初始化为0
2.类的组合
- 若在逻辑上A是B的一部分,则不允许B从A派生,而是用A和其他部分组合出B
3.动态特性
- 虚函数:子类重新实现虚函数叫"覆盖",重新实现非虚函数叫"重写"
- 抽象基类:接口类,成员函数为纯虚函数。可以提供入口函数来创建实现类的对象
- 动态绑定:每个多态类对象都有一个指向虚函数表(vtable:函数指针数组)的指针(vptr)
- 运行时多态:(*(p->_vptr[slotNum]))(p, arg-list):p为基类型指针,slotNum在编译时确定
- 多态数组:通过基类指针删除一个由派生类对象组成的数组,结果未定义。使用多态数组时必须在数组中使用基类指针或智能指针,不要使用对象;
4.对象模型
- 对象的内存映射:用户内存区(_vptr)、程序静态数据区(type_info、_vtable)、代码段;
- 多态类:_vptr(与_vtable一一对应)、_vtable、type_info(其指针存于_vtable中)
- 构造函数完成对_vptr的初始化,避免在构造函数和析构函数中使用虚函数
5.成员函数
- C++通过Name-Mangling技术把每个成员函数都转换成名字唯一的全局函数
- C++静态成员本质上就是一种全局函数或变量,静态函数没有this指针
10. C++对象的初始化、拷贝和析构
1.构造函数和析构函数
- 不要在构造函数中做与初始化对象无关的工作,不要在析构函数中做与销毁对象无关的工作
- 创建一个变量或动态对象时一定不要忘记初始化(注意直接初始化和复制初始化的区别)
- 最好为每个类显示的定义构造函数和析构函数,尤其类含有指针或引用成员时
- 编译器总是按照数据成员在类中排序来初始化,和初始化列表顺序无关
- 对于非内置类型的数据成员初始化,直接初始化的效率比复制初始化要高
- 静态对象定义时若没有提供初始值且类没有默认构造函数,则自动初始化为0
- 动态创建对象时对象的初始化方式则与接收指针是否为静态对象有关
2.复制构造函数和赋值操作符函数
- 区别:A a=1;//复制构造 a=b;赋值操作符函数
- 赋值操作符函数:检测自赋值 => 分配资源并拷贝 => 释放原有资源 => 返回本对象的引用
- 为提高赋值操作符函数的异常安全性,可以在函数中使用拷贝构造函数
3.派生类的基本函数
- 基类的构造函数、析构函数、复制构造函数、赋值操作符函数都不会被派生类继承
- 派生类应该在构造函数中的初始化列表显示的调用基类的构造函数
- 如果基类是多态类,必须把基类的析构函数定义为虚函数,实现动态绑定
- 在编写派生类的赋值操作符函数时不要忘了调用基类的赋值操作符函数A::operator=()
11. C++函数的高级特性
1.函数重载
- 编译器根据参数列表为每个重载函数产生不同内部标识符
- 全局函数和内的成员函数同名不算重载,因为他们的作用域不同
- 注意隐式转换可能导致的二义性,如double转为int/float
- 跨边界重载:在派生类中使用using A::foo
2.成员函数的重载、覆盖于隐藏
- 重载:作用域相同、函数名相同、参数列表不同(包括const和非const)
- 覆盖:派生类重新实现基类的虚拟成员函数,函数名相同、参数列表相同
- 隐藏:派生类的成员函数与基类的非虚拟成员函数函数名相同、与参数列表无关
3.参数的默认值
- 把参数的默认值放在函数的声明中,而不是函数的定义中,参数只能从后向前依次默认
- 使用默认参数有可能导致重载函数的二义性
4.运算符重载
- 运算符被重载为全局函数:一元运算符有一个参数,二元运算符有两个参数
- 运算符被重载为成员函数:一元运算符没有参数,二元运算符有一个参数
- 除了调用运算符"()"外,其他运算符重载函数不能有默认参数值
- 不能重载的运算符:. .* :: ?: sizeof() typeid() # ##
- ++/--前置版和后置版的区别:后置版本会先创建原对象的拷贝,再对原对象+1,再返回拷贝
- 对于非内置类型,前置版本比后置版本效率要高
5.函数内联
- 使用宏容易出错,常常产生意想不到的边际效应,无法操作私有数据成员
- 宏代码不可调试,宏一定要使用()括起,宏参数不要使用++/--
- inline在Debug版本中一般不会代码展开,要进行参数类型安全检查或类型自动转换
- 函数体被内联后,编译器可以根据上下文相关联的优化技术对结果代码执行更深一步的优化
- inline必须与函数的定义放在一起,定义在类声明中成员函数默认为inline
- inline函数会不会被编译器内联与函数的定义是有关系的(inline函数不能太复杂)
- 不要轻易让构造函数和析构函数成为inline,因为这些函数包含很多隐含操作
6.类型转换函数
- 类型转换:创建新的目标对象,并以源对象来初始化
- 带有参数的构造函数可看做类型转换函数(其他类转换为本类)(可以使用explicit来抑制默认构造)
- 类型转换函数(该类转换成其他类)定义:operator xx() const { }(xx为要转换成类型)
7.类型转换运算符
- const_cast<>:去除对象的const/volatile属性
- static_cast<>:在多重继承的情况下会调整指针的值,编译器可以进行的任何隐式转换
- reinterpret_cast<>:对内存单元的重新解释
- dynamic_cast<>:程序运行时转换,判断被转换对象是否可转换为目标类型,转换出错返回0
8.const成员函数
- 任何不会修改成员数据的函数都应该声明为const类型
- static成员函数不能定义为const
- const对象不能使用非const成员函数
12. C++的异常处理和RTTI
1.C++异常处理
- 异常处理机制实际上是一种运行时通知机制
- 任何对象都可以当成异常对象,异常仅仅通过类型而不是值来匹配
- 异常处理组成:抛出异常、提炼异常、捕获异常、异常对象
- 一个函数内不要出现多个try,也不要使用嵌套try块
- 异常对象是创建在专用的异常堆栈上,不要把局部对象的地址作为异常对象抛出
- catch参数类型匹配:类型相同、基类、基类指针、void*、catch(...)
- 应使用函数异常说明:void fun() throw();void fun() throw(T1,T2)
- 异常冲突:函数抛出的异常与异常说明不符合,系统会调用unexpected()处理(可以用set_unexpected()来设置其回调函数)
- 从try到throw语句之间构造起来的局部对象的析构函数将被自动调用,然后清退堆栈
- 对象构造异常:动态对象创建时抛出异常,内存会自动释放,不会造成内存泄露
- 对象析构异常:抛出异常前必须检测是否有未捕获的异常(防止析构是由外部异常引起)
- 全局对象的构造和析构异常,将永远不会被捕获
- 如果不使用异常处理机制就能安全而高效的消除错误,那就不要使用异常处理
- catch块参数应采用引用传递而不是值传递,效率高、可利用多态性
- 在异常处理中要合理安排处理层次:派生类的异常捕获要放于基类之前
- 异常重抛:在catch语句中使用throw;语句
2.RTTI及其构成
- typeid运算符:返回一个type_info对象的引用
- dynamic_cast<>:只能转换指针(失败返回NULL)或引用(失败抛出std::bad_cast)
- typeid是精确的对象匹配,dynamic_cast<>是模糊的对象匹配
- dynamic_cast<>在运行时只要带转换指针或引用可以转换为目标类型即可
- RTTI会在运行速度上和程序体积上都会带来额外的开销
13. 内存管理
1.内存分配方式
- 从静态存储区域分配(全局变量、static变量)
- 在堆栈上分配(局部变量,容量有限,容易溢出)
- 从堆或自由存储空间上分配(动态内存分配)
- 动态内存分配的开销:
- 调用内存管理模块,查看可使用内存块,可能需要碎片整理
- 动态分配结果检查
- 有可能出现内存泄露、非法访问等等问题
2.常见的内存错误
- 内存分配未成功,却使用了它
- 内存分配成功,但尚未初始化就使用它
- 内存分配成功,也正确初始化了,但内存访问越界
- 忘了释放内存或只释放了部分内存,造成内存泄露
- 释放了内存却还在继续使用它:
- 程序中的对象调用关系过于复杂,此时应重新设计数据结构
- 函数返回了局部栈内存的指针
- 释放内存后,没有将指针设置为NULL,产生野指针
- 多次释放同一块内存
3.动态内存使用规则
- 定义指针变量或数组时要赋初值(NULL或有效内存地址)
- 内存申请完后要立即检查指针值是否为NULL或进行异常处理
- 初始化动态内存,防止将未初始化的内存做右值使用
- 避免数组或指针访问越界,注意边界问题
- 动态内存的申请与释放必须配对,防止内存泄露
- 动态内存释放前要判断指针是否为NULL,释放后,立即将指针设置为NULL
4.malloc/free与new/delete
- new/delete可以带有构造函数/析构函数,而malloc/free不行
- 使用new/delete更加安全,因为new可以自动计算构造对象的字节数(包含隐含成员,边界调整)
- 我们可以为自定义类重载new/delete,实现自己的策略
- 在某些情况下malloc/free的效率比new/delete更高(有些STL使用malloc/free实现new/delete)
- 多次delete一个NULL指针不会出错,free一个NULL指针会出错
5.new的三种使用方式
- plain new/delete:普通的new,会抛出std::bad_alloc异常
- nothrow new/delete:new(nothrow) char[10],通过返回指针是否为NULL判断是否成功
- placement new/delete:new(p) char[10]在已经分配好的内存上重新构造对象或数组,主要用于反复使用一块较大的分配成功的内存来构造不同的对象或数组。不要对指针使用delete
6.用对象模拟指针
- 含有指针成员的对象初始化和拷贝方式:接管、深拷贝
- 拷贝方式,负责创建和销毁对象:STL容器
- 完全接管方式,不负责创建对象,但负责销毁对象:auto_ptr<>
- 接管方式,不负责创建和销毁对象:STL迭代器
- STL容器仅支持"值"语义而不支持"引用"语义,不要使用auto_ptr作为容器元素
- STL要求类必须要public的四个基本函数(默认构造、析构、拷贝、赋值操作符)