函数
对应教学日历:讲次 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); }递归调用
函数直接或间接地调用自身。
递归三要素:
- 递归出口(终止条件)——必须有!
- 递归表达式(将问题分解为更小规模)
- 每次递归向出口逼近
// 求阶乘 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局部变量保留上次调用值- 不能嵌套定义函数,但可以嵌套调用
- 返回值的类型必须与函数定义一致