当前位置: 首页 > news >正文

C 语言学习笔记(函数2)

内容提要

  • 函数
    • 函数的调用
    • 函数的声明
    • 函数的嵌套关系
    • 函数的递归调用
    • 数组做函数参数

函数

函数的调用

调用方式

①函数语句:

test ();       //对于无返回值的函数,直接调用
int res = max(2,4);   //对于有返回值的函数,一般需要在主调函数中接受被调函数的返回值

②函数表达式:

4 + max(2,4);
scanf("%d", &num) != 1
(c = getchar()) != '\0'

③函数参数:

printf("%d",(int)fabs(num));  //函数作为实参

注意:函数可以作为函数的实参,如果要作为形参,必须使用函数指针。

在一个函数中调用另一个函数具备以下条件:

  • 被调用的函数必须是已定义的函数。
  • 若是用库函数,应在本文件开头用#include包含其对应的头文件。
  • 若使用自定义函数,自定义函数又在主调函数的后面,则应在主调函数中对被调函数进行声明。声明的作用是把函数名、函数参数的个数和类型等信息通知编译系统,以便于在遇到函数时,编译系统能正确识别函数,并检查函数调用的合法性。

函数的声明

函数调用时,往往要遵循先定义后使用,但如果我们对函数的调用操作出现在函数定义之前,则需要对函数进行声明。

定义

完整的函数使用分三部分:

  • [函数声明]

    int max(int x, int y, double z);  //函数声明只保留函数头
    int max(int, int, double);       //s省略参数名称
    

    函数声明如果是在同一个文件,一定要定义在文件中所有的函数定义的最前面。如果有对应的.h文件,可以将函数的声明抽取的.h中。

  • 函数定义

    int max(int x, int y, double z)  //函数定义时不能省略参数的名称
    {return x > y ? x : y > z ? y : (int)z;
    }
    

    函数定义的时候,不能省略参数的数据类型,参数个数,参数名称,位置要和函数声明完全一 致。

  • 函数调用

    int main()
    {printf("%d\n",max(4,5,6));
    }
    
作用

​ C语言的函数声明是为了提前告诉编译器函数的名称、返回类型和参数,这样在函数实际定义之前就能安全调用它,避免编译错误,同时检査参数和返回值是否正确。相当于给编译器一个“预告",确保代码正确编译和运行。

使用

错误实例:被调函数写在主调函数之后

//主调函数
int main()
{printf("%d\n", add(12, 13));     //此时编译会报错,因为函数没有经过声明,编译系统无法检查函数调用的合法性
}//被调函数
int add(int x, int y)
{return x + y;
}

正确演示:被调函数写在了主调函数之前

//被调函数
int add(int x, int y)
{return x + y;
}//主调函数
int main()
{printf("%d\n", add(12, 13));     
}

注意:如果函数的调用比较简单,如果a函数调用b函数,b函数定义在a函数之前,此时是可以省略函数声明的。

正确演示:被调函数和主调函数无法区分前后,需要增加被调函数的函数声明

//函数声明
int add(int, int);//主调函数
int main()
{printf("%d\n", add(12, 13));     
}//被调函数
int add(int x, int y)
{return x + y;
}

注意:如果设计函数的复杂调用,如果a函数调用b函数,b函数调用a函数,无法区分前后,此时需添加函数声明。

声明的方式:

  • 函数头加上分号
int add(int a, int b);  
  • 函数头加上分号,可省略形参名称,但不能省略参数类型
int add(int, int);  

函数的嵌套调用

函数不允许嵌套定义,但允许嵌套调用

  • 正确示例:函数嵌套调用

    void a() {..}void b()
    {a();
    }int main()
    {printf(..)
    }
    
  • 错误示例:函数嵌套定义

    void a()
    {void b(){...}
    }
    

**嵌套调用:**在被调用函数内又主动去调用其他函数,这样的函数调用形式,称之为嵌套调用

案例
案例1
  • 需求:编写一个函数,判断给定的3~100的正整数是否是素数,若是返回1,否则返回0

  • 分析:

    • 素数:素数是大于1的自然数,除了1和它本身外,不能被其他整除。
  • 代码:

    /**
    * 定义一个函数,求素数
    * @param n:需要校验素数的整数
    *@return 返回flag 1-素数,0-非素数
    */ 
    int is_prime(int n){//默认是素数int flag = 1;//循环变量int i;// 生成2 ~ n/2for (i = 2; i <= n / 2; i++){// 判断是否出现整除的情况if (n % i == 0) {flag = 0;   //非素数break;}}return flag;
    }int main()
    {for (int i = 3; i <= 100; i++){if(is_prime(i)) printf("%-4d",i);}printf("\n");return 0}
    
