《高质量程序设计指南》读书笔记

基本信息 BASICS

  1. 作者:林锐、韩永泉
  2. 出版社:电子工业出版社
  3. 出版时间:2007年

 

阅读心得 LEARNINGS

  1. 编程需要遵循一定的规范,才能体现程序员的职业素养;
  2. 在阅读本书前,对C++的编程知之甚少,也是抱着“编程不过是利用MATLAB或者Python实现想法”的心态来阅读本书,但是一路读下来,反而被C/C++一板一眼的编程规范所感动。如果把实现功能看成是横练的外功,编程质量看做修炼的内功,大概学武一样,编程也要内外兼修才能成为一代武学大家吧。
  3. 读完作者的《大学十年》很有感触,和作者“兴风作浪”的大学时光相比,我的七年的大学生活就显得尤为的枯燥与苦闷。我努力地在那些既定的学业目标里面挣扎中毕了业。回头一看,原来除了一纸文凭以及半吊子的专业知识,就什么也没有留下。究其原因,大概是没有在大学阶段找到自己愿意为之奋斗的目标,做起事也就少了许多动力与激情,只剩下一味地迷惘与彷徨。好在毕业以后,似乎慢慢地找到了些许人生的方向,希望自己阅读和笔记的习惯能坚持下去,努力地吸收养分,最后长成自己的参天大树。

 

重点摘录 NOTES

  1. 主动去创造环境,否则你无法设计人生
  2. 生活和工作要充满激情,否则你无法体会到淋漓尽致的欢乐与痛苦。
  3. 越是怕指针,就越要使用指针不会正确使用指针,就算不上是合格的程序员;
  4. 必须养成“使用调试器逐步跟踪程序”的习惯,只有这样才能发现问题的根源。

 

第一章:高质量的软件开发之道

软件的质量属性:正确性、健壮性(容错能力和自我恢复能力)、可靠性(不出现故障的概率)、性能(程序时空效率)、易用性、清晰性(易理解)、安全性、可扩展性、兼容性(与其他软件交互的能力)、可移植性

CMM模型:评判软件过程能力。

SPP模型:软件开发的精简并行过程。

 

第二章:编程语言的发展简史

1972年,贝尔实验室发明C语言

80年代。发明C++

1995年,发明JAVA

能解决应用问题的编程语言就是好语言,要根据开发产品的特征,选择业界推荐并且自己推荐的编程语言来开发软件。

 

第三章:程序的基本概念

语言实现:编译器、连接器或者解释器的实现

初级编程方法:结构化编程、模块化编程、过程式编程

高级编程模式:基于对象、面向对象、面向组件、泛型编程、事件驱动编程

程序库:C runtime library、STL、MFC、VCL等。

集成开发环境(IDE):编辑器、编译器、连接器及调试器的集成。

程序的工作原理:存储程序控制原理

clipboard

 

第四章:C++/C程序设计入门

main()函数,程序的默认主函数,返回值为0,代表正常结束。

命令行参数:由启动参数截获并打包成字符串数组后传递给main()的一个形参argv。

全局变量存放在程序的静态数据区,在main()之前创建,在main()之后销毁,此时编译器会自动初始化为0。

局部变量是运行时在栈上创建的,动态内存在堆上分配所以需要程序员来初始化.

C运行库:启动函数、I/O函数、存储管理、动态链接库。

注意编译和运行的区别:容器越界访问、虚函数动态决议、动态内存分配、异常处理均在运行时才能发挥作用。

标准C语言没有bool类型,但是某些实现通过库提供了其映射。

 

typedef int BOOL; 
#define TRUE 1 
#define FALSE 0

big endian: 高字节、高字在前,或者地址大的字节结尾

自然对齐:基本数据类型的变量地址应该能被他们的大小整除,eg.int类型的变量应该能被4整除(32位系统下)

强制类型转换是显式的,隐式转换由编译器自动完成,需要格外注意。

运算符和结合律:

clipboard

