漫步云端

移动开发(Android、iPhone、Windows Mobile) | JAVA | C | C++ | .net | Objective C | 微软企业开发技术 | 嵌入式系统设计与开发
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

C++ 域和生命期(一):全局域和头文件

Posted on 2010-12-19 21:30  charley_yang  阅读(1571)  评论(0编辑  收藏  举报

C++支持三种形式的域,局部域( local scope),  名字空间域 (namespace scope) 以及类域 (class scope)。

1. 局部域是包含在函数定义或者函数块中的程序文本部分。

2. 名字空间域是不包含在函数声明、函数定义或者类定义内的程序文本部分。 程序的最外层的名字空间域被称作全局域 (global scope )或全局名字空间域 (global namespace scope ),程序员也可以利用名字空间定义(namespace definition) 来定义用户声明的 的名字空间 它们被嵌套在全局域内。

3. 每个类定义都引入了一个独立的类域。

 

一、局部域的一个注意点

if ( int *pi = getValue() )
{
// pi != 0 --这里可以使用*pi
int result = calc(*pi);
// ...
}
else
{
// pi 在这里也可见
// pi == 0
cout << "error: getValue() failed" << endl;
}

    在 if语句的条件中定义的变量,比如pi,只在该 if语句和相关的 else 语句,以及这些语句内部的嵌套域中可见,条件的值是变量初始化的值,如果 pi 被初始化为 0,即空指针值则该条件值为 false,执行 if语句的 else 部分,如果 pi 被任意其他非空指针值初始化,则该条件为 true,if部分被执行。

二、全局对象和函数

1. 声明和定义

    被调用的或者被取地址的全局函数必须有一个定义,同样地,程序中用到的全局对象也必须有一个定义。全局对象和非 inline 全局函数在一个程序内只能被定义一次;而只要给出的定义完全相同即可,inline 函数可以在一个程序中被定义多次。这要求全局对象和函数或者只有一个定义,或者在一个程序中有多个完全相同的定义,这样的要求被称为“一次定义法则( ODR,one definition rule)”。

    在全局域中定义的对象,如果没有指定显式的初始值,则该存储区被初始化为 0,因此下面两个定义中 var1和 var2有相同的初始值0 :
int var1 = 0;
int var2;

 

    在一个程序中,一个全局对象只能有一个定义,因为在使用文件中的对象之前必须先要声明这个对象,所以对于一个由多个文件构成的程序来说,它应该能够只声明一个对象而不定义它,我们该怎样声明一个对象呢 ?
关键字extern为声明但不定义一个对象提供了一种方法,实际上,它类似于函数声明 承诺了该对象会在其他地方被定义,或者在此文本文件中的其他地方 或者在程序的其他文
本文件中 例如 
extern int i;
对程序来说是一个保证,表示在其他某个地方存在一个如下所示的定义 
int i;
extern声明不会引起内存被分配,它可以在同一文件中或同一程序的不同文件中出现多次,典型情况下,全局对象的声明只在公共的头文件中出现一次,当一个程序文件需要引用这个全局对象时,它可以包含这个头文件: 
// 头文件
extern int obj1;
extern int obj2;
 
// 文本文件
int obj1 = 97;
int obj2;
既指定了关键字 extern 又指定了一个显式初始值的全局对象声明将被视为该对象的定义,编译器将会为其分配存储区,而且该对象后续的定义都被标记为错误,例如 
extern const double pi = 3.1416; // 定义
 
const double pi; // 错误: 重复定义 pi
关键字 extern也可以在函数声明中指定,惟一的影响是将该声明的隐式属性“在其他地方定义” 变为显式的,这样的声明有下列形式 :
extern void putValues( int*, int );

 

2.不同文件之间声明的匹配

    在多个文件中声明对象或函数的一个可能问题是,在不同文件中的声明可能会随时间而不同或改变, C++为检查不同文件中函数声明的差异提供了一些支持。
例如,在文件 token.C中,函数 addToken()被定义为带有一个unsigned char 型的参数,而在文件lex.C中 addToken()被调用,它被声明为带有一个char 型的参数 
// ---- token.C 中 ----
int addToken( unsigned char tok ) { /* ... */ }
 
// ---- lex.C 中 ----
extern int addToken( char );

在 lex.C中调用addToken()会导致链接阶段失败。

    在C++中有一种机制,通过它可以把函数参数的类型和数目编码在函数名中,该机制叫做类型安全链接( type-safe-linkage) 类型安全链接可用来帮助捕捉不同文件中函数声明不匹配的情况,在前面的例子中 unsigned char 型的参数和 char 型参数的类型不同,由于类型安全链接,在 lex.C中声明的addToken()将会被标记为未定义的函数,而 token.C中的定义则被视为定义了另外一个函数。 

    不同文件中出现的同一对象或函数声明的其他类型不匹配情况,在编译或链接时可能不会被捕捉到。 因为编译器一次只能处理一个文件,它不能很容易地检查到文件之间的类型违例,这些类型违例可能是程序严重错误的根源。例如,文件之间错误的对象声明或函数返问类型就不能被检测出来,这样的错误只能在运行时刻异常或程序的错误输出中才能被揭示出来。 
// token.C 中
unsigned char lastTok = 0;
unsigned char peekTok() { /* ... */ }
 
// in lex.C
extern char lastTok; // 最后一个 token
extern char peekTok(); // 查看 token

使用头文件是防止此类错误的基本法则

三、谈谈头文件

    头文件为所有 extern对象声明、函数声明以及inline 函数定义提供了一个集中的位置 ,这被称作声明的局部化(localization), 如果一个文件要使用或定义一个对象或函数时,它必须包含 include 相应的头文件。 
    头文件提供了两个安全保证。第一,保证所有文件都包含同一个全局对象或函数的同一份声明。第二, 如果需要修改声明,则只需改变一个头文件,从而不至于再发生只修改了某一个特殊的文件中的声明。addToken()例子给出了如下的 token.h 头文件 
// ---- token.h ----
typedef unsigned char uchar;
const uchar INLINE = 128;
// ...
const uchar LT = ...;
const uchar GT = ...;
extern uchar lastTok;
extern int addToken( uchar );
inline bool is_relational( uchar tok )
{ return (tok >= LT && tok <= GT); }
 
// ----- lex.C -----
#include "token.h"
// ...
 
// ----- token.C -----
#include "token.h"
// ...


    设计头文件有一些要注意的地方。头文件提供的声明逻辑上应该属于一个组。编译头文件也需要时间,如果头文件过大,或分散的元素太多,程序员可能会不愿意因为包含它而增加编译时间开销。为降低编译时间开销,有些 C++实现提供了预编译头文件支持,请查询系统的C++实现参考手册,了解怎样从一个普通的 C++头文件创建预编译头文件,如果应用程序有很大的头文件,则使用预编译头文件而不是普通头文件可以大大降低应用程序的编译时 间 
   

    第二个考虑是,头文件不应该含有非inline 函数或对象的定义。例如,下面的代码表示的正是这样的定义,因此不应该出现在头文件中:
extern int ival = 10;
double fica_rate;
extern void dummy() {}

    虽然 ival 是用 extern声明的 ,但是它的显式初始化使得它实际上是个定义,类似的情况 ,虽然 dummy()显式地声明为 extern,但是空花括号代表该函数的定义,尽管 fica_rate没有被显式地初始化,但是因为缺少 extern 因而也被视为C++中实际的定义,这些定义如果在同 一程序的两个或多个文件中被包含,就会产生重复定义的编译错误。 
    在前面给出的 token.h 头文件中 常量INLINE 和 inline 函数 is_relational()好像都违反了这条规则。但是,其实并非如此,虽然它们全是定义,但是符号常量定义以及 inline 函数定义是特殊的定义,符号常量和 inline 函数可以被定义多次。

 

    在程序编译期间,在可能的情况下,符号常量的值会代替该名字的出现,这个替代过程被称为常量折叠 (constant folding) 例如:当 INLINE 被用在一个文件中时,编译器用 128 代替名字 INLINE,为了使编译器能够用一个常量值替换它的名字,该常量的定义(它的初始值)必须在它被使用的文件中可见,因为这个原因,符号常量可以在同一程序的不同文件中被定义多次, 尽管理想情况下,一个具有初始值的常量可以被包含在多个不同的文件中 ,但是常量折叠使其变得并不必需,甚至在可执行文件只要出现一次就行 。 但是,在某些情况下不可能做到符号常量的常量折叠过程,在这样的情况下,最好把常量的初始化移到某一个程序文本文件中,这可以由显式地声明常量为 extern来实现。例如 :
// ----- 头文件 -----
const int buf_chunk = 1024;
extern char *const bufp;
 
// ----- 程序文本文件 -----
char *const bufp = new char[buf_chunk];
    虽然 bufp 被声明为 const 但是它的值却无法在编译时刻被计算出来 ,它的初始化值是一个要求调用库函数的new表达式,如果 bufp 在头文件中被初始化,那么它将在每个包含它的文件中被定义,这不但浪费了空间,而且可能与程序员的意图不符 。符号常量是任何 const 型的对象,当下面的声明被放到一个头文件中,并且由程序的两个独立的文件包含它时,就会导致链接错误。
// 喔! 不应该被放在一个头文件中
const char* msg = "?? oops: error: ";
问题出在 msg不是常量,它是一个指向常量值的非常量指针,常量指针的声明如下:

const char *const msg = "?? oops: error: ";
该常量指针的定义可以出现在多个文件中。

 

    与符号常量类似的情形也适用于 inline 函数,为使编译器能够在函数被调用的地方内联地展开函数体,它必须能够看到 inline 函数的定义 。因此 ,如果一个inline 函数将在多个文件中被用到,那么它必须被定义在头文件中。但是,指定一个函数为 inline,只是暗示该函数应该被内联,编译器实际上是否内联该函数,会随编译器的实现而不同,如果编译器在调用点上没有内联该函数则编译器会为该函数生成一个定义,放到可执行文件中,如果在多个文件中生成同一函数的定义,则会产生一个不必要的过大的可执行文件。 

如果出现下列情况,多数编译器都会产生警告,一般情况下,这要求打开编译器的警告模式: 
1.  函数的定义使其根本不可能做成 inline 函数。例如,编译器可能抱怨函数过于复杂而无法内联,在这种情况下,如果可能 ,就应重写该函数,否则,去掉 inline 指示符,把函数定义放到程序文本文件中。 
2.  函数的特殊调用不能被内联,例如,在 C++的最初实现 AT&T(cfront) 中,同—表达式中的一个 inline 函数的第二次调用就无法被内联,在这种情况下,我们可以把表达 式重新改写成两个独立的 inline 函数调用 

    在把一个函数声明为inline 之前,我们必须分析它的运行时刻行为,以确信该函数被内联对于这部分代码来说确实是必要的,建议把那些天生无法内联的函数不声明为inline ,并且不放在头文件中 。