C8 函数

1. 函数

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

函数的功能

  • 执行某些动作。比如printf()把数据打印在屏幕上
  • 返回值供其他程序使用。比如strlen()返回字符长度

为什么使用函数

  • 避免编写重复代码
  • 让程序更加模块化,提高代码可读性。

1.1 创建函数

#include <stdio.h>

void starbar(void);

int main() {
    starbar();
    printf("GIGATHINK, INC\n101 Megabuck Plaza\nMegapolis, CA 94904\n");
    starbar();
    return 0;
}

void starbar(void) {
    int count;
    for (count = 0; count < 40; count++) {
        putchar('*');
    }
    putchar('\n');
}
  • void starbar(void);函数原型function prototype)告诉编译器函数的类型。
  • starbar();函数调用function call)表明在此处执行函数。
  • void starbar(void) {...}函数定义function definition)指定函数要做什么。
  • 函数原型指明函数的返回值类型和函数接收的参数类型,这些信息被称为该函数的签名signature)。对于void starbar(void);,其签名是该函数不返回值也不接收参数。
  • 函数原型可以写在其他地方,但必须在函数调用或函数定义之前。
  • starbar()函数中的变量count是局部变量,该变量只属于starbar()函数。可以在其他地方使用count作为变量名。

1.2 函数参数

#include <stdio.h>
#include <string.h>

void show_n_char(int, char);

void show_n_char(int n, char ch) {
    for (int i = 0; i < n; i++) {
        putchar(ch);
    }
}

int main() {
    show_n_char(40, '*');
    putchar('\n');
    show_n_char((40 - strlen("GIGATHINK, INC")) / 2, ' ');
    printf("GIGATHINK, INC\n");
    show_n_char((40 - strlen("G101 Megabuck Plaza")) / 2, ' ');
    printf("101 Megabuck Plaza\n");
    show_n_char((40 - strlen("Megapolis, CA 94904")) / 2, ' ');
    printf("Megapolis, CA 94904\n");
    show_n_char(40, '*');
    return 0;
}

1.2.1 定义带形式参数的函数

函数头:void show_n_char(int n, char ch),该行告诉编译器show_n_char()使用两个参数int类型的nchar类型的ch,这两个参数是形式参数,简称形参,形参属于局部变量,这里的nchshow_n_char()函数私有。

⚠️注意:每个形参都必须声明类型,因此不能像声明变量一样使用同一类型的变量列表。

void func(int x, y, z)         // 无效的函数头
void func(int x, int y, int z) // 有效的函数头

1.2.2 声明带形式参数函数的原型

当函数接受参数时,函数原型用逗号分隔的列表指明参数的数量和类型。

void show_n_char(int, char);

⚠️注意

  • 函数原型中参数名为可选。
  • 函数原型中并没有实际创建变量,其中的变量类型仅代表一个对应类型的变量而已。
  • void func();的写法是ANSI C之前的,void func(void);才是标准的,应尽可能地使用标准的写法。

1.2.3 调用带实际参数的函数

在函数调用中,实际参数actual argument),简称实参,实际参数提供了参数的值。

形式参数是被调函数called function)中的变量,实际参数是主调函数call function)赋给被调函数的具体值。

实际参数可以常量、变量或表达式。

1.3 从函数中返回值:return

函数的返回值可以把信息从被调函数传回主调函数。

// 示例函数:返回两个值中的较小者

#include <stdio.h>

int imin(int, int);

int main() {
    printf("%d\n", imin(3, 4));
    printf("%d\n", imin(10, 4));
    return 0;
}

int imin(int a, int b) {
    return a < b ? a : b;
}

关键字return后面的表达式就是函数的返回值,返回值的类型应与函数原型中的类型相同,不相同时会进行自动类型转换,如果转换失败,函数就无法成功运行。

1.4 函数类型

声明函数时必须声明函数类型,带返回值的函数类型,应与返回值类型相同,没有返回值的函数应声明为void类型。

旧版本的C编译器会假定函数的类型是int,而C99标准不再支持int类型函数这种假定设置。

2. ANSI C函数原型

在ANSI C标准之前,声明函数只需要声明函数的类型,不用声明参数。

#include <stdio.h>

int imax();

int main() {
    printf("The maxinum of %d and %d is %d.\n", 3, 5, imax(3));
    printf("The maxinum of %d and %d is %d.\n", 3, 5, imax(3.0f, 5.0f));
    return 0;
}

