1.5 函数和预处理
在面向过程的结构化程序设计中,通常需要若干个模块实现较复杂的功能,而每一个模块自成结构,用来解决一些子问题。这种能完成某一独立功能的子程序模块,称为函数。一个较为复杂的程序一般是由一个主函数main与若干个子函数组合而成的。但C++是一种面向对象的程序设计语言,它与面向过程设计方法的最大不同是引入了“类和对象”的概念,而此时函数是构造“类”成员的一种手段。
1.5.1 函数的定义和调用
前面已提过,一个程序开始运行时,系统自动调用main主函数。主函数可以调用子函数,子函数还可以调用其他子函数。调用其他函数的函数称为“主调函数”,被其他函数调用的函数称为“被调函数”。
一般来说,C++程序中除主函数main外,其他函数可以是库函数或自定义函数。库函数又称标准函数,是ANSI/ISO C++编译系统预先定义好的函数,程序设计时可根据实际需要,直接使用这类函数,而不必重新定义。自定义函数是用户根据程序的需要,将某一个功能相对独立的程序定义成的一个函数,或将解决某个问题的算法用一个函数来组织。在C++程序中,与变量的使用规则相同,自定义函数一定要先说明并定义,然后才能被调用。
1.函数的定义
在C++程序中,定义一个函数的格式如下:
<函数类型> <函数名>( <形式参数表> ) { <若干语句> }
可以看出,一个函数的定义是由函数名、函数类型、形式参数表和函数体四部分组成的。函数类型决定了函数所需要的返回值类型,它可以是函数或数组之外任何有效的C++数据类型,包括构造的数据类型、指针等。如果不需要函数有返回值,只要定义函数的类型为void即可。
函数名必须是一个有效的C++标识符(注意命名规则),函数名后面必须跟一对圆括号“( )”,以区别于变量名及其他用户定义的标识名。
函数的形式参数写在括号内,参数表中的参数个数可以是0,表示没有参数,但圆括号不能省略,也可以是一个或多个参数,但多个参数间要用逗号分隔。
函数的函数体由在一对花括号中的若干条语句组成,用于实现这个函数执行的动作。C++不允许在一个函数体中再定义函数。
根据上述定义,可以编写一个函数。例如,下列函数的作用是计算两个整数的绝对值之和:
int sum(int x,int y) { if(x<0) x=-x; if(y<0) y=-y; int z = x + y; return z; }
其中,x和y是此函数的形式参数,简称形参。所谓形参,是指调用此函数所需要的参数个数和类型。一般地,只有当函数被调用时,系统才会给形参分配内存单元,而当调用结束后,形参所占的内存单元又会被释放。
上述函数定义中,int可以省略,因为C++规定凡不加类型说明的函数,一律自动按整型(int)处理。由于sum的类型是整型,因此必须要有返回值,且返回值的类型应与函数类型相同,也是整型;若返回值的类型与函数类型不相同,则按类型自动转换方式转换成函数的类型。关键字return负责将后面的值作为函数的返回值,并将流程返回到调用此函数的位置处。
由于return的后面可以是常量、变量或任何合法的表达式,因此函数sum也可简化为:
int sum(int x,int y) { if(x<0) x=-x; if(y<0) y=-y; return (x+y); // 括号可以省略,即return x+y; }
若函数类型是void,函数体就不需要return语句或return的后面只有一个分号。需要注意的是,因为return是返回语句,它将退出函数体,所以一旦执行return语句,在函数体内return后面的语句便不再被执行。例如:
void f1(int a) { if(a>10) return; // … }
在这里,return语句起了一个改变语句顺序的作用。
2.函数的调用
定义一个函数就是为了以后的调用。调用函数时,先写函数名,然后紧跟括号,括号里是实际调用该函数时所给定的参数,称为实际参数,简称实参,并与形参相对应。函数调用的一般形式为:
<函数名>( <实际参数表> );
调用函数时要注意:实参与形参的个数应相等,类型应一致,且按顺序对应,一一传递数据。例如,下面的示例用来输出一个三角形图案。
【例Ex_Call】 函数的调用
#include <iostream.h> void printline(char ch, int n) { for (int i = 0 ; i<n ; i++) cout<<ch; cout<<endl ; } int main() { int row=5; for (int i = 0; i<row; i++) printline('*',i+1); //A语句 return 0; }
程序运行结果如下:
*
**
***
****
*****
代码中,main函数的for循环语句共调用了5次printline函数(A语句),每次调用时因实参i+1的值不断改变,从而使函数printline打印出来的星号个数也随之改变。
printline函数由于没有返回值,因此它作为一个语句来调用。事实上,对于有返回值的函数也可进行这种方式的调用,只是此时不使用返回值,仅要求函数完成一定的操作。实际上,在C++中,一个函数的调用方式还有很多。例如,对于前面的sum函数还可有下列调用方式:
sum(3,4); //B语句 int c=2*sum(4,5); //C语句 c=sum(c,sum(c,4)); //D语句
其中,B语句是将函数作为一个语句,不使用返回值,只要求函数完成一定的操作;C语句把函数作为表达式的一部分,将返回值参与运算,结果为c = 18;D语句是将函数作为函数的实参,等价于“c = sum(18, sum(18,4));”,执行函数参数内的sum(18,4)后,等价于“c = sum(18,22) ;”,最后结果为c = 40。
3.函数的声明
在【例Ex_Call】中,由于函数printline的定义代码位置在调用A语句(在main函数中)之前,因而A语句执行不会有问题。但若将函数printline的定义代码位置放在调用A语句之后,即函数定义在后,而调用在前,就会产生“printline标识符未定义”的编译错误。此时必须在调用前进行函数声明。
函数声明消除了函数定义的位置的影响,也就是说,不管函数是在何处定义的,只要在调用前进行了函数的声明就可保证函数调用的合法性。虽然,函数不一定在程序的开始就声明,但为了提高程序的可读性,保证简洁的程序结构,最好将主函数main放在程序的开头,而将函数声明放在主函数main之前。
声明一个函数按下列格式进行:
<函数类型> <函数名>( <形式参数表> );
可见,函数声明的格式是在函数头的后面加上分号“;”。但要注意,函数声明的内容应和函数的定义相同。例如,对于前面遇到的sum函数和printline函数可有如下声明:
int sum(int x, int y); void printline(char ch, int n);
由于函数的声明仅是对函数的原型进行说明,即函数原型声明,其声明的形参变量名在声明语句中并没有任何语句操作它,因此这里的形参名和函数定义时的形参名可以不同,且函数声明时的形参名还可以省略,但函数名、函数类型、形参类型及个数应与定义时相同。例如,下面几种形式都是对sum函数原型的合法声明:
int sum(int a,int b); // 允许原型声明时的形参名与定义时不同 int sum(int,int); // 省略全部形参名 int sum(int a,int); // 省略部分形参名 int sum(int,int b); // 省略部分形参名
不过,从程序的可读性考虑,在声明函数原型时,为每一个形参指定有意义的标识符,并且和函数定义时的参数名相同,是一个非常好的习惯。
1.5.2 函数的参数传递
在讨论函数的参数传递前先简单介绍全局变量和局部变量的概念。
C++中每一个变量必须先定义后使用,若变量是在函数体内使用变量前定义的,则此变量就是一个局部变量,它只能在函数体内使用,在函数体外则不能使用它。若变量是在函数外部(如在main主函数前)定义的,它能被后面的所有函数或语句引用,这样的变量就是全局变量。但如果一个函数试图修改一个全局变量的值,也会引起结构不清晰、容易混淆等副作用。因此许多函数都尽量使用局部变量,而将形参和函数类型作为公共接口,以保证函数的独立性。
C++中函数的参数传递有两种方式,一种是按值传递,另一种是地址传递或引用传递。这里先来说明按值传递的参数传递方法。
所谓按值传递(简称值传递),是指当一个函数被调用时,C++根据实参和形参的对应关系将实际参数的值一一传递给形参,供函数执行时使用。函数本身不对实参进行操作,也就是说,即使形参的值在函数中发生了变化,实参的值也不会受到影响。
【例Ex_SwapValue】 交换函数两个参数的值
#include <iostream.h> void swap(float x, float y) { float temp; temp=x;x=y;y=temp; cout<<"x="<<x<<",y="<<y<<"\n"; } int main() { float a=20,b=40; cout<<"a="<<a<<",b="<<b<<"\n"; swap(a,b); cout<<"a="<<a<<",b="<<b<<"\n"; return 0; }
程序运行结果如下:
a = 20, b = 40
x = 40, y = 20
a = 20, b = 40
可以看出,虽然函数swap中交换了两个形参x和y的值,但交换的结果并不能改变实参的值,所以调用该函数后,变量a和b的值仍然为原来的值。
所以,当函数的形参是一般变量时,由于其参数传递方式是值传递,因此函数调用时所指定的实参可以是常量、变量、函数或表达式等,总之只要有确定的值就可以。例如前面的“printline('*', i+1);”、“c = sum(c, sum(c,4));”等。函数值传递方式的最大好处是保持函数的独立性。在值传递方式下,函数只能通过指定函数类型并在函数体中使用return来返回某一类型的数值。
1.5.3 带默认形参值的函数
在C++中,允许在函数的声明或定义时给一个或多个参数指定默认值。这样在调用时,可以不给出参数,而按指定的默认值进行工作。例如:
void delay(int loops=1000); // 函数声明 //… void delay(int loops) // 函数定义 { if (loops==0) return; for(int i=0;i<loops;i++); // 空循环,起延时作用 }
这样,当调用
delay(); // 和delay(1000)等效
时,程序都会自动将loops当成1000的值来进行处理。当然,也可重新指定相应参数值,例如:
delay(2000);
在设置函数的默认参数值时要注意:
(1)当函数既有原型声明又有定义时,默认参数只能在原型声明中指定,而不能在函数定义中指定。例如:
void delay(int loops); // 函数原型声明 //… void delay(int loops=1000) // 错误:此时不能在函数定义中指定默认参数 { // … }
(2)当一个函数中需要有多个默认参数时,则形参分布中,默认参数应严格从右到左逐次定义和指定,中间不能跳开。例如:
void display(int a,int b,int c=3); // 合法 void display(int a,int b=2,int c=3); // 合法 void display(int a=1,int b=2,int c=3); // 合法:可以对所有的参数设置默认值 void display(int a,int b=2,int c); // 错误:默认参数应从最右边开始 void display(int a=1,int b,int c=3); // 错误:多个默认参数中间不能有非默认参数
(3)当带有默认参数的函数调用时,系统按从左到右的顺序将实参与形参结合,当实参的数目不足时,系统将按同样的顺序用声明或定义中的默认值来补齐所缺少的参数。
【例Ex_Default】 在函数定义中设置多个默认参数
#include <iostream.h> void display(int a,int b=2,int c=3) // 在函数定义中设置默认参数 { cout<<"a = "<<a<<", b = "<<b<<", c = "<<c<<"\n"; } int main() { display(1); display(1, 5); display(1, 7, 9); return 0; }
程序运行结果如下:
a = 1, b = 2, c = 3
a = 1, b = 5, c = 3
a = 1, b = 7, c = 9
(4)由于对同一个函数的原型可作多次声明,因此在函数声明中指定多个默认参数时,可用多条函数原型声明语句来指定,但同一个参数的默认值只能指定一次。例如,例Ex_Default可改写为:
#include <iostream> using namespace std; // 下面两条函数说明语句等效于void display(int a,int b=2,int c=3); void display(int a,int b,int c=3); // 指定c为默认参数 void display(int a,int b=2,int c); // 指定b为默认参数 //…
默认参数值可以是全局变量、全局常量,甚至是一个函数。但不可以是局部变量,因为默认参数的函数调用是在编译时确定的,而局部变量的值在编译时无法确定。
1.5.4 函数的递归调用
C++允许在调用一个函数的过程中出现直接地或间接地调用函数本身的情况,称为函数的递归调用。递归(Recursion)是一种常用的程序方法(算法),相应的函数称为递归函数。
例如,用递归函数编程求n的阶乘n!。(n!=n*(n-1)*(n-2)*…*2*1)。它也可用下式表示:
由于n!和(n-1)!都是同一个问题的求解,因此可将n!用递归函数long factorial(int n)来描述,程序代码如下。
【例Ex_Factorial】 求n的阶乘n!
#include <iostream.h>
long factorial(int n);
int main()
{
cout<<factorial(4)<<endl; // 结果为24
return 0;
}
long factorial(int n)
{
long result=0;
if(0==n)
result = 1;
else
result=n*factorial(n-1); // 进行自身调用
return result;
}
主函数main调用了求阶乘的函数factorial,而函数factorial中的语句“result = n *factorial(n-1);”又调用了函数自身,因此函数factorial是一个递归函数。
程序运行结果如下:
24
下面来分析main函数中“factorial(4);”语句的执行过程,这一过程用图1.8来表示:
图1.8 factorial(4)递归函数执行过程
① 因n = 4,不等于0,故执行“result = 4*factorial(3);”,因语句中有函数factorial(3)调用,故进行下一步操作。
② 因n = 3,不等于0,故执行“result = 4*factorial(2);”,因语句中有函数factorial(2)调用,故进行下一步操作。
③ 因n = 2,不等于0,故执行“result = 4*factorial(1);”,因语句中有函数factorial(1)调用,故进行下一步操作。
④ 因n = 1,不等于0,故执行“result = 4*factorial(0);”,因语句中有函数factorial(0)调用,故进行下一步操作。
⑤ 因n = 0,故执行result = 1。然后执行函数后面的语句。
⑥ 当执行“return result;”后,factorial(0)函数返回到主调函数factorial(1)。在主调函数factorial(1)中,result = 1*1=1,然后执行函数后面的语句。
⑦ 当执行“return result;”后,factorial(1)函数返回到主调函数factorial(2)。在主调函数factorial(2)中,result = 2*1=2,然后执行函数后面的语句。
⑧ 当执行“return result;”后,factorial(2)函数返回到主调函数factorial(3)。在主调函数factorial(3)中,result = 3*2=6,然后执行函数后面的语句。
⑨ 当执行“return result;”后,factorial(3)函数返回到主调函数factorial(4)。在主调函数factorial(4)中,result = 4*6=24,然后执行函数后面的语句。
⑩ 当执行“return result;”后,factorial(4)函数返回到主调函数main。在主调函数main中,执行下一条指令,输出结果24。
可以看出,递归函数实际上是同名函数的多级调用。但要注意,递归函数中必须要有结束递归过程的条件,即函数不再进行自身调用,否则递归会无限制地进行下去。
1.5.5 内联函数
函数调用时,内部过程需要进行调用初始化,执行函数代码,调用后处理等步骤。当函数体比较小,且执行的功能比较简单时,这种函数调用方式的系统开销相对较大。为了解决这一问题, C++引入了内联函数的概念,它把函数体的代码直接插入到调用处,将调用函数的方式改为顺序执行直接插入的程序代码,这样可以减少程序的执行时间,但同时增加了代码的实际长度。
内联函数的使用方法与一般函数相同,只是在内联函数定义时,需在函数的类型前面加上inline关键字。
【例Ex_Inline】 用内联函数实现求两个实数的最大值
#include <iostream.h> inline float fmax(float x, float y) { return x>y?x:y; } int main() { float a; a=fmax(5,10); //A语句 cout<<"最大的数为:"<<a<<"\n"; return 0; }
这样,当程序编译时,A语句就变成了:
a = 5>10 ? 5 : 10;
程序运行结果如下:
最大的数为:10
要注意使用内联函数的一些限制:
(1)内联函数中不能有数组定义,也不能有任何静态类型(后面会讨论)的定义。
(2)内联函数中不能含有循环、switch和复杂嵌套的if语句。
(3)内联函数不能是递归函数。
总之,内联函数一般是比较小的、经常被调用的、大多可在一行写完的函数,并常用来代替以后要讨论的带参数的宏定义。
1.5.6 函数重载
函数重载是指C++允许多个同名的函数存在,但同名的各个函数的形参必须有区别:要么形参的个数不同,要么形参的个数相同,但参数类型有所不同。
【例Ex_OverLoad】 编程求两个或三个操作数之和
#include <iostream.h> int sum(int x, int y); int sum(int x, int y, int z); double sum(double x, double y); double sum(double x, double y, double z); int main() { cout<<sum(2,5)<<endl; // 结果为7 cout<<sum(2,5,7)<<endl; // 结果为14 cout<<sum(1.2,5.0,7.5)<<endl; // 结果为13.7 return 0; } int sum(int x, int y) { return x+y; } int sum(int x, int y, int z) { return x+y+z; } double sum(double x, double y) return x+y; } double sum(double x, double y, double z) { return x+y+z; }
程序运行结果如下:
7
14
13.7
从上面的例子可以看出,由于使用了函数的重载,因而不仅方便函数名的记忆,更主要的是完善了同一个函数的代码功能,给调用带来了许多方便。程序中各种形式的sum函数都称为sum的重载函数。需要说明的是,重载函数必须具有不同的参数个数或不同的参数类型,只有返回值的类型不同是不行的。例如:
void fun(int a, int b); int fun(int a, int b);
是错误的。因为如果有函数调用fun(2,3)时,编译器无法准确地确定应调用哪个函数。
同样,当函数的重载带有默认参数时,也应该注意避免上述的二义性情况。例如:
int fun(int a, int b = 0); int fun(int a);
是错误的。因为如果有函数调用fun(2)时,编译器也无法准确地确定应调用哪个函数。
1.5.7 作用域和可见性
作用域又称作用范围,是指程序中标识符(变量名、函数名、数组名、类名、对象名等)的有效范围。一个标识符是否可以被引用,称为标识符的可见性。在一个C++程序项目中,一个标识符只能在声明或定义它的范围内可见,在此之外是不可见的。根据标识符的作用范围,可将其作用域分为5种:函数原型作用域、函数作用域、块作用域、类作用域和文件作用域。其中,类作用域将在第2章介绍,这里介绍其他几种。
1.块作用域
这里的块就是前面已提到过的块语句(复合语句)。在块中声明的标识符,其作用域从声明处开始,一直到结束块的花括号为止。块作用域也称局部作用域,具有块作用域的变量是局部变量。即在块中定义的变量仅在块中有效,块执行后,变量被释放。例如:
void fun(void) // 在形参表中指定void,表示没有形参,void可不要 { int a; //a的作用域起始处 cin>>a; if (a<0) { a = -a; int b; //b的作用域起始处 //… } //b的作用域终止处 } //a的作用域终止处
代码中,声明的局部变量a和b处在不同的块中。其中变量a是在fun函数的函数体块中,因此在函数体这个范围内,该变量是可见的。而b是在if语句块中声明的,故它的作用域是从声明处开始到if语句结束处终止。
需要说明的是:
(1)当标识符的作用域完全相同时,不允许出现相同的标识符名。而当标识符具有不同的作用域时,允许标识符同名。
(2)在多层次块(块的嵌套)中,外层块与内层块之间具有不同的作用域。外层块的变量可在内层块中使用,但内层块中的变量仅能在内层块中使用。当外层块和内层块中有同名变量定义时,外层块的同名变量在内层块中不起作用。
(3)在Visual C++中,for语句声明的标识符的作用域是包含for语句的那个内层块,而不是仅仅作用于for语句,这与标准C++是不一样的。
2.函数原型作用域
函数原型作用域指的是在声明函数原型时所指定的参数标识符的作用范围。这个作用范围在函数原型声明中的左、右圆括号之间。正因为如此,在函数原型中声明的标识符可以与函数定义中说明的标识符名称不同。由于所声明的标识符与该函数的定义及调用无关,所以可以在函数原型声明中只作参数的类型声明,而省略参数名。例如:
double max(double x,double y);
和
double max(double,double);
是等价的。不过,从程序的可读性考虑,在声明函数原型时,为每一个形参指定有意义的标识符,并且和函数定义时的参数名相同,是一个非常好的习惯。
3.函数作用域
具有函数作用域的标识符在声明它的函数内可见,但在此函数之外是不可见的。在C++语言中,只有goto语句中的标号具有函数作用域。goto语句的滥用会导致程序流程无规则、可读性差,因此现代程序设计方法不主张使用goto语句。
4.文件作用域
在函数外定义的标识符或用extern说明的标识符称为全局标识符。全局标识符的作用域称为文件作用域,它从声明之处开始,直到文件结束一直是可见的。需要说明的是:
(1)全局的常量或变量的作用域是文件作用域,它从定义开始到源程序文件结束。例如:
const float PI=3.14; // 全局常量PI,其作用域从此开始到文件结束 int a; // 全局变量a,其作用域从此开始到文件结束 void main( ) { //… } void funA(int x) { // … }
其中,全局常量PI和全局变量a的作用域是文件作用域。
(2)若函数定义在后,调用在前,必须进行函数原型声明。若函数定义在前,调用在后,函数定义包含了函数的原型声明。一旦声明了函数原型,函数标识符的作用域是文件作用域,它从定义开始到源程序文件结束。例如:
void funA(int x); // 函数funA的作用域从此开始到文件结束 void funB() // 函数funB的作用域从此开始到文件结束 { // … } void main( ) { // … } void funA(int x) { // … }
(3)在C++中,若在块作用域内使用与局部标识符同名的块外标识符时,则须使用域运算符“::”来引用,且该标识符一定要是全局标识符,即它具有文件作用域。
【例Ex_Process】 在块作用域内引用文件作用域的同名变量
#include <iostream.h> int i=10; //A int main() { int i=20; //B { int i=5; //C int j; ::i=::i+4; //::i是引用A定义的变量i,不是B中的i j=::i+i; // 这里不加::的i是C中定义的变量 cout<<"::i = "<<::i<<", j = "<<j<<"\n"; } cout<<"::i="<<::i<<",i="<<i<<"\n"; // 这里不加::的i是B中定义的变量 return 0; }
程序运行结果如下:
::i = 14, j = 19
::i = 14, i = 20
1.5.8 存储类型
存储类型是针对变量而言的,它规定了变量的生存期。无论是全局变量还是局部变量,编译系统往往根据其存储方式定义、分配和释放相应的内存空间。变量的存储类型反映了变量在哪里开辟内存空间,以及占用内存空间的有效期限。
在C++中,变量有4种存储类型:自动类型、静态类型、寄存器类型和外部类型,这些存储类型是在变量定义时指定的,其一般格式如下:
<存储类型> <数据类型> <变量名表>;
1.自动类型(auto)
一般来说,用自动存储类型声明的变量都限制在某个程序范围内使用,即为局部变量。从系统角度来说,自动存储类型变量是采用动态分配方式在栈区中分配内存空间的。因此,当程序执行到超出该变量的作用域时,就释放它所占用的内存空间,其值也随之消失了。
在C++语言中,声明一个自动存储类型的变量是在变量类型前加上关键字auto,例如:
auto int i;
若自动存储类型的变量是在函数内或语句块中声明的,则可省略关键字auto,例如:
void fun() { int i; // 省略auto // … }
2.寄存器类型(register)
使用关键字register声明寄存器类型的变量的目的是将所声明的变量放入寄存器内,从而加快程序的运行速度。例如:
register int i; // 声明寄存器类型变量
但在使用register声明时,若系统寄存器已经被其他数据占据,寄存器类型的变量就会自动当做auto变量。
3.静态类型
从变量的生存期来说,一个变量的存储空间可以是永久的,即在程序运行期间该变量一直存在,如全局变量;也可以是临时的,如局部变量,当流程执行到它的说明语句时,系统为其在栈区中动态分配一个临时的内存空间,并在它的作用域中有效,一旦流程超出该变量的作用域时,就释放它所占用的内存空间,其值也随之消失。
但是,若在声明局部变量类型前面加上关键字static,则将其定义成了一个静态类型的变量。这样的变量虽具有局部变量的作用域,但由于它是用静态分配方式在静态数据区中来分配内存空间。因此,在这种方式下,只要程序还在继续执行,静态类型变量的值就一直有效,不会随它所在的函数或语句块的结束而消失。简单地说,静态类型的局部变量虽具有局部变量的作用域,但却有全局变量的生存期。
需要说明的是,静态类型的局部变量只在第一次执行时进行初始化,正因为如此,在声明静态类型变量时一定要指定其初值,若没有指定,编译器还会将其初值置为0。
【例Ex_Static】 使用静态类型的局部变量
#include <iostream.h> void count() { int i=0; static int j=0; // 静态类型 i++; j++; cout<<"i = "<<i<<", j = "<<j<<"\n"; } int main() { count(); count(); return 0; }
程序中,当第1次调用函数count时,由于变量j是静态类型,因此其初值设为0后不再进行初始化,执行j++后,j值为1,并一直有效。第2次调用函数count时,由于j已分配内存且进行过初始化,因此语句“static int j = 0;”被跳过,执行j++后,j值为2。
程序运行结果如下:
i = 1, j = 1
i = 1, j = 2
事实上,在程序中声明的全局变量总是静态存储类型。若在全局变量前加上static,使该变量只在这个源程序文件内使用,则称该变量为全局静态变量或静态全局变量。
4.外部类型
使用关键字extern声明的变量称为外部变量,一般是指定义在本程序外部的变量。当某个变量被声明成外部变量时可以直接在本程序中引用这个变量,不必再次为它分配内存。在C++中,只有在两种情况下需要使用外部变量。
第1种情况:在同一个源文件中,若定义的变量使用在前,声明在后,这时在使用前要声明为外部变量。
第2种情况:当由多个文件组成一个完整的程序,且在一个源程序文件中定义的变量要被其他若干个源文件引用时,引用的文件中要用extern对该变量作外部声明。
需要注意的是:
(1)可以对同一个变量进行多次extern的声明。若在声明时,给一个外部变量赋初值,则编译器认为是一个具体的变量定义,而不是一个外部变量的声明,此时要注意同名标识符的重复定义。例如:
extern int n=1; // 变量定义 … int n; // 错误:变量n重复定义
(2)虽然外部变量对不同源文件中或函数之间的数据传递特别有用。但也应该看到,这种能被许多函数共享的外部变量,其数值的任何一次改变,都将影响到所有引用此变量的函数的执行结果,其危险性是显然的。
1.5.9 编译预处理
在进行C++编程时,可以在源程序中加入一些编译命令,以告诉编译器如何对源程序进行编译。由于这些命令是在程序编译时被执行的,也就是说,在源程序编译以前,要先处理这些编译命令,所以,也把它们称为编译预处理。实际上,编译预处理命令不能算是C++语言的一部分,但它扩展了C++程序设计的能力,合理地使用编译预处理功能,可以使得编写的程序便于阅读、修改、移植和调试。
C++提供的预处理命令主要有三种:宏定义命令、文件包含命令、条件编译命令。这些命令在程序中都以“#”来引导,每一条预处理命令必须单独占用一行;由于它不是C++的语句,因此在结尾没有分号(;)。由于前面已介绍过文件包含命令的使用,故这里仅介绍宏定义命令和条件编译命令。
宏定义就是用一个指定的标识符来代替一个字符串,C++中宏定义是通过宏定义命令#define来实现的,它有两种形式:不带参数的宏定义和带参数的宏定义。
1.不带参数的宏定义
在以前的程序中,曾用过#define定义一个标识符常量,一般都不带参数。
不参数的宏定义命令的一般格式为:
#define <宏名> 定义内容
例如:
#define PI 3.141593
其中,#define是宏定义命令,PI称为宏名。在程序编译时,编译器首先将程序中的PI用3.141593来替换,然后再进行代码编译。需要注意的是:
(1)#define、PI和3.141593之间一定要有空格,且一般将宏名定义成大写,以与普通标识符相区别。
(2)宏后面的内容实际上是字符串,编译器本身不对其进行任何语法检查,仅仅用来在程序中作与宏名的简单替换。例如,若有:
#define PI 3.141ABC593
它是一个合法的宏定义。
(3)宏被定义后,使用下列命令可重新定义:
#undef 宏名
(4)一个定义过的宏名可以用来定义其他新的宏,但要注意其中的括号,例如:
#define WIDTH 80 #define LENGTH (WIDTH+10)
宏LENGTH等价于:
#define LENGTH (80+10)
但其中的括号不能省略,因为当
var = LENGTH * 20;
若宏LENGTH定义中有括号,则预处理后变成:
var = ( 80 + 10 ) * 20;
若宏LENGTH定义中没有括号,则预处理后变成:
var = 80 + 10 * 20;
显然,两者的结果是不一样的。
2.带参数的宏定义
带参数的宏定义命令的一般格式为:
#define <宏名>(参数名表) 定义内容
例如:
#define MAX(a,b) ((a)>(b)?(a):(b))
其中(a,b)是宏MAX的参数表,如果在程序中出现下列语句:
x = MAX(3, 9);
则预处理后变成:
x=((3)>(9)?(3):(9)); // 结果为9
很显然,带参数的宏相当于一个函数的功能,但却比函数简洁。但要注意:
(1)定义带参数的宏时,宏名与左圆括号之间不能留有空格。否则,编译器将空格以后的所有字符均作为替代字符串,而将该宏视为无参数的宏定义。
(2)带参数的宏内容字符串中,参数一定要加圆括号,否则不会有正确的结果。例如:
#define AREA(r) (3.14159*r*r)
如果在程序出现下列语句:
x = AREA(3+2);
则预处理后变成:
x=(3.14159*3+2*3+2); // 结果显然不等于3.14159*5*5
3.文件包含命令
所谓“文件包含”是指将另一个源文件的内容合并到源程序中。C++语言提供了#include命令用来实现文件包含的操作,它有下列两种格式:
#include <文件名> #include "文件名"
文件名一般以.h为扩展名,因而称它为“头文件”,如前面程序中的iostream.h是头文件的文件名。文件包含的两种格式中,第1种格式是将文件名用尖括号“< >”括起来的,用来包含那些由系统提供的并放在指定子目录中的头文件,这称为标准方式。第2种格式是将文件名用双引号括起来的,这时,系统先在当前工作目录中查找要包含的文件,这称为用户方式,若找不到再按标准方式查找(即再按尖括号的方式查找)。所以,我们一般用尖括号的方式来包含系统库函数所在的头文件,以节省查找时间;而用双引号来包含用户自己编写的文件。
“文件包含”命令是很有用的,它可以减少程序设计人员的重复劳动。例如,在编程中有时要使用一些符号常量(如PI=3.14159265, E=2.718),用户可以将这些宏定义命令组成一个文件,然后其他人都可以用#include命令将这些符号常量包含到自己所写的源文件中,避免了这些符号常量的再定义。
在使用#include命令时需要注意的是,一条#include命令只能包含一个文件,若想包含多个文件须用多条文件包含命令。例如:
#include <iostream.h> #include <math.h> //…
4.条件编译命令
一般情况下,源程序中所有的语句都参加编译,但有时也希望程序按一定的条件去编译源文件的不同部分,这就是条件编译。条件编译使得同一源程序在不同的编译条件下得到不同的目标代码。C++提供的条件编译命令有几种常用的形式,现分别介绍如下:
(1)第1种形式:
#ifdef <标识符> <程序段1> [#else <程序段2>] #endif
其中,#ifdef、#else和#endif都是关键字,程序段是由若干条预处理命令或语句组成的。这种形式的含义是:如果标识符被#define命令定义过,则编译程序段1,否则编译程序段2。
【例Ex_UseIfdef】 使用#ifdef条件编译命令
#include <iostream.h> #define LI int main() { #ifdef LI cout<<"Hello, LI!\n"; #else cout<<"Hello, everyone!\n"; #endif return 0; }
程序运行结果如下:
Hello, LI!
(2)第2种形式:
#ifndef <标识符> <程序段1> [#else <程序段2>] #endif
这与前一种形式的区别仅在于,如果标识符没有被#define命令定义过,则编译程序段1,否则就编译程序段2。
(3)第3种形式:
#if <表达式1> <程序段1> [#elif <表达式2> <程序段2> …] [#else <程序段n>] #endif
其中,#if、#elif、#else和#endif是关键字。它的含义是,如果表达式1为true或不为0就编译程序段1,如果表达式2为true或不为0就编译程序段2……,如果各表达式都不为true就编译程序段n。
【例Ex_UseIf】 使用#if条件编译命令
#include <iostream.h> #define A -1 int main() { #if A>0 cout<<"a>0\n"; #elif A<0 cout<<"a<0\n"; #else cout<<"a==0\n"; #endif return 0; }
程序运行结果如下:
a<0
若将“#define A -1”中的-1改为0,则程序的运行结果为:
a==0
以上是C++中最常用的预处理命令,它们都是在程序被正常编译之前执行的,而且它们可以根据需要放在程序的任何位置,但为了保证程序结构的清晰性,提高程序的可读性,应将它们放在程序的开头。