C++学习——第10章 程序文件和预处理器指令

来源:互联网 发布:宝宝树 知乎 编辑:程序博客网 时间:2024/04/28 21:32

1.       使用程序文件

C++程序包含两大类文件:

·  头文件:这种文件一般用文件扩展名.h来标识。这些文件包含类型定义和其他用于程序中一个或多个源文件的代码。

·  源文件:其扩展名通常是.cpp,也可以使用.c、.cxx或其他扩展名。这种文件包含要编辑成机器指令的代码——主要是函数定义。需要的头文件通过#include指令添加到源文件中。

 

头文件和源文件主要区别是它们的使用方式不同。根据约定,在头文件和源文件添加什么内容和处理他们方式如下:

·  .cpp文件主要包括确定程序做什么的代码,它们由函数定义组成。

·  .h文件包含函数声明(即函数原型,但不是定义)、内联函数定义、枚举和自己的类型定义,以及预处理器指令。应避免把函数定义放在.h文件中(inline函数时一个例外),否则该函数在程序总就有重复的定义,导致链接错误。

·  .h文件还可以包含在两个或多个.cpp文件中共享的常量定义。

·  每个.cpp文件必须包含它需要的头文件。

·  在编译程序时,只编译.cpp文件,根据#include指令,该文件包含了程序需要的头文件内容。

·  编译的每个.cpp文件都会生成一个对象文件。术语“对象文件”与类对象没有任何关系。对象文件只是包含编译器的二进制输出的文件,它常常用扩展名.obj来标识。对象文件又链接程序组合到一个可执行模块中,该模块的扩展名通常是.exe。

1.1   名称的作用域

对于程序中的一个名称来说,作用于用于指定该名称有效的语句的范围。变量名或其他在语句块中声明的名称(放在花括号中)有块作用域,这称为局部作用域,因为这种名称就包含在声明它的语句块中。

名称的隐藏:可以在外部作用域中定义一个与内部作用域相同的名称,此时,在内部作用域中定义的名称会遮挡外部作用域的名称。

如下面的代码:

int main()

{

         const int limit = 10;

         std::cout << "Outer limit is " << limit << std::endl;

         {

                   const int limit = 5;

                   std::cout << "Inner limit is " << limit << std::endl;   //limit为5,覆盖了上面的10

                   for(int i=1; i<=limit; i++)    //limit为5

                            std::cout << std::endl << i << "squared is " << i*i;

         }

}

访问隐藏的名称:可以使用作用域解析运算符“::”选择在外部作用域中定义的变量,而不是在当前作用域中的同名变量。

int main()

{

         const int limit = 10;

         std::cout << "Outer limit is " << limit << std::endl;

         {

                   const int limit = 5;

                   std::cout << "Inner limit is " << limit << std::endl;

                   for(int i=1; i<=::limit; i++)      //引用了上面的10

                            std::cout << std::endl << i << "squared is " << i*i;

         }

}

 

全局作用域

const int limit = 10;

 

int main()

{

                   for(int i=1; i<=limit; i++)

                            std::cout << std::endl << i << "squared is " << i*i;

}

当然定义为全局作用域的名称可以用同名的局部变量隐藏

const int limit = 10;

 

int main()

{

                   const int limit = 5;

                   std::cout << "Inner limit is " << limit << std::endl;

                   for(int i=1; i<=limit; i++)

                            std::cout << std::endl << i << "squared is " << i*i;

}

也可以用作用域解析运算符来访问全局变量limit

const int limit = 10;

 

int main()

{

                   const int limit = 5;

                   std::cout << "Inner limit is " << limit << std::endl;

                   for(int i=1; i<=::limit; i++)

                            std::cout << std::endl << i << "squared is " << i*i;

 

}

 

1.2   “一个定义”规则

在转换单元(它是添加了所有包含头文件内容的.cpp文件)中,每个不是局部块作用域的变量、函数、类类型、枚举类型或模板都只能定义一次。

例如:变量可以有多个声明,但它表示的内容只能有一个惟一的定义。

内联函数是这个规则的例外,内联函数的定义必须出现在调用该函数的每个转换单元中,但在所有的转换单元中,给定内联函数的所有定义都必须相同。

 

1.3   程序文件和链接

转换单元中的实体常常需要在另一个转换单元中访问。显然函数就是这方面的例子。

转换单元中的名称在编译/链接过程中处理的方式由属性linkage来确定。

linkage表示由一个名称表示的实体可以在程序代码的什么地方使用。

程序中使用的每个名称要么有链接属性,要么没有链接属性。

判断:当某个名称用于在声明该名称的作用域外部的代码块中访问程序的变量或函数时,就有链接属性,否则没有。

 

确定名称的链接属性

三个链接属性含义:

·  内部链接属性:该名称表示的实体可以在同一个转换单元的任何地方访问。例:在全局作用域中定义的、声明为const的变量名在默认情况下具有内部链接属性。

·  外部链接属性:具有这种链接属性的名称除了可以在定义它的转换单元中访问之外,还可以在另一个转换单元中访问。换而言之,该名称表示的实体可以在整个程序中共享和访问。例:前面编写的所有函数以及在全局作用域中定义的非const变量都有外部链接属性。

·  没有链接属性:名称如果没有链接属性,它表示的实体只能在应用于该名称的作用域中访问。

 

1.4   外部名称

在由几个文件组成的程序中,一个源文件中的函数调用与另一个文件中的函数定义之间的连接由链接程序建立(或解析)。

如果函数没有在调用它的转换单元中定义,编译器就会把这个调用标记为外部,让链接程序处理它。

变量名是不同的。编译器需要某种提示:某个名称的定义对于当前转换的单元来说是外部的。如果希望用一个名称访问当前转换单元外部的变量,就必须用extern关键字来声明该变量。

extern double pi;

这个语句声明名称pi在当前块的外部定义。类型必须对应于定义中的类型。在extern声明中不能有初始值。

把变量声明为extern,表示它是在另一个转换单元中定义的。因此编译器就把变量标记为具有外部链接属性。链接程序就会在名称和它引用的变量之间建立连接。

下面代码不会编译:

int main()

{

         int limit = 10;

         std::cout << "Local limit is " << limit << std::endl;

         extern int limit = 5;

                   std::cout << "Inner limit is " << limit << std::endl;

                   for(int i=1; i<=limit; i++)

                            std::cout << std::endl << i << "squared is " < i*i;

         return 0;

}

因为limit的extern声明和定义出现在同一个块作用域中,所以编译器会把它标记为一个错误。这是因为limit有两个定义:外部定义和本地定义。(违反一个定义规则)

但是下面的可以编译:

int main()

{

         int limit = 10;

         std::cout << "Local limit is " << limit << std::endl;

         {

                   extern int limit = 5;

                   std::cout << "Inner limit is " << limit << std::endl;

                   for(int i=1; i<=limit; i++)

                            std::cout << std::endl << i << "squared is " < i*i;

         }

         return 0;

}

 

 

2.       命名空间

命名空间是程序中的一个区域,该区域把一个额外的名称(即命名空间的名称)附加到该区域中的所有实体名上。

在一个程序中,常常使用一个命名空间名称表示所有有同一目标的代码集合。每个命名空间都表示函数、相关的全局变量和声明的某个逻辑组合。命名空间还可以用于完全包含一个发布单元,例如一个库。

标准库是在命名空间std中定义的。这就是说,标准库中的每个外部名称都加上了前缀std。

例如:std::cout。

std::cout<<std::endl<<value;

代码:使用命名空间(此代码包含两个.cpp文件)

第一个文件只包含一些常量的定义,在命名空间中定义。

#include <string>namespace data{extern const double pi = 3.14159265;extern const std::string days[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};}

另一个包含main()的转换单元中使用这些变量:

#include <iostream>#include <string>namespace data{extern const double pi;extern const std::string days[];}int main(){std::cout << std::endl  << "pi has the value "  << data::pi << std::endl;std::cout << "The second day of the week is "  << data::days[1] << std::endl;return 0;}

 

使用using声明

using namespace_name::identifier;

注意:在使用未限定的变量名时,编译器在使用前先在当前作用域中查找该变量的定义。如果没有找到,就查找包含当前块的外层块。这样一直继续下去,直到全局作用域为止。如果变量的声明没有在全局作用域中找到(它可以是extern声明),编译器就断定该变量没有定义。

 

函数和命名空间

函数定义不一定要放在命名空间块中。

 

函数模板和命名空间

可以在命名空间中定义函数模板。

 

扩展命名空间

 

未指定名称的命名空间

namespace{

//Code in the namespace, functions, etc.

}

每个未指定名称的命名空间在转换单元中都是惟一的。

在不同转换单元中的未指定名称的命名空间都是不同的命名空间。

两点重要意义:

·  指定名称的命名空间不在局命名空间中。

·  未指定名称的命名空间在转换单元中是惟一的。

作用:把函数定义放在未指定名称的命名空间中,与在全局命名空间中把函数声明为static有相同的效果。

未指定名称的命名空间可以更好地限制可访问性。

 

命名空间的别名

为命名空间名称定义别名的一般语法如下:

namespace 别名 = 原来的命名空间名称;

然后就可以使用别名代替原来的命名空间名称,来访问命名空间中的名称。

 

嵌套的命名空间

代码:

//outin.hnamespace outer{double max (double* data, const int& size){//body code...}double min (double* data, const int& size){//body code...}namespace inner{double* normalize(double* data, const int& size){//...double minValue = min(data, size);//...}}}

在命名空间inner中,函数normalize()可以直接调用函数min()(它在命名空间outer中)。

 

1.       预处理器

预处理器通常是编译器的一个组成部分。它是编译器把C++程序代码编译为机器指令之前执行的一个过程。

预处理器的任务是根据包含在源文件中的指令,使源代码正确进入编译阶段——这些指令称为预处理器指令。

所有的预处理器指令都以符号#开头,以便与C++语言语句区分开来。

 

在程序中包含头文件

#include <iostream>

 

程序中的置换

#define PI 3.14159265

使用#define指令的三个缺点:

·  #define指令不支持类型检查

·  #define指令不考虑作用域

·  符号名不能限制在一个命名空间中

从程序中删除标志

#define VALUE

这个指令的结果是在程序文件中,从这个指令后面的语句中删除所有的标识符VALUE。

取消宏名的定义

使用#undef指令可以取消对标示符的定义。

#undef VALUE

宏置换

允许调用多个参数化的置换,提供了更大范围的结果。

例子:

#define Print(Var) cout << (Var) <<endl

这个指令提供了两个级别的置换。在#define语句中,Print(Var)用其后的字符串来置换,var也可以进行置换。

例如:

Print(ival);

预处理器会把它转换为下面的语句:

cout << (ival) << endl;

置换指令的一般形式为:

#define 标识符(标识符列表) 置换字符串

注意:在第一个标识符和左括号之间不能有空格,否则括号就被解释为置换字符串的一部分。

放在多行代码中的预处理器指令

#define min(x,y) \

               ( (x)<(y) ? (x):(y) )

方法是在每行的末尾使用续行符“\”。

把字符串作为宏参数

#define MYSTR “This string”

定义了这个宏,下面这个语句

cout<<MYSTR;

就会置换为:

cout<<”This string”;

而如果语句中加上双引号,则不会置换,如:

cout << “MYSTR”;

则不会被置换,引号内的内容都会被看做字面量字符串,所以预处理器不会处理它。

把宏实现为字符串,例如:

#define PrintString(arg) cout << #arg

#放在参数arg的前面,表示在进行置换时,参数放在双引号中。

若编写如下语句:

PrintString(Hello);

预处理器就会转换为:

cout << “Hello”;

提示:没有这个技术,就不能把可变的字符串放在宏定义中。

这个机制的一种用法师把变量名转换为字符串。如:

#define show(var) cout<<#var<<” = ” (var)<<endl

这个宏创建了输出变量名及其值的一种缩写方式。如编写下面语句:

show(number);

就会生成下面的语句:

cout << “number”<<” = ” (number) << endl;

在宏表达式中连接参数

#define join(a,b) a##b

编写下面语句:

strlen(join(var, 123));

生成:

strlen(var123);

 

2.       逻辑预处理器指令

逻辑#if指令

使用方式:

1. 可以测试某个符号以前是否用#define指令定义过。

2. 可以测试某个常量表达式是否为真。

测试某个标识符是否存在

#if defined 标识符

如果指令的标识符已定义,#if后面的语句组就包含在要编译的源文件中。这个与剧组用如下指令结束:

#endif

现在看例子,假定在程序文件中有以下代码:

double average = 0.0;

 

#if defined CALCAVERAGE

int count=sizeof data/sizeof data[0];

for(int i=0; i<count; i++)

         average += data[i];

average /= count;

#endif

#if指令测试符号CALCAVERAGE是否已由以前的预处理器指令定义。如果已经定义,就把#if和#endif指令之间的代码编译为程序的一部分。否则就不包含这段代码。

测试标识符是否已定义还有一种缩写方式。下面的测试:

#if defined CALCAVERAGE

可以替换为:

#ifdef CALCAVERAGE

这样就更简明,以#ifdef开头的语句块应以#endif结束。

防止代码重复

也可以测试标识符是否不存在。

#if !defined 标识符

如果标示符以前没有定义,就把#if和#endif之间的语句包含在要编译的源文件中。其缩写方式:

#ifndef 标识符

使用#if指令来防止程序中出现重复的代码,在绝对不能重复的代码块的开头和结尾处,加入下面代码:

#if !defined MYHEADER_H

#define MYHEADER_H

 

// Block of code that must not be dupulicated

#endif

把#if#endif组合放在每个头文件内容的外部,是一种标准方式。这样可以确保每个头文件的内容在源文件中不会出现多次。

注意:以这种方式保护头文件的代码,是一种非常好的编程习惯。一旦把自己的函数收集到几个库中,就非常容易杜绝代码块重复的问题。

