函数

对应教学日历:讲次 17-19(5月21日-5月28日)


一、函数的基本概念

函数:具有独立功能的程序模块。C 程序由一个 main 函数和若干其他函数构成。

函数定义语法

返回类型 函数名(形式参数列表)
{
    函数体;
    return 返回值;
}
// 示例:计算三角形面积
double tarea(float a, float b, float c)
{
    double s = (a + b + c) / 2;
    return sqrt(s * (s-a) * (s-b) * (s-c));
}

函数声明(原型声明)

返回类型 函数名(参数类型列表);
 
double tarea(float a, float b, float c);   // 声明
// 或
double tarea(float, float, float);          // 可省略参数名

必须在调用前声明:函数定义在调用之后时,需要先声明。写在 main 之前或头文件中。

函数调用

函数名(实际参数列表);
 
result = tarea(3.0, 4.0, 5.0);    // 带返回值的调用
concatstr(a, b, c);               // void 函数的调用

二、参数传递

传值调用(Call by Value)

默认方式。将实参的复制给形参,函数内修改形参不影响实参。

void swap(int x, int y)
{
    int t = x; x = y; y = t;   // 只交换了形参,实参不变!
}
int main()
{
    int a=3, b=5;
    swap(a, b);               // a 和 b 的值不变!仍是 3 和 5
}

传地址调用(Call by Address)

将实参的地址传给形参(指针),函数内通过指针直接修改实参的值。

void swap(int *x, int *y)
{
    int t = *x; *x = *y; *y = t;   // 真正交换了实参!
}
int main()
{
    int a=3, b=5;
    swap(&a, &b);                  // a=5, b=3
}

数组名作参数

数组名本质是地址,传递数组名就是传递地址,函数内可修改数组元素。

void sort(int b[], int n)    // int b[] 等价于 int *b
{
    // 对数组 b 的修改会反映到实参
}
int main()
{
    int a[10];
    sort(a, 10);   // a 是首地址,不加 &
    sort(&a[0], 10);  // 等价
}

重要:数组名作为参数时,int b[]int *b 完全等价

三、函数的嵌套与递归

嵌套调用

一个函数调用另一个函数(C 语言不能嵌套定义函数,但可以嵌套调用)。

int max(int a, int b) { return a > b ? a : b; }
int max3(int a, int b, int c) { return max(max(a, b), c); }

递归调用

函数直接或间接地调用自身

递归三要素

  1. 递归出口(终止条件)——必须有!
  2. 递归表达式(将问题分解为更小规模)
  3. 每次递归向出口逼近
// 求阶乘 n!
int fact(int n)
{
    if (n == 0 || n == 1) return 1;   // 递归出口
    return n * fact(n - 1);            // 递归表达式
}
 
// 求年龄:第1个人10岁,后面每个人比前一个大2岁
int age(int n)
{
    if (n == 1) return 10;             // 递归出口
    return age(n-1) + 2;               // 递归表达式
}

易错:忘写递归出口 → 无限递归 → 栈溢出!

四、变量作用域与存储类别

局部变量

  • 在函数或复合语句 {} 内部定义
  • 只在本函数/语句块内有效
  • 函数调用时分配空间,返回时释放
  • 不同函数中的同名变量互不影响

全局变量

  • 在所有函数外部定义
  • 从定义处开始到文件末尾有效
  • 可以被多个函数共享
#include<stdio.h>
int sum = 10;        // 全局变量
 
int age(int n)
{
    if (n == 1) return sum;  // 使用全局变量
    return age(n-1) + 2;
}

全局变量与局部变量同名时,局部变量优先(屏蔽全局变量)。

存储类别

关键字生命周期作用域初始化
auto(默认)函数执行期间函数内每次调用时重新分配,不自动初始化
static 局部程序整个运行期函数内只在第一次调用时初始化,保留上次值
static 全局程序整个运行期本文件内编译时初始化
extern程序整个运行期跨文件声明外部变量
void count() {
    static int n = 0;   // 静态局部变量
    n++;
    printf("%d\n", n);   // 每次调用 n 递增
}
// 第一次调用 → 1, 第二次调用 → 2, ...

五、模块化程序设计

  • 将程序按功能拆分为多个函数(模块)
  • 每个函数职责单一
  • 函数之间通过参数和返回值通信
  • 头文件(.h)存放函数声明和宏定义
  • 源文件(.c)存放函数实现

作业示例

ex12-1:函数定义与调用——字符串连接

