title: C语言教程-12_2-深入分析函数和面向过程初识
tags: [C]
categories: C语言教程
description: 深入了解C语言中针对函数这一"功能"的具体实现,初步了解面向过程
提要:
- 分析C函数的调用过程与参数传递
- 分析函数声明与定义的区别
- 分析C函数与数学函数的区别
- 了解面向过程
前置知识:
- 了解C函数的基本结构
注:尽管看起来过度分块,但是为了更加清晰地突出内容,还是分出了许多的标题,望理解.
深入分析C函数
C函数的调用过程
上一部分已经讲解了C函数的基本结构,即:
<返回值类型> <标识符(函数名)>(形参列表){
// 代码块
return 返回值; // 可选的return 语句.
}
并且知道在一个函数A中调用一个函数B(假设B的声明是这样:int B(int a);
)只需要这样:
void A(){
// 其它代码
int a2 = B(3); // 向B传递一个3作为实参,返回一个int值赋值给a2
// 其它代码
// A返回值为void,所以不需要返回任何值,我们将会在后面讲解到
}
下面来详细讲解.
使用函数-函数调用,主调函数与被调函数
仍然是使用计算平方的这个例子:
#include <stdio.h>
int f(int x){
int y;
y = x*x;
return y;
}
int main(){
int x = 10,x2;
x2 = f(x);
printf("answer = %d\n",x2);
return 0;
}
-
函数调用
我们前面仅仅是定义了一个函数f,和数学一样,我们需要使用特定的
参数
(自变量)去使用f进行求值.调用一个函数,需要使用到函数调用表达式,只需要使用要调用的函数名,并在后面跟一个小括号,里面按顺序填写需要传递的特定参数:
<函数名>(需要传递的参数列表)
例如在main()函数中,第11行
x2 = f(x);
这条语句中,就调用了f(x),用于求x的平方,并且直接将f(x)
赋值给x2,因为函数表达式的值就是这个函数的返回值
. -
主调函数与被调函数
这是两个概念,需要了解.
主调函数
就是调用的发起者,也就是调用方
.这里就是main()函数,他调用了f()函数.被调函数就是被调用的一方,也就是
被调用方
,这里就是f()函数,因为他被main()函数调用了. -
函数调用的位置
函数调用可以出现在主调函数任何需要使用值的位置,前提是被调函数有返回值—如果一个函数的返回值为
void
,那么他没有返回值.例如,我们可以直接将f(x)传递给printf(),而无需多此一举赋值给x2:
int main(){ int x = 10; printf("answer = %d\n",f(x)); // 没有任何必要再引入一个x2来浪费时间和空间 return 0; }
当然,在调用一个函数并使用其值时,一定要注意参数类型和返回值类型的匹配!否则可能引发报错,甚至更严重的是,导致一些意想不到的结果.
参数问题-形参和实参
接下来就要讨论一个重要问题,即函数的参数.
我们可以看到,在main()函数中,函数调用是这样的:
int x2 = f(x);
但是我们看f(x)的声明(函数声明即仅给出函数头,并在最后加上;):
int f(int x);
可以看到有两个x
,事实上这两个x并不是一个东西.
int x2 = f(x);
中的x,是main()函数中的局部变量,我们仅仅是将这个x的值作为参数传递(复制其值)给f().这里的x叫做实际参数(实参)
,也就是真正的参数值.
int f(int x);
中的x,是为了指明f()函数的一个参数,它配合着函数体中的代码,来构成一个完整的函数,自身并没有值,需要主调函数为其传递一个特定值.这个x叫做形式参数(形参),它需要等待传入一个实参值并复制给他.
这里我们就可以大胆猜测(事实上前面已经有代码这么做),调用f()的时候,实参列表中的各个实参完全不必要和形参名一一对应,我们仅仅关心实参的值
!
这就又牵扯出一个关键点:C函数的一切参数传递全部都是按值传递!
下面进行讲解:
按值传递
考虑一个问题,如果我在f()中对形参x进行修改,那么main()中的实参x(或者是任何变量)的值会不会同步地发生变化?看代码:
#include <stdio.h>
int f(int x) {
x++;
return x;
}
int main() {
int x = 10, x2;
x2 = f(x);
printf("x=%d,x2=%d", x, x2);
return 0;
}
显然,main()函数中的x并没有发生变化,这意味着,使用x调用f(),仅仅是将main()函数中的变量x的值,也就是10,传递给f()函数,并将这个值"赋值给"f()中的形参x.
在f()中对形参x的任何操作都不会影响到main()中的x.
换言之,这两个x除了调用时进行了值的复制外,再没有任何关系!
return语句和返回值
return语句用于结束一个函数,并且它还负责返回一个计算好的返回值.
在f()中:
int f(int x){
int y;
y = x*x;
return y;
}
return y;
代表着两件事:
-
这个函数结束运行!
无论return后面还有没有其他语句,都要立即结束这个函数!这也意味着return并不是必须为最后一条语句.
-
将y作为返回值返回给主调函数!
这里的y可以替换为任意表达式,但是必须和
函数类型(函数的返回值类型)
相同或者可以转化为返回值类型!例如我们可以直接这样写:
int f(int x){ return x*x; // x*x是一个表达式,两个int相乘,结果仍为int,和返回值类型相匹配 }
此外,返回值类型和参数类型完全不必要完全相同,完全任意.f()
仅仅是作为一个例子而已.
函数声明,函数原型与函数定义
先放结论:
有关这些概念性问题,容易混淆,本人这里更倾向于这一种观点:
函数原型
,函数定义
,都属于函数声明
的一种,现代的C语言都统一使用函数原型式的风格
对其进行统一.
我们在使用一个函数之前,即进行函数调用之前,都必须知道这个函数的相关信息,包括函数名,函数参数个数,函数参数类型,函数返回值类型
,这些信息显然,在我们实现一个函数f()时,都已经给出:
// 显然这是一个完整的函数声明(包括了函数体)
int f(int x){
int y;
y = x*x;
return y;
}
并且这一部分都放在了main()函数之前.我们尝试改变一下f()的位置:
#include <stdio.h>
int main(){
int x = 10,x2;
x2 = f(x);
printf("answer = %d\n",x2);
return 0;
}
int f(int x){
int y;
y = x*x;
return y;
}
上面这段代码会产生一个警告(注意并不是错误),原因是没有找到f()这个函数的声明.换言之,编译器此时在第5行的位置之前,并未找到有关f()的任何信息,它并不认识这个函数,更谈何调用.
解决方法很简单,在调用之前加上一个声明
:
#include <stdio.h>
int f(int x);
int main(){
// int f(int x); // 或者放在这里也可以的,并且'函数类型声明'可以重复
int x = 10,x2;
x2 = f(x);
printf("answer = %d\n",x2);
return 0;
}
int f(int x){
int y;
y = x*x;
return y;
}
所以我们知道,对于一个函数调用,编译器必须知道这个函数的相关信息,才能正确地进行调用
.
而提供这些信息的操作就叫做函数声明
,现代的C语言中,函数声明有两种方式,分别是函数类型声明
和函数定义
,需要知道的是,它们都采用函数原型式的风格
.具体解释如下:
-
---WAHAHA
作为初学,我们无需去了解更加详细的细节,因为这涉及到C语言的发展.
我们只需要知道,函数原型提供了除了函数体以外的所有信息,也就是
int f(int x);
所提供给我们的所有信息,包括函数名,函数参数个数,函数参数类型,函数返回值类型
.编译器根据函数原型,就能够唯一确定一个函数,并且正确地调用这个函数.
-
函数类型声明
之所以不说函数声明,是因为声明是一个更加笼统的概念,
函数类型声明
属于其中的一种.现在的函数类型声明和函数原型一模一样,就是
int f(int x);
,即仅仅给出函数头,并在结尾加上;
有了这个信息,就相当于给出了函数原型,编译器就能找到正确的函数.
-
函数定义
函数定义,可以理解为最为全面的
函数声明
,他不仅提供了函数的原型
,还给出了具体的函数体
.所谓函数定义,就是这一部分:
int f(int x){ int y; y = x*x; return y; }
函数定义比函数类型声明更进一步,完全给出了一个函数,但是问题也很明显,只能使用一次,因为不能重复地给出一个函数体.
总结:
C语言有着很长的发展历史,这里给出的内容,已经是现代C语言的规范了,无论是函数类型声明
,还是函数定义
,都给出了一个函数最基本的各种信息,也就是都采用函数原型式的风格
.
C99把旧的非原型形式视为过时,因为他们是在C语言还未建立起如此规范的标准之前的写法.
我们在使用一个函数之前,一定要确保在调用点前有函数的声明存在,无论是仅仅给出声明(函数类型声明)还是给出完整的定义.
注:以上借鉴自https://www.cnblogs.com/pmer/archive/2011/09/04/2166579.html
数学函数?参数和返回值的有无问题
同数学函数不同,数学函数一定有参数(自变量)和函数值(因变量),而C函数更多的是为了实现一个过程
,而不是一定要计算出一个结果,甚至,这个过程不需要提供任何的参数作为前提.
例如,stdlib.h
有一个函数int rand();
这个函数不需要任何参数,所以参数列表是空的,其功能为生成一个伪随机数,当然不需要任何参数,最终将这个伪随机数作为函数返回值返回.
再例如,stdlib.h
有一个函数void free( void *ptr );
这个函数接受一个指针(这里的void *并不是没有参数的意思),用于释放其指向的内存空间,这个函数不需要返回任何值,仅仅释放空间后就直接结束.
如果一个函数不需要参数,或者不需要返回值,则可以使用void
类型来说明.
另外,一个函数可以既没有返回值也不需要参数,则同样可以这样:void func(void);
必须注意的是,参数列表为空,可以写void func(void)
,也可以写void func()
,往往没有什么影响,但是二者并不是没有区别:
括号里加void,表明这个函数严格意义地没有参数;
而如果没有加void,表示这个函数可以有任意多个参数—尽管这些参数不会被处理.
例如,下面的程序用C编译器编译不会报错,能够正常运行(但是使用c++编译器报错!):
#include <stdio.h>
int foo(){
printf("run foo function successfully!");
}
int main(){
foo(1,2,3);
return 0;
}
C和面向过程
什么是面向过程
面向过程是一种编程思想,其核心是怎么做
,专注于完成任务的具体细节.
一般的面向过程是从上往下步步求精,所以,面向过程的核心是模块化的思想
,清楚了程序的流程,就能够实现整个程序.
C和面向过程
面向过程是一种编程模式,其核心为模块化思想
,对于每一个模块的划分,不同的编程语言有着不同的实现.
对于C语言而言,使用函数
来实现模块的分离.将每一个子过程放到一个个的函数中,运行时依次按顺序进行调用即可.亦即一个函数就是一个最小的模块.这也就是所谓的函数式编程
.
不仅如此,我们可以将若干函数封装到同一个源文件中,这些功能相关的函数共同组成一个模块,用于实现一类操作.
关于面向过程的内容,还有很多,而且还有面向对象,面向切面等等的各种设计模式,碍于能力所限和主题限制,不在此讨论.
本部分分析了函数的各种使用细节,接下来将讲解进一步的使用方法.
---WAHAHA
注:文章原文在本人博客https://gngtwhh.github.io/上发布