int imax()
int n, m;
{
    return n > m ? n : m;
}

对于PC和VAX来说,主调函数会把它的参数存在被称为“stack)”的临时存储区。

在第一次调用imax()函数时,只传递了一个参数,也就是栈中只放了一个数据,那么函数会把恰好放在第一个参数旁的值作为第二个参数。

在第二次调用时,传入了两个浮点值,也就是栈中存放了128位的数据,但是形参定义的是两个int类型总长度只有64位,因此程序实际上是将第一个实参拆开,第二个实参根本没有读取到。

2.1 ANSI的解决方案

ANSI C的标准要求是使用函数原型,也就是在函数声明时还要声明变量的类型。

使用函数原型后,当实参数量与形参数量不对时,编译器会给出警告,当类型不同时,会发生自动类型转换。

2.2 无参数和未指定参数

支持ANSI C编译器会假定用户没有用函数原型来声明函数,它将不会检查参数。为了表明函数确实没有参数,应在圆括号中使用void关键字。

当函数的参数没有固定数量和类型时,可以使用...表示。

int func(int a, char ch, ...) {
    ……
}

⚠️注意:没有固定类型但不固定数量的表示。

3. 递归

递归recursion) 是指函数自己调用自己的过程。自己调用自己的函数叫作递归函数。

递归函数必须包含递归出口,也就是结束递归的条件。

3.1 尾递归

尾递归tail recursion)是指把递归调用置于函数末尾,即return之前,这种递归最简单,相当于循环。

3.2 递归和倒序计算

递归在处理倒序时非常方便。以下是一个将十进制数转换为二进制并打印的函数。

void func(int num) {
    if (num < 2) {
        printf("%d", num);
    }
    else {
        func(num / 2);
        printf("%d", num % 2);
    }
}

⚠️注意

  • 递归过程会占用大量内存空间
  • 递归函数不方便阅读和维护

4. 多文件编译

以上文的转换函数举例。

//main.c
#include <stdio.h>
#include "fun.h"

int main() {
    func(63);
    return 0;
}
//fun.h
void func(int);
//fun.c
#include <stdio.h>
#include "fun.h"

void func(int num) {
    if (num < 2) {
        printf("%d", num);
    }
    else {
        func(num / 2);
        printf("%d", num % 2);
    }
}

将用到的常量和函数声明写在头文件(.h)中,将函数实现写在源文件(.c)中。

⚠️注意:主程序文件和函数实现文件都需要包含头文件(例子中是fun.h)。

5. 查找地址:&运算符

指针pointer)是C语言最重要的概念之一,用于储存变量的地址。一元运算符&给出变量的储存地址。

#include <stdio.h>

void func(int, int);

void func(int num1, int num2) {
    printf("num1: %p num2: %p\n", &num1, &num2);
}

int main() {
    int num1, num2;
    printf("num1: %p num2: %p\n", &num1, &num2);
    func(num1, num2);
    return 0;
}
// 输出结果;
// num1: 000000315A4FF594 num2: 000000315A4FF5B4
// num1: 000000315A4FF570 num2: 000000315A4FF578

同名变量其地址不同,这代表在计算机看了,这是4个独立的变量。

5.1 更改主调函数中的变量

如果我们希望在不使用返回值的情况下,被调函数可以交换主调函数中两个变量的值,该如何做?

#include <stdio.h>

void func(int*, int*);

void func(int* num1, int* num2) {
    int temp = *num1;
    *num1 = *num2;
    *num2 = temp;
    return;
}

int main() {
    int num1 = 10;
    int num2 = 20;
    printf("num1: %d num2: %d\n", num1, num2);
    func(&num1, &num2);
    printf("num1: %d num2: %d\n", num1, num2);
    return 0;
}
// 输出结果:
// num1: 10 num2: 20
// num1: 20 num2: 10

*叫作间接运算符indirection operator),也叫解引用运算符dereferencing operator),它可以找出储存在变量地址中的值。

5.2 声明指针

声明指针类型的变量需要在相应类型后加*号。

int a;  // 错误,这是int类型的变量
int * b; // 正确,这是一个指针变量,它指向的变量类型是int

⚠️注意

  • 声明指针变量时*左右两边的空格可有可无。通常在声明时添加空格,解引用时省略空格。
  • 指针的值是一个地址,由无符号整数表示,但不可当做整数来处理。一些整数操作不能处理指针,反之亦然。例如不能将两个指针相乘。