#include<stdio.h>
void concatstr(char st1[],char st2[],char st3[]);
void main()
{
    char a[100],b[100],c[100];
    printf("input a:");
    scanf("%s",a);
    printf("input b:");
    scanf("%s",b);
    concatstr(a,b,c);
    printf("c=%s\n",c);
}
void concatstr(char st1[],char st2[],char st3[])
{
    int i,j;
    for(i=0;st1[i]!='\0';i++)
        st3[i]=st1[i];
    for(j=0;st2[j]!='\0';j++)
        st3[i+j]=st2[j];
    st3[i+j]='\0';        // 字符串结束符
}

要点

  • 先声明后使用(void concatstr(...) 在 main 之前声明)
  • 数组参数传递的是地址,函数内可修改
  • 手动拼接后必须加 \0

ex12-2:有返回值的函数——三角形面积

#include<stdio.h>
#include<math.h>
double tarea(float a,float b,float c);
 
int main()
{
    float a,b,c;
    printf("input a b c:");
    scanf("%f%f%f",&a,&b,&c);
    if(a+b>c && b+c>a && a+c>b)
        printf("三角形面积为:%f\n",tarea(a,b,c));
    else
        printf("输入三边不构成三角形。\n");
    return 0;
}
 
double tarea(float a,float b,float c)
{
    double s=(a+b+c)/2,area;
    area=sqrt(s*(s-a)*(s-b)*(s-c));     // 海伦公式
    return area;
}

要点

  • 函数返回 double 类型
  • 调用前先判断三角形合法性(两边之和大于第三边)
  • 海伦公式:area = √(s(s-a)(s-b)(s-c))

ex12-3:bool 函数——孪生素数判断

#include <stdio.h>
#include <stdbool.h>
 
bool isPrime(int m) {
    if (m < 2) return false;
    for (int i = 2; i * i <= m; i++) {
        if (m % i == 0) return false;
    }
    return true;
}
 
int main() {
    printf("100-500之间的孪生素数有:\n");
    for (int i = 100; i <= 498; i++) {
        if (isPrime(i) && isPrime(i + 2)) {
            printf("(%d, %d)\n", i, i + 2);
        }
    }
    return 0;
}

要点

  • bool 类型(<stdbool.h>),返回 true/false
  • 孪生素数:差为 2 的一对素数
  • return false 提前退出函数

ex12-4:函数调用——打印菱形图案

#include<stdio.h>
void a(int i)
{
    int j,k;
    for(j=1;j<=30-i;j++) printf("%c",' ');
    for(k=1;k<=2*i-1;k++) printf("%c",'A'+i-1);
    printf("\n");
}
 
void main()
{
    int i,n=10;
    for(i=1;i<=n;i++)
        a(i);              // 打印上半部分
    for(i=n;i>=1;i--)
        a(i);              // 打印下半部分(倒序)
}

要点

  • 将打印一行的逻辑封装为函数 a(i)
  • 利用循环两次调用(正序+倒序)打印对称图案
  • 参数 i 控制行号,函数内根据 i 计算空格数和字符

ex12-5:函数嵌套——各位数字和判断

#include <stdio.h>
 
int fac(int x)
{
    int wan, qian, bai, shi, ge;  
    wan = x / 10000 % 10; 
    qian = x / 1000 % 10;   
    bai = x / 100 % 10;     
    shi = x / 10 % 10;      
    ge = x % 10;             
    
    if (wan + qian + bai + shi + ge == 5)
        return 1;
    else
        return 0;
}
 
int main()
{
    int i, n = 0;
    for (i = 100; i <= 10000; i++)
        if (fac(i))
        {
            printf("%6d", i);
            n++;
            if (n % 5 == 0) printf("\n");
        }
    return 0;
}

要点

  • 提取各位数字:x / 10000 % 10(万位)
  • 函数返回 1/0 作为条件判断
  • 数字分解技巧:n / pow(10, k) % 10

ex13-1:数组参数——冒泡排序函数

#include<stdio.h>
int a[10];
void sort(int b[])
{
    int n=sizeof(a)/sizeof(int);     // 注意:这里用全局 a
    for(int i=0;i<n-1;i++)
        for(int j=0;j<n-1-i;j++)
        {
            if(b[j]>b[j+1])
            {
                int temp=b[j];
                b[j]=b[j+1];
                b[j+1]=temp;
            }
        }
}
 
