函数

Tutorial: 教程一 Category: C语言 Published: 2026-04-07 13:58:26 Views: 20 Likes: 0 Comments: 0
8. 函数

函数是 C 语言中用于组织和封装代码的基本单位。通过函数,可以将复杂的问题分解为更小、更易管理的部分,提高代码的可重用性、可读性和可维护性。理解函数的定义、调用、参数传递、返回值以及作用域等概念,是编写高效和结构化 C 程序的关键。

8.1 函数的定义与调用

函数是完成特定任务的一段代码块。C 语言允许程序员自定义函数,以便在多个地方重复使用相同的代码逻辑。

函数的定义

语法

返回类型 函数名(参数列表) {
    // 函数体
    // 可选的返回语句
}
  • 返回类型:函数执行完毕后返回的数据类型,如intfloatvoid等。
  • 函数名:函数的名称,用于在程序中调用该函数。
  • 参数列表:函数接受的输入参数,可以是零个或多个,每个参数由类型和名称组成,用逗号分隔。
  • 函数体:包含函数执行的具体代码。

示例

#include <stdio.h>

// 函数定义:打印欢迎信息
void greet() {
    printf("欢迎使用C语言程序!\n");
}

int main() {
    // 函数调用
    greet(); // 输出: 欢迎使用C语言程序!
    return 0;
}

输出

欢迎使用C语言程序!

详细解释

  • void greet(): 定义了一个名为greet的函数,返回类型为void,表示该函数不返回任何值。
  • main函数中,通过greet();调用了greet函数,执行其内部的printf语句。
函数的调用

函数调用是指在程序的某个位置执行已定义的函数。通过函数名和参数(如果有)来调用函数。

示例

#include <stdio.h>

// 函数定义:计算两个整数的和
int add(int a, int b) {
    return a + b;
}

int main() {
    int num1 = 5;
    int num2 = 10;
    int sum;

    // 调用add函数,传递num1和num2作为参数
    sum = add(num1, num2);
    printf("num1 + num2 = %d\n", sum); // 输出: num1 + num2 = 15

    return 0;
}

输出

num1 + num2 = 15

详细解释

  • int add(int a, int b): 定义了一个名为add的函数,接受两个int类型的参数,返回它们的和。
  • main函数中,通过add(num1, num2)调用add函数,将num1num2的值传递给add函数,并将返回值赋给sum变量。
更多示例
  1. 无参数且有返回值的函数

    #include <stdio.h>
    
    // 函数定义:返回固定值
    int getNumber() {
        return 42;
    }
    
    int main() {
        int number = getNumber();
        printf("获得的数字是: %d\n", number); // 输出: 获得的数字是: 42
        return 0;
    }
    

    输出

    获得的数字是: 42
    
  2. 带参数且无返回值的函数

    #include <stdio.h>
    
    // 函数定义:打印两数之和
    void printSum(int a, int b) {
        int sum = a + b;
        printf("a + b = %d\n", sum);
    }
    
    int main() {
        int x = 7;
        int y = 3;
        printSum(x, y); // 输出: a + b = 10
        return 0;
    }
    

    输出

    a + b = 10
    
  3. 函数嵌套调用

    #include <stdio.h>
    
    // 函数定义:计算平方
    int square(int num) {
        return num * num;
    }
    
    // 函数定义:计算立方
    int cube(int num) {
        return num * square(num); // 调用square函数
    }
    
    int main() {
        int number = 4;
        int result = cube(number);
        printf("number的立方是: %d\n", result); // 输出: number的立方是: 64
        return 0;
    }
    

    输出

    number的立方是: 64
    
注意事项
  • 函数名唯一性:在同一作用域内,函数名必须唯一,不能与其他变量或函数重名。
  • 返回类型一致性:函数的返回类型必须与函数体内的return语句返回的类型一致。
  • 参数类型匹配:函数调用时传递的参数类型应与函数定义中的参数类型匹配,否则可能导致隐式类型转换或错误。
8.2 函数参数与返回值

函数参数和返回值是函数与外界交互的主要方式。通过参数传递数据给函数,通过返回值将结果传递回调用者。

