1.存储类型
标准C语言为变量、常量、函数定义了4种存储类型:extern,auto,static,register,它们分别用一个关键字(存储类型说明符)来说明。这4种存储类型可分为两种生存期限:永久的(即整个程序执行期间都存在)【extern和static】和临时的(即暂时保存在堆栈和寄存器中)【auto和register】。
全局变量和全局函数默认存储类型为extern,能够被定义在它们之后的同一个编译单元内的函数所调用,如果变量和函数被显式地加上extern声明,那么其他编译单元中的函数也能调用它们。若变量和函数被显式地加上static声明,那么它们具有static存储类型,只能被同一个编译单元内的函数调用。
局部变量默认auto存储类型,除非用static和register来定义。但它们的作用域都是程序块作用域,连接类型都是内连接,进入函数时创建,退出函数时销毁。Register和auto只能用于声明局部变量和局部常量。
全局常量默认存储类型为static,除非在定义了它的编译单元之外的其他编译单元中显示地用extern声明,否则不能被访问。
局部符号常量默认auto,除非显式定义为static或register。
函数的形参是局部变量,一般为auto。
用register修饰的变量会被直接加载到CPU寄存器中,提高程序运行效率。
2.作用域规则
作用域是一个标识符能够起作用的程序范围。
标准C语言中,这些范围包括文件、函数、程序块、函数原型。
标准C++中,作用域类型还有类、名字空间。
标号是具有函数作用域的唯一一种标识符,能够在函数体内的任何一个地方被访问到,一般用在goto语句中。
局部变量具有程序块块作用域(由{}决定),创建在函数堆栈上,但是由于在嵌套的程序块中内层程序块定义的变量不能在其外层程序块中访问,即如果在函数中嵌套的程序块可以定义相同名字的变量,内层变量会遮蔽外层的同名变量。
当局部变量与一个全局变量同名时,在函数内部将遮蔽该全局变量。此时在函数内部可用一元作用域解析运算符(::)来引用全局变量,例如:::g_iCount++。
在任何函数、类型定义及名字空间定义之外的标识符具有文件作用域,包括函数定义、类型定义本身,它们从其定义位置开始直到所在文件结尾都是可见的,也叫文件级的标识符。
函数原型中的形参名称是具有函数原型作用域的唯一一种标识符,不会与其他函数的同名形参名称冲突,该形参名会被编译器忽略。但是函数定义中的形参是局部变量,具有程序块作用域。
C++的类是有行为能力的抽象数据类型(ADT),在类作用域中,类的非静态成员函数可以直接访问类的其他任何成员;但在类的作用域外,只能通过类的对象、对象指针及对象引用来访问类成员。成员函数中定义的局部变量具有程序块作用域,会遮蔽同名的类的数据成员,此时访问同名的数据成员方法:this->flag=flag;或ClassName::flag=flag;
3.连接类型
表明了一个标识符的可见性,分为
外连接:标识符能够在其他编译单元中或者在定义它的编译单元中的其他范围内被调用。需要分配运行时的存储空间。例如:
void f(bool flag){...} // 函数定义是外连接的
int g_int; //全局变量g_int是外连接的
extern const int MAX_LENGTH=1024;// MAX_LENGTH变成外连接的
namespace NS_H
{
long count; //NS_H::count是外连接的
bool g(); //NS_H::g是外连接的,原型是内连接
}
内连接:标识符能够在定义它的编译单元中的其他范围内被调用。但不能在其他编译单元中被调用。例如:
static void f2(){...} //f2为内连接的
union //匿名联合的成员为内连接的
{
long count;
char *p;
};
class Me{...}; //Me是内连接的
const int MAX_LENGTH=1024; // 常量是内连接的
typedef long Integer; //typedef为内连接的
无连接:仅能够在声明它的范围内被调用。例如:
void f()
{
int a; //a是无连接的
class B{...}; //局部类是无连接的,具有程序块作用域
}
4.表格总结
程序元素的存储类型、作用域、生存期限及连接类型
程序元素 |
存储类型 |
作用域 |
生存期限 |
连接类型 |
全局ADT/UDT定义 |
文件 |
内连接 |
||
嵌套的ADT/UDT定义 |
类 |
外连接 |
||
局部ADT/UDT定义 |
程序块 |
无连接 |
||
非静态全局函数和全局变量 |
extern |
文件 |
永久 |
外连接 |
静态全局函数和全局变量 |
static |
文件 |
永久 |
内连接 |
局部非静态变量/常量 |
auto |
程序块 |
临时 |
无连接 |
局部静态变量/常量 |
static |
程序块 |
永久 |
无连接 |
静态全局常量 |
static |
文件 |
永久 |
内连接 |
非静态全局常量 |
C和C++有所不同 |
|||
类的静态成员 |
static |
类 |
永久 |
内连接 |
类的非静态成员 |
类 |
内连接 |
||
名字空间的成员 |
不确定 |
名字空间 |
不确定 |
外连接 |
外部函数原型 |
文件 |
内连接 |
||
程序块中的函数原型 |
程序块 |
内连接 |
||
宏定义 |
文件 |
内连接 |
5.使用断言
断言(assert)语义:如果表达式的值为0(假),则输出错误消息并终止程序的执行(一般还会出现对话框,说明在什么地方引发了assert);如果表达式为真,则不进行任何操作。因此断言失败就表明程序存在一个bug。
C++/C的宏assert(expression)就是断言,表达式为假时,调用库函数abort()终止程序。
程序一般分为Debug版本和Release版本,Debug版本用于内部调试,Release版本发行给用户使用。由于assert(expression)的宏体全部被条件编译伪指令#ifdef_DEBUG 和#endif所包含,因此assert()只在Debug版本内有效。
在函数入口处,建议使用断言来检查参数的有效性。例如内存拷贝函数mencpy,如果assert的表达式为假,那么程序就会中止。
void *memcpy(void *pvTo,const void*pvFrom,size_t size)
{
//使用断言,防止pvTo或pvFrom为NULL
assret((pvTo!=NULL)&&(pvFrom!=NULL));typedef char byte;
byte *pbTo=(byte*)pvTo; //防止改变pvTo的地址
byte *pbFrom=(byte*)pvFrom; //防止改变pvFrom的地址
while(size-->0){ //不要与字符串拷贝相混淆!
*pbTo++=*pbFrom++;
}
return pvTo;
}
但上述情况也不能保证万无一失,如果给memcpy()传入两个未初始化的野指针(地址不为0但是没有指向合法的内存块),那么assert()就失去了作用。
使用断言的目的是捕捉在运行时不应该发生的非法情况,不同于错误情况,后者是程序运行过程中自然存在的并且是一定要做出主动处理的。
例如用malloc申请动态内存时,如果系统没有足够的内存可用,那么malloc返回NULL。动态申请内存失败不是非法情况,而是错误情况。所以要用if语句捕捉错误情况并给出错误处理代码,而不应该用assert。
//错误的示例
int *pBuf=(int*)malloc(sizeof(int)*1000);
assert(pBuf!=NULL);//错误的使用assert
...//do something
//正确的示例
int *pBuf=(int*)malloc(sizeof(int)*1000);
if(pBuf==NULL)
{
...//错误处理代码
}else
{
...// do something
}
要区分断言(assert)和跟踪语句(tracer)的不同,后者是指一些用于报告程序执行过程中当前状态的输出语句,但它们并不一定就是bug。
6.使用cons提高函数的健壮性
被const修饰的东西都受到C++/C语言实现的静态类型安全检查机制的保护,可以预防意外修改,能提高程序的健壮性。
例如:void StringCopy(char *strDest,const char *strSrc);
给strSrc加上const修饰符后,如果函数体内的语句试图改动strSrc指向的内存单元,编译器将指出错误。
如果还想保护指针本身,则声明指针本身为常量,防止该指针的值被改变:
void OutoutString(const char* const pStr);
如果输入参数采用值传递,一般不用const修饰,因为函数是用实参的拷贝初始化形参,及时在函数内部修改的该参数,改变的也只是堆栈上的拷贝而不是实参。
对于ADT/UDT的输入参数,应该将“值传递”改为“const&传递”,提高效率。对于基本数据类型的输入参数,不要这样改,会费解。
如果给“指针传递”的函数返回值加const修饰符,那么函数返回值是一种契约性常量,不能被直接修改,并且该返回值只能被赋值给加const修饰的同类型指针(除非强制转型)。
例如函数:const char *GetString(void);
调用时:const char *str=GetString();