int main()
{
    for(int i=0;i<10;i++)
        scanf("%d",&a[i]);
    sort(a);             // 数组名传递
    for(int i=0;i<10;i++)
        printf("%d ",a[i]);
}

要点

  • sort(a) 传递数组名(地址),函数内修改数组元素
  • 全局变量 a 和参数 b 指向同一数组
  • sizeof(a)/sizeof(int) 求数组长度(仅对全局/静态数组有效)

注意sizeof 在函数内对参数 b 使用只会得到指针大小(4/8字节),这里因为 a 是全局数组才行。

ex13-2:函数——交换数组最大与最小值

#include <stdio.h>
 
void swapMaxMin(int a[], int n) {
    int maxIdx = 0, minIdx = 0;
    int i, temp;
 
    for (i = 1; i < n; i++) {
        if (a[i] > a[maxIdx]) maxIdx = i;
        if (a[i] < a[minIdx]) minIdx = i;
    }
 
    temp = a[maxIdx];
    a[maxIdx] = a[minIdx];
    a[minIdx] = temp;
}
 
int main() {
    int a[10];
    int i;
    for (i = 0; i < 10; i++) scanf("%d", &a[i]);
    swapMaxMin(a, 10);
    for (i = 0; i < 10; i++) printf("%d ", a[i]);
    return 0;
}

要点

  • 记录最大值/最小值的下标而非值(方便交换)
  • 一次遍历同时找最大和最小

ex13-3:递归——求年龄

#include<stdio.h>
int sum=10;
 
int age(int n)
{
    if(n==1) return sum;       // 递归出口
    return age(n-1)+2;         // 递归表达式
}
 
int main()
{
    int n;
    scanf("%d",&n);
    printf("%d",age(n));
    return 0;
}

要点

  • 递归出口:n==1 时返回 10
  • 递推公式:age(n) = age(n-1) + 2
  • 全局变量 sum 作为基准值

ex13-4:全局变量——找最小值和次小值

#include<stdio.h>
int min_i=0,rmin_i=0;      // 全局变量记录下标
 
void min_fun(int a[])
{
    int min=a[0];
    for(int i=1;i<10;i++)
        if(min>a[i]) { min=a[i]; min_i=i; }
 
    int rmin;
    if(min_i==0) { rmin=a[1]; rmin_i=1; }
    else { rmin=a[0]; rmin_i=0; }
    
    for(int i=0;i<10;i++)
        if(i!=min_i && rmin>a[i])
        { rmin=a[i]; rmin_i=i; }
}
 
int main()
{
    int arr[10]={6,2,1,8,9,0,5,7,3,4};
    min_fun(arr);
    printf("%d %d",arr[min_i],arr[rmin_i]);
    return 0;
}

要点

  • 全局变量在函数间传递信息(min_i, rmin_i
  • 分两步:先找最小值→排除最小值后再找次小值
  • 函数无返回值,结果通过全局变量传递

ex13-5-1/5-2:同一功能的两种实现

自定义 strlen(数组版)

int Mystrlen(char *p)
{
    int len=0;
    for(;*p!='\0';p++)
        len++;
    return len;
}

删除字符(数组版 vs 指针版)

// 数组下标版
void delchar(char s[],char ch)
{
    int i,j;
    for(i=j=0;s[i]!='\0';i++)
    {
        if(s[i]!=ch)
        {
            s[j]=s[i];       // 保留非 ch 字符
            j++;
        }
    }
    s[j]='\0';
}
 
// 指针版(同一功能)
void delchar(char *s,char ch)
{
    char *p,*q;
    for(p=q=s;*p!='\0';p++)
    {
        if(*p!=ch)
        {
            *q=*p;           // 保留非 ch 字符
            q++;
        }
    }
    *q='\0';
}

要点

  • 同一功能可用不同风格实现(下标法 vs 指针法)
  • 用两个”指针”(或下标)——快慢指针思想
  • 删除操作实际上是覆盖,最后设置 \0

作业重点

  • 函数先声明后使用(或先定义后调用)
  • 传值调用:修改形参不影响实参;传地址调用:通过指针修改实参
  • 数组名作参数相当于传地址,函数内可修改数组元素
  • 递归必须有出口,否则栈溢出
  • 局部变量函数内有效;全局变量可跨函数共享;同名时局部优先
  • static 局部变量保留上次调用值
  • 不能嵌套定义函数,但可以嵌套调用
  • 返回值的类型必须与函数定义一致