案例2
  • 需求:输入四个整数,找到其中最大的数,用函数嵌套来实现,要求每次只能两个数比较。

  • 代码:

    //函数声明
    int max_2(int, int);
    int max_4(int, int, int, int);/**
    * 2个整数求最大值
    */ 
    int max_2(int a, int b) 
    {return a > b ? a : b;}/*** 4个整数求最大值*/ 
    int max_4(int a, int b, int c, int d){// 写法1// int max;// max = max_2(a,b);// max = max_2(max,c);// max = max_2(max,d);// 写法2return max_2(a,b) > max_2(c,d) ? max_2(a,b) : max_2(c,d);}int main(int argc,char *argv[]){int a,b,c,d;printf("从控制台录入4个整数:\n");scanf("%d%d%d%d",&a,&b,&c,&d);// 接收最大值int max = max_4(a,b,c,d);printf("%d,%d,%d,%d中的最大值是%d\n",a,b,c,d,max)return 0;}
    

函数的递归调用

定义

**递归调用的含义:**在一个函数中,直接或间接调用了函数本身,就称之为函数的递归调用

//直接调用  推荐
a()a();//间接调用
a()b()a();
a()b()..a();

递归调用的本质

递归调用是一种循环结构,它不同于我们之前学的while、for、do.while这样的循环结构,这些循环结构是借助于循环变量;而递归调用时利用函数自身实现虚幻结构,如果不加以控制,很容易产生死循环。

注意事项

①递归调用必须要有出口,一定要想办法终止递归(否则产生死循环)

②对终止条件的判断一定要放在函数递归之前。(先判断,再执行)

③进行函数的递归调用。

④函数递归的同时一定要将函数调用像出口逼近。

案例
案例1
  • 需求:

    递归案例-

    有5个人坐在一起,

    问第5个人多少岁?他说比第4个人大2岁。

    问第4个人岁数,他说比第3个人大2岁。

    问第3个人,又说比第2个人大2岁。

    问第2个人,说比第1个人大2岁。

    最后问第1个人,他说是10岁。

    请问第5个人多大。

  • 分析:

在这里插入图片描述

在这里插入图片描述

  • 代码:

    /*** 定义一个函数,求年龄
    * @param n:第n个人
    * @return 第n个人的年龄
    */ 
    int get_age(int n)
    {//创建一个变量,存储函数返回值,返回的是地n个人的年龄int age;//出口设置if (n == 1) //第一个人{age = 10;}else if (n > 1)  //第2,3,4,5个人{age = age(n-1) + 2; //当前第n个人的年龄 = 第n - 1个人的年龄 + 2
    }int main()
    {printf("%d\n",get_age(5));return 0;
    }
    
案例2
  • 需求:求n的阶乘

  • 分析:
    在这里插入图片描述

  • 代码

    /**
    * 定义一个函数,求n的阶乘值
    * @param n 上限
    * @return 阶乘运算结果
    */ size_t fac(int n)   // size_t 就是unsigned long int的别名,这个是等价的
    {// 定义一个变量,用作阶乘结果的接收size_t res;// 出口校验if(n < 0){printf("n的范围不能是0以下的数字\n");return -1;}else if(n == 0 || n == 1){res = 1; // 出口设置}else // n > 1{res = fac(n - 1) * n;}return res;
    }int main()
    {size_t n;printf("请输入一个整数:\n");scanf("%lu", &n);printf("%lu的阶乘结果是%lu\n", n, fac(n));return 0;
    }
    
排序算法:快速排序

快速排序是一种高效的分治(Divide and Conquer)排序算法,它的核心思想是通过选取一个基准值(pivot),将数组分为两个子数值:一个子数组的所有元素比基准值小,另一个子数组的所有元素比基准值大,然后递归地对子数维进行排序。

快速排序的基本步骤

1.选择基准值 :

  • 从数组中选择一个元素作为基准值(pivot)。常见的选择方式包括:
    • 第一个获最后一个元素(简单但可能效率不高,尤其是在已排序接近排序的数组中)。
    • 随机选择一个元素(减少最坏情况概率)。
    • 三数取中法(如第一个、中间、最后一个元素的中位数,提高分区均衡性)。

2.分区( Partition ) :

  • 数组重新排列,使得
    • 比基准值小的元素都在其左侧。
    • 比基准值大的元素都在其右侧。
  • 分区完成后,基准值的位置即为最终排序后的位置。
  • (注:分区是快速排序的关键步骤,常见的实现有Lomuto分区和Hoare分区方案。)

3.递归排序(Recursion) :

  • 对基准值左侧的子数组和右侧的子数组递归地应用快速排序。
  • 递归的终止条件是子数组的长度为0或1(已有序)。

4.合并结果(Combine)

  • 由于每次分区后基准值已位于正确位置,且左右子数组通过递归排序完成,因此无需显式合并操作,整个数组自然有序。

总结

​ 快速排序,实际上就是通过一个基准值不断的拆分数组,一直到拆无可拆为止(就是拆成一个个的 数据元素)

代码

在这里插入图片描述

/**
* 定义一个函数,实现快速排序
* @param arr:待排序数组
* @param n: 数组长度
*/
void QSort(int arr[], int n)  //函数传参,如果传递的是数组,形式参数收到的不是数组,而是数组的首地址(数组降级为指针)
{//出口限制:子数组长度小于等于1if (n < = 1) return;int i = 0, j = n - 1;// 定义的两个指针,i是从左往右,j是从右往左int pivot = arr[(n-1)/2];   // 选择中间元素元素作为基准值(可优化为随机或三位取中)//分区过程while(i < j) {// 从右向左找第一个小于等于基准值的元素(比基准值小的数)while(arr[j] > pivot && i < j) j--;// 从左向右找第一个大于基准值的元素(比基准值大的数)while(arr[i] <= pivot && i < j) i++;// 交换找到的两个元素if (i < j){int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}}// 将基准值放到正确位置 (i = = j)arr[0] = arr[i];arr[i] = pivot;// 递归排序左半部分(<=基准)QSort(arr,i);// 递归排序右半部分 (>基准) QSort(arr+i+1, n - 1 - i);
}int main()
{int arr[] = {23,45,56,24,14,78,22,19};int n = sizeof(arr) / sizeof(arr[0]);QSort(arr, n);//打印排序结果for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++){printf("%-4d",arr[i]);}printf("\n");return 0;
}

数组做函数参数

定义

当使用数组作为函数的实参时,形参应该使用数组形式或者指针变量来接收。需要注意的是:

1.这种传递方式并不是传递数组中的所有元素数据,而是传递数组首地址,此时数组降级为指针。

2.形参接收到这个地址后,形参和实参指向同一块内存空间。

3.因此,通过形参对数组元素的修改会直接影响到实参。

这种传递方式称为“地址传递”(或“指针传递”),它与“值传递”的不同:

  • 值传递:传递数据的副本,修改形参不影响实参
  • 地址传递:
  • 地址传递:传递数据的地址,通过形参可以修改实参。“地址传递"是逻辑上的说法,强调传递的是地址,而不是数据本身。数据本质上还是值传递。

在这里插入图片描述

当使用数组作为函数的形参时,通常需要额外传递一个参数表示数组的元素个数。这是因为:

1.数组形参退化为指针 在函数参数传递时,数组名会退化为指向其首元素的指针(即 int arr[]等价于 int *arr),因此函数内部无法直接获取数组的实际长度。

2.防止越界访问 由于形参仅知道数组的首地址,而不知道数组的实际大小,如果不传递元素个数,函数内部可能因错误计算或循环导致 数组下标越界(访问非法内存),引发未定义行为(如程序崩溃、数据损坏)。

3.通用性 即使实参数组的声明长度固定(如 int a[10]),函数仍应接收元素个数参数,因为函数可能需要处理不同长度的数组(例如动态数组或部分填充的数组)。

范例:

#include <stdio.h>// 定义一个函数,将数组作为参数
void fun(int arr[], int len) // 数组传参,形参只能接收到实参数组的首地址,并不是完整的数组
{for(inti=0;i< len; i++){printf("%-4d",arr[i]);}printf("\n");
}void main()
{int arr[]={11,22,33,44,55};int len=sizeof(arr)/sizeof(arr[0]);fun(arr, len);
}

但有一个例外,如果是用字符数组做形参,且实参数组中存放的是字符串数据(形参是字符数组,实参是字符串常量)。则不用表示数组个数的形参,原因是字符串本身会添加自动结束标志\0,举例:

#include <sidio.h>//定义一个函数,传递一个字符串
void fun(char arr[])
{char c;int i = 0;while(c = arr[i] ! = '\0'){printf("%c",c);i++;}
}void main()
{fun("hello");
}

为什么 sizeof 不能用于形参数组?在函数内部, sizeof(arr)返回的是指针大小(如4/8字节),而非数组总字节数。例如:

void printsize(int arr[]){printf("%zu\n"sizeof(arr));  // 输出指针大小(如 8),而非数组大小!
}
案例
案例1
  • 需求

    有两个数组a和b,各有5个元素,将它们对应元素逐个地相比(即a[0]与b[0]比,a[1] 与b[1]比……)。如果a数组中的元素大于b数组中的相应元素的数目多于b数组中元素大 于a数组中相应元素的数目(例如,a[i]>b]i]6次,b[i]>a[i] 3次,其中i每次为不同的值),则 认为a数组大于b数组,并分别统计出两个数组相应元素大于、等于、小于的个数。

    int a[10] = {12,12,10,18,5};
    int b[10] = {111,112,110,8,5};
    
  • 代码:

    #include <stdio.h>#define LEN 5/**
    * 定义一个函数,实现两个数字的比较
    * @param x,y  参与比较的两个数字
    * @return x>y --> 1, x < y --> -1, x == y --> 0
    */
    int get_large(int x, int y)
    {int flag = 0;if(x > y) flag = 1;else if(x < y) flag = -1;return flag;
    }int main()
    {//定义啊a,b两个测试数组int a[LEN] = {12,12,10,18,5};int b[LEN] = {111,112,110,8,5};int i, max = 0, min = 0, k = 0;// 遍历数组,进行比较for (i = 0; i < LEN; i++){//同一位置两个数比较int res = get_large(a[i],b[i]);if(res == 1) max++;else if (res == -1) min++;else k++;}printf("max=%d,min=%d,k=%d\n",max,min,k);return 0;
    }
    