函数参数

函数参数是函数接受的输入数据,用于在函数内部执行特定操作。参数可以是基本数据类型、数组、指针等。

传值与传址

  • 传值(Call by Value):将参数的值复制一份传递给函数,函数内部对参数的修改不会影响外部变量。
  • 传址(Call by Reference):传递参数的地址(指针),函数内部通过指针可以修改外部变量的值。

示例

  1. 传值示例

    #include <stdio.h>
    
    // 函数定义:交换两个数的值(传值)
    void swapByValue(int a, int b) {
        int temp = a;
        a = b;
        b = temp;
        printf("函数内部: a = %d, b = %d\n", a, b); // 输出交换后的值
    }
    
    int main() {
        int x = 5;
        int y = 10;
    
        printf("调用前: x = %d, y = %d\n", x, y); // 输出: x = 5, y = 10
        swapByValue(x, y);
        printf("调用后: x = %d, y = %d\n", x, y); // 输出: x = 5, y = 10
    
        return 0;
    }
    

    输出

    调用前: x = 5, y = 10
    函数内部: a = 10, b = 5
    调用后: x = 5, y = 10
    

    解释

    • swapByValue函数中,abxy的副本,函数内部的交换不影响main函数中的xy
  2. 传址示例

    #include <stdio.h>
    
    // 函数定义:交换两个数的值(传址)
    void swapByReference(int *a, int *b) {
        int temp = *a;
        *a = *b;
        *b = temp;
        printf("函数内部: a = %d, b = %d\n", *a, *b); // 输出交换后的值
    }
    
    int main() {
        int x = 5;
        int y = 10;
    
        printf("调用前: x = %d, y = %d\n", x, y); // 输出: x = 5, y = 10
        swapByReference(&x, &y);
        printf("调用后: x = %d, y = %d\n", x, y); // 输出: x = 10, y = 5
    
        return 0;
    }
    

    输出

    调用前: x = 5, y = 10
    函数内部: a = 10, b = 5
    调用后: x = 10, y = 5
    

    解释

    • swapByReference函数中,ab是指向xy的指针,通过指针修改了xy的实际值。
函数返回值

函数可以通过返回值将结果传递回调用者。返回值的类型由函数定义中的返回类型决定。

示例

  1. 返回单一值

    #include <stdio.h>
    
    // 函数定义:计算两个数的和
    int add(int a, int b) {
        return a + b;
    }
    
    int main() {
        int num1 = 7;
        int num2 = 3;
        int sum;
    
        // 调用add函数并接收返回值
        sum = add(num1, num2);
        printf("sum = %d\n", sum); // 输出: sum = 10
    
        return 0;
    }
    

    输出

    sum = 10
    
  2. 返回多个值(通过指针)

    #include <stdio.h>
    
    // 函数定义:计算两个数的和与差
    void calculate(int a, int b, int *sum, int *diff) {
        *sum = a + b;
        *diff = a - b;
    }
    
    int main() {
        int x = 15;
        int y = 5;
        int sum, difference;
    
        // 调用calculate函数
        calculate(x, y, &sum, &difference);
        printf("sum = %d, difference = %d\n", sum, difference); // 输出: sum = 20, difference = 10
    
        return 0;
    }
    

    输出

    sum = 20, difference = 10
    

    解释

    • 通过传递指针,calculate函数可以同时返回多个值(sumdiff)。
更多示例
  1. 函数不返回值

    #include <stdio.h>
    
    // 函数定义:打印学生信息
    void printStudentInfo(char name[], int age) {
        printf("学生姓名: %s\n", name);
        printf("学生年龄: %d\n", age);
    }
    
    int main() {
        char studentName[] = "张三";
        int studentAge = 20;
    
        // 调用printStudentInfo函数
        printStudentInfo(studentName, studentAge);
    
        return 0;
    }
    

    输出

    学生姓名: 张三
    学生年龄: 20
    
  2. 函数返回指针

    #include <stdio.h>
    #include <string.h>
    
    // 函数定义:返回两个字符串中较长的那个
    char* getLongerString(char *str1, char *str2) {
        if (strlen(str1) > strlen(str2)) {
            return str1;
        } else {
            return str2;
        }
    }
    
    int main() {
        char string1[] = "Hello";
        char string2[] = "Hello, World!";
        char *longer;
    
        // 调用getLongerString函数
        longer = getLongerString(string1, string2);
        printf("较长的字符串是: %s\n", longer); // 输出: 较长的字符串是: Hello, World!
    
        return 0;
    }
    

    输出

    较长的字符串是: Hello, World!
    
