一文讲解C语言函数

函数 function 是完成特定任务的独立程序代码单元。语法规则定义了函数的结构和使用方式。

当程序要多次完成某项任务,可以使用函数,省去编写重复代码;或只完成某项任务一次。因此,使用函数让程序模块化,提高程序代码可读性,更方便后期修改、完善。

一文讲解C语言函数

函数定义

函数定义的语法如下所示:

return_type function_name(parameter_list) {
    function_body
}
  • return_type:函数返回值的数据类型。当函数无返回值时,return_type 使用关键字 void 表示。
  • function_name:函数的实际名称。
  • parameter_list:参数列表,表示参数的类型、名称、顺序、数量。参数是可选的,有时函数可能不包含参数。
  • function_body:函数主体,包含一组定义函数执行任务的语句。

下面给出一个从 0 加到 n 的结果的例子。

int sum(int n) {
    int sum = 0;
    while (n > 0) {
        sum += n;
        n--;
    }
    return sum;
}

return

在函数中使用 return,将执行流返回到函数被调用的地方,并返回值。语法如下所示。

return expression;
  • expression:表达式的结果即为函数的返回值,表达式可选。当函数无需返回值时,可省略,函数声明时应把这种无需返回值的函数声明为 void

函数声明

函数声明位于函数定义之前,用来指定函数的名称、返回类型、存储类和其他特性,也称为函数原型 function prototype,函数的实际主体可以单独定义。函数声明如下:

int sum(int n);

函数原型会告诉编译器函数的参数数量和每个参数的类型以及返回值的类型,编译器根据原型检查该函数的调用,确保参数正确、返回值无误。当出现不匹配的情况时,如果转换可行,编译器会把不匹配的实参或返回值转换为正确的类型。

在原型中,参数名是可选的,但在函数原型中加入描述性的参数名可以给该函数提供有用的信息。如上所示,可以将参数名去掉。

int sum(int);

函数原型一般定义在 .h 头文件中,当其他源文件需要函数原型时,可以使用 #include 指定包含该文件。

// cmath.h
int sum(int n);

// main.c
#include "cmath.h"
int main() {
    int s = sum(10);
    return 0;
}

放在 .h 头文件的好处是

  1. 函数原型具有文件作用域,所以原型的一份拷贝可以作用于整个源文件,较之在该函数每次调用前单独书写一份函数原型要容易得多。
  2. 函数原型只书写一次,这样就不会出现多份原型的拷贝之间的不匹配现象。
  3. 如果函数的定义进行了修改,我们只需要修改原型,并重新编译所有包含了该原型的源文件即可。
  4. 如果函数的原型同时也被 #include 指令包含到定义函数的文件中,编译器就可以确认函数原型与函数定义的匹配。

函数的缺省认定:当程序调用一个无法见到原型的函数时,编译器便为该函数返回一个整形值。对于那些并不返回整形值的函数,这种认定可能会引起错误。

函数参数

函数定义中出现的参数可以看作是一个占位符,代码如下所示:

int sum(int n) {
    ...
}

这里的 n 变量被称为形式参数,简称形参。形参也是局部变量,属于该函数私有,每次调用函数,都会给这些变量赋值。

当调用函数时,回传进去实际的值,被函数内部使用,称为实际参数,简称实参,如下所示。

sum(10);

这里的实际参数就是 10 传给了函数 sum 的形参 n

从这可以看出,形式参数是被调函数中的变量,实际参数是主调函数赋给被调函数的具体值。实参可以是常量、变量,或表达式。无论实参是何种形式都要被求值,然后该值被拷贝给被调函数相应的形参。

C语句中的传参规则如下:

  1. 传递给函数的标量参数是传值调用的。实际上函数会获得参数值的一份拷贝。
  2. 传递给函数的数组参数或指针参数是传址调用的。数组名的值实际上是一个指针,传递给函数的是该指针的一份拷贝。

如下所示,实现一个可以交换所传递的两个值的函数。

/*
 * 交换调用程序中的两个整数(没有效果)
 */

void swap(int x, int y) {
    int temp;
    temp = x;
    x = y;
    y = temp;
}

因为C语言实际交换的是参数的拷贝,原先的参数值并未交换,所以调用该函数实现不了交换的效果。

int a = 10, b = 5;
swap(a, b);
// a = 10, b = 5

可以将函数传递的参数值改为指针,来实现两个值的交换。

/*
 * 交换调用程序中的两个整数.
 */

void swap(int *x, int *y) {
    int temp;
    temp = *x;
    *x = *y;
    *y = temp;
}

