C语言编程技巧
volatile的用法
volatile的本意是“易变的” EETOP专业博客---电子工程师自己的家园!~XP0Z,|Rz[#p‑~
M7p e6W{5d#k'^0由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:EETOP专业博客---电子工程师自己的家园9x+Q%ww_
static int i=0;jt[1]Y/W V5y1t0EETOP专业博客---电子工程师自己的家园$`7@(jC/w7G0hf
int main(void)EETOP专业博客---电子工程师自己的家园'C,yk @4x U M+]
{ ,Q,](b4J!? z p@0...
!xN"N~h[1]d;[0while (1)
‑W MG3S&^P-U7w&C0{EETOP专业博客---电子工程师自己的家园f/\h Zd*E
if (i) dosomething();
6mCWh I ~0}
1f&]I)t5Z0x&q^[1]c)T0}EETOP专业博客---电子工程师自己的家园0N-O$P'r2r8Pq/O8t
iu^ `i f [0/* Interrupt service routine. */EETOP专业博客---电子工程师自己的家园T}+i6MY
void ISR_2(void)
$v%^#{*IM4k u i0{EETOP专业博客---电子工程师自己的家园C:j(u\ b:`
i=1;EETOP专业博客---电子工程师自己的家园6A!G#w ?C-? }&kO
}cQM(ED"A0
#x3ix"?@)J‑Tk E I0程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。+Z{ e[,{/IzB0EETOP专业博客---电子工程师自己的家园
ADw VWf;[w&s8R
一般说来,volatile用在如下的几个地方:y \&V.atr,?-\*V0EETOP专业博客---电子工程师自己的家园(A-RcO&uY
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;EETOP专业博客---电子工程师自己的家园n?b'w'Aw‑A"u
b3HT:lj,v(K02、多任务环境下各任务间共享的标志应该加volatile;*c S.M'Y(r0EETOP专业博客---电子工程师自己的家园;t5g)AW'Ko } ?9m1Ha
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;EETOP专业博客---电子工程师自己的家园E*v |8j2v$b
7_6\#Y;~%p%j V[1]]:Upf0另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。
关于C++中的内联函数(inline)
在c++中,为了解决一些频繁调用的小函数大量消耗栈空间或者是叫栈内存的问题,特别的引入了inline修饰符,表示为内联函数。 ]&CMg[1]R6s%MU0
8O
I'D+eC&N3{‑X6c*k0 可能说到这里,很多人还不明白什么是栈空间,其实栈空间就是指放置程序的局部数据也就是函数内数据的内存空间,在系统下,栈空间是有限的,如果频繁大量的使用就会造成因栈空间不足所造成的程序出错的问题,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭。 下面我们来看一个例子: #include <iostream>
cR Y+Y']1ba&h+Hh3L0#include <string>
VHN&Z+J*u5e0using
namespace std; OS7a2RD{(~ J5}'M*{
6A \(Z$B6B0inline
string dbtest(int a); //函数原形声明为inline即:内联函数
%E[1]_'S{#Bq0K4c6lx k3N0 EETOP专业博客---电子工程师自己的家园8U
A@ dg/M9U3J8p eM7Z EETOP专业博客---电子工程师自己的家园+o%H)E?-I+V1a*]x
void
main()
:r Y Q&Oz*^Fs|'HN0{
5M [|UXx[1]j|o0 for (int i=1;i<=10;i++)
,p y3sc‑pHK)`&C0 { EETOP专业博客---电子工程师自己的家园-I6J I:S5E2_ G.C
K8^
l
cout << i << ":" << dbtest(i) << endl;
Bk"a‑Z/cr9Q0 } EETOP专业博客---电子工程师自己的家园C6~ SN6?$]l
cin.get(); EETOP专业博客---电子工程师自己的家园[1]O}h E2v7p"g
m`X!k,o$PlI0} EETOP专业博客---电子工程师自己的家园 t!s.o-if#X.|EETOP专业博客---电子工程师自己的家园 p;X;yHu
string dbtest(int a)//这里不用再次inline,当然加上inline也是不会出错的
EETOP专业博客---电子工程师自己的家园5D(TGk(u:Bw)F8w&i
{
7L2b-w‑sXZ!yJ [,P j0 return (a%2>0)?"奇":"偶"; EETOP专业博客---电子工程师自己的家园~,i"V:k-EQ EETOP专业博客---电子工程师自己的家园Motp#vO"q)F
}
上面的例子就是标准的内联函数的用法,使用inline修饰带来的好处我们表面看不出来,其实在内部的工作就是在每个for循环的内部所有调用dbtest(i)的地方都换成了(i%2>0)?"奇":"偶"这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗。 说到这里很多人可能会问,既然inline这么好,还不如把所谓的函数都声明成inline,嗯,这个问题是要注意的,inline的使用是有所限制的,inline只适合函数体内代码简单的函数使用,不能包含复杂的结构控制语句例如while switch,并且不能内联函数本身不能是直接递归函数(自己内部还调用自己的函数)。 说到这里我们不得不说一下在c语言中广泛被使用的#define语句,是的define的确也可以做到inline的这些工作,但是define是会产生副作用的,尤其是不同类型参数所导致的错误,由此可见inline有更强的约束性和能够让编译器检查出更多错误的特性,在c++中是不推荐使用define的。
关于const和#define的区别尽量用const和inline而不用#define 这个条款最好称为:“尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。这是问题之一。再看下面的语句: #define ASPECT_RATIO 1.653 编译器会永远也看不到ASPECT_RATIO这个符号名,因为在源码进入编译器之前,它会被预处理程序去掉,于是ASPECT_RATIO不会加入到符号列表中。如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是1.653,而不是ASPECT_RATIO。如果ASPECT_RATIO不是在你自己写的头文件中定义的,你就会奇怪1.653是从哪里来的,甚至会花时间跟踪下去。这个问题也会出现在符号调试器中,因为同样地,你所写的符号名不会出现在符号列表中。
解决这个问题的方案很简单:不用预处理宏,定义一个常量: const double ASPECT_RATIO = 1.653; 这种方法很有效。但有两个特殊情况要注意。
首先,定义指针常量时会有点不同。因为常量定义一般是放在头文件中(许多源文件会包含它),除了指针所指的类型要定义成const外,重要的是指针也经常要定义成const。例如,要在头文件中定义一个基于char*的字符串常量,你要写两次const: const char * const authorName = "Scott Meyers"; 关于const的含义和用法,特别是和指针相关联的问题,参见条款21。 另外,定义某个类(class)的常量一般也很方便,只有一点点不同。要把常量限制在类中,首先要使它成为类的成员;为了保证常量最多只有一份拷贝,还要把它定义为静态成员: class GamePlayer {
private:
static const int NUM_TURNS = 5; // constant eclaration
int scores[NUM_TURNS]; // use of constant
...
}; 还有一点,正如你看到的,上面的语句是NUM_TURNS的声明,而不是定义,所以你还必须在类的实现代码文件中定义类的静态成员: const int GamePlayer::NUM_TURNS; // mandatory definition;
// goes in class impl.file 你不必过于担心这种小事。如果你忘了定义,链接器会提醒你。 旧一点的编译器会不接受这种语法,因为它认为类的静态成员在声明时定义初始值是非法的;而且,类内只允许初始化整数类型(如:int, bool, char 等),还只能是常量。
在上面的语法不能使用的情况下,可以在定义时赋初值:
class EngineeringConstants { // this goes in the class
private: // header file
static const double FUDGE_FACTOR;
...
};
// this goes in the class implementation file
const double EngineeringConstants::FUDGE_FACTOR = 1.35; 大多数情况下你只要做这么多。唯一例外的是当你的类在编译时需要用到这个类的常量的情况,例如上面GamePlayer::scores数组的声明(编译过程中编译器一定要知道数组的大小)。所以,为了弥补那些(不正确地)禁止类内进行整型类常量初始化的编译器的不足,可以采用称之为“借用enum”的方法来解决。这种技术很好地利用了当需要int类型时可以使用枚举类型的原则,所以GamePlayer也可以象这样来定义:
class GamePlayer {
private:
enum { NUM_TURNS = 5 } // "the enum hack" — makes
// NUM_TURNS a symbolic name
// for 5
int scores[NUM_TURNS];// fine
}; 除非你正在用老的编译器(即写于1995年之前),你不必借用enum。当然,知道有这种方法还是值得的,因为这种可以追溯到很久以前的时代的代码可是不常见的哟。 回到预处理的话题上来。另一个普遍的#define指令的用法是用它来实现那些看起来象函数而又不会导致函数调用的宏。典型的例子是计算两个对象的最大值:
#define max(a,b) ((a) > (b) ? (a) : (b)) 这个语句有很多缺陷,光想想都让人头疼,甚至比在高峰时间到高速公路去开车还让人痛苦。
无论什么时候你写了象这样的宏,你必须记住在写宏体时对每个参数都要加上括号;否则,别人调用你的宏时如果用了表达式就会造成很大的麻烦。但是即使你象这样做了,还会有象下面这样奇怪的事发生: int a = 5, b = 0;
max(++a, b);// a 的值增加了2次
max(++a, b+10); // a 的值只增加了1次 这种情况下,max内部发生些什么取决于它比较的是什么值!
幸运的是你不必再忍受这样愚笨的语句了。你可以用普通函数实现宏的效率,再加上可预计的行为和类型安全,这就是内联函数(见条款33):
inline int max(int a, int b) { return a > b ? a : b; }
不过这和上面的宏不大一样,因为这个版本的max只能处理int类型。但模板可以很轻巧地解决这个问题:
template<class T>
inline const T& max(const T& a, const T& b)
{ return a > b ? a : b; } 这个模板产生了一整套函数,每个函数拿两个可以转换成同种类型的对象进行比较然后返回较大的(常量)对象的引用。因为不知道T的类型,返回时传递引用可以提高效率(见条款22)。 顺便说一句,在你打算用模板写象max这样有用的通用函数时,先检查一下标准库(见条款49),看看他们是不是已经存在。比如说上面说的max,你会惊喜地发现你可以后人乘凉:max是C++标准库的一部分。
有了const和inline,你对预处理的需要减少了,但也不能完全没有它。抛弃#include的日子还很远,#ifdef/#ifndef在控制编译的过程中还扮演重要角色。预处理还不能退休,但你一定要计划给它经常放长假。
|