为了防止歧义和提高可读性,建议用括号确定表达式的顺序

clipboard

clipboard

对于不同的C++语言,布尔变量FALSE的值是确定为0的,但是TRUE的值可以为1或者-1。

正确的浮点变量比较方式

if(abs(x-y)<=EPSILON) //x等于y
if(abs(x-y)>EPSILON) //X不等于y

同理,x与零值比较的正确方式为:

if(abs(x)<=EPSILON)  //x等于0
if(abs(x)>EPSILON)  //x不等于0

正确的指针变量与零值比较:

if(p==NULL) //P与NULL显式的比较,强调p是指针变量

而为了防止歧义和提高可读性,建议用括号确定表达式的顺序

程序中可能遇到if/else/return的组合,应该使用 return condition ? x : y;

clipboard

数组的遍历:

clipboard

goto语句可以实现无条件跳转,可以跳出多层嵌套循环体.

 

第五章:C++/C常量

字面常量:数字,字符、字符串,只能引用,不能修改

符号常量:#define定义和宏常量和const常量,可以取到其地址但是不能修改其值

契约性常量:并未使用const关键字,但是被看做是一个const常量

布尔常量:略

枚举常量:enum包含的常量

C++中需要将对外公开的常量放在头文件中,不对外公开的常量放在定义文件的头部

const和define的比较:const变量有数据类型,而宏常量没有,因此更建议使用const常量。

如果需要在类中建立恒定的常量,需要在类中使用enum,或者使用static const定义需要共享的常量

在实际应用中定义变量的方法:

1、在公用头文件中定义static并初始化,eg: static const int MAX_LENGTH = 1024,然后include该头文件;

2、在公用头文件中将变量声明为extern,然后在源文件中重新定义一次,并include该头文件(注意本方法,更节约内存);

3、如果是整性常量,在公用头文件中定义enum,然后在源文件中include该头文件即可

字符串常量最浪费空间,尤其是较长的字符串常量。

 

第六章:C++/C函数设计基础

函数的调用必须通过堆栈来完成,实际使用的是程序的堆栈段内存空间,是在调用函数时动态分配的。

函数堆栈的三个用途:噪进入函数前保存环境变量和返回地址、在进入函数是保存实参的拷贝,在函数体内保存局部变量。

函数参数的顺序:一般输出参数在前,输入参数在后,并且不要交叉出现输入输出参数。

void StringCopy(char *strDestination, const char *strSource)

定义函数时,不要忽略函数的返回值类型,如果没有,应声明为void.

clipboard

return语句不可返回指向堆栈内存的指针或者引用,因为该内存单元会在函数体结束时被自动释放。比如不要直接返回函数内部创建的指针,要返回const常量或者其它。

const char *Func1(void)
{
    
    const char *p="hello world"; //字符常量存放在程序的静态数据区
                                 // 末尾自动添加“\0"  
    cout<<sizeof(p)<<endl;       // 4
    cout<<strlen(p)<<endl;       // 11  
    return p;                    // 返回字符串常量的地址        
}

char *Func2(void)
{
    char str[] = "hello world"; //局部变量str的内存位于栈上
    ...   
    return str;                 // 将导致错误
}

全局变量和全局函数的存储类型是extern,全局常量的默认存储类型为static;

局部变量默认具有auto存储类型,register和auto只能用于声明局部变量和局部常量;

局部符号常量的默认存储类型为auto。

clipboard

assert可以看成是一个在任何系统状态下都安全使用的无害测试手段,可以在函数入口处,建议使用assert来检查参数的有效性/合法性(注意合法的程序不一定正确),例如

void *memcpy(void *pvTo, const void *pvForm, size_t, size)
{
    
    // 使用断言,防止pvTo或者pvForm为NULL
    assert((pvTo!=NULL) && (pvForm!=NULL));
  
    typedef char byte;
    byte *pbTo = (byte *)pvTo; //防止改变pvTo的地址 
    byte *pbForm = (byte *)pvForm; //防止改变pvForm的地址

    while(size—> 0)
    {              
        //不要与字符串拷贝混淆!
        *pbTo++ = *pbForm++;
    }
    
    return pvTo;
}

