提要:

  1. 分析C函数的调用过程与参数传递
  2. 分析函数声明与定义的区别
  3. 分析C函数与数学函数的区别
  4. 了解面向过程

前置知识:

  1. 了解C函数的基本结构

注:尽管看起来过度分块,但是为了更加清晰地突出内容,还是分出了许多的标题,望理解.

深入分析C函数

C函数的调用过程

上一部分已经讲解了C函数的基本结构,即:

1
2
3
4
<返回值类型> <标识符(函数名)>(形参列表){
// 代码块
return 返回值; // 可选的return 语句.
}

并且知道在一个函数A中调用一个函数B(假设B的声明是这样:int B(int a);)只需要这样:

1
2
3
4
5
6
void A(){
// 其它代码
int a2 = B(3); // 向B传递一个3作为实参,返回一个int值赋值给a2
// 其它代码
// A返回值为void,所以不需要返回任何值,我们将会在后面讲解到
}

下面来详细讲解.

使用函数-函数调用,主调函数与被调函数

仍然是使用计算平方的这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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:

    1
    2
    3
    4
    5
    int main(){
    int x = 10;
    printf("answer = %d\n",f(x)); // 没有任何必要再引入一个x2来浪费时间和空间
    return 0;
    }

    当然,在调用一个函数并使用其值时,一定要注意参数类型和返回值类型的匹配!否则可能引发报错,甚至更严重的是,导致一些意想不到的结果.

参数问题-形参和实参

接下来就要讨论一个重要问题,即函数的参数.

我们可以看到,在main()函数中,函数调用是这样的:

1
int x2 = f(x);

但是我们看f(x)的声明(函数声明即仅给出函数头,并在最后加上;):

1
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(或者是任何变量)的值会不会同步地发生变化?看代码:

1
2
3
4
5
6
7
8
9
10
11
#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;
}
image-20231104143147714

显然,main()函数中的x并没有发生变化,这意味着,使用x调用f(),仅仅是将main()函数中的变量x的值,也就是10,传递给f()函数,并将这个值"赋值给"f()中的形参x.

在f()中对形参x的任何操作都不会影响到main()中的x.

换言之,这两个x除了调用时进行了值的复制外,再没有任何关系!

return语句和返回值

return语句用于结束一个函数,并且它还负责返回一个计算好的返回值.

在f()中:

1
2
3
4
5
int f(int x){
int y;
y = x*x;
return y;
}

return y;代表着两件事:

  1. 这个函数结束运行!

    无论return后面还有没有其他语句,都要立即结束这个函数!这也意味着return并不是必须为最后一条语句.

  2. 将y作为返回值返回给主调函数!

    这里的y可以替换为任意表达式,但是必须和函数类型(函数的返回值类型)相同或者可以转化为返回值类型!

    例如我们可以直接这样写:

    1
    2
    3
    int f(int x){
    return x*x; // x*x是一个表达式,两个int相乘,结果仍为int,和返回值类型相匹配
    }

此外,返回值类型和参数类型完全不必要完全相同,完全任意.f()仅仅是作为一个例子而已.

函数声明,函数原型与函数定义

先放结论:

有关这些概念性问题,容易混淆,本人这里更倾向于这一种观点:

函数原型,函数定义,都属于函数声明的一种,现代的C语言都统一使用函数原型式的风格对其进行统一.

我们在使用一个函数之前,即进行函数调用之前,都必须知道这个函数的相关信息,包括函数名,函数参数个数,函数参数类型,函数返回值类型,这些信息显然,在我们实现一个函数f()时,都已经给出:

1
2
3
4
5
6
// 显然这是一个完整的函数声明(包括了函数体)
int f(int x){
int y;
y = x*x;
return y;
}

并且这一部分都放在了main()函数之前.我们尝试改变一下f()的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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()的任何信息,它并不认识这个函数,更谈何调用.

解决方法很简单,在调用之前加上一个声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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语言中,函数声明有两种方式,分别是函数类型声明函数定义,需要知道的是,它们都采用函数原型式的风格.具体解释如下:

  1. ---WAHAHA

    作为初学,我们无需去了解更加详细的细节,因为这涉及到C语言的发展.

    我们只需要知道,函数原型提供了除了函数体以外的所有信息,也就是int f(int x);所提供给我们的所有信息,包括函数名,函数参数个数,函数参数类型,函数返回值类型.

    编译器根据函数原型,就能够唯一确定一个函数,并且正确地调用这个函数.

  2. 函数类型声明

    之所以不说函数声明,是因为声明是一个更加笼统的概念,函数类型声明属于其中的一种.

    现在的函数类型声明和函数原型一模一样,就是int f(int x);,即仅仅给出函数头,并在结尾加上;

    有了这个信息,就相当于给出了函数原型,编译器就能找到正确的函数.

  3. 函数定义

    函数定义,可以理解为最为全面的函数声明,他不仅提供了函数的原型,还给出了具体的函数体.

    所谓函数定义,就是这一部分:

    1
    2
    3
    4
    5
    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++编译器报错!):

1
2
3
4
5
6
7
8
#include <stdio.h>
int foo(){
printf("run foo function successfully!");
}
int main(){
foo(1,2,3);
return 0;
}

C和面向过程

什么是面向过程

面向过程是一种编程思想,其核心是怎么做,专注于完成任务的具体细节.

一般的面向过程是从上往下步步求精,所以,面向过程的核心是模块化的思想,清楚了程序的流程,就能够实现整个程序.

C和面向过程

面向过程是一种编程模式,其核心为模块化思想,对于每一个模块的划分,不同的编程语言有着不同的实现.

对于C语言而言,使用函数来实现模块的分离.将每一个子过程放到一个个的函数中,运行时依次按顺序进行调用即可.亦即一个函数就是一个最小的模块.这也就是所谓的函数式编程.

不仅如此,我们可以将若干函数封装到同一个源文件中,这些功能相关的函数共同组成一个模块,用于实现一类操作.

关于面向过程的内容,还有很多,而且还有面向对象,面向切面等等的各种设计模式,碍于能力所限和主题限制,不在此讨论.


本部分分析了函数的各种使用细节,接下来将讲解进一步的使用方法.

---WAHAHA



上一篇:C语言教程-12_1-初识函数

下一篇:C语言教程-12_3-函数的其他用法和特性