注意事项
  • 函数声明:在函数被调用之前,必须有函数的声明(函数原型),或者将函数定义放在调用之前。否则,编译器无法识别函数,可能导致错误。
  • 返回类型一致性:函数返回的值必须与函数定义中的返回类型一致,否则会产生类型不匹配的错误。
  • 指针安全:在使用指针作为函数参数或返回值时,确保指针指向有效的内存,避免出现悬挂指针或野指针。
8.3 局部变量与全局变量

变量的作用域决定了变量在程序中的可见范围。C 语言中的变量分为局部变量全局变量两种类型。

局部变量

定义:局部变量是在函数或代码块内部定义的变量,其作用域仅限于定义它的函数或代码块内。

特点

  • 作用域有限:只能在定义它的函数或代码块内部访问。
  • 生命周期短:当函数或代码块执行结束时,局部变量被销毁。
  • 命名冲突少:不同函数可以使用相同的局部变量名,不会互相影响。

示例

#include <stdio.h>

void display() {
    int count = 10; // 局部变量
    printf("count = %d\n", count);
}

int main() {
    display(); // 输出: count = 10

    // printf("count = %d\n", count); // 错误:count在main中不可见
    return 0;
}

输出

count = 10
全局变量

定义:全局变量是在所有函数外部定义的变量,其作用域覆盖整个文件,从定义的位置到文件结束。

特点

  • 作用域广:在定义它的文件中的所有函数都可以访问。
  • 生命周期长:程序运行期间全局变量一直存在。
  • 易引发命名冲突:不同文件中的全局变量如果同名,会导致命名冲突,需谨慎管理。

示例

#include <stdio.h>

int globalVar = 100; // 全局变量

void display() {
    printf("globalVar = %d\n", globalVar); // 访问全局变量
}

int main() {
    printf("globalVar 在main中 = %d\n", globalVar); // 输出: 100
    display(); // 输出: 100

    globalVar = 200; // 修改全局变量
    printf("修改后的 globalVar 在main中 = %d\n", globalVar); // 输出: 200
    display(); // 输出: 200

    return 0;
}

输出

globalVar 在main中 = 100
globalVar = 100
修改后的 globalVar 在main中 = 200
globalVar = 200
局部变量与全局变量的区别
特性局部变量全局变量
定义位置函数内部或代码块内所有函数外部
作用域仅限于定义它的函数或代码块内整个文件内的所有函数
生命周期从定义到函数或代码块结束从程序开始到程序结束
初始化如果未初始化,默认值不确定如果未初始化,默认值为 0(静态存储)
命名冲突低(不同函数可用相同名称)高(同名全局变量会冲突)

示例比较

#include <stdio.h>

int global = 50; // 全局变量

void func1() {
    int local = 10; // 局部变量
    printf("func1 - local = %d, global = %d\n", local, global);
}

void func2() {
    // printf("func2 - local = %d\n", local); // 错误:local在func2中不可见
    printf("func2 - global = %d\n", global);
}

int main() {
    int local = 20; // main函数的局部变量
    printf("main - local = %d, global = %d\n", local, global);
    func1();
    func2();
    return 0;
}

输出

main - local = 20, global = 50
func1 - local = 10, global = 50
func2 - global = 50
注意事项
  • 命名规范:为了避免命名冲突和提高代码可读性,建议遵循命名规范。例如,全局变量可以使用前缀g_,如g_counter
  • 变量的生命周期:理解变量的生命周期有助于合理地管理内存和资源,避免出现内存泄漏或悬挂指针。
  • 避免滥用全局变量:过多使用全局变量可能导致程序难以维护和调试,建议仅在必要时使用全局变量。