注意正确使用assert,动态分配内存失败不是非法情况,二是错误情况,要使用if捕捉,而不应该使用assert

clipboard

 

第七章:C++/C指针、数组和字符串

指针的值是内存单元的地址

typedef int*       IntPtr;
typedef int**      IntPtrPtr;
typedef IntPtrPtr* IntPtrPtrPtr;
typedef char*      CharPtr;
typedef void*      VoidPtr;
typedef CharPtr*   CharPtrPtr;


IntPtr pa, pb, pc;
IntPtrPtr ppa, ppb, ppc;
CharPtr  pChar1, pChar2;
VoidPtr  pVoid;
CharPtrPtr ppChar1, ppChar2;

注意不要定义int* a,b,c;此时编译器会理解为a是int类型的指针,而b和c仍然是int类型的变量。

当把“&”用于指针时,是在提取指针变量的地址;

当把“*”用于指针时,是在提取指针所指向的变量。

数组元素的地址内存是连续的

声明数组时的要点:

int b[100];                       // sizeof(b)=400bytes,未初始化
int c[] = {1, 2, 3, 4, 5};        //元素个数为5,sizeof(c) = 20bytes,初始化
int d[5] = {1 ,2, 3, 4, 5, 6, 7}; //错误,初始值越界
int e[10] = {5, 6, 7, 8, 9};      //元素个数为10,指定了前5个元素的初始值,
                                  // 剩下的元素自动初始化为0
int f[10] = {5, , 12, , 2};       // 错误,不能跳过中间某些元素

数组和指针的等价关系:

一维数组等价于元素的指针,二维数组等价于指向一维数组的指针,eg. int b[3][4] <=> int(* const b)[4]

数组的传递:不能从return 语句返回,但是可以作为函数的参数,但是实际上,出于节约资源的考虑,C将数组传递转换成了指针传递。对于二维数组的传递,需要指出第二维的具体长度,eg

void output(const int a[][20],int line)

动态创建数组与删除

int *q = new int[1024];//创建数组
delete []q;//删除数组

字符数组是元素为字符变量的数组,而字符串是以’\0‘为结束字符的字符数组,字符数组不一定是字符串。

函数指针,在注册回调函数时,我们常常使用函数指针:

double __cdecl(*fp[5])(double)={sqrt, fabs, cos, sin, exp};

for(int k=0;k<5; k++)
{   
    cout<<"Result:"<<fp[k](10.25)<<endl;
}

引用和指针的比较:引用’&‘是C++新增的概念,引用相当于别名:int m; int& n = m;此时n是m的引用。

 

第八章:C++/C高级数据类型
结构(struct):默认成员访问权限为public,而class为private。
结构体实现链表:
struct A
{   
    int count;   
    char *pName;    // A holds-a string  
    B  *pb;         // A holds-a B
};

struct B
{    
    char ch;  
    A  *pa;         // B holds-a A 
    B  *pNext;      // B 自应用
};

其中A为链表头的类型,B为链表节点的类型。

位域的设计,不要让位域成员跨越一个不完整的字节来存放,这样会增加计算机的开销。

struct DateTime
{
    unsigned int year;
    unsigned int month   :8;
    unsigned int day     :8;
    unsigned int hour    :8;
}

结构体的成员对其,最好使用编译器支持的方法(编译器指令一般不可移植)来为每一个复合数据指定对齐方式。同时从大到小依次声明每个数据成员,例如:

#ifdef _MSC_VER
#pragma pack(push, 8)//按8字节边界对齐
#endif

struct Sedan
{  
    double      m_price;  
    Color       m_color;
    bool        m_hasSkylight;
    bool        m_isAutoShift;
    BYTE       m_seatNum; 
};

