函数先后关系
有自定义函数的时候,自定义函数往往写在主函数之前,原因是C的编译器会按照自上而下的顺序逐行读取分析代码。在编译器读取到主函数中函数调用部分时,如果自定义函数没有写在主函数之前,程序调用自定义函数就会出错。
#include <stdio.h>
int main ()
{
sum(5,10);
sum(20,30);
sum(35,45);
return 0;
}
void sum(int begin,int end)
{
int d;
int sum = 0;
for(d = begin;d <= end;d++){
if(isPrime(d) == 1){
sum += d;
}
}
printf("%d\n",sum);
}
int isPrime(int d)
{
int isPrime = 1;
int e;
for(e = 2;e < d ;e++){
if (d % e == 0){
isPrime = 0;
break;
}
}
return isPrime;
}
[Warning] conflicting types for 'sum'
12
52
121
--------------------------------
Process exited after 0.01176 seconds with return value 0
严格按照C99的标准来看,上述程序的顺序是错误的,程序执行会出现warning告警,有可能出现error报错,不同的编译器对C99标准执行的力度不同。 ::: tip LLVM中是检查比较严格的编译器,上述写法会出现的报错。
warning: implicit declaration of function 'sum'is invalid in C99 [-wimplicit-funct] sum(1,10);
error: conflicting types for 'sum 'void sum(int begin,int end)
warning的提示是说第5行sum函数中隐藏声明是无效的,在第5行中,函数sum对编译器来说是未知的,在C99之前,C的编译器会假设有一个sum(int,int)函数对应sum(5,10)等并会返回int,即int sum(int,int)。warning提示这种做法在C99是无效的,但仍可以编译下去。 在第11行中出现sum函数,类型是viod。所以会提示报错error:sum函数类型冲突。意为11行中出现的void类型的sum,和编译器假设的int类型冲突。这个报错会导致程序无法编译执行。 :::
按照习惯,为了方便程序阅读,希望将主函数main放在前面,需要对程序做出改动,可以将自定义函数的函数头放在主函数main的前面,如下:
#include <stdio.h>
void sum(int begin,int end);
int isPrime(int d);
int main ()
{
sum(5,10);
sum(20,30);
sum(35,45);
return 0;
}
void sum(int begin,int end)
{
int d;
int sum = 0;
for(d = begin;d <= end;d++){
if(isPrime(d) == 1){
sum += d;
}
}
printf("%d\n",sum);
}
int isPrime(int d)
{
int isPrime = 1;
int e;
for(e = 2;e < d ;e++){
if (d % e == 0){
isPrime = 0;
break;
}
}
return isPrime;
}
上述程序可以正常执行。 上述程序中被移到程序头部的3、4行叫做“函数原型声明”。13行、25行叫做“定义”。主函数会根据声明判断函数调用是否正确。 函数原型是函数头加分号构成。 ::: tip 声明不是函数,只是说明了函数的格式,如返回类型、参数类型等,在后面主函数中,遇到相应的函数调用时,编译器就无需猜测函数的类型等,可以直接根据函数声明来判断。 ::: 在13行、25行定义函数的时候,编译器会再次判断定义和声明是否一致,如果不一致,函数依然会报错。 ::: warning 函数原型声明传递给编译器的只有函数名、参数类型、返回类型,即void sum(int begin,int end);中可以不写参数。编译器检查时候不会检查参数的名称。 一般会写明参数,虽然对于编译器无意义,但对于程序读者来说是有意义的。 :::
参数传递
如果函数有参数,调用时候必须传递给它数量类型正确的值,可以传递给函数的值是表达式的结果,包括:字面量、变量、函数返回值、计算结果。如下:
c = max(10,12);
c = max(a,b);
c = max(c,23);
c = max(max(a,b),c)
c = max(23+45,c);
如果调用函数时给的值与参数类型不匹配,编译器会自动转换类型,但可能不是所期望的类型,如下:
#include <stdio.h>
void cheer(int i)
{
printf("cheer %d\n",i);
}
int main()
{
cheer(2.4);
return 0;
}
[warning] implicit conversion from 'double' to 'int' changes value from 2.4 to 2
cheer 2
--------------------------------
Process exited after 0.01752 seconds with return value 0
上述程序第3行自定义函数原型声明中希望传递一个整型变量i并打印出来,而在第10行主函数中调用时给cheer一个整数的数字就没有问题,如果如上给了2.4的值,编译器编译过程中会提示出现了隐含的从double到int的转换,会将值从2.4变为2。warning不会影响编译器编译,程序仍然会输出结果。 ::: danger 调用函数时给的值与参数类型不匹配,编译器会自动转换类型,即C语言没有检查出类型不同,可能会导致赋值输出结果不一样,这是C语言最大的漏洞。后续的C++和JAVA在函数调用中值与类型的匹配检查严格的多。 ::: 修改上述程序:
#include <stdio.h>
void cheer(int i)
{
printf("cheer %d\n",i);
}
int main()
{
double f = 2.4;
cheer(f);
return 0;
}
cheer 2
--------------------------------
Process exited after 0.02159 seconds with return value 0
考虑一个问题,函数调用过程中传递的是什么。 下面程序是交换函数值的程序:
#include <stdio.h>
void swap(int a,int b);
int main()
{
int a = 6;
int b = 8;
swap(a,b);
printf("a的值为%d,b的值为%d",a,b);
return 0 ;
}
void swap(int a,int b)
{
int t = a;
a = b;
b = t;
}
a的值为6,b的值为8
--------------------------------
Process exited after 0.01642 seconds with return value 0
从上述结果和程序调试的结果来看,函数a和b的值交换并不成功。 C语言在调用函数时只能传值给函数。 ::: warning 在上述程序中,main函数里面的变量a、b和swap函数里面的变量a、b是毫无联系的,它们仅仅是同名而已,第8行的swap(a,b)只是说两个函数中的同名变量的值是函数调用过程中传递过去的,即main中a的值给了swap中的a,main中b的值给了swap中的b。 ::: 所以在函数swap中,对a和b的操作,只是对函数swap的参数a、b进行的,不能影响到函数main。 ::: tip 综上每个函数都有自己的变量空间,参数就位于这个独立的空间中,不受其他函数的参数影响。 ::: 下图右边部分,过去把函数表中的参数叫做形式参数(包括原型声明和函数定义中的参数),函数调用时候给的值称为实际参数。 下图左边部分是现在的普遍称呼,把函数表中的参数称为“参数”,把函数调用时称为“值”,即“参数”和“值”的关系。这样更有利于来理解函数调用就是传值的过程。
本地变量(局部变量)
函数每一次运行产生一个独立的变量空间,在这个变量空间中的变量是函数这一次运行所独有的,称作本地变量。所有定义在函数内部的变量是本地变量。写在函数参数表中的参数也是本地变量,具有一样的生存期和作用域。 ::: tip 生存期:什么时候这个变量开始出现,到什么时候它消亡 作用域:指在(代码的)什么范围内可以访问这个变量,即这个变量可以起作用 生存期和作用域都指的是大括号内的部分,即:块。 ::: 在devc++中如果某个变量不存在,编译器会提示“Not found in current context”,如果变量存在,编译器会给出值。可以通过编译器的这个特性观察变量的出现消亡。
本地变量是定义在块内的(可以定义在函数块内,也可以定义在语句块内)。如下图截取部分,变量i是if语句的块中的变量,它的生存期和作用域只在6-8行的大括号之间,即变量i只存在于这个语句块中:
int main()
{
int a = 6;
int b = 8;
swap(a,b);
if(a > b){
int i = 6;
}
printf("a的值为%d,b的值为%d",a,b);
return 0 ;
}
上述程序中受限于if语句,只有在if语句成立时,变量i才会出现在这个块中。
本地变量规则
本地变量的定义甚至可以直接在函数中写一对大括号{},在其中定义作用域是大括号内的本地变量。在这个大括号内,定义的本地变量可以运算可以输出。虽然这个大括号没有依附于任何语句,但对于C语言来说是合理的写法。
在块的外面定义的变量,在块内仍然有效,可以在块内参与运算和输出。
在块内定义的变量如果和块外的变量同名,块内的变量会掩盖块外的变量,如下:
#include <stdio.h>
void swap(int a,int b);
int main()
{
int a = 6;
int b = 8;
swap(a,b);
{
int a = 0;
printf("a=%d\n",a);
}
printf("a的值为%d,b的值为%d",a,b);
return 0 ;
}
void swap(int x,int y)
{
int t = x;
x = y;
y = t;
}
a=0
a的值为6,b的值为8
--------------------------------
Process exited after 0.01083 seconds with return value 0
如上图9到11行,块内定义的本地变量a掩盖了块外定义的变量a在块内生效。
同一个块内不能定义同名变量。
本地变量不会被默认初始化,即不会得到默认的初始值。
其他细节
在函数没有参数时,“void f(void);”和“void f();”是不一样的,前者在参数表中放了void表示函数不接受参数,后者的参数表中不放东西,表示参数表未知,而不是没有参数。
void swap();
int main()
{
int a = 6;
int b = 8;
swap(a,b);
}
上述程序中第一行swap函数原型声明中参数表中没有写,在第6行函数调用时遇到(a,b),编译器会猜测函数swap有两个参数,且类型为int。
#include <stdio.h>
void swap();
int main()
{
int a = 5;
int b = 6;
swap(a,b);
printf("a=%d,b=%d",a,b);
return 0;
}
void swap(double a,double b)
{
int swap;
int t = a;
printf("in swap,a=%f,b=%f\n",a,b);
a = b;
b = t;
}
in swap, a=0.00000, b=-48636609591527741727015516146270102912541102026377338361271230684801492485289658594753266034
a=5,b=6
[Finished in 0.2s]
上述程序在第5行原型声明中没有注明参数和类型,第10行函数调用时编译器猜测给到的参数类型是int整数类型,而在第14行函数实际类型中给到的是double。原型的作用不仅仅是用来检查对函数的调用是否正确,也用于检查函数的定义是否正确。函数原型没有给出参数类型,就有可能是double类型,第14行函数定义给出的double类型就不会报错,而第10行函数调用时编译器猜测给到swap的参数类型是两个整数int,传递到第14行的swap函数中的两个double时出现错误。 ::: danger 由上所述,在函数的原型声明中应注明参数和类型,如果没有参数,就在括号内写入void。 :::
在函数调用时f(a,b)的圆括号内的逗号不是运算符,只是标点符号,如果写成f((a,b))的形式,里面的圆括号内是表达式,逗号是运算符,需要先计算里面的圆括号内。 这两种写法的区别是传递了两个还是一个参数。 ::: tip 逗号运算符是二元运算符:表达式1 , 表达式2。逗号运算符确保操作数被顺序地处理:先计算左边的操作数,再计算右边的操作数。右操作数的类型和值作为整个表达式的结果。逗号运算符的优先级是所有运算符中最低的。 :::
C语言不允许函数嵌套定义。即函数中可以放入另一个函数的声明,但是不能放另一个函数的定义。如下:
int i,j,sum(int a,int b);
C语言接受上述写法,但一般建议将函数sum的声明写在函数外面。
return(i);
如上return是一个语句,后面要跟一个表达式,i外的圆括号没有意义,变量+括号=表达式,括号是运算符,只是不参与运算,加上圆括号不算错误,但是容易被误解return是一个函数,语句的意思是调用return函数把i的值传递给return函数的参数。所以return后的变量一般不要加上圆括号。
int main也是一个函数,int main()可以写成int main(void)。main是C语言程序的入口,但并不是程序运行的第一条代码,程序运行前需要做准备工作,然后调用main函数。(return 0就是在main函数结束时候把0返回给调用main函数的地方,系统会通过返回内容决定后续)。 ::: tip 传统上一个程序返回0表示正常运行结束,如果返回非0值表示在程序运行中出现错误。 :::