8.4 递归函数

递归函数是指在函数内部调用自身的函数。递归是一种强大的编程技术,适用于解决可以分解为相似子问题的问题,如阶乘计算、斐波那契数列、树的遍历等。

递归的基本概念
  • 基准情形(Base Case):递归的终止条件,当满足基准情形时,不再进行递归调用。
  • 递归情形(Recursive Case):函数通过调用自身来解决问题的一部分,逐步接近基准情形。
递归函数的定义与示例

示例 1:计算阶乘

阶乘是递归函数的经典示例。阶乘定义为:n! = n * (n-1)!,其中0! = 1

#include <stdio.h>

// 函数定义:递归计算阶乘
long factorial(int n) {
    if (n == 0) { // 基准情形
        return 1;
    } else { // 递归情形
        return n * factorial(n - 1);
    }
}

int main() {
    int number = 5;
    long fact = factorial(number);
    printf("%d 的阶乘是: %ld\n", number, fact); // 输出: 5 的阶乘是: 120
    return 0;
}

输出

5 的阶乘是: 120

详细解释

  • n等于0时,函数返回1,这是递归的终止条件。
  • 否则,函数返回n * factorial(n - 1),逐步递减n,直到达到基准情形。

示例 2:斐波那契数列

斐波那契数列的定义为:F(n) = F(n-1) + F(n-2),其中F(0) = 0F(1) = 1

#include <stdio.h>