#ifdef _MSC_VER
#pragma pack(pop)
#endif

联合(union):在同一时间只能存储一个成员的值,只有一个数据是活跃的,内存大小取决于字节数最多的成员,而不是累加。

C++中对union进行了扩展,,除了数据成员外还可以定义成员的访问说明符,定义成员函数,析构函数和构造函数。

枚举(enum):允许定义特定用途的一组符号常量

enum Week {Sum, Mon=125, Tue, Wed, Thu=140, Fri, Sat};
Week weekday = Sun;

标准C中,枚举类型的内存大小等于sizeof(int),但是在标准C++中,可以更大或者更小。

clipboard

文件操作,并不是C++/C语言的组成部分,他是通过标准的I/O函数库实现的。

文件操作的流程:

声明一个FILE结构的指针,调用库函数fopen(),动态创建FILE结构对象并分配一个文件句柄,读入FCB结构并填入FCB剧组,然后返回FILE结构的地址,最后调用fclose()函数销毁动态创建的FILE结构对象。

 

第九章:C++/C编译预处理

预编译指令都不会进入编译阶段,一般以#打头,一般包括文件包含,宏定义,条件编译,以及一些预编译伪指令和符号常量。

#include<头文件名称>:用来包含和开发环境提供的库头文件

#include"头文件名称":用来包含自己编写的头文件,如,#include ".\myinclude\abc.h"

慎用宏定义,宏在编译预处理阶段只做文本替换,不做类型检查和语法检查。

带参数的宏体,形参要用括号括起来,比如 #define SQUARE(x) ((x) * (x))

条件编译:可以控制预处理器选择不同的代码段作为编译器的输入,有利于程序的移植与调试。

以#if开始,以#endif结束。

#ifdef的用法:等价于#if defined(XYZ),XYZ称为调试宏,例如:

#define XYZ
...
#ifdef XYZ

DoSomethng();
#endif

如果不想DoSomething被编译,那么删除#define XYZ即可.

如果不想头文件被重复引用,可以用ifndef/def/endif预处理块

#ifndef GRAPHICS_H  //防止graphics.h被重复引用
#define GRAPHICS_H
#endif

#pragma:用于实现执行语言的所定义的动作

#pragma pack(push, 8)   /*对象成员对齐字节数*/
#pragma pack(pop)
#pragma warning(disable:4069)  /*不要产生第C4069编译警告*/
#pragma comment(lib,"kernel32.lib")
#pragma comment(lib,"user32.lib")
#pragma comment(lib,"gdi32.lib")

预定义符号常量

clipboard

 

第十章:C++/C文件结构和程序版式

他们并不影响功能,但是能反应开发者的职业化程度。类似于书法,追求清晰美观。

工程目录结构:

include目录存放头文件,source目录存放源文件,shared目录存放共享文件,resource目录存放资源文件包括图片,音频等,Bin目录存放程序员自己创建的lib文件和dll文件。

头文件元素的顺序结构安排:

1)注释;

2)内部包含卫哨(#ifndef XXX) ;

3)#include其他头文件;

4)外部变量和全局函数声明;

5)常量和宏定义;

6)类型前置声明和定义;

7)全局函数原型和内联函数的定义;

8)内部包含哨结束:#endif;

9)文件版本即修订说明。

版权和版本信息示例:

clipboard

建议:局部变量在定义时就应当初始化因为系统不会自动初始化局部变量,在运行时他们的内存单元将保留上次使用以来留下的脏值。

clipboard

clipboard

函数头注释示例:

clipboard

 

第十一章:C++/C应用程序命名规则

不要使程序中出现局部变量和全局变量同名的现象;

变量应当使用名字或者“形容词+名词”的格式命名;

全局函数应使用动词或者“动词+名词”的格式命名;

尽量避免名字中出现数字编号,如value1,value2等。

建议类型名和函数名均已大写字母开头的单词组合而成,例如void SetValue(int value);

