基本信息 BASICS
- 作者:林锐、韩永泉
- 出版社:电子工业出版社
- 出版时间:2007年
阅读心得 LEARNINGS
- 编程需要遵循一定的规范,才能体现程序员的职业素养;
- 在阅读本书前,对C++的编程知之甚少,也是抱着“编程不过是利用MATLAB或者Python实现想法”的心态来阅读本书,但是一路读下来,反而被C/C++一板一眼的编程规范所感动。如果把实现功能看成是横练的外功,编程质量看做修炼的内功,大概学武一样,编程也要内外兼修才能成为一代武学大家吧。
- 读完作者的《大学十年》很有感触,和作者“兴风作浪”的大学时光相比,我的七年的大学生活就显得尤为的枯燥与苦闷。我努力地在那些既定的学业目标里面挣扎中毕了业。回头一看,原来除了一纸文凭以及半吊子的专业知识,就什么也没有留下。究其原因,大概是没有在大学阶段找到自己愿意为之奋斗的目标,做起事也就少了许多动力与激情,只剩下一味地迷惘与彷徨。好在毕业以后,似乎慢慢地找到了些许人生的方向,希望自己阅读和笔记的习惯能坚持下去,努力地吸收养分,最后长成自己的参天大树。
重点摘录 NOTES
- 主动去创造环境,否则你无法设计人生
- 生活和工作要充满激情,否则你无法体会到淋漓尽致的欢乐与痛苦。
- 越是怕指针,就越要使用指针不会正确使用指针,就算不上是合格的程序员;
- 必须养成“使用调试器逐步跟踪程序”的习惯,只有这样才能发现问题的根源。
第一章:高质量的软件开发之道
软件的质量属性:正确性、健壮性(容错能力和自我恢复能力)、可靠性(不出现故障的概率)、性能(程序时空效率)、易用性、清晰性(易理解)、安全性、可扩展性、兼容性(与其他软件交互的能力)、可移植性
CMM模型:评判软件过程能力。
SPP模型:软件开发的精简并行过程。
第二章:编程语言的发展简史
1972年,贝尔实验室发明C语言
80年代。发明C++
1995年,发明JAVA
能解决应用问题的编程语言就是好语言,要根据开发产品的特征,选择业界推荐并且自己推荐的编程语言来开发软件。
第三章:程序的基本概念
语言实现:编译器、连接器或者解释器的实现
初级编程方法:结构化编程、模块化编程、过程式编程
高级编程模式:基于对象、面向对象、面向组件、泛型编程、事件驱动编程
程序库:C runtime library、STL、MFC、VCL等。
集成开发环境(IDE):编辑器、编译器、连接器及调试器的集成。
程序的工作原理:存储程序控制原理
第四章: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位系统下)
强制类型转换是显式的,隐式转换由编译器自动完成,需要格外注意。
运算符和结合律:
为了防止歧义和提高可读性,建议用括号确定表达式的顺序
对于不同的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/else/return的组合,应该使用 return condition ? x : y;
数组的遍历:
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.
return语句不可返回指向堆栈内存的指针或者引用,因为该内存单元会在函数体结束时被自动释放。比如不要直接返回函数内部创建的指针,要返回const常量或者其它。
const char *Func1(void) { const char *p="hello world"; //字符常量存放在程序的静态数据区 // 末尾自动添加“ " 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。
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
第七章: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;//删除数组
字符数组是元素为字符变量的数组,而字符串是以’ ‘为结束字符的字符数组,字符数组不一定是字符串。
函数指针,在注册回调函数时,我们常常使用函数指针:
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的引用。
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++中,可以更大或者更小。
文件操作,并不是C++/C语言的组成部分,他是通过标准的I/O函数库实现的。
文件操作的流程:
声明一个FILE结构的指针,调用库函数fopen(),动态创建FILE结构对象并分配一个文件句柄,读入FCB结构并填入FCB剧组,然后返回FILE结构的地址,最后调用fclose()函数销毁动态创建的FILE结构对象。
第九章:C++/C编译预处理
预编译指令都不会进入编译阶段,一般以#打头,一般包括文件包含,宏定义,条件编译,以及一些预编译伪指令和符号常量。
#include<头文件名称>:用来包含和开发环境提供的库头文件
#include"头文件名称":用来包含自己编写的头文件,如,#include ".myincludeabc.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")
预定义符号常量
第十章:C++/C文件结构和程序版式
他们并不影响功能,但是能反应开发者的职业化程度。类似于书法,追求清晰美观。
工程目录结构:
include目录存放头文件,source目录存放源文件,shared目录存放共享文件,resource目录存放资源文件包括图片,音频等,Bin目录存放程序员自己创建的lib文件和dll文件。
头文件元素的顺序结构安排:
1)注释;
2)内部包含卫哨(#ifndef XXX) ;
3)#include其他头文件;
4)外部变量和全局函数声明;
5)常量和宏定义;
6)类型前置声明和定义;
7)全局函数原型和内联函数的定义;
8)内部包含哨结束:#endif;
9)文件版本即修订说明。
版权和版本信息示例:
建议:局部变量在定义时就应当初始化因为系统不会自动初始化局部变量,在运行时他们的内存单元将保留上次使用以来留下的脏值。
函数头注释示例:
第十一章:C++/C应用程序命名规则
不要使程序中出现局部变量和全局变量同名的现象;
变量应当使用名字或者“形容词+名词”的格式命名;
全局函数应使用动词或者“动词+名词”的格式命名;
尽量避免名字中出现数字编号,如value1,value2等。
建议类型名和函数名均已大写字母开头的单词组合而成,例如void SetValue(int value);
建议变量名和参数名使用第一个字母小写而后面的单词字母大写的组合,例如int drawMode;'
建议符号常量和宏名用全大写的单词组合而成;
建议静态变量加前缀s_,例如 static int s_initValue;
建议全局变量加前缀g_,例如int g_howManyPeople;
建议类的成员加前缀m_(表示member),这样可以避免数据成员和成员函数的参数同名;
第十二章:C++面向对象程序设计方法
C++的基本面向对象特性:封装、继承、多态,运行时绑定;
高级面向对象特性:多重继承、虚拟继承、静态的多态和动态的多态;
封装,即信息隐藏,
类的继承:表示类的一般与特殊。如果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; ... }
构造函数和析构函数的调用时机:
拷贝构造函数:在对象被创建并用另一个已经存在的对象来初始化它时调用的
拷贝赋值函数:把一个对象赋值给另一个已经存在的对象,使得已经存在的对象具有和源对象相同的状态
示例:类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 = '