简述C语言程序的结构,C++语言程序的三种基本结构
C语言系列:4、函数与程序结构文章目录C语言系列:4、函数与程序结构1。函数基础知识2。返回非整数值的函数3。外部变量。范围规则5。头文件6。静态变量6.1修改外部变量6.2修改函数6.3修改内部变量7。寄存器变量8。块状结构9。10.递归11。c预处理器11.1文件包含11.2宏替换11.3条件包含1。函数的基本知识。首先,让我们设计并编写一个程序,打印出输入中包含特定“模式”或字符串的行(这是UNIX程序grep的特例)。例如,在以下一组文本行中查找包含字符串“ould”的行:
啊爱!你和我能和命运一起合谋吗
为了全面理解这一可悲的计划,
我们不会把它粉碎成碎片,然后
重塑它,让它更贴近内心的渴望!程序执行后,输出以下结果:
啊爱!你和我能和命运一起合谋吗
我们不会把它粉碎成碎片,然后
重塑它,让它更贴近内心的渴望!这项任务可以清楚地分为以下三个部分:
While(仍有未处理的行)
If(该行包含指定的模式)虽然我们可以把所有的代码放在主程序main中,但是最好还是利用它的结构把每个部分设计成一个独立的功能。分开处理三个小部分比处理一个大的整体更容易,因为它可以隐藏函数中不相关的细节,从而减少不必要交互的机会。而且,这些功能也可以在其他程序中使用。
该函数定义如下:
返回值类型函数名(参数声明表)
{
声明和陈述
}函数定义中的所有组件都可以省略。最简单的函数如下:
Dummy() {}这个函数什么也不做,也不返回值。这个什么也不做的函数有时很有用。可以用来在程序开发时预留位置(以后补代码)。如果函数定义中省略了返回值类型,则默认为int类型。
一个程序可以看作是一组变量定义和函数定义。函数之间的通信可以通过参数、函数返回值和外部变量来进行。函数在源文件中出现的顺序可以是任意的。只要每个函数不分割成多个文件,源程序就可以分割成多个文件。
被调用的函数通过return语句向调用方返回值,该语句后面可以跟任何表达式:
必要时返回,表达式将被转换为函数的返回值类型。表达式通常用一对括号括起来,这里的括号是可选的。
我们使用函数getline实现“仍有未处理的行”,这在第1章已经介绍过;使用printf
实现了“打印本行”功能。这个功能是现成的,别人已经提供了。也就是我们只需要写一个确定“这一行包含指定模式”的函数。
我们编写函数strindex(s,t)来实现这个目标。这个函数返回字符串T在字符串s中出现的位置。
起始位置或索引。当s不包含t时,返回值为-1。由于C语言数组的下标从0开始,下标的值只能是0或正数,所以可以用-1这样的负数来表示失败。如果以后需要更复杂的模式匹配,只需替换strindex函数,程序其余部分可以保持不变。(标准库中提供的库函数strstr类似于strindex函数,但它返回的是指针而不是下标值。)
这样的设计完成后,整个程序的编写细节就简单明了了。下面是一个完整的程序,读者可以看到各部分是如何组合在一起的。我们现在寻找的模式是字符串文字,这不是最常见的机制。这里只简单讨论一下字符数组的初始化方法。第五章将介绍如何在程序运行时将模式作为参数传递给函数。其中getline功能与之前版本略有不同。读者可以通过与第一章中的版本进行比较来获得一些启发。
#包含stdio.h
#define/*最大输入行长度*/
int getline(char line[],int max)
int strindex(char source[],char search for[]);
char pattern[]= Ould ;/*要搜索的模式*/
/*查找匹配模式的所有行*/
主()
{
char line[MAXLINE];
int found=0;
while (getline(line,MAXLINE) 0)
if (strindex(line,pattern)=0) {
printf(%s ,行);
找到了;
}
发现退货;
}
/* getline:获取s中的行,返回长度*/
int getline(char s[],int lim)
{
int c,I;
I=0;
while ( - lim 0 (c=getchar())!=EOF c!=\n )
s[I]=c;
if (c==\n )
s[I]=c;
s[I]= \ 0 ;
返回I;
}
/* strindex:返回t在s中的索引,如果没有则为-1 */
int strindex(char s[],char t[])
{
int i,j,k;
for(I=0;s[i]!=\0;i ) {
for (j=i,k=0;t[k]!=\0的[j]==t[k];j,k)
;
if (k 0 t[k]==\0 )
返回I;
}
return-1;
}调用函数可以忽略返回值。此外,return语句后不一定需要表达式。当return语句后没有表达式时,函数不会将值返回给调用者。当被调用的函数执行到最后一个右花括号时,控制权也会返回给调用方(没有返回值)。如果一个函数从一个地方返回一个返回值,但是没有从另一个地方返回,那么这个函数不是非法的,但是它可能是一个信号,表明有问题。无论如何,如果函数没有成功返回值,那么它的“值”肯定是没用的。
在上面的模式查找器中,主程序main返回一个状态,即匹配的数量。返回值可以在调用程序的环境中使用。
在不同的系统中,存储在多个源文件中的C语言程序的编译和加载机制是不同的。例如,在UNIX系统中,您可以使用第1章中提到的cc命令来执行这项任务。假设有三个函数分别存储在三个名为main.c、getline.c和strindex.c的文件中,可以使用cc main.c getline.c strindex.c命令编译这三个文件,将生成的目标代码分别存储在main.o、getline.o和strindex.o文件中,然后将这三个文件一起加载到可执行文件a.out中,如果源程序中有错误(比如main.c文件中有错误), 可以通过命令cc main.c getline.o strindex.o重新编译main.c文件,并将编译后的结果与之前编译的目标文件getline.o和strindex.o一起加载到可执行文件中,cc命令使用的是"。 c“和”。o”扩展名来区分源文件和目标文件。
2.返回非整数值的函数。到目前为止,我们讨论的函数都是不返回值(void)或者只返回值类型为int的函数。如果一个函数必须返回其他类型的值呢?很多数值函数(比如sqrt,sin,cos等。)返回双精度值,而一些特殊函数返回其他类型的值。我们使用函数atof(s)来说明函数返回非整数值的方法。此函数将字符串s转换为相应的双精度浮点数。Atof函数是atoi函数的扩展,atoi函数的几个版本已经在第二章和第三章讨论过了。Atof函数需要处理可选符号和小数点,并考虑可能缺少整数或小数部分。这里写的版本不是高质量的输入转换功能,占用空间太大。标准库包含具有类似功能的atof函数,这些函数在头文件stdlib.h中声明
首先,因为atof函数的返回值类型不是int,所以函数必须声明返回值的类型。返回值的类型名应该放在函数名之前,如下所示:
#包含ctype.h
/* atof:将字符串s转换为double */
双atof(字符s[])
{
双val,功率;
int i,sign
for(I=0;is space(s[I]);i ) /*跳过空白*/
;
sign=(s[i]==-)?-1 : 1;
if (s[i]== s[i]==-)
我;
for(val=0.0;is digit(s[I]);我)
val=10.0 * val(s[I]- 0 );
if (s[i]== . )
我;
for(幂=1.0;is digit(s[I]);i ) {
val=10.0 * val(s[I]- 0 );
幂*=10;
}
返回符号* val/power;
}其次,调用函数必须知道atof函数返回的是非整数值,这一点也很重要。为了实现这个目标,一种方法是在调用函数中显式声明atof函数。下面显示的基本计算器程序中也有类似的语句(仅适用于支票簿计算)。程序读取每行中的一个数字(数字前面可能有一个符号),对它们求和,并在每次输入后打印这些数字的累积和:
#包含stdio.h
#定义
/*基本计算器*/
主()
{
double sum,atof(char[]);
char line[MAXLINE];
int getline(char line[],int max);
sum=0;
while (getline(line,MAXLINE) 0)
printf(\t%g\n ,sum=atof(line));
返回0;
}其中声明语句
double sum,atof(char[]);
指示sum是一个双精度变量,atof函数采用char[]参数并返回一个双精度值。
函数atof的声明和定义必须一致。如果atof函数与调用它的主函数放在同一个源代码中
文件,并且类型不一致,编译器将检测到错误。但是,如果atof函数是单独编译的(这种可能性更大),则无法检测到这种不匹配错误。atof函数将返回一个double值,而main函数将返回值视为int类型,最终结果值无意义。
根据前面关于函数声明如何与定义一致的讨论,出现不匹配似乎令人惊讶。原因之一是,如果没有函数原型,函数将在第一个表达式中隐式声明,例如:
总和=atof(行)
如果之前未声明的名称出现在表达式中,后跟一个左括号,则上下文会将该名称视为函数名,函数的返回值将被假定为int类型,但上下文不会对其参数做出任何假设。如果函数声明不包含参数,例如:
double atof();
那么编译器不会对函数atof的参数做任何假设,会关闭所有的参数检查。对空参数表的这种特殊处理是为了使新的编译器能够编译旧的C语言程序。但是,在新编写的程序中不建议这样做。如果函数有参数,就声明它们;如果没有参数,使用void来声明。
在正确声明的函数atof的基础上,我们可以用它来编写函数atoi(将string转换为int类型):
/* atoi:使用atof将字符串s转换为整数*/
int atoi(char s[])
{
双atof(char s[]);
return(int)atof;
}请注意声明和return语句的结构。以下面的形式返回语句:
回归(表情);其中表达式的值在返回之前将被转换为函数的类型。因为函数atoi的返回值是int类型,所以return语句中atof函数的double值会自动转换为int值。但是,这种操作可能会导致信息丢失,一些编译器可能会给出警告消息。在该函数中,由于采用了类型转换的方法来明确指示要执行的转换操作,因此可以阻止相关的警告信息。
3.外部变量C语言程序可以看作是由一系列外部对象组成,这些对象可能是变量,也可能是函数。形容词external是internal的反义词,用来描述函数内部定义的函数参数和变量。外部变量是在函数之外定义的,所以可以在很多函数中使用。因为C语言不允许在一个函数中定义其他函数,所以函数本身就是“外部的”。默认情况下,外部变量和函数具有以下属性:所有同名的对外部变量的引用(即使这样的引用来自不同的单独编译的函数)实际上引用的是同一个对象(这个属性在标准中称为外部链接)。从这个意义上说,外部变量类似于Fortran或Pascal中最外层程序块中声明的变量。我们将在后面解释如何定义只能在某个源文件中使用的外部变量和函数。
因为可以全局访问外部变量,所以它提供了一种在函数之间交换数据的方式,而不是函数参数和返回值。任何函数都可以通过名字访问外部变量。当然,这个名字需要通过某种方式来声明。
如果函数需要共享大量变量,使用外部变量比使用长参数表更方便有效。但是,正如我们在第一章中已经指出的,这样做必须非常谨慎,因为这种方法可能会对程序结构产生不良影响,并且可能会导致程序中各种函数之间的数据连接过多。外部变量的目的是比内部变量有更大的作用域和更长的生存期。自动变量只能在函数内部使用。它们在所在的函数被调用时存在,在函数退出时消失。外部变量是永久的,从一次函数调用到下一次函数调用,它们的值保持不变。所以,如果两个函数必须共享一些数据,但是两个函数并不互相调用,这种情况下最方便的方法就是将这些共享的数据定义为外部变量,而不是作为函数参数传递。
4.由作用域规则组成的C语言程序的函数可以与外部变量分开编译。一个程序可以存储在几个文件中,以前编译过的函数可以从库中加载。我们感兴趣的问题是:
如何进行声明以保证变量在编译时正确声明?如何安排声明的位置,保证程序的各个部分在加载时都能正确连接?如何组织程序中的语句,保证只有一个副本?如何初始化外部变量?为了讨论这些问题,我们对之前的计算器程序进行了重组,将其分散到多个文件中。从实用的角度来看,计算器程序比较小,不值得存放在几个文件中,但是可以很好的解释在较大的程序中遇到的类似问题。
名字的作用域指的是程序中可以使用名字的部分。对于在函数开头声明的自动变量,其作用域是声明变量名的函数。在不同函数中声明的同名局部变量之间没有关系。函数的参数也是如此,实际上可以看作是一个局部变量。
外部变量或函数的作用域从声明它的地方开始,到它所在的文件(待编译)的末尾结束。例如,如果main、sp、val、push和pop是在一个文件中依次定义的五个函数或外部变量,如下:
main() {.}
int sp=0;
double val[MAXVAL];
无效推送(双f) {.}
双重(无效){.}然后,在push和pop函数中不需要任何声明就可以按名称访问变量sp和val,但是这两个变量名不能用在主函数中,push和pop函数也不能用在主函数中。
另一方面,如果要在定义外部变量之前使用它,或者如果外部变量的定义与变量的使用不在同一个源文件中,则必须在相应的变量声明中使用关键字extern。
严格区分外部变量的声明和定义很重要。变量声明用来描述变量的属性(主要是变量的类型),变量的定义也会引起内存的分配。如果将以下语句放在所有函数之外:
int sp
double val[MAXVAL];然后这两条语句会定义外部变量sp和val,并给它们分配存储单元。同时,这两条语句也可以用作源文件其余部分的声明。还有下面两行语句:
extern int sp
extern double val[];对于源文件的其余部分,声明了一个int类型的外部变量sp和一个double array类型的外部变量val(这个数组的长度在别处确定),但是这两个声明没有建立变量或者为它们分配存储单元。
在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern声明来访问它(定义外部变量的源文件也可以包含外部变量的extern声明)。数组的长度必须在外部变量的定义中指定,但extern声明不必指定数组的长度。
外部变量的初始化只能出现在它们的定义中。
假设函数push和pop在一个文件中定义,而变量val和sp在另一个文件中定义,它们是
初始化(通常不可能这样组织程序),你需要通过下面的定义和声明将这些函数和变量“绑定”在一起:
在文件file1中:
extern int sp
extern double val[];
无效推送(双f) {.}
双重(无效){.}在文件file2:
int sp=0;
double val[MAXVAL];由于文件file1中的外部声明不仅放在函数定义的外面,而且放在它们的前面,所以它们适用于该文件中的所有函数。对于file1,这样一组声明就足够了。如果您想先使用变量sp和val,然后在同一个文件中定义它们,您也需要以这种方式组织文件。
5.头文件接下来,让我们考虑把上面的计算器程序分成几个源文件的情况。如果程序的组成部分很长,就有必要这样做。我们这样划分:把主函数main单独放在文件main.c中;将push和pop函数及其外部变量放在第二个文件stack.c中;将getop函数放在第三个文件getop.c中;把getch和ungetch函数放在第四个文件getch.c之所以分成多个文件,是因为在实际程序中,它们来自单独编译的库。
此外,还必须考虑在这些文件之间共享定义和声明。我们尽量把共享的部分放在一起,这样只需要一个副本,改进的时候很容易保证程序的正确性。我们把这些常用的部分放在头文件calc.h中,需要使用头文件时通过#include指令包含它(4.11节将介绍#include指令)。这样,程序的形式如下:
我们折衷了以下两个因素:一方面,我们期望每个文件只能访问完成其任务所需的信息;另一方面,现实中很难维护更多的头文件。我们可以得出结论,对于一些中等大小的程序,最好只用一个头文件来存储程序各部分共享的对象。更大的程序需要使用更多的头文件,我们需要仔细组织它们。
6.静态变量6.1修改一些外部变量,比如在文件stack.c中定义的变量sp和val,在文件getch.c中定义的变量
数量buf和bufp,只供所在源文件中的函数使用,其他函数无法访问。使用静态声明来限制外部变量和函数,在它们之后声明的对象的范围可以被限制到编译后的源文件的其余部分。通过静态定义外部对象,可以达到隐藏外部对象的目的。比如getch-ungetch复合结构需要共享两个变量buf和bufp,所以buf和bufp必须是外部变量,但是这两个对象不应该被getch和ungetch函数的调用方访问。
要将对象指定为静态存储,可以在普通对象声明前面加上关键字static。
如果在一个文件中编译上述两个函数和两个变量,将如下所示:
静态char buf[BUFSIZE];/* un get的缓冲区*/
静态int bufp=0;/* buf中的下一个空闲位置*/
int getch(void) {.}
Void ungetch(int c) {.}那么其他函数就不能访问变量buf和bufp,所以这两个名字不会和同一个程序中其他文件的同名冲突。类似地,变量sp和val可以通过将它们声明为静态类型来隐藏,这些静态类型由执行堆栈操作的push和pop函数使用。
6.2修饰函数外的静态声明通常用于变量。当然也可以用来声明函数。通常,函数名是全局可访问的,对整个程序的所有部分都是可见的。但是,如果一个函数被声明为静态类型,那么除了声明该函数的文件之外,不能访问该函数的名称。
6.3修改内部变量static也可以用来声明内部变量。像自动变量一样,静态类型的内部变量是一个特定的
函数的局部变量只能在这个函数中使用,但它和自动变量的区别在于,不管它的函数是否被调用,它总是存在的,不像自动变量,它随着它的函数的调用和退出而存在和消失。换句话说,静态类型的内部变量是一个只能在特定函数中使用但总是占用存储空间的变量。
7.寄存器变量register语句告诉编译器,它声明的变量在程序中经常使用。这个想法是
寄存器变量放在机器的寄存器里,可以让程序更小更快。但是,编译器可以忽略此选项。
注册声明的形式如下:
寄存器int x;
寄存器char c;寄存器声明只适用于函数的自动变量和形参。以下是后一种情况的一个例子:
f(寄存器无符号m,寄存器长n)
{
寄存器int I;
.
}在实际使用中,底层硬件环境的实际情况会对寄存器变量的使用有一些限制。每个函数中只有几个变量可以保存在寄存器中,并且只允许某些类型的变量。但是,过多的寄存器声明没有坏处,因为编译器可以忽略过多的或不支持的寄存器变量声明。此外,不管寄存器变量实际上是否存储在寄存器中,它的地址都不能被访问(更多细节我们将在第5章讨论)。在不同的机器中,对寄存器变量的数量和类型的具体限制是不同的。
8.块结构C语言不是Pascal等语言中的块结构语言。它不允许在函数中定义函数。然而,变量可以以块结构的形式定义在函数中。的变量声明(包括初始化)可以跟在函数开头的花括号后面,也可以跟在标识复合语句开头的任何其他左花括号后面。以这种方式声明的变量可以在块外隐藏同名的变量,这些变量彼此之间没有关系,在匹配左花括号的右花括号出现之前一直存在。例如,在下面的程序段中:
if (n 0) {
int I;/*声明新的i */
for(I=0;I n;我)
.
}变量I的作用域是if语句的“真”分支,这个I与块外声明的I无关。每次进入块,块中声明和初始化的自动变量都会被初始化。静态变量在第一次进入块时只初始化一次。
自动变量(包括形参)也可以隐藏同名的外部变量和函数。在下面的语句中:
int x;
int y;
f(双x)
{
双y;
}函数F中的变量X是指函数的参数,类型为double;在曲面函数f之外,x是一个int类型的外部变量。这段代码中的变量y也是如此。
在好的编程风格中,应该避免变量名在外部作用域中隐藏同名,否则可能会造成混乱和错误。
9.初始化我们之前多次提到过初始化的概念,但是从来没有详细讨论过。本节将总结前面讨论的各种存储类的初始化规则。
没有显式初始化,外部变量和静态变量都会被初始化为0,而自动变量和寄存器变量的初始值是未定义的(即初始值是无用信息)。
定义标量变量时,可以在变量名称后立即用等号和表达式初始化变量:
int x=1;
char squota= \
长日=1000L * 60L * 60L * 24L/* millions/day */对于外部变量和静态变量,初始化表达式必须是常量表达式,并且只初始化一次(概念上是在程序开始执行前初始化)。对于自动变量和寄存器变量,它们将在每次进入函数或块时被初始化。
对于自动变量和寄存器变量,初始化表达式可能不是常量表达式:表达式可能包含在此表达式之前已经定义的任何值,包括函数调用。第3.3节中介绍的二分搜索法程序的初始化可以采用以下形式:
int binsearch(int x,int v[],int n)
{
int low=0;
int high=n-1;
int mid
.
}而不是原来的形式:
int低、高、中;
低=0;
高=n-1;实际上,自动变量的初始化相当于简写赋值语句。采取哪种形式取决于个人的习惯。考虑到变量声明中的初始化表达式容易被忽略,而且离用的地方很远,我们一般使用显式赋值语句。
数组的初始化后面可以跟一列初始化表达式,这些表达式用大括号括起来,用逗号分隔。例如,如果要用一年中每个月的天数来初始化数组days,其变量定义如下:
int days[]={31,28,31,30,31,30,31,31,30,31,30,31,30,31 };当省略数组长度时,编译器将把花括号中初始化表达式的个数作为数组长度,在本例中为12。
如果初始化表达式的数目小于数组元素的数目,那么对于外部变量、静态变量和自动变量,没有初始化表达式的元素将被初始化为0。如果初始化表达式的数量大于数组元素的数量,则为错误。不能一次将一个初始化表达式分配给多个数组元素,也不能通过跳过前面的数组元素来直接初始化后面的数组元素。
数组的初始化是特殊的:可以用一个字符串代替用花括号括起来并用逗号分隔的初始化表达式序列。例如:
char pattern[]= Ould ;它等效于以下语句:
char pattern[]={ o , u , l , d };在这种情况下,数组的长度是5(4个字符加上一个字符串结束符 \0 )。
10.递归C语言中的函数可以递归调用,即函数可以直接或间接调用自己。让我们考虑将一个数字打印成一个字符串。如前所述,数字是按逆序生成的:低位先生成,但必须按逆序打印。
有两种方法可以解决这个问题。一种方法是将生成的数依次存储在一个数组中,然后按逆序打印,类似于3.6节itoa函数的处理方法。另一种方法是使用递归。函数printd首先调用自己打印前面(高位)的数字,然后打印后面的数字。这里写的函数不能处理最大的负数。
#包含stdio.h
/* printd:以十进制打印n */
void printd(整数)
{
if (n 0) {
putchar(-);
n=-n;
}
如果(n/10)
printd(n/10);
putchar(n % 10“0”);
}当函数递归调用自身时,每次调用都会得到一个不同于之前自动变量集的新的自动变量集。因此,当调用printd(123)时,第一次调用printd的参数n=123。它将12传递给printd的第二个调用,后者又将1传递给printd的第三个调用。对printd的第三次调用将首先打印1,然后返回到第二次调用。从第三次调用返回后的第二次调用也会先打印2,然后返回第一次调用。当你返回到第一次调用时,你会击3,然后函数的执行就结束了。
另一个可以更好地说明递归的例子是快速排序。它是由C. A. R .霍尔在1962年发明的。对于给定的数组,从中选择一个元素,以该元素为边界,将其余元素分成两个子集。一个子集中的所有元素都小于此元素,另一个子集中的所有元素都大于或等于此元素。这个过程在两个子集上递归执行。当一个子集中的元素个数小于2时,该子集不需要再次排序,递归终止。
就执行速度而言,下面这个版本的快速排序函数可能不是最快的,但却是最简单的算法之一。每次划分子集时,算法总是选择每个子阵列的中间元素。
/* qsort:v排序[左].v[右]成递增顺序*/
void qsort(int v[],int left,int right)
{
int i,last
void swap(int v[],int i,int j);
if (left=right) /*如果数组包含*/
返回;/*少于两个元素*/
swap(v,left,(左右)/2);/*移动分区元素*/
last=左;/*至v[0] */
for (i=左1;i=对;i ) /*分区*/
if (v[i] v[left])
swap(v,last,I);
swap(v,左,最后);/*恢复分区元素*/
qsort(v,左,last-1);
qsort(v,最后1,右);
}这里之所以把数组元素交换操作放在单独的函数swap中,是因为在qsort函数中要用到3次。
/* swap:互换v[i]和v[j] */
void swap(int v[],int i,int j)
{
内部温度;
temp=v[I];
v[I]=v[j];
v[j]=temp;
}标准库中提供了一个qsort函数,可以用来对任何类型的对象进行排序。
递归不会节省内存开销,因为在递归调用期间,必须在某个地方维护一个用于存储已处理值的堆栈。递归的执行速度并不快,但是递归代码比较紧凑,比相应的非递归代码更容易编写和理解。在描述递归定义的数据结构(如树)时,使用递归特别方便。我们将在6.5节介绍一个更好的例子。
11.C预处理器C语言通过预处理器提供了一些语言功能。从概念上讲,预处理器是编译过程的第一步。两个最常用的预处理器指令是:#include指令(在编译期间将指定文件的内容包含到当前文件中)和#define指令(用任意字符序列替换标签)。本节还将介绍预处理器的一些其他特性,如条件编译和宏参数。
1.1文件包含文件包含指令(即#include指令)使得处理大量#define指令和声明更加方便。
在源文件中,类似于:
#包含“文件名”或
#include文件名的所有行都将被替换为该文件名指定的文件内容。如果文件名用引号括起来,请在源文件的位置查找该文件。如果在这个位置找不到文件,或者文件名用尖括号和括起来,将根据相应的规则找到文件,这与具体的实现有关。包含文件本身也可以包含#include指令。
在源文件的开头通常有多个#include指令,用于包含常见的#define语句和
Extern声明,或者从头文件中访问库函数的函数原型声明,比如stdio.h(严格来说,这些内容不需要单独存储在文件中;访问头文件的细节与具体的实现相关。)
在大型程序中,#include指令是将所有声明绑定在一起的更好方法。它保证了所有的源文件都有相同的定义和变量声明,可以避免一些不必要的错误。当然,如果包含文件的内容发生变化,所有依赖于包含文件的源文件都必须重新编译。
1.2宏以下列形式取代宏定义:
#define这是最简单的宏替换3354。所有随后出现的名称标记将被替换为替换文本。#define指令中的名称以与变量名相同的方式命名,替换文本可以是任何字符串。通常,#define指令占据一行,替换文本是#define指令行末尾的所有剩余内容。但是,也可以将较长的宏定义分成几行,在这种情况下,需要在要继续的行尾添加一个反斜杠。#define指令定义的名称范围从它的定义点开始,到编译后的源文件的末尾结束。也可以使用宏定义中较早出现的宏定义。替换只在标记上执行,对引号中的字符串没有影响。例如,如果YES是由#define指令定义的名称,则在printf("YES ")或YESMAN中不会执行替换。
替代文本可以是任意的,例如:
#define/* infinite loop */该语句为无限循环定义一个新的永久名称。
宏定义也可以带参数,这样不同的可选文本可以用于不同的宏调用。例如,以下宏定义定义了宏最大值:
#define using macro max看起来像是一个功能词,但是宏调用直接将替换文本插入到代码中。每次出现形参(这里是A或B)都会被相应的实参替换。因此,声明:
x=max(p q,r s);将替换为以下形式:
x=((p q) (r s)?(p q):(r s);如果对各种类型参数的处理是一致的,那么同一个宏定义可以应用于任何数据类型,而不需要为不同的数据类型定义不同的max函数。
如果你仔细考虑max的扩展,你会发现它有一些缺陷。其中作为参数的表达式要计算两次,如果表达式有副作用(比如自动递增运算符或者输入/输出),就会不正确。例如:
Max(i,j)/* error */它将对每个参数执行两次自动递增操作。同时必须注意的是,括号的使用要恰当,以保证计算顺序的正确性。考虑以下宏定义:
#define/*错*/用squrare(z 1)调用宏定义会怎样?
但是,宏还是很有价值的。stdio.h头文件中有一个实际的例子:getchar和
Putchar函数在实践中往往被定义为宏,这样可以避免处理字符时调用函数的运行时开销。ctype.h头文件中定义的函数往往是用宏来实现的。
可以通过#undef指令取消名字的宏定义,这样可以保证后续调用是函数调用,而不是宏调用:
#undef
Getchar (void) {.}形参不能用带引号的字符串代替。但是,如果在替换文本中参数名以#为前缀,结果将扩展为带引号的字符串,其中实际参数替换参数。例如,您可以将它与字符串连接操作结合起来,编写一个调试打印宏:
#使用语句定义
当dprint(x/y)调用这个宏时,它将被展开为:
printf(x/y =g\n ,x/y);其中字符串被连接在一起,因此宏调用的效果相当于
printf(x/y=g\n ,x/y);在实际参数中,每个双引号将被替换为,反斜杠\将被替换为\,因此被替换的字符串是合法的字符串常量。
预处理运算符# #提供了一种连接宏展开的实际参数的方法。如果替换文本中的参数与# #相邻,将被实际参数替换,删除# #前后的空格,重新扫描替换后的结果。例如,下面定义的宏粘贴用于连接两个参数。
#define因此,宏调用paste(name,1)的结果将建立令牌name1。
1.3条件包含还可以通过使用条件语句来控制预处理本身,条件语句的值是在预处理的执行过程中计算出来的。该方法提供了一种在编译过程中根据计算的条件值选择性地包括不同代码的手段。
#if语句计算常量整数表达式(不能包含sizeof、类型转换运算符或枚举常量)。如果表达式的值不等于0,将包含以下行,直到遇到#endif、#elif或#else语句(预处理器语句#elif类似于else if)。表达式defined (name)可以用在#if语句中,其值遵循以下规则:当已经定义了名称时,其值为1;否则,其值为0。
例如,为了确保hdr.h文件的内容只包含一次,可以将该文件的内容包含在以下条件语句中:
#如果
#定义
/* HDR . h文件的内容放在这里*/
#endif将定义HDR这个名字;当它第一次包含头文件hdr.h时;之后,当你再次包含头文件时,你会发现名字已经定义好了,所以你会直接跳转到#endif。也可以使用类似的方法来避免重复包含同一个文件。如果多个头文件可以以这种方式一致地使用,那么每个头文件就可以包含它所依赖的任何头文件,用户就不必考虑和处理头文件之间的各种依赖关系。
下面的预处理代码首先测试系统变量SYSTEM,然后根据该变量的值确定包含哪个版本的头文件:
#如果
#定义
#elif
#定义
#elif
#定义
#否则
#定义
#endif
#includeC语言专门定义了两个预处理语句#ifdef和#ifndef,用于测试名称是否已经定义。上面关于#if的第一个例子可以用下面的形式重写:
#ifndef
#定义
/* HDR . h文件的内容放在这里*/
#endif
否则将追究法律责任。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。