因为函数接受的参数是指针,所以调用它就可以实现两个值的交换。

int a = 10, b = 5;
swap(&a, &b);
// a = 5, b = 10

递归

递归函数指函数体内直接或间接调用自身的函数。如斐波那契数列中的每个数的值是它前面两个数的和,实现斐波那契数列的代码如下所示。

/*
 * 用递归方法计算第 n 个斐波那契数的值。
 */

long fibonacci(int n) {
    if (n <= 2)
        return 1;
    return fibonacci(n-1) + fibonacci(n-2);
}

但斐波那契数列使用递归实现中,每个递归调用都触发另两个递归调用,这样,冗余计算的数量增长会非常快,造成的额外开销也会非常恐怖!

斐波那契数列也可以使用迭代方式实现,如下所示。

/*
 * 用迭代方法计算第 n 个斐波那契数的值。
 */

long fibonacci(int n) {
    long result;
    long previous_result;
    long next_older_result;
    
    result = previous_result =1;
    
    while ( n > 2) {
        n -= 1;
        next_older_result = previous_result;
        previous_result = result;
        result = previous_result + next_older_result;
    }
    return result;
}

有时候使用递归解决问题会造成额外的开销,如果迭代也能解决问题并且比递归的效率要高,哪怕代码可读性稍差一些也可以使用迭代方式。

可变参数列表

如果想让 average 函数实现多个参数的平均值,除了将值存储于数组中,也可以设置多个形参,如下所示。

/*
 * 计算指定数目的值的平均值(差的方案)
 */

double average(int n_values, int v1, int v2, int v3, int v4, int v5) {
    float sum = v1;
    if (n_values >= 2) {
        sum += v2;
    }
    if (n_values >= 3) {
        sum += v3;
    }
    if (n_values >= 4) {
        sum += v4;
    }
    if (n_values >= 5) {
        sum += v5;
    }
    return sum / n_values;
}

当调用上述函数时,就可以获得平均值。

average(5261232);

但当传入的实参个数与函数的形参个数不匹配时,如 average(3, 10, 4, 7),会报  error: too few arguments to function 'average' 错误。因此需要一种机制,能够以一种良好定义的访问数量未定的参数列表。

stdarg

C 语言提供了 stdarg.h 头文件提供的宏来实现可变参数列表。该头文件声明了一个类型 va_list 和三个宏 va_startva_argva_end

  • va_list:类型,该类型的变量用于存储可变参数的信息。
  • va_start:宏定义,开始获取可变参数列表中的第一个参数,接收两个变量,第一个参数为 va_list 变量,第二个参数为省略号前最后一个有名字的参数。
  • va_arg:宏定义,循环获取到可变参数列表中的参数,第一个参数指向下一个参数地址,第二个参数为下一个参数的类型。
  • va_end:宏定义,表示可变参数列表的结束。

可以声明一个类型为 va_list 的变量,在与这几个宏配合使用,访问参数的值。

/*
 * 计算指定数量的值的平均值。
 */

double average(int n_values, ...) {
    va_list var_arg;
    int count;
    float sum = 0;
    /*
     * 准备访问可变参数。
     */

    va_start(var_arg, n_values);
    /*
     * 添加取自可变参数列表的值。
     */

    for (count = 0; count < n_values; count += 1) {
        sum += va_arg(var_arg, int);
    }
    /*
     * 完成处理可变参数
     */

    va_end(var_arg);
    return sum / n_values;
}

可变参数的限制

可变参数必须从头到尾按照顺序逐个访问。可以访问前几个可变参数后半途而废,但不可以直接访问参数列表中间的参数。由于参数列表中的可变参数部分并没有原型,所以,所有作为可变参数传递给函数的值都将执行缺省参数类型提升。

对于这些宏,存在两个基本的限制。一个值的类型无法简单地通过检查它的位模式来判断,这两个限制就是这个事实的直接结果。

  1. 这些宏无法判断实际存在的参数的数量。
  2. 这些宏无法判断每个参数的类型。

要回答这两个问题,就必须使用命名参数。在上述程序中,命名参数指定了实际传递的参数数量,不过它们的类型被假定为整型。printf 函数中的命名参数是格式字符串,它不仅指定了参数的数量,而且指定了参数的类型。

如果 va_arg 指定了错误的类型,那么结果不可预测,因为 va_arg 无法正确识别作用于可变参数之上的缺省参数类型提升。



一文讲解C语言函数


原文始发于微信公众号(海人为记):一文讲解C语言函数

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/27491.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之家——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!