案例2:
  • 需求:编写一个函数,用来分别求数组score-1(有5个元素)和数组score_2(有10个元素)各元素的平均值。

  • 代码:

    /**
    * 定义一个函数,求数组中元素的平均值
    * @param scores 成绩数组
    * @param len 数组大小
    * @return 平均值
    */ 
    float get_avg(float scores[],int len)
    {int i;float aver,sum = scores[0]; // 平均分,总分// 遍历数组for(i = 1; i < len; i++){sum += scores[i];}//求平均分aver = sum / len;return aver;
    }int main()
    {//测试数组float scores1[] = {66,78,86,56,46};float scores2[] = {77,88,98,87,67,65,55,45,67,78};printf("%6.2f,%6.2f\n",get_avg(scores1,sizeof(scores1)/sizeof(scores1[0])),get_avg(scores2,sizeof(scores2)/sizeof(scores2[0])));return 0;
    }
    

相关文章:

  • AI在网络安全中的应用之钓鱼邮件检测
  • Python列表 vs 元组:全面对比解析(新手友好版)
  • MYSQL8.0常用窗口函数
  • input组件使用type=“number“的时候,光标自动跳到首位
  • 【Tools】VMware Workstation 17.6 Pro安装教程
  • 在 CentOS 7.9 上部署 node_exporter 并接入 Prometheus + Grafana 实现主机监控
  • PyMOL命令行和脚本
  • 精益数据分析(70/126):MVP迭代中的数据驱动决策与功能取舍
  • AI神经网络降噪 vs 传统单/双麦克风降噪的核心优势对比
  • 公网ip是固定的吗?动态ip如何做端口映射?内网ip怎么让外网远程访问?
  • 组态王通过开疆智能profinet转ModbusTCP网关连接西门子PLC配置案例
  • 学习BI---QuickBI介绍
  • ngx_http_scgi_module 技术指南
  • 问题 | 代码审查:函数是否包含返回语句
  • 调研函模板可参考,以无人机职业技能调研为例
  • 开源表单设计器FcDesigner配置多语言教程
  • 2024年ASOC SCI2区TOP,多机制群优化算法+多风场输电线路巡检中多无人机任务分配与路径规划,深度解析+性能实测
  • Easy PLC和IT7000触摸屏的无实物仿真调试
  • Rust 学习笔记:关于错误处理的练习题
  • JavaScript 中的五种继承方式进行深入对比
  • 4名中学生同服处方药后身体不适,一心堂一药店未凭处方售药被罚1万元
  • 红星控股重整期间实控人被留置后续:重整草案不会修改,涉车建兴职责已调整
  • 马上评|房屋“注胶堵漏”骗局何以屡屡得逞
  • 一季度支持科技创新和制造业发展减税降费及退税4241亿元
  • 19国入境团抵沪并游玩,老外震惊:“怎么能有这么多人?”
  • 人民日报评论员观察:稳就业,抓好存量、增量、质量