关键字registe请求编译器尽可能的将变量存在CPU的寄存器中。有以下几点注意的地方。
对register的使用是有限制条件的,这个体现在具体硬件上。
每个函数中只有很少的变量能够存放在寄存器中,而且,只是确定的类型可以存放在寄存器中。过多的使用寄存器变量并没有坏处,然而,由于“register”会被忽略的。
特别注意:不能用&取地址符号,去获取一个register变量的地址,无论这个变量是否被实际得存放在寄存器里了
register变量必须是能被CPU寄存器所接受的类型,这通常意味着register变量必须是一个单个的值,并且其长度应小于或等于整型的长度。但是,有些机器的寄存器也能存放浮点数。
register变量可能不存放在内存中,所以不能用取址符运算符“ & ”。
只有局部变量和形参可以作为register变量,全局变量不行。
静态变量不能定义为register。
static用法小结
static关键字是C, C++中都存在的关键字, 它主要有三种使用方式, 其中前两种只指在C语言中使用, 第三种在C++中使用(C,C++中具体细微操作不尽相同, 本文以C++为准).
(1)局部静态变量
(2)外部静态变量/函数
(3)静态数据成员/成员函数
下面就这三种使用方式及注意事项分别说明
一、局部静态变量
在C/C++中, 局部变量按照存储形式可分为三种auto, static, register
(<c语言程序设计(第二版)>谭浩强, 第174-175页)
与auto类型(普通)局部变量相比, static局部变量有三点不同
1. 存储空间分配不同
auto类型分配在栈上, 属于动态存储类别, 占动态存储区空间, 函数调用结束后自动释放, 而static分配在静态存储区, 在程序整个运行期间都不释放. 两者之间的作用域相同, 但生存期不同.
2. static局部变量在所处模块在初次运行时进行初始化工作, 且只操作一次
3. 对于局部静态变量, 如果不赋初值, 编译期会自动赋初值0或空字符, 而auto类型的初值是不确定的. (对于C++中的class对象例外, class的对象实例如果不初始化, 则会自动调用默认构造函数, 不管是否是static类型)
特点: static局部变量的”记忆性”与生存期的”全局性”
所谓”记忆性”是指在两次函数调用时, 在第二次调用进入时, 能保持第一次调用退出时的值.
示例程序一
#include <iostream>
using namespace std;
void staticLocalVar()
{
static int a = 0; // 运行期时初始化一次, 下次再调用时, 不进行初始化工作
cout<<"a="<<a<<endl;
++a;
}
int main()
{
staticLocalVar(); // 第一次调用, 输出a=0
staticLocalVar(); // 第二次调用, 记忆了第一次退出时的值, 输出a=1
return 0;
}
应用:
利用”记忆性”, 记录函数调用的次数(示例程序一)
利用生存期的”全局性”, 改善”return a pointer / reference to a local object”的问题. Local object的问题在于退出函数, 生存期即结束,. 利用static的作用, 延长变量的生存期.
示例程序二:
// IP address to string format
// Used in Ethernet Frame and IP Header analysis
const char * IpToStr(UINT32 IpAddr)
{
static char strBuff[16]; // static局部变量, 用于返回地址有效
const unsigned char *pChIP = (const unsigned char *)&IpAddr;
sprintf(strBuff, "%u.%u.%u.%u", pChIP[0], pChIP[1], pChIP[2], pChIP[3]);
return strBuff;
}
注意事项:
1. “记忆性”, 程序运行很重要的一点就是可重复性, 而static变量的”记忆性”破坏了这种可重复性, 造成不同时刻至运行的结果可能不同.
2. “生存期”全局性和唯一性. 普通的local变量的存储空间分配在stack上, 因此每次调用函数时, 分配的空间都可能不一样, 而static具有全局唯一性的特点, 每次调用时, 都指向同一块内存, 这就造成一个很重要的问题 ---- 不可重入性!!!
这样在多线程程序设计或递归程序设计中, 要特别注意这个问题.
(不可重入性的例子可以参见<effective C++ (2nd)>(影印版)第103-105页)
下面针对示例程序二, 分析在多线程情况下的不安全性.(为方便描述, 标上行号)
① const char * IpToStr(UINT32 IpAddr)
② {
③ static char strBuff[16]; // static局部变量, 用于返回地址有效
④ const unsigned char *pChIP = (const unsigned char *)&IpAddr;
⑤ sprintf(strBuff, "%u.%u.%u.%u", pChIP[0], pChIP[1], pChIP[2], pChIP[3]);
⑥ return strBuff;
⑦ }
假设现在有两个线程A,B运行期间都需要调用IpToStr()函数, 将32位的IP地址转换成点分10进制的字符串形式. 现A先获得执行机会, 执行IpToStr(), 传入的参数是0x0B090A0A, 顺序执行完应该返回的指针存储区内容是:”10.10.9.11”, 现执行到⑥时, 失去执行权, 调度到B线程执行, B线程传入的参数是0xA8A8A8C0, 执行至⑦, 静态存储区的内容是192.168.168.168. 当再调度到A执行时, 从⑥继续执行, 由于strBuff的全局唯一性, 内容已经被B线程冲掉, 此时返回的将是192.168.168.168字符串, 不再是10.10.9.11字符串.
二、外部静态变量/函数
在C中static有了第二种含义:用来表示不能被其它文件访问的全局变量和函数。, 但为了限制全局变量/函数的作用域, 函数或变量前加static使得函数成为静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)。注意此时, 对于外部(全局)变量, 不论是否有static限制, 它的存储区域都是在静态存储区, 生存期都是全局的. 此时的static只是起作用域限制作用, 限定作用域在本模块(文件)内部.
使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名。
示例程序三:
//file1.cpp
static int varA;
int varB;
extern void funA()
{
……
}
static void funB()
{
……
}
//file2.cpp
extern int varB; // 使用file1.cpp中定义的全局变量
extern int varA; // 错误! varA是static类型, 无法在其他文件中使用
extern vod funA(); // 使用file1.cpp中定义的函数
extern void funB(); // 错误! 无法使用file1.cpp文件中static函数
三、静态数据成员/成员函数(C++特有)
C++重用了这个关键字,并赋予它与前面不同的第三种含义:表示属于一个类而不是属于此类的任何特定对象的变量和函数. 这是与普通成员函数的最大区别, 也是其应用所在, 比如在对某一个类的对象进行计数时, 计数生成多少个类的实例, 就可以用到静态数据成员. 在这里面, static既不是限定作用域的, 也不是扩展生存期的作用, 而是指示变量/函数在此类中的唯一性. 这也是”属于一个类而不是属于此类的任何特定对象的变量和函数”的含义. 因为它是对整个类来说是唯一的, 因此不可能属于某一个实例对象的. (针对静态数据成员而言, 成员函数不管是否是static, 在内存中只有一个副本, 普通成员函数调用时, 需要传入this指针, static成员函数调用时, 没有this指针. )
请看示例程序四(<effective c++ (2nd)>(影印版)第59页)
class EnemyTarget {
public:
EnemyTarget() { ++numTargets; }
EnemyTarget(const EnemyTarget&) { ++numTargets; }
~EnemyTarget() { --numTargets; }
static size_t numberOfTargets() { return numTargets; }
bool destroy(); // returns success of attempt to destroy EnemyTarget object
private:
static size_t numTargets; // object counter
};
// class statics must be defined outside the class;
// initialization is to 0 by default
size_t EnemyTarget::numTargets;
在这个例子中, 静态数据成员numTargets就是用来计数产生的对象个数的.
另外, 在设计类的多线程操作时, 由于POSIX库下的线程函数pthread_create()要求是全局的, 普通成员函数无法直接做为线程函数, 可以考虑用Static成员函数做线程函数.
const用法主要是防止定义的对象再次被修改,定义对象变量时要初始化变量
下面我就介绍一下几种常见的用法
1.用于定义常量变量,这样这个变量在后面就不可以再被修改
const int Val = 10;
//Val = 20; //错误,不可被修改
2. 保护传参时参数不被修改,如果使用引用传递参数或按地址传递参数给一个函数,在这个函数里这个参数的值若被修改,
则函数外部传进来的变量的值也发生改变,若想保护传进来的变量不被修改,可以使用const保护
void fun1(const int &val)
{
//val = 10; //出错
}
void fun2(int &val)
{
val = 10; //没有出错
}
void main()
{
int a = 2;
int b = 2;
fun1(a); //因为出错,这个函数结束时a的值还是2
fun2(b);//因为没有出错,函数结束时b的值为10
}
如果只想把值传给函数,而且这个不能被修改,则可以使用const保护变量,有人会问为什么不按值传递,按值传递还需要把这个值复制一遍,
而引用不需要,使用引用是为了提高效率//如果按值传递的话,没必要加const,那样根本没意义
3. 节约内存空间,
#define PI 3.14 //使用#define宏
const double Pi = 3.14 //使用const,这时候Pi并没有放入内存中
double a = Pi; //这时候才为Pi分配内存,不过后面再有这样的定义也不会再分配内存
double b = PI; //编译时分配内存
double c = Pi; //不会再分配内存,
double d = PI; //编译时再分配内存
const定义的变量,系统只为它分配一次内存,而使用#define定义的常量宏,能分配好多次,这样const就很节约空间
4.类中使用const修饰函数防止修改非static类成员变量
class
{
public:
void fun() const //加const修饰
{
a = 10; //出错,不可修改非static变量
b = 10; //对,可以修改
}
private:
int a ;
static int b;
}
5.修饰指针
const int *A; 或 int const *A; //const修饰指向的对象,A可变,A指向的对象不可变
int *const A; //const修饰指针A, A不可变,A指向的对象可变
const int *const A; //指针A和A指向的对象都不可变
6.修饰函数返回值,防止返回值被改变
const int fun();
接收返回值的变量也必须加const
const int a = fun(); //接收的变量也要是const的,int a = fun()是错误的
7.修饰类的成员变量
使用const修饰的变量必须初始化,在类中又不能在定义时初始化,
如;
class
{
private:
int a = 10;
const int b = 10;
static const int c = 10;
//这样初始化都是错的,
}
初始化const int类型(没有static),在类的构造函数上初始化
Class Test
{
Public:
Test():b(23) //构造函数上初始化b的值为23
{
}
private:
const int b ;
}
初始化staticconst int这个类型的(带有static的),在类的外面初始化
class Test
{
private:
static const int c;
}
const int Test::c=10; //类的外部初始化c为10
8.const定义的对象变量只能作用于这个程序该C/C++文件,不能被该程序的其他C/C++文件调用,
如file1.cpp中 const int val;
在file2.cpp中, extern intval; //错误,无法调用,
要想const定义的对象变量能被其他文件调用,定义时必须使用extern修饰为
extern const int val;
非const变量默认为extern,要是const能被其他文件访问必须显示指定为extern
先来讲一讲#define的优点与缺点:
[优点]
当在程序中想要修改某一个值得时候,而且这个值又在很多地方引用。这时候就能体现出宏定义的强大优点了。
例如:
#define LINK LED0 //定义LINK的灯为LED0
- 1
因为在程序中我们可能很多地方要对LINK灯进行操作,当我们想换LINK灯的时候,直接将:
#define LINK LED1
- 1
这样就很方便了,不用一处处去找,而且不用担心有漏掉的可能。
[缺点]
宏定义有一些缺点:
(1) 无法对宏定义中的变量进行类型检查
此缺点,是相对于const变量来说的
[define与const的区别的简单总结]
define定义的变量,是Compile-Time时期的变量,系统在编译时候,就将其全部替换,而不会对其变量进行类型等属性检查,相对不是很安全,可能存在潜在的问题,而没有发现.
正因为其仅仅是编译时期替换,所以其定义的变量,是不会在运行时候分配内存的,不占用内存空间.
const定义的变量,是 Run-Time时期的变量,如果类型不匹配,系统在运行时候,就会发现并提示或报错,对应的,const变量在运行时期,也是一种变量,系统会为其分配内存.
(2) 边界效应
A. 未加括号带来的边界效应
由于宏定义的时候,其各个分量未加括号,而在使用宏定义的时候,传递的参数是变量的表达式,然后经过系统展开后,由于优先级的原因,导致其结果不是你所希望的.
[例子]
#define MUL(A,B) A*B
- 1
而在使用的时候,这样的调用:
int a=1,b=2,c=3,d=0;
d=MUL(a+b,c)
经过编译时候展开,就变成了
d=a+b*c
而不是我们所希望的
d=(a+b)*c
[解决办法]
其解决办法也很简单,就是给每个分量,都加上括号,就可以避免此类问题
即,在宏定义的时候,如此定义:
#define MUL(A,B) ((A)*(B))
- 1
- #define的变体,即#ifndef,可以防止头头文件的重复引用
[释]
#ifdef和 #define组合,一般用于头文件中,用以实现防止多个文件对此同一个头文件的重复引用.实际使用中,即使你的头文件暂时没有被多个文件所引用,为了增加程序可读性,移植性,健壮性等,还是最好都加上。其用法一般为:
#ifndef <</SPAN>标识>
#define <</SPAN>标识>
……… // include or define sth.
#endif
- 1
- 2
- 3
- 4
<标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的;标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,
如:stdio.h对应的就是:
#ifndef _STDIO_H_
#define _STDIO_H_
……… // include or define sth.
#endif
- 1
- 2
- 3
- 4
举个例子:
#ifndef CHARDEV_H
#define CHARDEV_H
#include
#define MAJOR_NUM 100
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
……
……
#endif
例子中的chardev.h中,就用到了
#ifndef CHARDEV_H
#define CHARDEV_H
…
#endif
- 1
- 2
- 3
- 4
这样做之后,以后某个文件引用此头文件,就包含了chardev.h文件,而其他文件如果再引用此头文件的话,编译器那么就会判断出来,已经define了CHARDEV_H,已经有其他文件引用了此文件,so,the complier will not include this header file. 就可以发现解决重复引用头文件的问题了.
2. #define的变体,即#ifdef,可以实现加入自己需要的模块(源文件)
[例子]
在源文件中加入
#ifdef MYSELF_H
#include "myself.c"
#endif
- 1
- 2
- 3
可以实现在源文件中加入myself.c的代码,将其实现的功能加进来, 即加入了myself模块
3. 简单的define定义
#define MAXTIME 1000
- 1
一个简单的MAXTIME就定义好了,它代表1000,如果在程序里面写
if(i<MAXTIME){.........}
- 1
编译器在处理这个代码之前会对MAXTIME进行处理替换为1000。
这样的定义看起来类似于普通的常量定义CONST,但也有着不同,因为define的定义更像是简单的文本替换,而不是作为一个量来使用,这个问题在下面反映的尤为突出。
4. define的“函数定义”
define可以像函数那样接受一些参数,如下
#define max(x,y) (x)>(y)?(x):(y);
- 1
这个定义就将返回两个数中较大的那个,看到了吗?因为这个“函数”没有类型检查,就好像一个函数模板似的,当然,它绝对没有模板那么安全就是了。可以作为一个简单的模板来使用而已。
但是这样做的话存在隐患,例子如下:
#define Add(a,b) a+b;
- 1
在一般使用的时候是没有问题的,但是如果遇到如:c * Add(a,b) * d的时候就会出现问题,代数式的本意是a+b然后去和c,d相乘,但是因为使用了define(它只是一个简单的替换),所以式子实际上变成了
c*a + b*d
- 1
另外举一个例子:
#define pin (int*);
- 1
pin a,b;
- 1
本意是a和b都是int型指针,但是实际上变成int* a,b;
a是int型指针,而b是int型变量。
这是应该使用typedef来代替define,这样a和b就都是int型指针了。
即:
typedef pin (int *)
pin a,b;
- 1
- 2
- 3
此时:a和b都是 int*型的。
所以我们在定义的时候,养成一个良好的习惯,建议所有的层次都要加括号。
5. 宏的单行定义
#define A(x) T_##x
#define B(x) #@x
#define C(x) #x
- 1
- 2
- 3
我们假设:x=1,则有:
A(1)------〉T_1
B(1)------〉'1'
C(1)------〉"1"
- 1
- 2
- 3
6. define的多行定义
define可以替代多行的代码,例如MFC中的宏定义(非常的经典,虽然让人看了恶心)
#define MACRO(arg1, arg2) do { /
/* declarations */ /
stmt1; /
stmt2; /
/* ... */ /
} while(0) /* (no trailing ; ) */
- 1
- 2
- 3
- 4
- 5
- 6
关键是要在每一个换行的时候加上一个”/”
7 .在大规模的开发过程中,特别是跨平台和系统的软件里,define最重要的功能是条件编译。
即:
#ifdef WINDOWS
......
......
#endif
#ifdef LINUX
......
......
#endif
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
可以在编译的时候通过#define设置编译环境
8. 如何定义宏、取消宏
//定义宏
#define [MacroName] [MacroValue]
//取消宏
#undef [MacroName]
//普通宏
#define PI (3.1415926)
带参数的宏
#define max(a,b) ((a)>(b)? (a),(b))
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
关键是十分容易产生错误,包括机器和人理解上的差异等等。
9. 条件编译
#ifdef XXX…(#else) … #endif
例如
#ifdef DV22_AUX_INPUT
#define AUX_MODE 3
#else
#define AUY_MODE 3
#endif
#ifndef XXX … (#else) … #endif