4.10 编译预处理所谓编译预处理就是编译器在对源程序进行正式编译前,根据预处理指令先做一些特殊的处理工作,然后将源程序与预处理的结果一起进行编译。
C++ 语言的预处理主要包括 3 方面的操作:嵌入指令、宏定义和条件编译指令。
应当说明的是,编译预处理指令不属于 C++ 语言的语法范畴。
因此,预处理指令具有其特有语法:起始字符必须为,#”,
并以“回车”结束。
4.10.1 嵌入指令嵌入指令的一般形式为:
#include <file_name> 或 #include "file_name"
其中,file_name 是一个可以带有全路径的文本文件名。例如:
#include <myhead.h>
#include "C:\mypath\ahead.h"
嵌入指令的作用是指示编译器将 file_name 所指定的文本文件插入到该指令所在的位置,然后再进行编译。例如:
// MAINPROG.CPP // INCFILE.CPP
#include "incfile.cpp" #include <iostream.h>
void main() void f(void)
{ {
f(); cout << "Hello\n";
} }
则经编译器预处理后,文件 MAINPROG.CPP 将变成如下的形式:
// MAINPROG.CPP
// 这里是 INCFILE.CPP 中的内容
// 这里是 iostream.h 中 的内容
void f(void)
{
cout << "Hello\n";
}
void main()
{
f();
}
4.10.2 宏
4.10.2.1 无参宏定义
#define macro_mame <text>
其中,macro_name 是一个标识符,它是程序员为宏所取的名字;可选的 text 是一段文本,叫做替换正文,它就是程序员为宏所作的具体定义。例如:
#define ONE 1
#define TWO 2
将使编译器把源程序中所有的 ONE 都解释成 1;所有的
TWO 解释成 2。
几点说明:
1,宏定义必须以“回车”结束,“回车”符前边所有的字符均属于宏定义:
#define TRUE 1;
将使得编译器把宏 TRUE 解释成 1;。
2,宏定义的替换正文可以为空:
#define _MYHEAD_H_
这里,将标识符 _MYHEAD_H_ 作为一个标志,以便于条件编译预处理指令进行测试。
3,替换正文可以是 C++ 语言的关键字:
#define UL unsigned long int
4,一个已存在的宏可以用作另一个宏定义的替换正文:
#define THREE (ONE + TWO)
注意:由于编译器在宏替换时是逐级进行的,所以在定义宏时要防止出现二义性。
例如,若将宏 THREE定义成:
#define THREE ONE + TWO
则赋值语句:
i = THREE * 3;
将使 i 的值为 7,而不是预期的 9。
编译器对该语句的解释过程为:
i = ONE + TWO * 3;
i = 1 + 2 * 3;
i = 1 + 6;
i = 7;
4.10.2.2 定义带参宏
#define macro_name(arg_list) text
其中,arg_list 为宏的形参表。例如:
#define MAX(x,y) ((x) >= (y)? (x),(y))
这时,宏 MAX 就如同一个带参数的内联函数。例如:
m = MAX(a,b);
cout << MAX(3 * i / k,4 + j * k) << endl;
注意:由于带参宏出现二义性的可能性更大,所以在实用中建议多用内联函数而少用带参宏。
4.10.2.3 #undef 指令
#undef macro_name
该指令的作用是撤消对宏 macro_name 的定义。例如:
#define MAXLEN 512
// 对宏 MAXLEN 的一些应用
#undef MAXLEN
#define MAXLEN 128
// 对宏 MAXLEN 的一些应用
4.10.3 条件编译条件编译指令是用来控制编译器有选择地对源程序进行编译的。它们指示编译器可以忽略除 #if,#ifdef,#ifndef,#elif
和 #endif 指令外的所有预处理指令,以及由于条件编译指令的结果而不用参加编译的行。
4.10.3.1 #if,#elif,#else 和 #endif 条件指令这几条预处理指令与 C++ 语言中的 if 语句类似。例:
#include <iostream.h>
#define NUM 100
void main()
{
#if NUM == 100
cout << "This line is compiled\n";
#else
cout << "This line is not compiled\n";
#endif
}
4.10.3.2 #ifdef,#ifndef 指令和 defined 运算符这几条指令可以用来测试某个宏是否定义,进而决定编译器的下一步操作。例:
// MYHEAD.H
#ifndef _MYHEAD_H_
#define _MYHEAD_H_
// 文件 MYHEAD.H 中的内容
#endif
等价表示:
// MYHEAD.H
#if !defined _MYHEAD_H_
#define _MYHEAD_H_
// 文件 MYHEAD.H 中的内容
#endif
注意:在较新的 C++ 标准中,
#ifdef 和 #ifndef 预处理指令是为保持兼容性而保留的,新标准推荐使用 defined 运算符。
#ifdef? #if defined
#ifndef? #if !defined
4.10.3.3 #error 指令
#error errmsg
该指令用来捕捉一些不可预知的编译条件。在正常情况下,
条件为假,若条件为真,编译器将输出一条错误信息并终止编译。
不同的具体实现常常还提供有更多的预处理指令,有些竟达一百多条。使用时请参见具体的软件包。
4.11 程序的多文件组织
4.11.1 连接属性
4.11.1.1 内部连接内部连接也称静态连接。具有内部连接的名字局部于它所处的模块,只能被同一模块中的函数访问。所有局部和静态全局名字均具有内部连接属性。例:
// FILE1.CPP // FILE2.CPP
static int sum(int x,int y) void main()
{ {
return x + y; int sum(int,int);
} int i = sum(3,5);
// 错误,sum 不可访问
}
4.11.1.2 外部连接具有外部连接属性的名字在其它模块中经引用性说明后就可以被访问。所有外部全局名字均具有外部连接属性。例:
// FILE1.CPP // FILE2.CPP
int sum(int x,int y) void main()
{ {
return x + y; extern int sum(int,int);
} int i = sum(3,5);
}
通常,为了使用上的方便,常将具有外部连接属性的名字写在一个称为头文件的源文件中,而在使用这些名字的文件中利用嵌入预处理指令将相应的头文件插入到该文件中。对于 C++
语言程序来讲,头文件是一种极为重要的辅助文件,程序员通常将程序中要用到的函数原型、宏、具有外部连接属性的全局变量以及自定义的数据结构集中地写在一个头文件中,供所有使用它们的源文件嵌入用。
// AHEAD.H
#if !defined _AHEAD_H_ // 防止多次嵌入该文件
#define _AHEAD_H_
extern int Sum;
int Add(int,int);
int InputData(void);
void OutputData(void);
#endif
// FUNCS.CPP
#include <iostream.h>
#include "ahead.h"
int Sum;
int Add(int a,int b)
{
return a + b;
}
int InputData(void)
{
cout << "Enter a integer,";
cin >> x;
return x;
}
void OutputData(void)
{
cout << "The sum is " << Sum << endl;
}
// MAINPROG.CPP
#include "ahead.h"
void main()
{
int a,b;
a = InputData();
b = InputData();
Sum = Add(a,b);
OutputData();
}
4.11.2 分割编译分割编译是指将一个大型软件分解成一个个的模块,并按功能将不同类别的模块存放在不同的源文件中;然后将各个源文件分别进行编译,并设计相应的测试用例对各模块进行调试;最后,将所有模块连接成一个完整的程序。
第 5章 数组
5.1 数组
5.1.1 一维数组
5.1.1.1 说明一维数组
<storage> type array_name[constant];
其中,type 是除 void 之外的任一数据类型(包括用户自定义的数据类型); array_name 为数组的名字; constant 为一非负的整型常量,也可以是一个具有确定值的整型表达式,它给出了数组的大小。例如:
int a[10],b[3 + 5];
5.1.1.2 访问一维数组元素数组也叫数组变量,它是一个集合概念,即数组变量是由一个个元素组成的。数组中的一个元素就相当于一个简单变量,叫做下标变量,其表示方式为数组名后跟一个包含整型表达式的方括号。例如,a[3],b[i]。
既然下标变量就等效一个简单变量,因此对下标变量的访问与访问非数组变量的方法完全相同。
应当说明的是,C++ 语言对下标超界不作任何检查,保证下标在正确范围内是程序员的责任。一个包含 n 个元素的数组变量其下标取值范围为 0~n-1。另外,除用作函数的参数外,
数组变量不可以整体访问。
5.1.1.3 一维数组的初始化与普通变量相同,在说明数组变量的同时也可以给它赋以初值。例如:
int iArr[5] = {2,4,6,8,10};
float fA[6] = {,,,4.5,5.6,6.7};
char cArr[] = {'a','b','c','d'};
5.1.1.4 一维数组的存储形式
char ca[5]
ca[0]
ca[1]
ca[2]
ca[3]
ca[4]
int ia[3]
ia[0]
ia[1]
ia[2]
float fa[2]
fa[0]
fa[1]
5.1.2 多维数组
5.1.2.1 多维数组的说明和访问
<storage> type array_name[c1][c2]…[c3];
其中,方括号的对数就是多维数组的维数,自左至右地叫做第一维、第二维、第三维,… 。例如:
int ia[4][4],ib[2][3] = {{1,2,3},{4,5,6}};
for(int i = 0; i < 4; i ++)
for(int j = 0; j < 4; j ++)
ia[i][j] = (i + 1) * (j + 1);
5.1.2.2 多维数组的存储形式
char carr[3][3]
carr[0][0]
carr[0][1]
carr[0][2]
carr[1][0]
carr[1][1]
carr[1][2]
carr[2][0]
carr[2][1]
carr[2][2]
carr[0]
carr[1]
carr[2]
5.1.3 数组间的赋值
C++ 语言不允许数组整体赋值,两个类型相同的数组相互赋值时,必须逐元素地进行。例:
int ia[] = {1,2,3,4,5},ib[5],i;
for(i = 0; i < 5; i ++)
ib[i] = ia[i];
5.1.4 数组与函数
5.1.4.1 单个数组元素用作函数的参数由于下标变量等同于普通变量,所以单个元素用作参数时与变通变量作为参数没有什么区别。例:
int Square(int a)
{
return a * a;
}
int Arr[5] = {1,2,3,4,5};
for(int i = 0; i < 5; i ++)
cout << Square(Arr[i]) << endl;
5.1.4.2 整个数组用作函数的参数当整个数组作为函数的参数时,函数的形参应当说明成数组形式,而实参应为数组变量(用数组名表示)。例:
void Squares(int ia[],int n)
{
for(i = 0; i < n; i ++)
ia[i] *= ia[i];
}
int iArr[5] = {1,2,3,4,5};
Squares(iArr,5);
for(int i = 0; i < 5; i ++)
cout << iArr[i] << endl;
由上例可以看出,将整个数组作为参数调用函数时,函数有可能改变数组元素的值。这种调用方式叫做 地址调用 。
由于函数不可能返回多于 1 个的值,利用函数可以改变数组参数的元素值这一现象,就能使函数表现出返回多个值的能力。
另外,若不希望函数修改参数值,可以将数组参数说明成
const 的。
int Quadratic(const double factor[3],double result[2])
{
double delta;
delta = factor[1] * factor[1] - 4 * factor[0] * factor[2];
if(delta < 0)
return 0;
delta = sqrt(delta);
result[0] = (-factor[1] + delta) / (2 * factor[0]);
result[1] = (-factor[1] - delta) / (2 * factor[0]);
return 1;
}
5.2 字符串
5.2.1 字符串的存储形式字符串变量简称字符串(或串),它实际上就是一个一维字符型数组,因此它与一维数组的用法完全相同。
应当注意的是,字符串除了像其它数组那样要注意下标超界外,
还应当考虑为字符串结束标志预留一个字节的存储单元。因而,
一个存放 n 个有效字符的字符串变量其大小至少得定义为 n +
1。例:
char str[5] = "C++";
char s[] = "C++ language";
char St[] = {'T','h','e',' ','C','+','+','\0'};
5.2.2 字符串数组由于字符串是一个一维字符型数组,那么一个一维的字符串数组就应当表示成一个二维字符型数组、一个二维的字符串数组就应当表示成一个三维字符型数组,等等。例:
char StrArr[3][7] = {"C++","BASIC","Pascal"};
5.2.3 字符串间的赋值由于字符串从本质上讲是数组类型,所以它们之间也不能整体赋值。但是,由于字符串的自身特点,使得它们的赋值操作比普通数组来得简练。例:
for(int i = 0; ; i ++) {
Targ[i] = Src[i];
if(Targ[i] = '\0’)
break;
}
5.2.4 字符串与函数虽然字符串是数组,但从功能上讲却是一个整体。因此字符串用作函数的参数时,总是以整个数组的形式进行传递的。例:
void StrCopy(char Targ[],const char Src[])
{
int i = 0;
while(Src[i] != 0) {
Targ[i] = Src[i];
i ++;
}
Targ[i] = 0;
}
5.3 字符串处理库函数为了便于用户使用,各种 C++ 语言的具体实现都随软件包提供了大量的字符串处理库函数,而且大都具有很好的移植性。
无论哪种具体实现,其字符串处理库函数的原型均说明在一个文件名为 string.h 的头文件中。下面列出几个常用的字符串处理库函数:
unsigned strlen(const char* str);
char* strcpy(char* str1,const char* str2);
char* strcat(char* str1,const char* str2);
int strcmp(const char* str1,const char* str2);
const char* strstr(const char* str,const char* sub);
const chat* strchr(const char* str,int ch);
习题:
8,9,10,11,12,13,14