C语言程序设计(Part Ⅵ)的整理笔记,若有错误,欢迎指正。
C语言程序设计(Part Ⅶ)
参考书目:《C语言程序设计(第3版) 谭浩强 著》
变量的作用域和生存期
如果一个C程序只包含一个main函数,数据的作用范围比较简单,在函数中定义的数据在本函数中定义点之后都是是有效的。但是,若一个程序包含多个函数,就会产生一个问题,在A函数中定义的变量在B函数中能否使用?这就是数据的作用域问题。
变量的作用域—局部变量和全局变量
- 局部/内部变量:在函数或复合语句中定义的变量,只在本函数或复合语句内范围内有效(从定义点开始到函数或复合语句结束)。只有在本函数或复合语句内才能使用它们,在此函数或复合语句以外是不能使用这些变量的。
- 主函数中定义的变量也只在主函数中有效,而不会因为是在主函数中定义的而在整个文件或程序中有效。主函数也不能使用其他函数中定义的变量。
- 不同函数中可以使用相同名字的变量,它们代表不同的对象,在内存中占不同的单元,互不干扰。
- 形式参数也是局部变量。在函数中可以使用本函数声明的形参,在函数外不能引用了。
- 在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句中有效。
- 全局/外部/全程变量:在函数之外定义的变量是全局变量。
- 全局变量的有效范围为从定义变量的位置开始,到本源程序文件结束,在此范围内可以为本程序文件中所有函数所共用。
- 在一个函数中既可以使用本函数中的局部变量,又可以使用有效的全局变量。
- 如果在同一个源文件中,外部变量与局部变量同名,则在局部变量的作用范围内,外部变量被“屏蔽”了,即它不起作用,此时只有局部变量是有效的。
例:有4个学生,5门课的成绩,要求输出其中的最高成绩以及它属于第几个学生、第几门课程
#include< stdio.h >
int Row, Column; //定义全局变量Row和Column
int main()
{
float highest_score(float array[4][5]);
float score[4][5] = { {61, 73, 85.5, 87, 90}, {72,84,66,88,78},
{75, 87, 93.5, 81, 96}, {65,85,64,76,71} };
printf("The highest score is %6.2f\n", highest_score(score));
printf("Student No. is %d\nCourse No.is %d\n", Row, Column);
return 0;
}
float highest_score(float array[4][5])
{
int i, j;
float max;
max = array[0][0];
for (i = 0; i < 4; i++)
for (j = 0; j < 5; j++)
if (array[i][j] > max)
{
max = array[i][j];
Row = i; //将行的序号赋给全局变量Row
Column = j; //将列的序号赋给全局变量Column
}
return (max);
}
说明:
- 设置全局变量的作用是增加了函数间数据联系的渠道。由于同一源程序文件中的所有函数都能引用全局变量的值,因此如果在一个函数中改变了全局变量的值,就能影响到其他函数,相当于各个函数间有直接的传递通道。
- 由于函数的调用只能带回一个返回值,因此有时可以利用全局变量增加函数间的联系渠道,在调用函数时有意改变某个全局变量的值,这样,当函数执行结束后,不仅能得到一个函数返回值,而且能使全局变量获得一个新值,从效果上看,相当于通过函数调用能得到一个以上的值。
- 为了便于在阅读程序时区别全局变量和局部变量,在C程序设计人员中习惯(但非规定)将全局变量名的第一个字母用大写表示。
!注意:虽然全局变量有以上优点,但建议不在必要时不要使用全局变量,因为:
- ①全局变量在程序的全部执行过程中都占用存单元,而不是仅在需要时才开辟单元。
- ②它使函数的通用性降低了,因为函数在执行时要依赖其所在的程序文件中定义的外部变量。如果将一个函数移到另一个文件中,还要将有关的外部变量及其值一起移过去。但若该外部变量与其他文件的变量同名时,就会出现冲突,降低了程序的可靠性和通用性。在程序设计中,在划分模块时要求模块的“内聚性”强、与其他模块的”耦合性”弱。即模块的功能要单一(不要把许多互不相千的功能放到一个模块中),与其他模块的相互影响要尽量少,而用全局变量是不符合这个原则的。一般要求把C程序中的函数做成一个封闭体,除了可以通过"实参一形参"的渠道与外界发生联系外,没有其他渠道。这样的程序移植性好,可读性强。
- ③使用全局变量过多,会降低程序的清晰性,人们往往难以清楚地判断出每个时各个外部变量的值。在各个函数执行时都可能改变外部变量的值,程序容易出错。因此,要限制使用全局变量。
模块的独立程度可以由两个定性标准度量:内聚性和耦合性。
- 耦合衡量不同模块彼此间互相依赖(连接)的紧密程度。
- 内聚衡量一个模块内各个元素彼此结合的紧密程度。
模块的内聚性越高,模块间的耦合性就越低,可见模块的内聚性和耦合性是相互关联的。因此,好的软件设计,应尽量做到高内聚、低耦合。
变量的存储方式和生存期
除了作用域以外,变量还有一个重要的属性:变量的生存期,即变量值存在的时间。有的变量在程序运行的整个过程都是存在的,而有的变量则是在调用其所在的函数时才临时分配存储单元,而在函数调用结束后就马上释放了,变量不再存在了。
变量的存储方式—静态存储方式和动态存储方式。
- 静态存储方式:是指在程序运行期间由系统在静态存储区分配存储空间的方式,在程序运行期间不释放
- 动态存储方式:是指在函数调用期间根据需要在动态存储区分配存储空间的方式
- 全局变量采用静态存储方式,在程序开始执行时给全局变量分配存储区,程序执行完毕释放。在程序执行过程中它们占据固定的存储单元,而不是动态地进行分配和释放。
- 在函数中定义的变量,在函数调用开始时分配动态存储空间,函数结束时释放这些空间,在程序执行过程中,这种分配和释放是动态的。
每一个变量和函数有两个属性:数据类型和数据的存储类别。在定义变量时,除了需要定义数据类型外,在需要时还可以指定其存储类别。
C语言中可以指定以下存储类别
函数中的形参和在函数中定义的变量(包括在复合语句中定义的变量),都属于此类。在调用该函数时,系统给这些变量分配存储空间,在函数调用结束时就自动释放这些存储空间。因此这类局部变量称为自动变量。自动变量用关键字auto作存储类别的声明。
例:
auto int b,c=3;
关鍵字"auto”可以省略,此时默认为“自动存储类别”,它属于动态存储方式。函数中大多数变量属于自动变量。
在以下情况下需要指定为 static存储类别:
希望函数中的局部变量的值在函数调用结束后不消失而继续保留原值,即其占用的存储单元不释放,在下一次该函数调用时,该变量已有值,就是上一次函数调用结束时的值。这时就应用关键字"static"指定该局部变量为"静态局部变量"。
例:输出1到5的阶乘值
#include< stdio.h>
int main()
{
int fac(int n); //对fac函数进行声明
int i;
for (i = 1; i <= 5; i++) //先后5次调用fac函数
printf("%d!=%d\n", i, fac(i)); //每次计算并输出主!的值
return 0;
}
int fac(int n) //对fac函数进行定义
{
static int f = 1; // f保留了上次调用结東时的值
f = f * n; // 在上次的f值的基础上再乘以n
return(f); //返回值f是n!的值
}
对静态局部变量的说明:
- 静态局部变量属于静态存储类別,在静态存储区内分配存储单元。在程序整个运行期间都不释放。而自动变量(即动态局部変量)属于动态存储类别,占动态存储区空间而不占静态存储区空间,函数调用结束后即释放。
- 对静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上次函数调用结東时的值。而对自动变量赋初值,不是在编译时进行的,而是在函数调用时进行,每调用一次函数重新给一次初值,相当于执行一次赋值语句。
- 如在定义局部变量时不赋初值,则对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符(对字符变量)。而对自动变量来说,如果不赋初值则它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另外分配存储单元,而所分配的单元中的值是不可知的。
- 虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的。因为它是局部变量,只能被本函数引用,而不能被其他函数引用。
- 用静态存储要多占内存(长期占用不释放,而不能像动态存储那样一个存储单元可供多个变量使用,节约内存),而且降低了程序的可读性,当调用次数多时往往弄不清静态局部变量的当前值是什么。因此,若非必要,不要多用静态局部变量。
静态外部变量的说明:
- 有时在程序设计中希望某些外部变量只限于被本文件引用,而不能被其他文件引用。这时可以在定义外部变量时加一个static声明。这种加上static声明、只能用于本文件的外部变量称为静态外部变量。在程序设计中,常由若干人分别完成各个模块,各人可以独立地在其设计的文件中使用相同的外部变量名而互不相干。只需在每个文件中的外部变量前加上static即可。这就为程序的模块化、通用性提供方便。如果已确其他文件不需要引用本文件的外部变量,就可以对本文件中的外部变量都加上static,成为静态外部变量,以免被其他文件误用。这就相当于把变量对外界“屏蔽”起来,从其他文件的角度看,这个变量是看不见的。
注意:static对局部变量和全局变量的作用不同。
— 对局部变量来说,它使变量由动态存储方式改变为静态存储方式。
— 对全局变量来说,它使变量局部化(局部于本文件),但仍为静态存储方式。
一般情况下,变量(包括静态存储方式和动态存储方式)的值是存放在内存中的。当程序中用到哪一个变量的值时,由控制器发出指令将内存中该变量的值送到运算器中。经过运算器进行运算,如果需要存数,再从运算器将数据送到内存存放。
为提高执行效率,C语言允许将局部变量的值放在CPU中的寄存器(寄存器可以认为是一种超高速的存储器),需要用时直接从寄存器取出参加运算,由于对寄存器的存取速度远高于对内存的存取速度,因此这样做可以提高执行效率。这种变量叫做寄存器变量,用关键字register作声明。
例:register int f; //定义f为寄存器变量
由于现在计算机的速度愈来愈快,性能愈来愈高,优化的编译系统能够识别使用频繁的变量,从而自动地将这些变量放在寄存器中,而不需要程序设计者指定。因此现在实际上用register声明变量是不必要的。
全局变量的生存期是固定的,存在于程序的整个运行过程。但是,对全局变量来说,还有一个问题尚待解决,就是它的作用域究竟从什么位置起,到什么位置止。其作用域是包括整个文件范围呢,还是文件中的一部分范围?是在一个文件中有效呢,还是在程序的所有文件中都有效?这就需要指定不同的存储类别。
1. 在一个文件内扩展外部变量的作用域
- 如果外部变量不在文件的开头定义,其有效的作用范围只限于定义点到文件结束。在定义点之前的函数不能引用该外部变量。如果由于某种考虑,在定义点之前的函数需要引用该外部变量,则应该在引用之前用关键字"extern"对该变量作“外部变量声明”。
- 例:
extern A;
表示把该外部变量A的作用域扩展到此位置。有了此声明,就可以从声明处起,合法地使用该外部变量。- 提倡将外部变量的定义放在引用它的所有函数之前,这样可以避免在函数中多加一个extern声明。
2. 将外部变量的作用域扩展到其他文件
- 如果一个程序包含两个文件,在两个文件中都要用到同一个外部变量Num,不能分别在两个文件中各自定义一个外部变量Num。否则在进行程序的连接时会出现“重复定义”的错误。正确的做法是:在任一个文件中定义外部变量Num,而在另一个文件中用extern对Num作“外部变量声明”。例:
extern Num;
。在编译和连接时,系统会由此可知Num是一个已在别处定义的外部变量,并将在另一个文件中定义的外部变量的作用域扩展到本文件,在本文件中可以合法地引用外部变量Num。
综上可知,对一个数据的定义,需要指定两种属性:数据类型和存储类别,分别使用两个关键字。
例:
static int a; //静态内部整型变量或静态外部整型变量
auto char c; //自动变量,在函数内定义
regester int d; //寄存器变量,在函数内定义
此外,还可以用extern声明已定义的外部变量。
例:extern b; //声明b将已定义的外部变量b的作用域扩展至此
关于作用域和生存期的小结
对一个变量的属性可以从两个方面分析:①作用域 ②变量值存在时间的长短,即生存期。
前者从空间的角度分析,后者从时间的角度分析。
- 如果一个变量在某个文件或函数范围内是有效的,则称该范围为该变量的作用域,在此作用域内可以引用该变量,所以在专业书中称变量在此作用域内“可见”,这种性质又称为变量的可见性。
- 如果一个变量值在某时刻是存在的,则认为这一时刻属于该变量的“生存期”,或称该变量在此时刻“存在”。
变量存储类别 | 函数内 | 函数外 | ||
---|---|---|---|---|
|
|
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
(只限本文件) |
|
|
|
|
|
|
表中“√”表示“是”,“×”表示“否”。
- 自动变量和寄存器变量在函数内外的“可见性”和“存在性”是一致的,即离开函数后,值不能被引用,值也不存在。
- 静态外部变量和外部变量的可见性和存在性也是一致的,在离开函数后变量值仍存在,且可被引用。
- 静态局部变量的可见性和存在性不一致,离开函数后,变量值存在,但不能被引用。
内部函数和外部函数
函数本质上是全局的,因为一个函数要被另外的函数调用,但是,也可以指定函数不能被其他文件调用。根据函数能否被其他源文件调用,将函数区分为内部函数和外部。
static 类型标识符 函数名(形参表);
//例:
static int fun(int a, int b);
内部函数又称静态函数,因为它是用static声明的。使用内部函数,可以使函数的作用域只局限于所在文件,在不同的文件中有同名的内部函数,互不干扰。也就是使它对外界"屏蔽"了。这样不同的人可以分别编写不同的函数,而不必担心所用函数是否会与其他文件中函数同名,通常把只能由同一文件使用的函数和外部变量放在一个文件中,在它们前面都冠以static使之局部化,其他文件不能引用。
- 如果在定义函数时,在函数首部的最左端加关键字extern,则此函数是外部函数,可供其他文件调用。
例:extern int fun (int a, int b);
这样,函数fun就可以为其他文件调用。C语言规定,如果在定义函数时省略extern,则隐含为外部函数。- 在需要调用此函数的文件中,用extern对函数作声明,表示该函数是在其他文件中定义的外部函数。
例:有一个字符串,内有若干个字符,现输入一个字符,如果字符中包含此字符,则把它删去。(用外部函数实现)
//fi1e1.c(文件1)
#include <stdio.h>
#include<string.h>
int main() //主函数
{ //以下3行是对在本函数中将要调用的在其他文件中定义的3个函数进行声明
extern void enter_string(char str[],int n);
extern void delete_string(char str[], char ch);
extern void print_string(char str[]);
char c; //c是准备删除的字符
char str[21]; //定义字符数组
enter_string(str,20); //调用enter_ string函数,输入字符串
scanf("%c", &c); //输入希望删除的字符
delete_string(str,c); //调用 delete_ string函数,删除字符
print_string(str); //调用print_string函数, 输出已删除字符后的字符串
return 0;
}
//fi1e2.c(文件2)
#include <stdio.h>
#include<string.h>
void enter_string(char str[21],int n)
//定义外部函数enter_string, 用来读入字符串
{
gets_s(str,79); //向字符数组输入字符串
}
//fi1e3.c(文件3)
#include < stdio.h>
#include<string.h>
void delete_string(char str[], char ch)
//定义外部函数 delete_ string,用来删除字符
{
int i, j;
for (i = j = 0; str[i] != '\0'; i++)
if (str[i] != ch)
str[j++] = str[i];
str[j] = '\0';
}
//fi1e4.c(文件4)
#include < stdio.h>
#include<string.h>
void print_string(char str[]) //定义外部函数 print string,用来输出字符串
{
printf("%s\n", str);
}
程序分析:
运行结果:
To be or not to be? //输入字符串
o //输入要删去的字符
T be r nt t be? //最终结果
- 此代码中最多可输入19个字符(包含空格),超过则会出现错误提示"Buffer is to small"。
- 程序中3个函数都定义为外部函数。在main函数中声明这些函数时,加上关键字extern表示调用的"enter _string、delete_string、prmt_string"函数是在其他文件中定义的部函数。
- 使用extern声明就能够在一个文件中调用其他文件中定义的函数,或者把该函数的作用域扩展到本文件。由于函数在本质上是外部的,在程序中经常要调用外部函数,为方便编程,C语言允许在声明函数时省写extern。
例: 主函数中的三行声明可改为:
void enter_string(char str[],int n);
void delete_string(char str[], char ch);
void print_string(char str[]);
小结
变量的存储类别共有4种:auto、static、register、extern。前三个用于局部变量,可改变变量的生存期。第4个只用于全局变量,可改变变量的作用域。
下面从不同角度对变量的作用域和生存期做归纳:
- 从作用域角度分,有局部变量和全局变量。
按 作 用 域 角 度 分 { 局 部 变 量 { 自 动 ( a u t o ) 变 量 , 即 动 态 局 部 变 量 ( 离 开 函 数 , 值 就 消 失 ) 静 态 ( s t a t i c ) 局 部 变 量 ( 离 开 函 数 , 值 仍 保 留 ) 寄 存 器 ( r e g i s t e r ) 变 量 ( 离 开 函 数 , 值 就 消 失 ) ( 形 式 参 数 可 以 定 义 为 自 动 变 量 或 寄 存 器 变 量 ) 全 局 变 量 { 静 态 ( s t a t i c ) 外 部 变 量 ( 只 限 本 文 件 引 用 ) 外 部 变 量 ( 即 非 静 态 的 外 部 变 量 , 允 许 其 他 文 件 引 用 ) 按作用域角度分\begin{cases} 局部变量\begin{cases} 自动(auto)变量,即动态局部变量(离开函数,值就消失)\\ 静态(static)局部变量(离开函数,值仍保留) \\ 寄存器( register)变量(离开函数,值就消失) \\ (形式参数可以定义为自动变量或寄存器变量) \\ \end{cases} \\ 全局变量\begin{cases} 静态( static)外部变量(只限本文件引用) \\ 外部变量(即非静态的外部变量,允许其他文件引用) \\ \end{cases} \end{cases} 按作用域角度分⎩⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎧局部变量⎩⎪⎪⎪⎨⎪⎪⎪⎧自动(auto)变量,即动态局部变量(离开函数,值就消失)静态(static)局部变量(离开函数,值仍保留)寄存器(register)变量(离开函数,值就消失)(形式参数可以定义为自动变量或寄存器变量)全局变量{静态(static)外部变量(只限本文件引用)外部变量(即非静态的外部变量,允许其他文件引用)
- 从变量存在的时间(生存期)来区分,有动态存储和静态存储两种类型。静态存储是程序整个运行时间都存在,而动态存储则是在调用函数时临时分配单元。
按 变 量 存 在 的 时 间 分 { 动 态 存 储 { 自 动 变 量 ( 本 函 数 内 有 效 ) 寄 存 器 变 量 ( 本 函 数 内 有 效 ) 形 式 参 数 ( 本 函 数 内 有 效 ) 静 态 存 储 { 静 态 局 部 变 量 ( 函 数 内 有 效 ) 静 态 外 部 变 量 ( 本 文 件 内 有 效 ) 外 部 变 量 ( 用 e x t e r n 声 明 后 , 其 他 文 件 可 引 用 ) 按变量存在的时间分\begin{cases} 动态存储\begin{cases} 自动变量(本函数内有效) \\ 寄存器变量(本函数内有效) \\ 形式参数(本函数内有效) \\ \end{cases} \\ 静态存储\begin{cases} 静态局部变量(函数内有效) \\ 静态外部变量(本文件内有效) \\ 外部变量(用extern声明后,其他文件可引用) \\ \end{cases} \end{cases} 按变量存在的时间分⎩⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎧动态存储⎩⎪⎨⎪⎧自动变量(本函数内有效)寄存器变量(本函数内有效)形式参数(本函数内有效)静态存储⎩⎪⎨⎪⎧静态局部变量(函数内有效)静态外部变量(本文件内有效)外部变量(用extern声明后,其他文件可引用)
- 从变量值存放的位置来区分
按 变 量 值 存 放 的 位 置 分 { 内 存 中 静 态 存 储 区 { 静 态 局 部 变 量 静 态 外 部 变 量 ( 函 数 外 部 静 态 变 量 ) 外 部 变 量 ( 可 为 其 他 文 件 引 用 ) 内 存 中 动 态 存 储 区 : 自 动 变 量 和 形 式 参 数 C P U 中 的 寄 存 器 : 寄 存 器 变 量 按变量值存放的位置分\begin{cases} 内存中静态存储区\begin{cases} 静态局部变量 \\ 静态外部变量(函数外部静态变量) \\ 外部变量(可为其他文件引用) \\ \end{cases} \\ 内存中动态存储区:自动变量和形式参数 \\ CPU中的寄存器:寄存器变量 \\ \end{cases} 按变量值存放的位置分⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧内存中静态存储区⎩⎪⎨⎪⎧静态局部变量静态外部变量(函数外部静态变量)外部变量(可为其他文件引用)内存中动态存储区:自动变量和形式参数CPU中的寄存器:寄存器变量
区别对变量的定义与声明
定义变量时,要指明数据类型,编译系统要据此给变量分配存储空间,又称为定义性声明。凡不引起空间分配的变量声明(如extern声明)不必指定数据类型,因为数据类型已在定义时指定了。这种声明只是为了引用的需要,这种声明称为引用性声明,简称声明。在一个作用域内,对同一变量,只能出现一次定义,而声明可以出现多次。