建议变量名和参数名使用第一个字母小写而后面的单词字母大写的组合,例如int drawMode;'

建议符号常量和宏名用全大写的单词组合而成;

建议静态变量加前缀s_,例如 static int s_initValue;

建议全局变量加前缀g_,例如int g_howManyPeople;

建议类的成员加前缀m_(表示member),这样可以避免数据成员和成员函数的参数同名;

 

第十二章:C++面向对象程序设计方法

C++的基本面向对象特性:封装、继承、多态,运行时绑定;

高级面向对象特性:多重继承、虚拟继承、静态的多态和动态的多态;

封装,即信息隐藏,

clipboard

类的继承:表示类的一般与特殊。如果A是基类,B是A的派生类,那么B将继承A的数据与函数

class A{
public: 
    void Func1(void);  
    void Func2(void);    
};

class B:public A{
public:
    void Func3(void); 
    void Func4(void);
};

main()
{   
    B b;
    b.Func1(); //B从A继承了函数Func1    
    b.Func2(); //B从A继承了函数Func2  
    b.Func3();
    b.Func4();
}

如果一个事物同时具有另外几个事物的多重特点,那么需要使用多重继承,例如沙发床,它既是沙发也是床,所以定义时要使用多重继承。

class Sofa{
public:      
    virtual void Seating(Man&); //人可就坐    
    ...
};

class Bed{
public:       
    virtual void Lying(Man&); //人可躺下   
    ...
};

class Sofabed:public Sofa,public Bed{
public:   
    virtual void Seating(Man&); //人可就坐   
    virtual void Lying(Man&); //人可躺下    
    ...
}

另外多重继承一个重要的用途就是在已有接口和实现类的基础上创建自己的接口和实现类,这在基于别人开发的类库和开发应用程序时很有用。

类的组合:表示类的整体与部分。比如眼睛、鼻子、口,耳朵是头的一部分,此时类Head应该类Eye、Nose、Mouth、Ear组合而成,而不是派生。具体实现逻辑上A是B的一部分,则不允许B从A派生。

class Eye{
public:
    void Look(void);    
};

class Nose{
public:   
    void Smell(void);    
};

class Mouth{
public:   
    void Eat(void);    
};

class Ear{
public:    
    void Listen(void);    
};


class Head{
public:
    //调用传递  
    void Look(void){ m_eye.Look();}
    void Smell(void){ m_nose.Smell();}
    void Eat(void){ m_mouth.Eat();}
    void Listen(void){ m_ear.Listen();}    
}

private:
    //调用传递  
    Eye m_eye; 
    Nose m_nose;
    Mouth m_mouth;
    Ear m_ear;   
};

动态特性:程序的功能到运行时刻才确定下来,C++的虚函数、抽象基类、动态绑定,多态构成其动态特性。

虚函数:在基类函数的前面加上关键字virtual,举例,基类shape中有很多circle,rectangle,ellipse等派生类,此时可以将shape中的函数Draw()声明为虚函数,然后在派生类中重新定义Draw()使之绘制正确的形状,这种方法叫做覆盖。

抽象基类:不能被实例化的对象的类

纯虚函数:声明是将其“初始化”为0的函数,如果基类的虚函数被声明为纯虚函数,那么该类将被定义为抽象基类。

抽象基类的用途:接口与实现分离,将数据函数都隐藏在实现类中,而提供丰富的接口函数供调用。

动态绑定:如果将基类shape的Draw()声明为virtual,然后指向派生类对象的基类指针来调用Draw(),那么程序在运行时会选择该派生类的Draw(),这种特性为动态绑定。

动态绑定可以是供应商只发行头文件和二进制文件,不公开源码,不透露技术秘密。

多态:许多派生类继承了共同的基类,每一个派生类的对象都可以被当成基类的对象,而派生类对象可以对同一函数的调用做出不同的反映。

多态和指针算数运算不能混合运用,而数组操作几乎总是会涉及到指针运算,因此多态和数组不应该混合运用。

 