注释:#ifdef和#ifndef常常用于包含某个操作系统环境中专用的代码。

测试特定值的指令

#if 常量表达式

常量表达式必须是不包含强制转换的整数常量表达式。它可以包含预处理器宏,但在进行完所有的置换后,常量表达式必须是一个整数表达式。

这常常应用于测试前面预处理器指令赋予一个标示符的特定值。例如:

#if CPU==PENTIUM4

//Code taking advantage of Pentium 4 capability

#endif

只有标识符CPU在以前的#define指令中定义为PENTIUM4,#if和#endif指令之间的语句才会包含到程序中。

多个代码选择块

#if CPU==PENTIUM4

cout<<” PENTIUM4 code version.”<<endl;

#else

cout <<”Older Pentium code version.”<<endl;

#endif

预处理器还允许利用#if的一种特殊形式进行多项选择,可以从几个代码块中选择一个代码块:

#elif 常量表达式

例子:

#if LANGUAGE==ENGLISH

#define Greeting”Good Morning.”

#elif LANGUAGE==GERMAN

#definGreeting”Guten Tag.”

#elif LANGUAGE==FRENCH

#define Greeting”Bonjour.”

#else

#define Greeting”hi.”

#endif

std::cout << Greeting << std::endl;

标准的预处理器宏

预处理器定义了几个标准的宏,可以在需要时调用它们。(略)

#error和#pragma指令

预处理阶段,如果出席西安了错误,#error指令可以生成一个诊断信息。因此这个指令一般在测试某个条件(例如#if指令)后执行。执行#error指令的结果是把指令行中包含的内容显式为一个编译错误消息,并立即停止编译。例如:

#ifndef _cplusplus

#error “Error – Should be C++”

#endif

还可以在#error指令行中包含其他预处理器宏。

#pragma指令专门实现预先定义好的选项,其结果在编译器说明文档中进行了详细解释。

 

3.       调试方法

介绍大多数调试系统通用的基本方法,并讨论C++库中标准的基本调试技巧。

集成调试器

常见功能:

·  跟踪程序流

·  设置断点

·  设置观察窗口

·  检查程序元素

调试中的预处理器指令

(略)

使用assert宏

诊断工具assert()宏是在标准库中提供的。其在库头文件<cassert>中声明,它可以测试逻辑表达式,若指定的逻辑表达式是false,assert()就会终止程序,并显示诊断消息。

代码:演示assert宏

#include <iostream>#include <cassert>using std::cout;using std::endl;int main(){int x=0;int y=5;cout << endl;for(x=0; x<20; x++){cout << "x= " << x << " y= " << y << endl;assert(x<y);}return 0;}

关闭断言机制

如果在重新编译程序时,,在程序文件的开头定义符号NDEBUG,就可以关闭断言机制:

#define NDEBUG

如果把这个指令放到上面的代码的开头,就会得到从0到19的所有x值,且不显示诊断信息。

注意:这个指令仅放在<cassert>的#include语句之前才有效。如:

ord<� e�P)=0 cellpadding=0 style='border-collapse:collapse;border:none;mso-border-alt:solid windowtext .5pt; mso-yfti-tbllook:1056;mso-padding-alt:0cm 5.4pt 0cm 5.4pt'>

using namespace_name::identifier;

注意:在使用未限定的变量名时,编译器在使用前先在当前作用域中查找该变量的定义。如果没有找到,就查找包含当前块的外层块。这样一直继续下去,直到全局作用域为止。如果变量的声明没有在全局作用域中找到(它可以是extern声明),编译器就断定该变量没有定义。

 

函数和命名空间

函数定义不一定要放在命名空间块中。

 

函数模板和命名空间

可以在命名空间中定义函数模板。

 

扩展命名空间

 

未指定名称的命名空间

namespace{

//Code in the namespace, functions, etc.

}

每个未指定名称的命名空间在转换单元中都是惟一的。

在不同转换单元中的未指定名称的命名空间都是不同的命名空间。

两点重要意义:

·  指定名称的命名空间不在局命名空间中。

·  未指定名称的命名空间在转换单元中是惟一的。

作用:把函数定义放在未指定名称的命名空间中,与在全局命名空间中把函数声明为static有相同的效果。

未指定名称的命名空间可以更好地限制可访问性。

 

命名空间的别名

为命名空间名称定义别名的一般语法如下:

namespace 别名 = 原来的命名空间名称;

然后就可以使用别名代替原来的命名空间名称,来访问命名空间中的名称。

 

嵌套的命名空间

代码:

#include <iostream>#define NDEBUG#include <cassert>using std::cout;using std::endl;int main(){int x=0;int y=5;cout << endl;for(x=0; x<20; x++){cout << "x= " << x << " y= " << y << endl;assert(x<y);}return 0;}