}
如果未用volatile,由于while循环是一个空循环,编译器优化后(编译器并不知道此变量在中断中使用)将会把循环优化为空操作!
这就显然不对了。
规则二:
不要编写一条过分复杂的语句,紧凑的C++/C代码并不见到能得到高效率的机器代码,却会降低程序的可理解性,程序出错误的几率也会提高。
规则三:
变量类型编程中应用原则:
尽量采用小的类型(如果能够不用“Float”就尽量不要去用)以及无符号Unsigned类型,因为符号运算耗费时间较长;同时函数返回值也尽量采用Unsigned类型,由此带来另外一个好处:
避免不同类型数据比较运算带来的隐性错误。
1.2.4其他
规则一:
不要编写集多种功能于一身的函数,在函数的返回值中,不要将正常值和错误标志混在一起。
规则二:
不要将BOOL值TRUE和FALSE对应于1和0进行编程。
大多数编程语言将FALSE定义为0,任何非0值都是TRUE。
VisualC++将TRUE定义为1,而VisualBasic则将TRUE定义为-1。
例如:
BOOLflag;
…
if(flag){//dosomething}//正确的用法
if(flag==TRUE){//dosomething}//危险的用法
if(flag==1){//dosomething}//危险的用法
if(!
flag){//dosomething}//正确的用法
if(flag==FALSE){//dosomething}//不合理的用法
if(flag==0){//dosomething}//不合理的用法
规则三:
小心不要将“==”写成“=”,编译器不会自动发现这种错误。
规则四:
建议统一函数返回值为无符号整形,0代表无错误,其他代表错误类型。
1.3模块化的C编程
C语言虽然不具备C++的面向对象的成分,但仍应该吸收面向对象的思想,采用模块化编程思路。
面向对象的思想与面向对象的语言是两个概念。
非面向对象的语言依然可以完成面向对象的编程,想想C++的诞生吧!
C++没有理由对C存在傲慢与偏见,不是任何场合C++方法都是解决问题的良药,譬如面对嵌入式系统效率和空间的双重需求。
注意我们谈的是方法,而不是指编译器。
C在软件开发上存在的首要问题是缺乏对数据存取的控制(封装),C编程者乐而不疲的使用着大量extern形式的全局变量在各模块间交换着数据,“多方便啊”编程者乐曰,并传授给下一个编程者。
这样多个变量出现在多个模块中,剪不断理还乱,直到有一天终于发现找一个“人”好难。
一个东西好吃,智者浅尝之改进之,而愚者只会直至撑死。
这世上本没有什么救世主,应在C上多下功夫,程序员和C缔造者早就有过思考,相信野百合也有春天,还是看看C语言如何实现模块化编程方法,在部分程度上具备了OO特性封装与多态。
在具体阐述之前,需要明确生存期与可见性的概念。
生存期指的是变量在内存的生存周期,可见性指的是变量在当前位置是否可用。
两者有紧密联系,但不能混为一谈。
一个人存在但不可见只能解释成上帝或灵魂,一个变量存在但不可见却并非咄咄怪事,模块化方法正是利用了静态函数、静态变量这些“精灵”们特殊的生存期与可见性。
最后需要明确一点的是这里的模块是以一个.C文件为单位。
规则一:
利用函数命名规则和静态函数
模块中不被其他模块调用的内部函数采用以下命名规则:
用全部小写,单词间采用带下划线的形式。
如底层图形函数:
pixel、lineto以及读键盘函数get_key等。
这些函数应定义为static静态函数,这样在其他模块错误地调用这些函数时编译器能给出错误(如BC编译器)。
(注意:
有些编译器不能报告错误,但为了代码风格一致和函数层次清晰,仍建议这样作)。
规则二:
利用静态变量
模块中不能被其他模块读写的全局变量应采用static声明,这样在其他模块错误地读写这些变量时编译器能给出警告(C51编译器)或错误(BC编译器)。
规则三:
引入OO接口概念和指针传参
模块间的数据接口(也就是函数)应该事先较充分考虑,需要哪些接口,通过接口需要操作哪些数据,尽量作到接口的不变性。
模块间地数据交换尽量通过接口完成,方法是通过函数传参数,为了保证程序高效和减少堆栈空间,传大量参数(如结构)应采用传址的方式,通过指针作为函数参数或函数返回指针,尽量杜绝extern形式的全局变量,请注意是extern形式的全局变量,模块内部的全局变量是允许和必须的。
传指针参数增加的开销主要是作参数的指针和局部指针的数据空间(嵌入式系统(如C51)往往由于堆栈空间有限,函数参数会放到外部RAM的堆栈中),增加的代码开销仅是函数的调用,带来的是良好的模块化结构,而且使用接口函数会比在代码中多处直接使用全局变量大大节约代码空间。
需注意一点的事物总有他的两面性,水能载舟,也能覆舟。
对于需要频繁访问的变量如果仍采用接口传递,函数调用的开销是巨大的,这时应考虑仍采用extern全局变量。
以下演示了两个C模块交换数据:
//Module1.C
OneStruct*voidGetOneStruct(void);//获取模块1数据接口
voidSetOneStruct(OneStruct*pOneStruct);//写模块1数据接口
structOneStruct
{
intm_imember;
//……
}t1;//模块1的数据
//t1初始化代码…..
OneStruct*voidGetOneStruct(void)
{
OneStruct*pt1;//只需定义一个局部变量
t1.imember=15;
pt1=&t1;
returnpt1;
}
voidSetOneStruct(OneStruct*pOneStruct)
{
t1.imember=pOneStruct->imember;
//…….
}
//Module2.C
voidOperateOneStruct(void);//模块2通过模块1提供的接口操作模块1的数据
OneStruct*voidGetOneStruct(void);
voidSetOneStruct(OneStruct*pOneStruct);
voidOperateOneStruct(void)
{
OneStruct*pt2;//只需定义一个局部变量
pt2=GetOneStruct();//读取数据
SetOneStruct(pt2);//改写数据
}
采用接口访问数据可以避免一些错误,因为函数返回值只能作右值,全局变量则不然。
例如cOneChar==4;可能被误为cOneChar=4;
规则四:
有限的封装与多态
不要忘记C++的class源于C的struct,C++的虚函数机制实质是函数指针。
为了使数据、方法能够封装在一起,提高代码的重用度,如对于一些与硬件相关的数据结构,建议采用在数据结构中将访问该数据结构的函数定义为结构内部的函数指针。
这样当硬件变化,需要重写访问该硬件的函数,只要将重写的函数地址赋给该函数指针,高层代码由于使用的是函数指针,所以完全不用动,实现代码重用。
而且该函数指针可以通过传参数或全局变量的方式传给高层代码,比较方便。
例如:
structOneStruct
{
intm_imember;
int(*func)(int,int);
//……
}t2;
1.4OO方法与C++
时下程序员与编程者无一不在使用C++编译器,但并非每个人都在使用OO方法编程,新瓶装旧酒者大有人在,尤其在嵌入式系统领域。
是什么限制了C++方法的使用,古云:
士欲善其事必先利其器,现利器在手却如同钝刀,牛刀给只会杀鸡的妇人能杀牛吗?
我认为还是对OO方法与C++的理解存在一些问题。
你或许和我一样曾存在许多问题:
何时应使用友元、继承与组合有什么不同、何时应用虚函数,什么是静态成员……等等,这些问题让我们共同一一思考吧。
1.4.1C++运行效率分析
OO方法有三个基本概念:
“类与对象”、“继承与组合”、“虚函数与多态”。
理解这些概念,有助于提高程序的质量,特别是提高“可复用性”与“可扩充性”。
C++实现了OO方法的三个基本概念,与C比较在效率上有什么不同呢?
我们应该分清楚编译器期行为和运行期行为,只有运行期行为才会影响程序的速度,编译期行为对代码/数据的开销有影响。
另外时间与空间两个因素对于程序而言,永远是一对矛盾,我们需要作的是折衷,不是时间换空间,便是空间换时间,在此不由感叹中国悠久的中庸之道的光辉,却不明其何以未能为中国的现代文明带来辉煌。
分析一:
C++引入类与对象,类比于C的结构和函数原型列表,C++根据pubic、private关键字决定数据与方法访问的允许性,这些决定在编译期完成,因此单纯加入类即不会影响代码的执行速度,也不会影响代码的大小,仅仅影响了成员函数参数表的大小,C++会在成员函数参数表中加入一个this指针,这个开销是很小的。
分析二:
默认参数值也是基本没有运行损失的,编译器只是加入代码使得在每次函数被无参数调用时传递一个默认的值。
保持良好的调用习惯是必须的,默认参数值可以作为一种容错手段。
值得注意的是代码大小增加了,这点开销看你是否能接受,若不行可以不用默认参数值,这不是拒绝使用C++的理由。
分析三:
函数名重载也是在编译时的修改,具有相同名字但不同参数的函数在编译过程中分配了一个唯一的名字,由编译器编译连接时进行正确的匹配。
函数名重载对代码在时间空间上没有任何损失,只是增加了编译期的时间。
操作符重载与函数名重载基本相同。
分析四:
引用与传递指针是等效的,而且显得比较简明,这个特性也并不是必须的。
分析五:
构造函数和析构函数有一些相关的损失,其保证对象在创建和超出范围时被自动调用,增加了运行开销,然而这个小量的开销对于减少错误而言是合理的代价。
分析六:
继承机制的运行开销(不采用虚函数)基本没有,相反提供了良好的代码重用机制。
分析七:
C++真正运行开销较大的是虚函数和模板。
虚函数需要为每个继承链上的对象增加一个vtable表,模板需要在水平方向适应不同类型。
但两者能保持合理的性能代价比,实现代码的真正重用。
综上所述,C++三大特性中对运行特性影响较大的是多态性,记住世上永没有免费的午餐,是否采用C++的新特性,完全可以因地制宜。
对于PC系统,C++增加的开销完全是可以被带来的增益所抵消,关于这一点,在本书后续的章节中会以具体实现加以论述。
对于嵌入式系统,64K以上代码的系统是可以考虑采用类、继承特性,适当采用虚函数特性,对于模板则要慎重;64K代码以下的系统由于代码数据的资源均有限,可以采用上节提到的C模块化方法。
1.4.2封装程度原则
对象(Object)是类(Class)的一个实例(Instance)。
如果将对象比作房子,那么类就是房子的设计图纸。
所以面向对象程序设计的重点是类的设计,而不是对象的设计。
类可以将数据和函数封装在一起,其中函数表示了类的行为(或称服务)。
其基本原则是数据私有,函数共有,只有成员函数才能访问私有的数据。
类提供关键字public、protected和private用于声明哪些数据和函数是公有的、受保护的或者是私有的。
这样可以达到信息隐藏的目的,即让类仅仅公开必须要让外界知道的内容,而隐藏其它一切内容。
我们不可以滥用类的封装功能,封装不是万能的真主,只有成员函数才能访问私有数据在某些场合将成为效率的灾难。
例如:
classA
{
private:
intimember;
public:
intGeti(){r