第十三章:对象的初始化.拷贝和析构

每个类只有一个析构函数,可以有多个构造函数(包含一个拷贝构造函数,其他为普通构造函数)和多个赋值函数(包含一个拷贝赋值函数,其他为普通赋值函数)

把对象初始化的工作放在构造函数中,销毁对象放在析构函数中。

初始化和赋值的区别:

初始化:创建一个对象同时用初值填充对象的内存单元

赋值:对象创建好以后任何时候都可以调用并且可以多次调用的函数,调用的是“=”运算符。

构造函数的成员初始化列表:在构造函数的参数表之后,在函数体{}之前,类的分静态const数据成员只能在初始化列表里初始化,类的数据成员初始化可以采用初始化列表和函数体内赋值两种方式,

class A{   
    ...
    A(void);   //默认的构造函数
    A(const A& other); // 拷贝构造函数
    A& operator = (const A& other); //赋值函数    
};

class B{
public:    
    B(const A& a); //B的构造函数
private:
    A m_a;         //成员对象        
};

//(1) 采用初始化列表的方式初始化
B::B(const A& a):m_a(a)
{   
    ...
}

//(2) 采用函数体内赋值的方式初始化
B::B(const A& a)
{  
    m_a = a;
    ...
}

构造函数和析构函数的调用时机:

clipboard

clipboard

clipboard

拷贝构造函数:在对象被创建并用另一个已经存在的对象来初始化它时调用的

拷贝赋值函数:把一个对象赋值给另一个已经存在的对象,使得已经存在的对象具有和源对象相同的状态

示例:类string的构造函数和析构函数

class String{
public:
   String(const char *str=""); //默认构造函数
    String(const String& other); //拷贝构造函数 
    String& operator=(const String& other);//赋值函数
    ~String();                  // 析构函数   

private:
    size_t m_size;             // 保存当前长度
    char   *m_data;            // 指向字符串的指针       
}

//String的默认构造函数
String::String(const char *str)
{   
    if(str == NULL)
    {
        m_data = nw char[1];
        *m_data = '\0';
        m_size = 0;
    }
else
{
        int length = strlen(str);
        m_data = new char[length+1];
        strcpy(m_data,str)
        m_size = length;   
    }
}

//拷贝构造函数
String::String(const String& other)
{   
    //提示:操作other的私有成员m_data
    size_t len = strlen(other.m_data);
    m_data = new char[len+1];
    strcpy(m_data, other.m_data)
    m_size = len;
}

//赋值函数
String& operator=(const String& other)
{  
    //(1)检查自赋值
if(this != &other)
    { 
    //(2)分配新的内存资源,并复制内容
        char *temp = new char[strlen(other.m_data)+1];
        strcpy(temp, other.m_data); //copy '0' together
    //(3)释放原有的内存资源
        delete []m_data;
        m_data = temp;
        m_size = strlen(other.m_data);   
    }   
   
    //(4)返回本对象的引用  
    return *this; 
}

//String的析构函数
String::~String(void)
{ 
    delete [] m_data;
}

 

第十四章:C++函数的高级特性

重载:将语义、功能相似的几个函数用同一个名字表示,即函数名被重载。

全局函数和类的成员函数同名不算重载,因为他们的作用域不同,如果要调用全局函数,应该在函数名前加一元作用域解析运算符(::),否则成员函数将遮蔽同名的全局函数。

成员函数被重载的特征:

具有相同的作用域;函数名字相同;参数类型,数目不同;virtual关键字可有可无;

覆盖:派生类重新实现或者改写了基类的成员函数,其特征是:

作用域不同(分别位于派生类和基类中);参数列表完全相同;基类函数必须是虚函数

隐藏:派生类的成员函数遮蔽了预期同名的基类成员函数.

 

