我们想要设计一个学生信息管理的程序。经过分析,该程序可分解成学生信息录入、查询、修改、删除 4个在功能上相对独立的部学生信息管理系统分。这样,就把这个大的问题分解成4个小问题来逐个解决,这就是模块化程序设计思想的初步,如图 6-1所示。
在设计复杂程序时,我们常常采用模块化的解决方法,将大问题拆分成多个小部分,每个部分再进一步细分成若干子模块,例如录入模块、查询模块、修改模块、删除模块等等。通过这种方式,我们可以解决较小的问题并逐步组合这些模块,最终得到解决原问题的方案。这种解决方法被称为"功能模块"。
每个功能模块可以单独设计和实现,然后解决所有子问题,并最终将所有模块组合在一起,形成整个程序的解决方案。这就是所谓的"自顶向下"的模块化程序设计方法。模块化程序设计能够将复杂问题简化,并且使程序结构更加清晰、层次分明,便于编写和维护。
举个例子,我们可以将汽车的生产过程看作是由各种部件的生产和组装完成的,而这些部件的生产又依赖于零件的生产和组装。有些部件可以直接购买而不需要自己制造。通过这种方式,汽车的生产效率显著提高。在软件编写中,我们也采用类似的方法。首先编写各个子模块,然后将它们组合成一个整体的程序。这种方法可以显著提高软件编写的效率。
在C语言中,函数是构成C语言程序的部件,是实现模块化程序设计的工具。通过将功能拆分成不同的函数,我们可以实现模块化设计,提高程序的可读性和可维护性。
函数是C语言程序的基本功能模块,它是一段程序,用于完成某个相对独立的任务。在使用函数时,我们可以通过简单的方法提供必要的数据,系统会自动执行函数中的代码,并保存执行结果,然后程序会回到原来的位置继续执行其他代码。这种可以在程序中反复使用的程序片段被称为函数的形式。
一个较大的C程序通常由多个函数模块组成,每个模块用来实现特定的功能。函数可以相互调用,不仅主函数可以调用其他函数,其他函数之间也可以相互调用。同一个函数可以被一个或多个其他函数调用任意多次。
具体来说,C语言的源文件由一个或多个函数以及其他相关内容(如命令行参数、数据定义等)组成。函数是最小的功能单位,可以被其他不同源文件中的函数调用。在C语言中,以文件为编译单位。
C程序由一个或多个程序模块组成,每个程序模块都是一个源文件。一个源文件可以被不同的程序使用。多个源文件分别进行编写、编译和连接,以提高调试效率。
C程序的执行总是从主函数开始并结束,其他函数只有在与主函数发生调用关系时才会起作用。在主函数的执行过程中,可以调用其他函数,并将程序的执行控制权交给被调用的函数。当被调用函数执行完毕后,程序会返回到主函数继续执行,直到主函数执行结束,整个程序的执行过程才会结束。
函数之间是相互独立的,一个函数不属于另一个函数,即函数不能被嵌套定义。但是,函数可以相互调用,不过不能调用主函数(main()函数),因为main()函数是由系统调用的。
在不同的源文件之间进行组装可以通过工程文件来实现。
可以将函数视为一个"黑匣子",这个"黑匣子"可以完成特定的任务。对于调用函数的一方,只需要关心"黑匣子"的入口(已知量)和出口(未知量),而不需要知道"黑匣子"内部如何根据入口得到出口的具体过程。因此,对于调用者来说,函数的入口和出口是透明的,具体的实现过程是不透明的。例如,当我们使用scanf()等系统库函数时,程序设计者不需要关心它的具体实现代码,只需要关注函数的功能和调用格式。
说了那么多,C语言里面都有哪些函数呢?我们又该如何让使用这些函数呢?
1. 从用户使用的角度分类:
标准函数(库函数):这些函数由系统提供,用户无需自己定义,可以直接使用。每个系统提供的库函数的数量和功能可能不同,但通常会包含一些基本的共同函数。
下面是一些常见的C标准库函数及其功能:
提供输入和输出操作的函数,比如printf(), scanf()等。 | |
提供动态内存分配、类型转换、随机数生成等函数,比如malloc() 、free() 、atoi() 、rand() |
|
提供字符串处理函数,比如strcpy() 、strlen() 、strcmp() 等。 |
|
提供数学函数,比如三角函数、指数函数、对数函数等,以及常用的数学常量,如π、e等。 | |
|
提供字符处理函数,比如判断字符的类型(字母、数字、空格等),字符大小写转换等。 |
提供日期和时间相关的函数,如获取系统时间、设置时间等。 |
用户自定义函数:这些函数由用户根据自己的特定需求进行定义,用于解决用户特定的问题或实现特定功能。例如:
// 定义add函数
int add(int num1, int num2)
{
int sum = num1 + num2;
return sum;
}
在上面的例子中,用户定义了一个名为add
的函数,它接受两个整数参数并返回它们的和。
2. 从函数的形式分类:
无参函数:在调用无参函数时,主调函数不会传递任何数据给被调用函数,通常只用于执行一组特定的操作。无参函数可以有返回值,也可以没有返回值,但通常更常见的是无返回值的情况。例如:
// 定义welcome函数
void welcome(void)
{
printf("Welcome to the program!\n");
}
在上面的例子中,用户定义了一个名为welcome
的无参函数,它在调用时会打印一条欢迎消息。
有参函数:在调用有参函数时,主调函数和被调用函数之间会传递参数,主调函数可以将数据传递给被调用函数使用,而被调用函数可以通过参数将处理结果或其他数据返回给主调函数使用。例如:
#include
// 定义一个有参函数,计算两个数的平均值
double calculateAverage(double num1, double num2)
{
double average = (num1 + num2) / 2;
return average;
}
int main()
{
double num1 = 5.5;
double num2 = 7.8;
// 调用calculateAverage函数,并将num1和num2作为参数传递
double result = calculateAverage(num1, num2);
printf("Average: %lf\n", result);
return 0;
}
在上面的例子中,calculateAverage()
是一个有参函数,用于计算两个数字的平均值。在 main() 函数中,我们定义了两个变量 num1
和 num2
并赋予它们初始值。然后,调用了 calculateAverage()
函数,并将 num1
和 num2
的值作为参数传递给该函数。函数内部将两个数相加后除以 2,计算得到平均值,然后将结果返回给调用者。最后,我们在主函数中使用 printf()
函数打印出平均值。
就像变量一样,标准函数之外的函数也应该先进行定义后再使用。只有在函数定义完毕之后,该函数才真正存在,才能够被调用。
一个函数包括函数头和函数体,其中主函数main()也是一个函数。函数头提供了函数的相关信息,类似于一个“黑匣子”的入口和出口,而函数体则具体实现了函数的功能。函数体又可以分为数据说明部分和算法实现部分。数据说明部分用于定义在函数中要使用的变量和其他数据,而算法实现部分则由一系列语句组成,真正实现了函数的功能。
通过将功能性的代码封装在函数内部,我们可以提高代码的重用性和可读性。在程序中,我们可以根据需要调用已经定义好的函数,从而简化代码的编写和维护过程。
无参函数在被调用时不能从主调函数中得到数据信息,而有参函数可以通过参数接收主调函数传来的数据信息。无参函数和有参函数的定义也有所不同。
(1)无参函数的定义
定义形式如下:
类型标识符 函数名()
{
说明部分
语句部分
}
说明:
①其中的类型标识符用于指定函数值的类型,即函数带回来的值的类型,默认情况下为整型;若函 数无返回值,应用 void 说明。
②函数名的命名方法与标识符相同,不能和关键字、库函数名等同名。
③函数名后的括号是函数的象征,不能省略。
④无参函数一般不需要带回函数值,因此可以不写类型标识符。但无参函数也可以有返回值,这是由主调函数的具体需要决定的
(2)有参函数的定义
定义形式如下:
类型标识符 函数名(形式参数说明表)
{
说明部分
语句部分
}
说明:
有参函数中的参数是主调函数和被调用函数的数据通道,可分为形式参数(形参)和实际参数(实参)两种。例如:
int max(int x, int y) /*形式参数说明*/
{
int z; /*函数体中的说明部分*/
z=x>y?x:y;
return z;
}
这是一个求 x和y中最大值的函数,其中第一行是函数头(也叫函数首部),定义了一个返回值为 int型的并且带有两个 int型参数的 max()函数。x和 y为形式参数,这是因为此时没具体的值,只是代表两个整数。
从第2行的花括号开始到函数结束,是函数体。在函数体的花括号内的
int z;
表示此变量的有效范围只能是此函数体。return语句的作用是将z的值作为函数的返回值带回到主调函数。return后面的括号中的值作为函数带回的值(或称函数返回值)。在函数定义时已指定 max()函数为整型,在函数体中定义之为整型,二者是一致的,将之作为函数max()的值带回调用函数。
若在定义函数时不指定函数类型,系统会隐含指定函数类型为 int型。
函数定义时需要注意,在一个C语言程序中,可以定义多个函数。包括主函数 main()在内的所有函数都是平行的。一个函数的定义,可以放在程序中的任意位置,主函数 main()之前或之后。但在一个函数的函数体内,不能再定义另一个函数,即函数不能嵌套定义。
通常,希望通过函数调用使主调函数能得到一个确定的值,这就是函数的返回值,简称函数值。函数的数据类型就是函数返回值的类型,称为函数类型。
(1)函数的返回值通过函数中的返回语句 return将被调用函数中的一个确定的值带回到主调函数中去。return语句的用法如下:
return(表达式);
或
return表达式;
或
return;
例如:
return z;
return (z);
return (x>y? x:y);
如果需要从被调用函数带回一个函数值(供主调函数使用),被调用函数中必须包含return语句。如果不需要从被调用函数带回函数值可以不要 return 语句。
一个函数中可以有一个以上的 return语句,执行到哪一个 return语句,哪一个语句就会起作用。
return语句的作用是使程序控制从被调用函数返回主调函数中,同时把返回值带给主调函数;释放在函数的执行过程中分配的所有内存空间。
(2)既然函数有返回值,这个值当然应属于某一个确定的类型,应当在定义函数时指定函数值的类型;凡不加类型说明的函数,一律自动按整型处理。
如果函数值的类型和 return语句中表达式的值不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换。(即函数类型决定返回值的类型。)
(3)可以使用 void将函数定义为空类型或无类型,表示函数不带回返回值。此时,函数体内可以没有 return语句。
void类型的函数和有返回值类型的函数在定义时没有区别.,只是在调用时不同。有返回值的函数可以将函数调用放在表达式的中间,将返回值用于计算,而void类型的函数不能将函数调用放在表达式当中,只能在语句中单独使用。void 类型的函数多用于完成一些规定的操作,而主调函数本身不再对被调用函数的执行结果进行引用。
#include
//定义一个void类型的函数
void add_1(int a,int b)
{
int sum;
sum = a + b;
}
//定义一个int类型的函数
int add_2(int a,int b)
{
int sum;
sum = a + b;
return sum;
}
int main()
{
int result;
add_1(2,3); //void类型的函数不能将函数调用放在表达式当中,只能在语句中单独使用。
result = add_2(2,3); //有返回值的函数可以将函数调用放在表达式的中间,将返回值用于计算
return 0;
}
函数的调用是指在程序中使用已经定义过的函数。其过程与其他语言的子程序调用相似。函数调用的一般形式如下:
函数名(实参表列);
按照函数在程序中出现的位置划分,调用函数方式有以下3种:
(1)函数语句:
C语言中的函数可以只进行某些操作而不返回函数值,这时的函数调用作为一条独立的语句。例如上面的 add_1(2,3);
(2)函数表达式:
函数作为表达式的一项,出现在表达式中,以函数返回值参与表达式的运算。这种方式要求函数是有返回值的。例如上面的 result = add_2(2,3);
其中, add_2()函数是表达式的一部分,它的返回值值会赋给整型变量result。
(3)函数实参:
函数作为另一个函数调用的实际参数出现。这种情况下,会把该函数的返回值作为实参进行传送,因此要求该函数必须是有返回值的。例如:
#include
int add(int a,int b)
{
int sum =a + b;
return sum;
}
int main()
{
int result;
result = add(10,add(3,5));
return 0;
}
其中, add(3,5)是一次函数调用,它的值作为add()函数另一次调用的实参。最后将结果赋值给result变量,结果为15
又如:
#include
int add(int a,int b)
{
int sum =a + b;
return sum;
}
int main()
{
printf("3 + 5 = %d",add(3,5));
return 0;
}
也是把 add(3,5)作为printf()函数的一个参数。函数调用作为函数的参数,实质上也是函数表达式形式调用的一种,因为函数的参数本来就要求是表达式形式。
说明:
①调用函数时,函数名称必须与具有该功能的自定义函数名称完全一致。如果是调用无参函数则实参表列可以没有,但括号不能省略。
②实际参数表中的参数(简称实参),可以是常数、变量或表达式。如果实参不止一个,则相邻实参之间用逗号分隔。
③实参的个数、类型和顺序,应该与被调用函数所要求的参数个数、类型和顺序一致,才能正确地进行数据传递。如果类型不匹配,C编译程序将按赋值兼容的规则进行转换。如果实参和形参的类型不赋值兼容,通常并不给出出错信息,且程序仍然继续执行,只是得不到正确的结果。
④对实参表求值的顺序并不是确定的,有的系统按自左至右顺序求实参的值,有的系统则按自右至左顺序。
在函数定义之前,若要调用该函数,应对该函数进行说明,这与使用变量之前要先进行变量说明是一样的。在调用函数中对被调用函数进行说明的目的是使编译系统知道被调用函数返回值的类型以及函数参数的个数、类型和顺序,便于调用时,对调用函数提供的参数值的个数、类型及顺序是否一致等进行对照检查。
对被调用函数进行说明,其一般格式如下:
函数类型 函数名(数据类型 1[参数名1],数据类型 2[参数名 2],…,数据类型 n[参数名 n]);
由于编译系统并不检查参数名,所以每个参数的参数名是什么都可以,带上参数名,只是为了提高程序的可读性。因此每个参数的参数名可以省略。
函数的“定义”和“说明”是两个不同的内容。“定义”是指对函数功能的确立,包括指定函数名、返回值类型、形参类型、函数体等,它是一个完整的、独立的函数单位。在一个程序中,一个函数只能被定义一次,而且是在其他任何函数之外进行。
而“说明”(有的书上也称为“声明”)则是把函数的名称、返回值类型、参数的个数、类型和顺序通知编译系统,以便在调用该函数时系统对函数名称正确与否、参数的类型、数量及顺序是否一致等进行对照检查。在一个程序中,除上述可以省略函数说明的情况外,所有调用函数都必须对被调用函数进行说明,而且是在调用函数的函数体内进行。
在对库函数进行调用时,不需要再做说明,但必须把该函数相应的头文件用#include命令包含在源文件前部。
在调用函数时,主调函数和被调用函数之间在大多数情况下是有数据传递关系的。这就是前面提到的有参函数。在定义函数时,函数名后面括号中的变量名称为形式参数(简称形参),在调用函数时,函数名后面括号中的表达式称为实际参数(简称实参)。
形参出现在函数定义中,其作用域是本函数体。实参出现在主调函数中,进入被调函数后,实参变量便不能使用。形参和实参的功能都是用于数据传送。在函数调用时,主调函数把实参的值传送给被调函数的形参从而实现主调函数向被调函数的数据传送。
在 C语言中,实参向形参传送数据的方式是“值传递”。形参变量与实参变量的值在函数间传递的过程类似于日常生活中的“复印”操作:甲方委托乙方办理业务,并为乙方复印了一份文件的复印件,乙方凭复印件办理业务并将结果汇报给甲方。在乙方办理业务的过程中,可能在复印件上进行涂改、增删、加注释、盖章等操作,但乙方对复印件的任何修改都不会影响甲方手中的原件。
值传递的优点就在于,被调用的函数不可能改变主调函数中变量的值,而只能改变它的临时副本。这样就可以避免被调用函数的操作对调用函数中的变量产生副作用。
例如:
#include "stdio. h"
int max(int x, int y)
/*定义有参函数 max,x和y为形参,接收来自主调函数的原始数据*/
{
int z;
z=x>y?x:y;
return(z); /*将函数的结果返回主调函数*/
}
void main()
{
int a,b,c;
printf("input integer a,b:");
scanf("%d,%d",&a,&b);
c=max(a,b); /*主函数内调用功能函数 max,实参为a 和b*/
printf("max is %d\n",c);
}
运行情况如下:
input integer a,b: 4,5.
max is 5
程序从主函数开始执行,首先输入 a、b的数值 4 和 5,接下来调用函数 max(a,b)。具体调用过程如下:
(1)给形参 x、y分配内存空间。
(2)将实参a 的值传递给形参x,b的值传递给形参y,于是x的值为4,y的值为5。
(3)执行函数体。给函数体内的变量分配存储空间,即给,z分配存储空间,执行算法实现部分得到 z的值为5,执行 return语句返回 main()函数,返回时要完成以下功能:
①将返回值返回主函数,即将 z的值返回给 main()函数。
②释放函数调用过程中分配的所有内存空间,即释放 x、y、z的内存空间。
③结束函数调用,将流程控制权交给主调函数。
(4)继续执行 main()函数的后续语句。
说明:
(1)函数中的形参变量,在未出现函数调用时,它们并不占内存中的存储单元。只有在发生函数调用时函数中的形参才被分配内存单元。在调用结束后,形参所占的内存单元也被释放。
(2)实参可以是常量、变量或表达式,例如:
max(4,a+b);
但要求它们有确定的值。在调用时将实参的值赋给形参变量(如果形参是数组名,则传递的是数组首地址,而不是变量的值)。
(3)实参与形参的类型相同或赋值兼容。
(4)C语言规定,实参变量对形参变量的传递是“值传递”,即单向传递,只由实参传给形参,而不能由形参传回来给实参。在内存中,实参单元对形参单元是不同的单元。如图 6-3 所示。调用结束后,形参单元被释放,即形参x、y占用的存储单元被释放。实参单元仍保留并维持原值。
若形参的值如果发生改变,并不会改变主调函数的实参的值。例如,若在执行函数过程中x和y的值变为 12 和 21,而a 和 b仍为4和5,如图 6-4 所示。
例:分析下列C程序(程序中 swap(x,y)函数的功能是实现变量 x与 y值的交换)。
#include
void swap(int x, int y) /*定义函数 swap() */
{
int t;
t=x;
x=y;
y=t;
printf("swap函数中交换完成后x y:%d,%d\n",x,y);
}
int main()
{
int x,y; /*实参*/
printf("输入x,y:");
scanf("%d %d",&x,&y);
swap(x,y); /*调用函数*/
printf("交换后在主函数中输出x,y:%d,%d\n",x,y);
return 0;
}
程序运结果如下所示:
输入x,y:3 5
swap函数中交换完成后x y:5,3
交换后在主函数中输出x,y:3,5
从运行结果可见, swap()函数使 x和 y的值交换了。但在最后的 main()函数中,输出值还是输入的 4 和5。没有达到交换 x 和 y值的目的。
这是因为在C语言中,函数参数的传递方式是按值传递。在调用swap函数时,函数的形参x和y被分配了新的内存空间,并且将实参x和y的值分别拷贝到了这两个新的内存空间中。所以在swap函数内部对x和y的交换操作只是在新的内存空间中进行的,并不会影响到主函数中的实参x和y的值。所以在主函数中输出x和y的值时,其值仍然是最初输入的值。
一般来说,程序中所定义的任何变量经相应的编译系统处理后,每个变量都占据一定数目的内存单元,不同类型的变量所分配的内存单元的字节数是不一样的。在C语言中,一个字符型变量占 1B的存储空间;一个整型变量占 4B的存储空间;一个单精度实型变量占4B的存储空间。内存区每个字节的空间都有一个编号,这就是“地址”。
变量所占内存单元的首字节地址称作变量的地址。在程序中一般是通过变量名来对内存单元进行存取操作,其实程序经过编译后已经将变量名转换为变量的地址,由此可知,程序在执行过程中,对变量的存取实际上是通过变量的地址来进行的。
在 C语言中,可以通过变量名直接存取变量的值,这种方式称为“直接访问”方式。例如:
int x=3,y; /*定义了整型变量 x和 y,为 x赋初值3*/
y=x+1;
/*取出变量 x所占内存单元中的内容(值为 3)进行计算,然后将计算结果(即表达式的值)存放到变量 y的内存单元中 */
还可以采用间接访问的方式将变量的地址存放在另一个变量中。一个变量的地址称为该变量的指针。存放变量地址的变量就称为指针变量。指针变量的值(即指针变量中存放的值)就是指针(地址)。当要存取一个变量值时,首先从存放变量地址的指针变量中取得该变量的存储地址,然后再从该地址中存取该变量值。例如:
int x, * px;
/*定义了整型变量 x,还定义了一个用于存放整型变量所占内存地址的指针变量 px*/
px=&x; /*将x所占的内存地址赋给指针变量 px*/
*px=3; /*在 px所指向的内存地址中赋以整型值 3*/
的效果等价于
int x;
x=3;
假设编译时系统分配 2000~2003 这4B存储空间给 x,3000~3003 这4B存储空间给 px,则内存单元中存储的数据如图6-6 所示。
地址是指针变量的值,称为指针。指针变量也简称指针,因此,指针一词可以指地址值、指针变量,应根据具体情况加以区分。
赋值语句
px=&x;
和
*px=3;
中用到了两个运算符&和*,关于这两个运算符的使用,将在后面进行详细说明。
指针说明(定义)是说明指针变量的名字和所指对象的类型。例如:
int * px; /*说明 px是一个整型指针*/
指针的类型是指指针所指对象的数据类型,例如 px是指向整型变量的指针,简称整型指针,整型指针是基本类型的指针之一。除各种基本类型之外,C语言还允许使用指向数组、函数、结构、联合甚至是指针的指针。指针的类型多种多样,说明的语法各不相同且比较复杂。
指针说明的形式如下:
类型区分符 *指针变量名,…;
例如:
int *pi
说明标识符 pi是指向整型变量的指针。
(1) &:取地址运算符。
(2) *:指针运算符或间接访问运算符。
&和*这两个运算符的优先级别相同,按自右向左的方向结合。
例如, px=&x 中,&x表示取变量x的地址。&是单目运算符,该表达式的值为操作数变量的地址,称为 px指向 x或px是指向 x的指针;被px指向的变量 x称为 px的对象。“对象”就是一个有名字的内存区域即一个变量。
赋值语句*px=3中运算符*反映指针变量和它所指变量之间的联系。它是&的逆运算,也是单目运算符,它的操作数是对象的地址,*运算的结果是对象本身。例如 px是指向整型变量 x的指针,则*(&x)和*px都表示一个整型对象 x,即
*(&px)=3;
* px=3;
x=3;
上面这 3个操作的效果相同,都是将 3存入变量 x所占的内存单元中。
下列语句表明如何说明一个简单的指针,如何使用&和*运算符及如何引用指针的对象。
int x=1,y=2,* px; /*定义了整型变量x,y和整型指针 px, px可以指向 x,y*/
px=&x; /*使 px指向 x*/
y= *px; /*使 y的值为 1,因为 *px= * (&x)=x=1*/
&和*两个运算符的使用需要注意以下几点:
(1) &运算符只能作用于变量,包括基本类型的变量、数组元素、结构变量或结构的成员,不能作用于数组名、常量、非左值表达式或寄存器变量。例如,若
int x=1,y=2,* px; /*定义了整型变量x,y和整型指针 px, px可以指向 x,y*/
px=&x; /*使 px指向 x*/
y= *px; /*使 y的值为 1,因为 *px= * (&x)=x=1*/
则&r、&a[0]、&a[i]是正确的,而 &(2*r)、&a、&k是非法操作。
(2)如果 px指向 x,则*px可以出现在 x可以出现的任何位置,因为*px即表示 x。例如:
y= *px+1; //等价于 y=x+1;
(*px)++; //等价于 x++;
*px=y; //等价于 x=y;
scanf("%d", px); //等价于 scanf("%d",&x);
注意:其中px已经表示x的地址,在它的前面不能再使用取地址运算符&。
(3) px也可以指向数组 a中的一个元素。
px=&a[0]; /*或 px=a;使 px指向数组 a的第0个元素*/
px=&a[1]; /*使 px指向数组 a 的第1个元素*/
(4)如果已经执行了语句
px=&x;
则&*px表示先进行*px运算,就是变量x,再执行&运算。因此&*px和&x相同,表示变量 x的地址。
(5)*&x表示先进行&x运算,得到变量x的地址,再进行*运算,即&x所指向的变量。*&x和*px的作用是一样的,等价于变量x,即 *&x=x。
注意: &*x是不合法的,因为 x不是指针,不能进行间接的访问。
(6)(*px)++相当于 x++。如果没有括号,即成为*px++,那么因为++和*为同一优先级别,结合方向为自右向左,因此它表示先对 px进行*运算,得到 x的值,然后使px的值增1,这样 px 就不再指向 x了。
下面举一个指针变量应用的例子。
例:6-6 输入a 和b两个整数,按从小到大的顺序输出a和b。
#include
int main()
{
int a,b;
int *p1,*p2,*p;
printf("请输入两个整数(用逗号分隔):");
scanf("%d,%d",&a,&b);
/*把变量a、b的地址赋给指针p1、p2*/
p1=&a;
p2=&b;
/*如果 a>b,则交换两个指针的内容*/
if(a>b)
{
p=p1;
p1=p2;
p2=p;
}
printf("a=%d,b=%d\n",a,b);
printf("min=%d, max=%d\n", *p1,* p2); /*输出 p1、p2所指向的地址中的内容*/
return 0;
}
运行情况如下:
请输入两个整数(用逗号分隔):3,10
a=3,b=10
min=3, max=10
交换前的情况如图 6-7(a)所示,交换后的情况如图 6-7(b)所示。
注意:变量a和 b并未交换, pl 和p2的值发生了改变。这个问题的算法是不交换整型变量的值,而是交换两个指针变量的值。
(1)指针变量名前的“*”表示该变量为指针变量,而指针变量名不包含“*”。
(2)指针变量只能指向同一类型的变量。例如下列用法是错误的:
int * p;
float y;
p=&y;
这是因为指针变量 p只能指向整型变量。
(3)只有当指针变量指向确定地址后才能被引用。例如下列用法是错误的:
int *p;
* p= 5;
这是因为虽然已经定义了整型指针变量 p,但还没有让该指针变量指向某个整型变量之前,如果要对该指针变量所指向的地址赋值,就有可能破坏系统程序或数据,因为该指针变量中的随机地址有可能是系统所占用的。可以做如下修改:
int *p, x;
p=&x;
*p=5;
(4)指针的类型可以为 void *,它代替传统C中的 char *作为一般指针类型,例如在传统 C 中标准库函数 malloc()的返回值说明为 char *,在标准C 中被说明为 void *。
(5)一种类型的指针赋给另一类型的指针时必须用类型强制符来转换。例如:
int * pi;
char buf [100], *buffp=buff; /*使 buff指向字符数组 buf*/
pi= (int * ) buff; /* buff经强制类型转换赋给 pi,使 pi也指向 buff*/
用 buffp访问 buff时每次存取一个字符,用 pi访问 buf时每次存取一个整型长度决定的字节。例如:
char buf [10]={'a','b','c','d','e','f'};
char * buffp=buff;
pi=(int * ) buff?;
buffp++;
pi++;
printf("%c, %c", *buffp, * pi);
则输出结果如下:
b, e
(6)任何指针可以直接赋给 void指针,反之,需要经过强制类型转换。例如:
int x, *pi=&x;
void *p;
则
p=pi;
或
pi= (int `*)p;
是正确的指针赋值语句。
例:定义一个 swap()函数,然后在主函数中调用此函数实现对主调函数中变量值的交换。
#include
void swap(int *x, int *y)
{
int t;
t= * x;
*x= * y;
*y=t;
}
int main()
{
int a,b;
printf("input a,b:");
scanf("%d,%d",&a,&b);
swap(&a,&b);
printf("交换后a,b:%d,%d\n",a,b);
return 0;
}
由于在程序的 swap()函数中,参数 x、y是指针类型,接收的值是对应实参 a、b的地址。在 swap()函数中,通过*x访问到的是 a 的内容,*y访问到的是 b的内容,交换的就是主调函数中变量(即实参)a、b的值,所以可以在 swap()函数中实现对实参 a与 b的交换。
当我们探讨C语言中的局部变量和全局变量时,我们需要了解作用域的概念。在之前讨论函数的形参变量时,我们提到形参变量只在函数调用期间分配内存单元,并在调用结束后立即释放。这意味着形参变量只在函数内部有效,在函数外部无法使用。这种变量的有效性范围被称为变量的作用域或可见性。
除了形参变量,C语言中的其他所有变量都有自己的作用域。变量的声明方式不同,其作用域也不同。在C语言中,我们可以将变量根据其作用域范围分为两种类型:局部变量和全局变量。
在一个函数或复合语句内定义的变量,称为局部变量,局部变量也称为内部变量。局部变量仅在定义它的函数或复合语句内有效。例如函数的形参是局部变量。
编译时,编译系统不为局部变量分配内存单元。在程序的运行中,当局部变量所在的函数被调用时,编译系统会根据需要临时分配内存,函数调用结束,局部变量的空间被释放。
举个例子,我们在函数中声明的变量就是局部变量:
void exampleFunction()
{
int localVar = 10; // 局部变量
// 其他代码
}
在上面的的代码中,我们在f1()函数内定义了3个变量,a为形参,b、c为一般变量。在 f1()函数的范围内变量a、b、c有效,或者说变量 a、b、c的作用域限于f1()函数内。同理,x、y、z的作用域限于 f2()函数内,在f2()函数内有效。m、n的作用域限于 main()函数内,在 main()函数内有效。
说明:
(1)主函数中定义的变量只能在主函数中使用,不能在其他函数中使用。同时,主函数中也不能使用其他函数中定义的变量。因为主函数也是一个函数,与其他函数是平行关系。
(2)形参变量是属于被调函数的局部变量,实参变量是属于主调函数的局部变量。
(3)允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互不干扰,也不会发生混淆。
(4)在复合语句中也可定义变量,其作用域只在复合语句范围内。例如:
变量s只在复合语句内有效,离开该复合语句就无效,释放内存单元。
全局变量又称为外部变量,是在函数外部定义的变量。它不属于任何一个函数,仅属于一个源程序文件,其作用域是整个源程序文件,可以被本文件中的所有函数共用。
在函数中使用全局变量时,一般应做全局变量说明。只有在函数内经过说明的全局变量才能使用。但在一个函数之前定义的全局变量,在该函数内使用可不再加以说明。例如:
其中,m、n、c1 和 c2 都是全局变量,但它们的作用域不同。在 main()函数和fl()函数中可以使用全局变量m、n、c1 和 c2,但在 ff()函数中只能使用全局变量 m 和 n,而不能使用 cl 和c2,这是因为全局变量的作用域是从定义点开始到本文件尾。
在一个函数中既可以使用本函数中的局部变量,又可以使用有效的全局变量,打个通俗的比方:国家有统一的法律和法令,各地方还可以根据需要制定地方的法律、法令,一个地方的居民既遵守国家统一的法律法令,又要遵守本地方的法律法令。而另一个地方的居民则应遵守国家统一的和该地方的法律法令。
说明:
(1)对于局部变量的定义和说明,可以不加区分,而对于外部变量则不然,外部变量的作用域是从定义点到本文件结束。如果定义点之前的函数需要引用这些外部变量,则需要在函数内对被引用的外部变量进行说明。
外部变量的定义和外部变量的说明并不是一回事。外部变量定义必须在所有的函数之外且只能定义一次。其一般形式如下:
类型说明符变量名 1,变量名 2,…,变量名 n;
外部变量说明出现在要使用该外部变量的各个函数内,在整个程序内可能出现多次。
在C语言中,外部变量是在函数外部声明的全局变量。外部变量可以被程序中的所有函数访问和修改。在使用外部变量之前,需要进行外部变量的声明,以告诉编译器变量的类型和名称。一般将这些外部变量的声明放在函数之外,在所有函数的上方。
1. 声明并定义外部变量:在全局范围内声明并定义一个变量。例如:
int count = 0; //声明并定义一个名为count的全局变量
这样的声明将分配内存给变量,可以直接使用。
2. 声明外部变量:只在全局范围内声明一个变量,而不进行定义。例如:
extern int count; //声明一个名为count的全局变量,定义在其他地方
这样的声明告诉编译器该变量在其他地方定义,不进行内存分配。
需要注意的是,如果在函数内部出现与外部变量同名的局部变量,则优先使用局部变量。如果要在函数内部访问外部变量,可以使用关键字`extern`来进行声明。
例如:
#include
int count = 0; //外部变量声明并定义
void incrementCount()
{
extern int count; //函数内部声明使用外部变量
count++; //访问并修改外部变量
printf("Count: %d\n", count);
}
int main()
{
incrementCount(); //调用函数
incrementCount();
return 0;
}
运行结果:
Count: 1
Count: 2
这样,在函数内部就可以访问和修改外部变量了。外部变量的声明使得它具有全局的作用域,可以被程序中的所有函数使用。
使用全局变量的作用是增加了函数间数据联系的渠道。由于同一个文件中的所有函数都能引用全局变量的值,因此如果在一个函数中改变了全局变量的值,就能影响其他函数,相当于各个函数间有直接的传递通道。由于函数的调用只能带回一个返回值,因此有时可以利用全局变量增加函数联系的渠道,从函数得到一个以上的返回值。
虽然外部变量可加强函数模块之间的数据联系,但是又使函数要依赖这些变量,因而使得函数的独立性降低。从模块化程序设计的观点来看,这是不利的,因此尽量不要使用全局变量。
从变量的作用域(空间)角度来分,可以分为全局变量和局部变量。从变量值存在的时间(生存期)角度来分,可以分为静态存储变量和动态存储变量。所谓静态存储方式是指在程序运行期间分配固定的存储空间的方式,而动态存储方式则是在程序运行期间根据需要进行动态的分配存储空间的方式
图 6-8为内存中的供用户使用的存储空间的情况。这个存储空间可以分为3个部分:程序区、静态存储区和动态存储区。
数据被分别存放在静态存储区和动态存储区中。全局变量存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放。在程序执行过程中它们占据固定的存储单元,而不是动态地分配和释放的。
在动态存储区中存放以下数据:
(1)函数形参变量。在调用函数时给形参变量分配存储空间。
(2)局部变量(未加 static 说明的局部变量,即自动变量)。
(3)函数调用时的现场保护和返回地址等。
对以上这些数据,在函数调用开始时分配动态存储空间,函数结束时释放这些空间。在程序执行过程中,这种分配和释放是动态的,如果在一个程序中两次调用同一函数,分配给此函数中局部变量的存储空间地址可能是不相同的。一个程序包含若干个函数,每个函数中的局部变量的生存周期并不等于整个程序的执行周期,它只是其中的一部分。根据函数调用的需要,动态地分配和释放存储空间。
在C语言中每一个变量和函数有两个属性:数据类型和数据的存储类型。因此在介绍了变量的存储类型之后,对一个变量的说明不仅应说明其数据类型,还应说明其存储类型。所以变量说明的完整形式应如下:
存储类型说明符数据类型说明符 变量名1,变量名2,…,变量名 n;
例如:
static int a,b; /* a,b为静态整型变量*/
auto char c1,c2; /*c1,c2为自动字符变量 */
extern int x,y; /* x,y为外部整型变量 */
存储类型指的是数据在内存中存储的方法。存储方法分为两大类:静态存储类和动态存储类。具体包含 4种:自动的(auto)、静态的(static)、寄存器的(register)和外部的(extern)。自动变量和寄存器变量属于动态存储方式,外部变量和静态变量属于静态存储方式。
如果不对函数中的局部变量,做特别说明(说明为静态的存储类别),都会为其在动态存储区中动态分配存储空间进行存储。这些分配和释放存储空间的工作是由编译系统自动处理的,因此这类局部变量称为自动变量。自动变量用关键字 auto做存储类型的说明。例如:
int f (int x) /*定义 f()函数,x为形参*/
{
auto int m,n=3; /*定义m,n为自动变量 */
}
x是形参,m、n是自动变量,对 n赋初值 3。执行完函数后自动释放其所占的存储单元。auto也可以省略, auto不写则隐含确定为“自动存储类型”,它属于动态存储类型。前面介绍的函数中定义的变量都没有说明为 auto,都隐含确定为自动变量在函数体中。例如:
auto int m,n=3;
int m,n=3;
二者等价
自动变量具有以下特点:
(1)自动变量的作用域仅限于定义该变量的个体内。在函数中定义的自动变量,只在该函数内有效。在复合语句中定义的自动变量只在该复合语句中有效。例如:
(2)自动变量属于动态存储方式,只有在使用它,即定义该变量的函数被调用时才给它分配存储单元,开始它的生存期。函数调用结束,释放存储单元,结束生存期。因此函数调用结束之后,自动变量的值不能保留。在复合语句中定义的自动变量,在退出复合语句后也不能再使用,否则将引起错误。例如:
#include
int main()
{
auto int a;
printf("输入一个整数:");
scanf("%d",&a);
if (a>0)
{
auto int s,p,
s=a+a;
p=a*a;
}
printf("s=%dp=%d\n",s,p);
return 0;
}
程序在编译时会出现错误:
error C2065:'s' : undeclared identifier
error C2065: 'p' : undeclared identifier
其中,s、p是在复合语句内定义的自动变量,只能在该复合语句内有效。而程序的第 12行却是退出复合语句之后用 printf语句输出s、p的值,因此会引起错误。
(3)由于自动变量的作用域和生存期都局限于定义它的个体内(函数或复合语句内),因此不同的个体中允许使用同名的变量而不会混淆。即使在函数内定义的自动变量也可与该函数内部的复合语句中定义的自动变量同名。例如:
#include
int main()
{
auto int a,s=10,p=10;
printf("input a number:\n");
scanf("%d",&a);
if(a>0)
{
auto int s,p;
s=a+a;
p=a*a;
printf("s=%dp=%d\n",s,p);
}
printf("s=%dp=%d\n",s,p);
return 0;
}
程序运行结果:
input a number:3
s=6p=9
a=10 p=10
本程序在 main()函数和复合语句内两次将变量s、p定义为自动变量。按照C语言的规定,在复合语句内,应由复合语句中定义的s、p起作用,故s的值为a+a、p的值为a*a。退出复合语句后的s、p应为main()所定义的s、p,其值在初始化时给定,均为10。从输出结果可以分析出,两个s和两个p虽然变量名相同,但却是两个不同的变量。
若希望函数中局部变量的值在函数调用结束后不消失,即其占用的存储单元不释放,在下一次该函数调用时,该变量存有上一次函数调用结束时的值,则应该指定该局部变量为静态局部变量,用 static 加以说明。例如:
#include
int f (int a)
{
auto int b=0;
static int c=2 ;
b=b+1;
c=c+1;
return(a+b+c);
}
int main()
{
int a=1,i;
for(i=0;i<3;i++)
printf("%3d",f(a));
return 0;
}
运行结果如下:
5 6 7
在第 1次调用f()函数时,b的初值为0,c的初值为 2,第1次调用结束时b=1、c=3、a+b+c=5。由于c是局部静态变量,在函数调用结束后,它并不释放,仍保留c=3。在第2次调用f()函数时 b 的初值为0,而 c 的初值为3(上次调用结束时的值)。
静态局部变量属于静态存储方式,具有以下特点。
(1)静态局部变量需要在函数内定义,不像自动变量那样随时调用,退出函数时自动消失。静态局部变量始终存在着,也就是说它的生存期为整个源程序。
(2)局部静态变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有初值,以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值。对自动变量赋初值,不是在编译时进行,而在函数调用时进行的,每调用一次函数重新赋一次初值,相当于执行一次赋值语句。
(3)静态局部变量的生存期虽然为整个源程序,但是其作用域仍与自动变量相同,即只能在定义该变量的函数内使用。退出该函数后,尽管该变量还继续存在,但不能使用它。
(4)若在定义局部变量时不赋初值,则对静态局部变量来说,编译时自动赋以初值0(对数值型变量)或空字符(对字符变量)。而对自动变量来说,如果不赋初值则它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,而所分配的单元中的值是不确定的。
可以看出,静态局部变量虽然离开定义它的函数后不能使用,但如再次调用定义它的函数时,它又可继续使用,而且保存了前次被调用后留下的值。因此,当多次调用一个函数且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量。
静态全局变量只允许被本源文件中的函数引用(与局部变量相反)。在一个含有多个源程序文件的工程中,有时在程序设计中有这样的需要,希望某些全局变量只限于被本文件引用而不能被其他文件引用。这时可以在定义外部变量时前面加一个 static 说明。举例如下。
文件f1. c:
static int a;
int main()
{
//代码语句
return 0;
}
文件f2. c:
extern int a;
void fun(n)
{
int n;
a=a*n;
//代码语句
}
在 f1. c中定义了一个全局变量 a,但它有 static 说明,因此只能用于本文件,虽然在f2. c文件中用了
extern int a;
但 f2. c文件中无法使用 f1. c中的全局变量a,这种加上 static 说明,只能用于本文件的外部变量(全局变量)称为静态全局变量或函数外部静态变量。
在程序设计中,常由若干人分别完成各个模块,各人可以独立地在其设计的文件中使用相同的外部变量名而互不相干。这就为程序的模块化、通用性提供方便。一个文件与其他文件没有数据联系,可以根据需要任意地将所需的若干文件组合,而不必考虑变量有否同名和文件间的数据交叉。不用时对文件中所有外部变量都加上 static,成为静态外部变量,以免被其他文件误用。
注意:
对全局变量加 static 说明,并不意味着这时才是静态存储(存放在静态存储区中),两种形式的全局变量都是静态存储方式,只是作用范围不同而已,都是在编译时分配内存的。
静态局部变量和静态全局变量同属静态存储方式,但两者区别较大:
(1)定义的位置不同。静态局部变量在函数内定义,静态全局变量在函数外定义。
(2)作用域不同。静态局部变量属于内部变量,其作用域仅限于定义它的函数内;虽然生存期为整个源程序,但其他函数是不能使用它的。静态全局变量在函数外定义,其作用域为定义它的源文件内;生存期为整个源程序,但其他源文件中的函数也是不能使用它的。
(3)初始化处理不同。静态局部变量仅在第 1次调用它的函数中被初始化,在再次调用它的函数中,不必再初始化,其中已存有一次调用结束时的值。静态全局变量是在函数外定义的,不存在静态局部变量的“重复”初始化问题,其当前值由最近一次给它赋值的操作决定。
外部变量是在函数外部定义的全局变量,编译时分配在静态存储区。全局变量可以为程序中各个函数所引用。
一个C程序可以由一个或多个源程序文件组成。如果程序只由一个源文件组成,使用全局变量的方法前面已经介绍。如果由多个源程序文件组成,那么如果在一个文件中要引用在另一文件中定义的全局变量,应该在需要引用它的文件中,用 extern 进行说明——允许被其他源文件中的函数引用。例如:
extern 数据类型 变量名;
在编译和连接时,系统会由此知道 变量是一个已在别处定义的全局变量,并将在另一个文件中定义的全局变量的作用域扩展到本文件,在本文件中可以合法地引用该全局变量。
例如:
//文件6-2. c:
#include
extern int a; // 外部变量声明
void changeCount() {
a++; // 使用外部变量
}
int main() {
changeCount();
printf("Count: %d\n", a);
return 0;
}
程序说明:程序中,6-2. c文件中的开头有一个 extern 说明(注意这个说明不是在函数的内部。函数内用 extern说明使用本文件中的全局变量的方法,前面已做了介绍),它说明了在本文件中出现的变量 a是一个已经在其他文件中定义过的全局变量,本文件不必再次为它分配内存。
本来全局变量的作用域是从它的定义点到文件结束,但可以用extern 说明将其作用域扩大到有 extern说明的其他源文件。假如一个C程序有5个源文件,只在一个文件中定义了外部整型变量 a,那么其他 4个文件都可以引用 a,但必须在每一个文件中都加上一个语句
extern int a;
进行说明。在各文件经过编译后,将各目标文件链接成一个可执行的目标文件。
注意:
使用这样的全局变量要十分慎重,因为在执行一个文件中的函数时,可能会改变该全局变量的值,进而影响到另一个文件中函数的执行结果。
变量(包括静态存储方式和动态存储方式)的值一般存放于内存中。当程序中用到哪一个变量的值时,由控制器发出指令将其从内存送到运算器中。经过运算器进行运算后,如果需要存放,再从运算器将数据送到内存存放。因此,当对一个变量频繁读写时,必须反复访问内存储器,花费大量的存取时间。为了解决这个问题,C语言提供了另一种变量,即寄存器变量。这种变量存放在 CPU 的寄存器中,使用时,不需要访问内存,而是直接从寄存器中读写,提高了效率。寄存器变量的说明符是 register。例如:
#include
int f(int n)
{
register int i,f=1; /*定义寄存器变量*/
for(i=1;i<=n;i++)
f=f*i;
return (f);
}
int main()
{
int i;
for(i=1;i<=5;i++)
printf("%d!=%d\n",i,f(i));
return 0;
}
程序运行结果:
1!=1
2!=2
3!=6
4!=24
5!=120
程序中将局部变量f和i定义为寄存器变量,当n的值越大,节约的执行时间越多。对于循环次数较多的循环控制变量和循环体内反复使用的变量一般均可定义为寄存器变量。
(1)只有局部自动变量和形式参数才可以定义为寄存器变量。因为寄存器变量属于动态存储方式。凡需要采用静态存储方式的变量不能定义为寄存器变量。
(2)对寄存器变量的实际处理,随系统而异。例如在微型计算机上,MS C和 Turbo C会将寄存器变量当作自动变量处理。
(3)由于CPU中寄存器的个数是有限的,因此允许使用的寄存器数目是有限的,不能定义任意多个寄存器变量。
提示:当今的优化编译系统能够自动识别使用频繁的变量,并将其放入寄存器中,不需要程序设计者指定,因此在实际工作中用 register 声明变量是不必要的。读者对它有一定的了解即可。 )
通过上述讨论可知,对一个数据的定义,需要指定两种属性:数据类型和存储类型,分别用两个关键字(数据类型标识符和存储类别标识符)进行定义。
从不同角度进行归纳如下:
1. 从变量的作用域角度分类
变量从作用域角度可分为局部变量和全局变量,如图 6-9 所示。
2. 从变量的生存期分类
从变量的生存期来区分有动态存储和静态存储两种类型。静态存储是程序整个运行时间都存在,而动态存储则是在调用函数时临时分配单元,如图 6-10所示。
3. 按变量值存放的位置分类
按变量值存放位置的不同,可分为内存中静态存储区、内存中动态存储区和 CPU中的寄存器中的变量,如图 6-11所示。
4. 关于作用域和生存期的概念。
从前面叙述可以知道,对一个变量的性质可以从两个方面分析,一是从变量的作用域,一是从变量值存在时间的长短,即生存期。前者是从空间的角度,后者是从时间的角度。二者有联系但不是同一回事。图 6-12 是作用域的示意图,图6-13 是生存期的示意图。
如果一个变量在某个文件或函数范围内是有效的,则称该文件或函数为该变量的作用域,在此作用域内可以引用该变量,所以又称变量在此作用域内“可见”,这种性质又称为变量的“可见性”,例如变量 a、b在 fl()函数中“可见”。如果一个变量值i在某一时刻是存在的,则认为这一时刻属于该变量的“生存期”,或称该变量在此时刻“存在”。表 6-1 表示各种类型变量的作用域和存在性的情况。
(六)6.7 编译预处理
编译预处理是指在系统对源程序进行编译之前,对程序中某些特殊的命令行的处理,预处理程序将根据源代码中的预处理命令修改程序,使用预处理功能,可以改善程序的设计环境,提高程序的通用性、可读性、可修改性、可调试性、可移植性和方便性,易于模块化。
预处理程序的位置在主函数之前,定义一次,可在程序中多处展开和调用,它的取舍决定于实际程序的需要。预处理程序一般包括,宏定义、宏替换、文件包含(又称头文件)、条件编译。其处理过程如图 6-17所示。
注意:预处理命令是一种特殊的命令,为了区别一般的语句,必须以“#”开头,结尾不