// 函数定义:递归计算斐波那契数
int fibonacci(int n) {
    if (n == 0) { // 基准情形1
        return 0;
    } else if (n == 1) { // 基准情形2
        return 1;
    } else { // 递归情形
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

int main() {
    int term = 10;
    printf("斐波那契数列第 %d 项是: %d\n", term, fibonacci(term)); // 输出: 斐波那契数列第 10 项是: 55
    return 0;
}

输出

斐波那契数列第 10 项是: 55

详细解释

  • n等于01时,函数返回01,作为基准情形。
  • 否则,函数递归调用自身,计算前两项的和。
递归的优缺点

优点

  • 简洁:递归可以用简短的代码解决复杂的问题,代码易于理解。
  • 适用性强:适用于分治法、树的遍历、图的搜索等问题。

缺点

  • 效率低:递归调用会增加函数调用栈的开销,可能导致性能下降。
  • 栈溢出:过深的递归调用可能导致栈溢出,程序崩溃。
  • 重复计算:某些递归算法会进行大量重复计算,影响效率(如斐波那契数列)。
优化递归
  1. 使用尾递归:将递归调用放在函数的最后一步,某些编译器可以优化尾递归,减少栈空间使用。
  2. 记忆化:存储已经计算过的结果,避免重复计算。
  3. 转换为迭代:将递归逻辑转换为迭代逻辑,通常更高效。

示例:使用记忆化优化斐波那契数列

#include <stdio.h>

// 定义一个数组用于存储已计算的斐波那契数
long memo[100] = {0};

// 函数定义:递归计算斐波那契数(带记忆化)
long fibonacciMemo(int n) {
    if (n == 0) {
        return 0;
    } else if (n == 1) {
        return 1;
    }

    // 如果已经计算过,直接返回结果
    if (memo[n] != 0) {
        return memo[n];
    }

    // 计算并存储结果
    memo[n] = fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
    return memo[n];
}

int main() {
    int term = 50;
    printf("斐波那契数列第 %d 项是: %ld\n", term, fibonacciMemo(term)); // 输出: 斐波那契数列第 50 项是: 12586269025
    return 0;
}

输出

斐波那契数列第 50 项是: 12586269025

解释

  • 通过memo数组存储已计算的斐波那契数,避免重复计算,大幅提升效率。
注意事项
  • 确保基准情形:递归函数必须有明确的基准情形,避免无限递归。
  • 控制递归深度:避免过深的递归调用,防止栈溢出。
  • 理解递归流程:递归函数的执行流程较为复杂,需仔细分析每一步的调用和返回。
8.5 函数的声明与分离编译

在大型项目中,函数的声明与定义通常需要分离,以提高代码的组织性和可维护性。C 语言通过函数声明(函数原型)和分离编译实现这一目标。

函数的声明(函数原型)

函数声明告诉编译器函数的名称、返回类型和参数类型,但不包含函数的具体实现。函数声明通常放在头文件中,供多个源文件引用。

语法

返回类型 函数名(参数类型1, 参数类型2, ...);

示例

// function.h
#ifndef FUNCTION_H
#define FUNCTION_H

// 函数声明
int add(int a, int b);
void greet();

#endif
函数的定义

函数定义包含函数的具体实现,通常放在源文件(.c文件)中。

示例

// function.c
#include <stdio.h>
#include "function.h"

// 函数定义:计算两个数的和
int add(int a, int b) {
    return a + b;
}

// 函数定义:打印欢迎信息
void greet() {
    printf("欢迎使用C语言函数示例!\n");
}
分离编译

分离编译是将代码分为多个源文件和头文件,分别编译,然后链接生成最终的可执行文件。这样可以提高代码的组织性,方便多人协作和代码重用。

示例项目结构

project/
│
├── main.c
├── function.c
├── function.h
└── Makefile

main.c

#include <stdio.h>
#include "function.h"

int main() {
    int x = 10;
    int y = 20;
    int sum;

    // 调用greet函数
    greet(); // 输出: 欢迎使用C语言函数示例!

    // 调用add函数
    sum = add(x, y);
    printf("sum = %d\n", sum); // 输出: sum = 30

    return 0;
}

function.h

#ifndef FUNCTION_H
#define FUNCTION_H

// 函数声明
int add(int a, int b);
void greet();

#endif

function.c

#include <stdio.h>
#include "function.h"

// 函数定义:计算两个数的和
int add(int a, int b) {
    return a + b;
}

// 函数定义:打印欢迎信息
void greet() {
    printf("欢迎使用C语言函数示例!\n");
}

Makefile(用于自动化编译):

# Makefile

CC = gcc
CFLAGS = -Wall -g

# 目标可执行文件
TARGET = main

# 源文件
SRCS = main.c function.c

# 头文件
HEADERS = function.h

# 生成可执行文件
$(TARGET): $(SRCS) $(HEADERS)
	$(CC) $(CFLAGS) -o $(TARGET) $(SRCS)

# 清理编译生成的文件
clean:
	rm -f $(TARGET)

编译与运行

在项目目录下,使用以下命令编译项目:

make

然后运行生成的可执行文件:

./main

输出

欢迎使用C语言函数示例!
sum = 30
优点
  • 代码组织性强:将函数声明和定义分离,代码更易于管理和维护。
  • 重用性高:头文件可以被多个源文件引用,方便代码重用。
  • 编译效率高:修改一个源文件无需重新编译所有文件,只需重新编译受影响的文件。
注意事项
  • 头文件保护:使用预处理指令(如#ifndef#define#endif)防止头文件被多次包含,导致重复定义错误。
  • 一致性:确保函数声明和定义中的参数类型、返回类型一致,否则可能导致编译错误或未定义行为。
  • 依赖管理:在使用多个源文件时,确保正确管理文件之间的依赖关系,避免链接错误。
8.6 总结

函数是 C 语言中组织和封装代码的基本单位,通过函数的定义和调用,可以实现代码的模块化和重用。掌握函数的参数传递、返回值、作用域、递归以及函数声明与分离编译等概念,对于编写高效、可维护的 C 程序至关重要。

  • 函数的定义与调用:理解如何定义函数,如何在程序中调用函数,确保函数的参数和返回值类型匹配。
  • 函数参数与返回值:掌握传值和传址的区别,了解如何通过函数返回单一值或多个值。
  • 局部变量与全局变量:了解变量的作用域和生命周期,合理使用局部变量和全局变量以提高代码的可读性和安全性。
  • 递归函数:理解递归的基本概念,掌握递归函数的定义和优化方法,避免递归调用导致的性能问题。
  • 函数的声明与分离编译:学会将函数声明放在头文件中,实现分离编译,提高代码的组织性和可维护性。