include<iostream.h>
class Base
{ 
public: 
    virtual void f(float x){cout<<"Base::f(float)"<<x<<endl;} 
    void g(float x){cout<<"Base::g(float)"<<x<<endl;} 
    void h(float x){cout<<"Base::h(float)"<<x<<endl;} 
}; class Derived: public Base { public: virtual void f(float x){cout<<"Base::f(float)"<<x<<endl;} void g(float x){cout<<"Base::g(float)"<<x<<endl;} void h(float x){cout<<"Base::h(float)"<<x<<endl;} };

在上述代码中:

函数Derived::f(float)覆盖了Base::f(float);

函数Derived::g(int)隐藏了Base::g(float),而不是重载;

函数Derived::h(float)隐藏了Base::h(float),而不是覆盖;

参数的默认值:可以出现在函数的声明中,但是不可以出现在函数的定义体中。

运算符重载:在C++中可以用关键字operator加上运算符表示函数。例如:

Complex opereator +(const Complex& a, const Complex&b);

Complex a,b,c;

c = Add(a,b);//调用普通函数

c = b+c;// 调用重载的运算符“+”

对于运算符“++”、“--”应用于基本类型时,前置版本和后置版本效率没有太大的差别,但是对于大对象时,后置版本要创建一个临时对象,在退出时还要销毁它,因此如果可以,尽量选择前置版本。

内联:函数内联,可以提高函数的执行效率。但是内联是以代码拷贝为代建,仅仅省去了函数参数压栈、跳转、退栈和返回的操作,如果函数执行时间比函数调用的时间大得多,那么inline效率小,反而消耗了更多的内存空间。

使用规范:使用关键字line

void Foo(int x, int y);
inline void Foo(int x, int y)  //inline应该与函数定义体放在一起
{  
    ...
}

const:为了提高函数的健壮性,任何不会修改数据成员的成员函数都应该声明为const类型

const成员函数的声明,其关键字只能放在函数声明的尾部,例如:

class Stcack{
public:
    void Push(int elem);
    int Pop(void);    
int GetCount(void) const;  //const成员函数

private:
    int m_num;
    int m_data[100];        
} 

int Stack::GetCount(void) const
{  
    ++m_num;     //编译错误,企图修改数据成员m_num 
    Pop();       //编译错误,企图调用非const成员函数  
    return m_num;
}

 

第十五章:C++异常处理和RTTI

C语言的异常处理操作:1)函数返回状态编码表示操作是否成功;2)全局变量来保存错误编码,每个函数运行时都检查一遍;3)出错时终止程序运行。

异常处理机制的本质:在真正导致错误的语句即将执行之前,用自定义的软件异常代替它,从而阻止它,因此当异常被抛出时,真正的错误实际并未发生。

异常处理的组成:抛出异常,提炼异常、捕获异常和异常对象本身

throw、try{}、catch{}。try{}中包含了异常抛出的代码段,catch{}块则包含用户自定义的异常处理代码。

clipboard

RTTI(run-time type identification)机制:

typeid运算符,可以返回一个匹配的const type_info对象,表明对象的确切类型;

dynamic_cast<>运算符,执行运行时的类型识别和转换,可以转换指针和引用,但是不能转换对象。

RTTI会在执行速度和程序体积上都带来额外的开销。

 

第十六章:内存管理

内存分配的3种方式:

1)静态存储区分配,如全局变量,static变量;

2)在堆栈上分配,如函数内的局部变量;

3)在堆上或者自由存储空间上分配(动态内存分配)

一般的原则,如果使用堆栈和静态存储就能满足应用要求,就不要使用动态存储。

内存错误及其对策

clipboard

函数的指针参数申请内存:使用“指向指针的指针”或者“指针的引用”来申请内存,也可以使用函数返回值来传递动态内存。

// 使用指向指针的指针或者指针引用
void GetMemory1(char **p, int num) //或者是char *& rp
{
    *p = (char *)malloc(sizeof(char) * num); //rp = ...
}

void Test1(void)
{
    char *str = NULL;
    GetMemory1(&str,200);  //注意参数是&str, 而不是str
    strcpy(str, "hello");
}
// 使用函数的返回值
void *GetMemory2(int num)
{
   char *p = (char *)malloc(sizeof(char) * num); 
   return p;
}

void Test2(void)
{
    char *str = NULL;
    str = GetMemory2(200);  
    strcpy(str, "hello");
    cout<<str<<endl;    
    free(str);
}

注意:free和delete只是把指针所指的内存释放掉,并没有把指针本身删掉。

编程时要杜绝野指针:即指向“非法”内存的指针。if(p != NULL)对其不起作用。

野指针的成因:

1)没有初始化指针变量。因此在指针创建时要么将其设置为NULL,要么指向有效的内存

char *p = NULL; //NULL为合法指针值
char *str = (char *)malloc(100); //指向有效内存区

2)指针p被free()或者delete之后,没有设置为NULL.

3) 指针操作超越了变量的作用范围。例如指针指向对象时,该对象已经不存在了。

malloc/free和new/delete的区别与联系

malloc/free是标准库函数,new/delete是运算符。对于非内部数据类型,调用构造和析构函数时,malloc/free无法满足动态对象的需求。

使用new/delete更加安全,new直接返回目标类型的指针,不需要显示地转换类型,而malloc返回的是void *,必须做类型转换,例如:int *p = (int *)malloc(sizeof(int) * length);

new/delete的3种形式:

clipboard

内存耗尽处理方式:

1)判断指针是否为NULL,如果是立即用return终止函数,if(a==NULL) return;

2) 判断指针是否为NULL,如果是立即用exit(1)终止程序运行 if(a==NULL)exit(1);

3)为new和malloc()预设异常处理函数;

4) 捕获new跑出的异常,并尝试从中恢复

普通指针、auto_ptr以及smartPtr的比较:

clipboard

 

第十七章:学习和使用STL

STL(standard template library)标准化模板库,包括:I/O流,string类,容器类(container),迭代器(iterator)、存储分配器(allocator)、适配器(adapter)、函数对象(functor)、泛型算法、数值计算、国际化和本地化支持,以及标准异常类。

1)容器类头文件

clipboard

当元素的有序比搜索速度更重要时,应选用set/multiset、map或multimap.否则,选用hash_set、hash_multiset、hash_map或hash_multimap。

一种可以动态增大和减小的模型,其元素对象保存在自由内存(Free Memory)或者堆(heap)上

clipboard

2)泛型算法

分布在<algorithm>和<utility>,包括:

  • 查找算法:find()、search()、binary_search()、find_if()等;
  • 排序算法:sort()、merge()等;
  • 数学计算:accumulate()、inner_product()、partial_sum()等;
  • 集合运算:set_union()、set_intersection()、include()等;
  • 容器管理:copy()、replace()、transform()、remove()、for_each()等;
  • 统计运算:max()、min()、count()、max_element();
  • 堆管理:make_heap()、push_heap()、pop_heap()、sort_heap();
  • 比较运算:equal().

3)迭代器

本质上,迭代器可以将标志控制和计数器控制的循环统一为一种控制方法。

vector的迭代器为随机访问迭代器,因为他就是原始指针;

list的迭代器是双向迭代器,因为list是双向链表;

slist的迭代器是前进迭代器;

deque的迭代器是随机访问迭代器;

set/map的迭代器是双向迭代器

clipboard

定义在头文件<iterator>中

4)数学运算库

clipboard

5)通用工具

clipboard

存储分配器:容器元素的存储空间是动态分配和释放的。

allocator类是STL中负责存储管理的类文件操作的流程:

适配器:在已有的通用数据结构上实现更加具体的、更加贴近应用的数据结构.

 

附录:C++/C试题

这里直接转载别人已经整理好的试卷及答案,留作自测

https://www.cnblogs.com/jmzz/archive/2011/03/15/1984727.html

 

 

 

 

 

posted @ 2020-02-29 15:47  Super_orange  阅读(625)  评论(0)    收藏  举报