基于AI的C语言程序设计(微课版)课件 蒋亚平 第8-13章 函数实现模块化程序设计 -数据结构和算法应用_第1页
基于AI的C语言程序设计(微课版)课件 蒋亚平 第8-13章 函数实现模块化程序设计 -数据结构和算法应用_第2页
基于AI的C语言程序设计(微课版)课件 蒋亚平 第8-13章 函数实现模块化程序设计 -数据结构和算法应用_第3页
基于AI的C语言程序设计(微课版)课件 蒋亚平 第8-13章 函数实现模块化程序设计 -数据结构和算法应用_第4页
基于AI的C语言程序设计(微课版)课件 蒋亚平 第8-13章 函数实现模块化程序设计 -数据结构和算法应用_第5页
已阅读5页,还剩299页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第8章

函数实现模块化程序设计函数驱动模块化目录CONTENTS01函数是什么02函数的定义和调用03数组作为函数参数04递归05图解排序算法06函数综合编程案例07本章小结问题导入解决代码冗余在开发一个学生成绩管理系统时,需要多次计算不同班级的平均分(如计算高一(1)班、(2)班、(3)班的平均分)。如果不用函数,每次计算都重复编写“求和→除以人数”的代码,会出现什么问题?函数如何通过“封装重复逻辑”解决代码冗余和修改繁琐的问题?如何解放main函数开发一个包含登录验证、数据录入、成绩查询、信息修改的多功能程序时,如果所有代码都写在main函数中,会导致什么后果?函数如何通过“按功能分模块”让程序结构更清晰,便于多人协作开发和后期维护?01函数是什么为什么需要函数代码冗余问题在学生成绩管理系统中,若不使用函数,每次计算不同班级平均分都需重复编写求和与除法代码,导致代码冗长、难以维护。函数封装重复逻辑,可有效解决这一问题。模块化优势函数将程序分解为独立模块,如建筑的分工,提高代码可读性与可维护性,便于团队协作开发,是模块化编程的核心工具。函数是什么:生活类比01函数与菜谱类比函数类似于菜谱,输入食材(参数),按步骤加工(执行代码),输出菜肴(返回值)。C语言中,函数是可重用的代码块,可接收参数并返回结果。02借还书流程类比通过“借还书流程”类比,函数定义对应借还规则,参数对应借书证与书名,返回值对应书籍与借阅成功标志,帮助初学者理解抽象概念。03函数的输入输出函数可以有输入参数,也可有返回值。例如,计算两个数之和的函数,接收两个数作为输入,返回它们的和。02函数的定义和调用函数的定义和调用函数的定义和调用返回类型函数名(参数列表){函数体;//功能实现代码return返回值;//返回类型非void时必须有,void无需返回}函数的定义语法接收的输入数据(如intcount),无参数时写()或(void)符合标识符规则函数返回值的类型函数名(实参列表);函数的调用语法函数的定义和调用打印金字塔任务以打印5行金字塔为例,展示如何将任务分解为多个函数:printSpaces打印空格,printStars打印星号,printLine整合两者并换行,printPyramid循环调用printLine。void类型与省略当函数无返回值时,返回类型使用void。在printPyramid函数中,void表明该函数仅执行任务,不返回任何值。示例:实现金字塔形状。函数的定义和调用#include<stdio.h>//函数定义:打印指定数量的空格voidprintSpaces(intcount){for(inti=0;i<count;i++){printf("");}}//函数定义:打印指定数量的星号voidprintStars(intcount){for(inti=0;i<count;i++){printf("*");}}//函数定义:打印金字塔的一行voidprintLine(intline,inttotalLines){intspaces=totalLines-line;intstars=2*line-1;printSpaces(spaces);//调用打印空格的函数printStars(stars); //调用打印星号的函数printf("\n");//换行}运行结果://函数定义:打印完整的金字塔voidprintPyramid(inttotalLines){for(inti=1;i<=totalLines;i++){printLine(i,totalLines);//调用打印一行的函数}}intmain(){intn=5; //金字塔的行数printf("金字塔形状:\n");printPyramid(n); //调用打印金字塔的函数return0;}调用链与参数匹配调用链与参数匹配main函数调用printPyramid(5)启动调用链,printPyramid循环调用printLine(i,5),printLine再调用printSpaces与printStars。实参与形参类型、数量必须一致,否则编译错误。03数组作为函数参数数组作为函数参数一维数组名作为参数一维数组名作为函数参数的内存存储示意图函数定义时,形参的写法有两种函数名(类型

数组名[])(推荐,直观表示数组)函数名(类型*指针名)(本质与数组名等价,因数组名即地址)voidfunc(intarr[]);voidfunc(int*arr);一维数组名作为参数示例:计算一维数组元素的平均值。#include<stdio.h>//函数:计算数组元素的平均值(形参用数组形式)floatcalculateAverage(intarr[],intlength){intsum=0;for(inti=0;i<length;i++){sum+=arr[i];//通过数组下标访问元素}return(float)sum/length;//转换为浮点型返回}运行结果:intmain(){intnumbers[]={10,20,30,40,50};//计算数组长度intlen=sizeof(numbers)/sizeof(numbers[0]);//传递数组名和长度floatavg=calculateAverage(numbers,len); printf("数组元素:");for(inti=0;i<len;i++){printf("%d",numbers[i]);}printf("\n平均值:%.2f\n",avg);return0;}AI辅助:

必须传递数组长度:数组作为参数时需手动传递长度,避免遍历越界;地址传递特性:数组传参为地址传递,函数内修改将同步到原数组二维数组的行地址作为参数列数必须指定二维数组传参时,形参必须给出列数,例如,intarr[][3],以便编译器计算arr[i][j]的偏移。第一维可省略,但第二维必须明确。地址计算与错误若列数不匹配,地址计算错误将导致数据错乱。例如,matrix[3][3]实参与arr[][3]形参耦合时,列数必须一致。第一维可省略,但第二维必须明确。AI辅助:

实参二维数组与形参的列数必须完全一致!比如实参是intmatrix[3][3](3行3列),形参就必须定义为intarr[][3](列数3不能改),若写成intarr[][2],列数不匹配会直接导致地址计算错误,程序运行异常。二维数组的行地址作为参数示例:计算二维数组每行元素的和。#include<stdio.h>//函数:计算二维数组每行的元素和(形参指定列数为3)voidcalculateRowSum(intarr[][3],introws,intcols){for(inti=0;i<rows;i++){intsum=0;for(intj=0;j<cols;j++){sum+=arr[i][j];//访问第i行第j列元素}printf("第%d行元素之和:%d\n",i+1,sum);}}运行结果:intmain(){//定义3行3列的二维数组intmatrix[3][3]={{1,2,3},{4,5,6},{7,8,9}};introws=3,cols=3;printf("二维数组元素:\n");for(inti=0;i<rows;i++){for(intj=0;j<cols;j++){printf("%d",matrix[i][j]);}printf("\n");}//传递二维数组名(行地址的起始)calculateRowSum(matrix,rows,cols);return0;}04递归递归递归递归:德罗斯特效应递归是一种非常强大的编程技术,它允许函数调用自身。在递归过程中,函数会不断地调用自己,直到达到某个终止条件为止。德罗斯特效应(Drosteeffect)是递归的一种视觉形式,指一张图片的某个部分与整张图片相同,产生无限循环的效果。递归双要素:基线与递推01基线条件递归函数必须有基线条件终止无限自我调用。例如,斐波那契数列中,n==0或1时直接返回n,避免栈溢出。02递推条件递归函数需将大问题拆成同构子问题。斐波那契数列中,F(n)=F(n-1)+F(n-2),不断拆分至基线。03递归的思想递归的思想类似于数学中的归纳法,它将一个复杂的问题分解为一个或多个相似但规模更小的子问题。斐波那契数列示例:递归实现斐波那契数列。斐波那契数列是一个经典的数学序列,它的定义如下:(1)第0项为0(2)第1项为1(3)从第2项开始,每一项都是前两项的和,即:F(n)=F(n-1)+F(n-2)#include<stdio.h>//递归函数:计算斐波那契数列的第n项intfibonacci(intn){//基线条件if(n==0){return0;}if(n==1){return1;}//递归条件returnfibonacci(n-1)+fibonacci(n-2);}intmain(){intn=10;//计算前10个斐波那契数printf("斐波那契数列前%d项:\n",n);for(inti=0;i<n;i++){printf("%d",fibonacci(i));}printf("\n");return0;}数字全排列示例:递归实现数字全排列。数字全排列是指将给定的n个数字进行所有可能的排列。例如,对于数字1、2、3,它们的全排列有:123、132、213、231、312、321。//递归函数:生成从start到end的全排列voidpermute(intarr[],intstart,intend){if(start==end){//基线条件:当start等于end时,输出当前排列for(inti=0;i<=end;i++){printf("%d",arr[i]);}printf("\n");}else{//递归条件:生成所有可能的排列for(inti=start;i<=end;i++){swap(&arr[start],&arr[i]);//交换当前元素permute(arr,start+1,end);//递归生成后续元素的排列swap(&arr[start],&arr[i]);//回溯,恢复原来的顺序}}}intmain(){intarr[]={1,2,3};intn=sizeof(arr)/sizeof(arr[0]);printf("数字全排列:\n");permute(arr,0,n-1);return0;}运行结果:#include<stdio.h>//交换两个元素的值voidswap(int*a,int*b){inttemp=*a;*a=*b;*b=temp;}基线条件递归条件回溯主函数交换函数最大公约数与最小公倍数示例:使用递归实现计算

“最大公约数和最小公倍数”。最大公约数(GCD)是指两个或多个整数共有约数中最大的一个。例如,12和18的最大公约数是6。最小公倍数(LCM)是指两个或多个整数公有的倍数中最小的一个,最小公倍数

=两数的乘积/最大公约数。例如,12和18的最小公倍数是36。计算最大公约数和最小公倍数有多种方法,其中最常用的是欧几里得算法(辗转相除法)。#include<stdio.h>//递归函数:计算最大公约数intgcd(inta,intb){//基线条件:当b为0时,返回aif(b==0){returna;}//递归条件:递归计算b和a%b的最大公约数returngcd(b,a%b);}//函数:计算最小公倍数intlcm(inta,intb){return(a*b)/gcd(a,b);}intmain(){intnum1=12,num2=18;printf("数字%d和%d的最大公约数是:%d\n",num1,num2,gcd(num1,num2));printf("数字%d和%d的最小公倍数是:%d\n",num1,num2,lcm(num1,num2));return0;}基线和递归条件最小公倍数杨辉三角示例:使用递归实现生成杨辉三角。杨辉三角(也称为帕斯卡三角)是一个由数字排列成的三角形,其特点是每个数等于它上方两数之和。//函数:打印杨辉三角的前n行voidprintPascalTriangle(intn){for(inti=0;i<n;i++){//打印前导空格,使杨辉三角居中显示for(intj=0;j<n-i-1;j++){printf("");}//打印当前行的数值for(intj=0;j<=i;j++){printf("%d",binomialCoefficient(i,j));}printf("\n");}}intmain(){intn=10;//打印前10行printf("杨辉三角的前%d行:\n",n);printPascalTriangle(n);return0;}#include<stdio.h>//递归函数:计算杨辉三角中第row行第col列的值intbinomialCoefficient(introw,intcol){//基线条件:当col为0或col等于row时,返回1if(col==0||col==row){return1;}//递归条件:计算上一行中两个数的和returnbinomialCoefficient(row-1,col-1)+binomialCoefficient(row-1,col);}05图解排序算法插入排序插入排序基本思想插入排序是一种简单的排序算法。它将待排序的元素逐个插入到已经排好序的部分数组中,直到整个数组有序。插入排序通常适用于数据量较小或接近有序的情况。组成部分在插入排序中,我们将数组分为两部分:已经排序好的部分和未排序部分。每次从未排序部分选择一个元素,并将其插入到已排序部分的适当位置。直到整个数组排序完成。直接插入排序01直接插入排序插入排序主要分为二种:直接插入排序和折半插入排序。直接插入排序(StraightInsertionSort)是一种最基本的插入排序方法。它的主要思路是:从第二个元素开始,逐步将每个元素插入到前面的已排序部分中。每次插入时,从后向前扫描已排序部分,直到找到插入位置。直接插入排序的过程直接插入排序示例:直接插入排序。//测试插入排序intmain(){intarr[]={12,11,13,5,6};intlength=sizeof(arr)/sizeof(arr[0]);//获取数组长度printf("排序前:");for(inti=0;i<length;i++){//使用传统的for循环printf("%d",arr[i]);}insertionSort(arr,length);//传递数组和长度printf("\n排序后:");for(inti=0;i<length;i++){//使用传统的for循环printf("%d",arr[i]);}return0;}#include<stdio.h>voidinsertionSort(intarr[],intlength){//从第二个元素开始,假设第一个元素是已排序的for(inti=1;i<length;i++){//取出当前元素intcurrentElement=arr[i];intj=i-1;//向左移动已排序部分的元素,

//直到找到当前元素应插入的位置while(j>=0&&arr[j]>currentElement){arr[j+1]=arr[j];//右移元素j--;}//将当前元素插入到找到的位置arr[j+1]=currentElement;}}运行结果:折半插入排序02折半插入排序插入排序主要分为二种:直接插入排序和折半插入排序。折半插入排序(BinaryInsertionSort)是一种改进的插入排序,它通过使用二分查找来找到合适的位置插入元素,从而减少了元素比较的次数。与直接插入排序不同,折半插入排序使用二分查找来确定插入位置,但插入操作仍然是通过向右移动元素来完成的。插入排序示意图给定一个包含n个元素的序列,其中前i个元素已经是有序的,现在需要将下标为i的元素插入到正确的位置。折半插入排序02折半插入排序插入排序主要分为二种:直接插入排序和折半插入排序。“折半”示意图折半插入排序分为两步:一是找到待插入元素的位置,二是将其插入。折半插入排序的核心思想是利用二分法在有序序列a[0]到a[i-1]中找到a[i]应该插入的位置。1)比较a[i]和a[mid]的大小。如果a[i]>a[mid],说明a[i]应该插入到mid之后,更新low=mid+1,即将搜索范围缩小到[mid+1,high]。如果a[i]<=a[mid],说明a[i]应该插入到mid之前,更新high=mid-1,即将搜索范围缩小到[low,mid-1]。2)重复上述过程,不断调整low和high,直到low大于high时跳出循环,此时low即为a[i]应该插入的位置。找到插入位置后,我们将a[low]到a[i-1]之间的元素依次后移一个位置,然后将a[i]插入到low位置。折半插入排序示例:折半插入排序。//将已排序部分的元素后移,为当前元素腾出位置for(intj=i;j>insertPosition;j--){arr[j]=arr[j-1];}//将当前元素插入到合适的位置arr[insertPosition]=currentElement;}}//测试折半插入排序intmain(){intarr[]={12,11,13,5,6};intlength=sizeof(arr)/sizeof(arr[0]);//获取数组长度printf("排序前:");for(inti=0;i<length;i++){printf("%d",arr[i]);}binaryInsertionSort(arr,length);//调用折半插入排序printf("\n排序后:");for(inti=0;i<length;i++){printf("%d",arr[i]);}return0;}#include<stdio.h>//二分查找函数,用于在已排序部分找到插入位置intbinarySearch(intarr[],inttarget,intlow,inthigh){while(low<=high){intmid=low+(high-low)/2;if(arr[mid]==target){returnmid;}elseif(arr[mid]<target){low=mid+1;}else{high=mid-1;}}returnlow;//返回插入的位置}//折半插入排序函数voidbinaryInsertionSort(intarr[],intlength){for(inti=1;i<length;i++){intcurrentElement=arr[i];//使用二分查找来找到当前元素的插入位置intinsertPosition=binarySearch(arr,currentElement,0,i-1);

交换排序交换排序:冒泡排序01冒泡排序常见的交换排序算法有冒泡排序和快速排序。冒泡排序(BubbleSort)是一种简单的交换排序算法。其基本思想是:通过多次遍历数组,将相邻元素两两比较并交换位置,使得每一轮遍历后,最大(或最小)的元素“冒泡”到数组的末端。这个过程会重复进行,直到整个数组有序。交换排序的基本思想交换排序是基于比较的排序算法,排序过程中通过交换元素的位置来使得数组有序。交换排序:冒泡排序冒泡排序的过程冒泡排序的基本步骤(以升序为例)

从第一个元素开始,依次比较相邻的两个元素。

如果当前元素大于下一个元素,则交换它们的位置。

每一次遍历结束后,当前未排序部分中的最大元素被交换到最后。

重复步骤1到步骤3,直到整个数组有序。最大值:90交换排序:冒泡排序示例:冒泡排序。//测试冒泡排序intmain(){intarr[]={64,34,25,12,22,11,90};intlength=sizeof(arr)/sizeof(arr[0]);//获取数组长度printf("排序前:");for(inti=0;i<length;i++){//使用传统的for循环遍历数组printf("%d",arr[i]);}bubbleSort(arr,length);//调用冒泡排序printf("\n排序后:");for(inti=0;i<length;i++){//使用传统的for循环遍历数组printf("%d",arr[i]);}return0;}#include<stdio.h>//冒泡排序函数voidbubbleSort(intarr[],intlength){//外循环控制排序轮数for(inti=0;i<length-1;i++){//内循环执行每一轮的比较与交换for(intj=0;j<length-1-i;j++){if(arr[j]>arr[j+1]){//交换位置inttemp=arr[j];arr[j]=arr[j+1];arr[j+1]=temp;}}}}运行结果:交换排序:快速排序02快速排序快速排序(QuickSort)是由冒泡排序改进而得的,它是一种分治法的排序算法,它通过一个“分区”操作将数组分为两部分,并递归地排序这两部分,最终达到整个数组有序。快速排序通常被认为是最优的交换排序算法,尤其在处理大数据时表现出色。交换排序:快速排序快速排序第一轮操作过程快速排序第一轮操作的基本步骤1、第一个元素作为基准数,左侧下标用left表示,右侧下标用right表示。2、分别从序列两端出发。第一次交换:从右侧向左寻找小于6的数字(找到5),接着从左侧向右寻找大于6的数字(找到7),交换数字7和数字5,此时完成第一次交换第二次交换:right继续向左移动;left继续向右移动。right和left再次分别向左和向右移动,依次类推,完成第一轮操作。3、左侧子序列和右侧子序列继续进行相同的操作。AI辅助:每次必须right先出发。交换排序:快速排序快速排序算法的完整过程快速排序的基本步骤选择一个“基准”元素,一般选择数组的第一个、最后一个或中间的元素。对数组进行一次分区操作,使得所有小于基准元素的值都排在基准元素的左边,所有大于基准元素的值都排在基准元素的右边。递归地对基准元素左右的子数组进行快速排序,直到子数组的大小为1。交换排序:快速排序示例:快速排序。#include<stdio.h>//交换数组中两个元素的位置voidswap(intarr[],inti,intj){inttemp=arr[i];arr[i]=arr[j];arr[j]=temp;}//分区函数,返回基准元素的位置intpartition(intarr[],intlow,inthigh){//选择最左边的元素作为基准intpivot=arr[low];intleft=low+1;intright=high;while(1){//向右移动,直到找到大于或等于基准的元素while(left<=right&&arr[left]<=pivot){left++;}//向左移动,直到找到小于或等于基准的元素while(left<=right&&arr[right]>=pivot){right--;}

//如果左指针小于右指针,则交换if(left<right){swap(arr,left,right);}else{break;}}//把基准元素放到正确的位置swap(arr,low,right);returnright;}//快速排序主函数voidquickSort(intarr[],intlow,inthigh){if(low<high){//找到基准元素的位置intpivotIndex=partition(arr,low,high);

//递归排序基准元素左侧和右侧的子数组quickSort(arr,low,pivotIndex-1);quickSort(arr,pivotIndex+1,high);}}//主函数,用于测试intmain(){intarr[]={6,1,2,7,9,3,4,5,10,8};//获取数组长度intlength=sizeof(arr)/sizeof(arr[0]);printf("原始数组:\n");for(inti=0;i<length;i++){printf("%d",arr[i]);}//调用快速排序quickSort(arr,0,length-1);printf("\n排序后的数组:\n");for(inti=0;i<length;i++){printf("%d",arr[i]);}

return0;}选择排序选择排序选择排序(SelectionSort)是一种简单的排序算法,其基本思想是每一轮遍历数组,选择一个最小(或最大)的元素放到已排序部分的末尾。选择排序的基本思想:

从未排序的部分中找到最小(或最大)元素。

将最小(或最大)元素与未排序部分的第一个元素交换位置。

将已排序部分和未排序部分分开,并继续处理未排序部分。

重复步骤1和步骤2,直到所有元素有序。选择排序选择排序的过程选择排序的步骤:

初始阶段:开始时未排序部分是整个数组,已排序部分为空。

第一轮遍历:找出未排序部分中的最小(或最大)元素,并将其与当前未排序部分的第一个元素交换。

第二轮遍历:在未排序部分剩余元素中找出最小(或最大)元素,交换到第二个位置。

重复:继续找出未排序部分的最小(或最大)元素,交换直到所有元素排序完成。选择排序运行结果://测试选择排序intmain(){intarr[]={64,25,12,22,11};intlength=sizeof(arr)/sizeof(arr[0]);//获取数组长度printf("排序前:");for(inti=0;i<length;i++){//使用传统的for循环遍历数组printf("%d",arr[i]);}selectionSort(arr,length);//调用选择排序printf("\n排序后:");for(inti=0;i<length;i++){//使用传统的for循环遍历数组printf("%d",arr[i]);}return0;}#include<stdio.h>//选择排序函数voidselectionSort(intarr[],intlength){//遍历整个数组for(inti=0;i<length-1;i++){//假设当前索引i是未排序部分的最小值的索引intminIndex=i;//从未排序的部分找到最小值for(intj=i+1;j<length;j++){if(arr[j]<arr[minIndex]){minIndex=j;//更新最小值的索引}}//如果最小值的索引不是当前索引i,交换它们if(minIndex!=i){inttemp=arr[i];arr[i]=arr[minIndex];arr[minIndex]=temp;}}}示例:选择排序。运行结果:排序对比算法选型建议

稳定性标注

代码优化小规模数据用直接插入排序,大规模数据用快速排序;冒泡、插入排序稳定,快速、选择排序不稳定;对于冒泡排序,添加标志位,若某轮无交换则数组已有序,提前终止循环。06函数综合编程案例函数综合编程案例综合编程案例#include<stdio.h>//求数组最大值intgetMax(intarr[],intlen){intmax=arr[0];for(inti=1;i<len;i++){if(arr[i]>max)max=arr[i];}returnmax;}//求数组最小值intgetMin(intarr[],intlen){intmin=arr[0];for(inti=1;i<len;i++){if(arr[i]<min)min=arr[i];}returnmin;}案例一:求数组元素的最大值和最小值(函数处理批量数据)。运行结果:

intmain(){intnums[]={12,45,7,99,33,61};

//计算数组长度intlength=sizeof(nums)/sizeof(nums[0]);intmax=getMax(nums,length);intmin=getMin(nums,length);printf("数组元素:");for(inti=0;i<length;i++){printf("%d",nums[i]);}printf("\n最大值:%d,最小值:%d\n",max,min);return0;}综合编程案例#include<stdio.h>#include<string.h>//递归函数:反转字符串(从start到end的字符)voidreverseString(charstr[],intstart,intend){if(start>=end)return;//终止条件:首尾索引相遇//交换首尾字符chartemp=str[start];str[start]=str[end];str[end]=temp;//递归处理剩余子串(缩小问题规模)reverseString(str,start+1,end-1);}案例二:递归实现字符串反转(递归函数应用)。运行结果:

intmain(){charstr[100];printf("请输入一个字符串:");scanf("%s",str);intlen=strlen(str);reverseString(str,0,len-1);//调用递归函数printf("反转后的字符串:%s\n",str);return0;}综合编程案例#include<stdio.h>#include<ctype.h>//包含isspace函数(判断空白字符)//读取文本到缓冲区voidreadText(chartext[],intmaxSize){printf("请输入一段文本(以回车结束):");fgets(text,maxSize,stdin);//读取一行文本(包含空格)}//判断当前字符是否为单词分隔符(空白字符)intisSeparator(charc){returnisspace(c)||c=='\0'||c=='\n';//空格、结束符、换行符均为分隔符}//统计文本中单词数量intcountWords(chartext[]){intcount=0;intinWord=0;//标记是否在单词中(0:不在,1:在)

案例三:统计文本中单词的数量(复杂问题模块化拆分)for(inti=0;text[i]!='\0';i++){if(!isSeparator(text[i])&&!inWord){//遇到单词的第一个字符count++;inWord=1;}elseif(isSeparator(text[i])&&inWord){//离开单词inWord=0;}}returncount;}intmain(){chartext[1000];readText(text,1000);intwordCount=countWords(text);printf("文本中的单词数量:%d\n",wordCount);return0;}07本章小结函数模块化五维总结定义语法从定义语法、调用机制、数组传参、递归思想到排序算法,本章构建“概念-机制-应用”三层框架。调用机制强调函数封装带来可读性、可维护性、复用性三大收益;数组地址传参提升效率但需手动管理长度。数组传参递归以基线+递推为核心,注意深度与重复计算;排序算法按数据规模与稳定性选型。递归思想从定义语法、调用机制、数组传参、递归思想到排序算法,本章构建“概念-机制-应用”三层框架。感谢您的观看THANKYOUFORWATCHING汇报人:蒋亚平第9章

预处理命令编译前的魔法:C语言预处理命令全解析目录CONTENTS工作原理及指令宏定义条件编译文件包含01020304本章小结05问题导入宏定义编写计算圆面积、周长及球体体积的程序时,圆周率π(如3.14159)需要在3处使用(面积S=πr2、周长C=2πr、体积V=(4/3)πr3)。若直接写数值,后期想将π精度调整为3.1415926,需逐个修改所有用到π的代码,不仅繁琐还易遗漏。宏定义如何通过“一次定义、多处引用”解决“重复常量修改困难”的问题?条件编译开发跨平台工具时,Windows系统需调用Sleep()函数(头文件<windows.h>),Linux系统需调用sleep()函数(头文件<unistd.h>),若不做处理,代码在其中一个系统编译会因“头文件或函数不存在”报错。条件编译如何根据“系统环境”选择执行对应代码块?多个文件都需使用“学生信息结构体”定义,文件包含如何实现“代码复用”避免重复编写?01工作原理及指令预处理:编译的隐形前哨编译四阶段C语言代码从文本到可执行文件需经历预处理、编译、汇编、链接四个阶段。预处理作为首个阶段,主要对源代码进行文本级处理,为后续编译工作做准备。预处理的作用预处理器逐行扫描源文件,处理以#号开头的指令,如#include、#define等,对文本进行替换、插入或删除操作,生成中间文件。预处理的优势通过宏定义、条件编译、文件包含等机制,预处理可以实现代码的复用、灵活适配和可移植性,让代码更加简洁、高效。工作原理(四步流程):指令·注释·宏·中间文件1.处理预处理指令(1)文件包含(#include):遇到#include<文件名>或#include"文件名"时,预处理器会查找指定的头文件(.h),并将其全部内容替换到#include所在的位置。

用<>时,优先从编译器预设的系统头文件目录(如/usr/include)查找;

用""时,优先从当前源文件所在目录查找,找不到再搜索系统目录。(2)宏定义与替换(#define)

对于#define宏名

替换文本(如#definePI3.14),预处理器会记录“宏名→替换文本”的映射,后续在源文件中遇到该宏名时(除在字符串或注释中),会直接替换为对应的文本(仅做文本替换,不计算值)。

对于带参数的宏(如#defineMAX(a,b)((a)>(b)?(a):(b))),会按参数匹配规则进行替换(如MAX(2,3)替换为((2)>(3)?(2):(3)))。工作原理(四步流程):指令·注释·宏·中间文件2.处理注释预处理器会将源文件中的所有注释(//单行注释或/**/多行注释)替换为一个空格,避免注释内容被后续编译阶段误解析。(3)条件编译(#ifdef、#ifndef、#if、#else、#elif、#endif)预处理器会根据条件判断保留或删除部分代码块。预处理后,不符合条件的代码块会被直接删除,仅保留符合条件的代码。(4)其他指令

#undef:取消已定义的宏(如#undefPI后,PI不再作为宏生效);

#line:修改当前行号和文件名(主要用于编译器报错时定位);

#error:在预处理阶段触发错误提示(如#error"必须定义VERSION")。工作原理(四步流程):指令·注释·宏·中间文件3.处理预定义宏C语言预设了部分宏(无需#define定义),预处理器会在预处理阶段将其替换为对应内容,例如:

__FILE__:当前源文件名(字符串);

__LINE__:当前代码行号(整数);

__DATE__:编译日期(如"Aug132026");

__TIME__:编译时间(如"15:30:45")。例如,printf("文件:%s,行号:%d\n",__FILE__,__LINE__)会被替换为包含实际文件名和行号的代码。4.生成预处理文件完成上述处理后,预处理器会输出一个不含预处理指令、注释已被替换、宏已展开的中间文件(.i),该文件仅包含C语言的核心语法(变量、函数、语句等),作为下一步编译阶段的输入。工作原理(四步流程):指令·注释·宏·中间文件预处理任务预处理器依次完成解析预处理指令、处理注释、处理预定义宏、生成中间文件四个任务,确保源代码在编译前达到规范要求。中间文件特点中间文件不含预处理指令与注释,所有宏已被字面量取代,所有条件分支只剩有效代码,为编译器进行语法分析提供清晰的输入。02宏定义宏定义无参宏:常量印章一键盖无参宏的定义与作用无参宏通过#define指令定义,将常量或表达式赋予一个标识符。无参宏的优势使用无参宏可以提高代码的可读性,方便代码的修改,只需在宏定义处修改一次,即可在所有引用处生效。无参宏定义,实现代码中常量的复用和快速替换。#define标识符

替换文本示例:无参宏定义。#defineEXCELLENT90#defineGOOD80#defineAVERAGE70#definePASS60

无参宏:常量印章一键盖示例:使用宏代替具体分数。intmain(){ if(score>=EXCELLENT){ printf("优秀\n"); }elseif(score>=GOOD){ printf("良好\n"); }elseif(score>=AVERAGE){ printf("中等\n"); }elseif(score>=PASS){ printf("及格\n"); }else{ printf("不及格\n"); }}预处理器将所有的宏标识符替换为其定义的内容:intmain(){ intscore=88; if(score>=90){ printf("优秀\n"); }elseif(score>=80){ printf("良好\n"); }elseif(score>=70){ printf("中等\n"); }elseif(score>=60){ printf("及格\n"); }else{ printf("不及格\n"); }}带参宏:模板式代码生成带参宏的定义带参宏允许定义类似函数的宏,接受参数并返回结果。带参宏允许定义类似函数的宏,接受参数并返回结果,实现参数化的代码复用。#define宏名(参数列表)替换文本定义语法:宏名(实参列表);调用语法:示例:带参宏定义。#definePI3.14#defineCIRCLE_AREA(r)(PI*(r)*(r))带参宏的注意事项带参宏中的参数和整体需要用括号括起来,以避免替换后出现运算符优先级的问题,确保宏展开后的代码逻辑正确。#defineSQUARE(x)(x*x)错误定义:SQUARE(a+1)替换结果:(a+1*a+1)#defineSQUARE(x)((x)*(x))正确定义:SQUARE(a+1)替换结果:((a+1)*(a+1))√×调用时写

CIRCLE_AREA(5.0),预处理后会变成(3.14*(5.0)*(5.0))。

带参宏:模板式代码生成示例:使用宏计算圆的面积。intmain(){ floatradius=5.0; floatarea=CIRCLE_AREA(radius); printf("半径为%.2f的圆的面积是%.2f\n",radius,area);}预处理器将所有的宏标识符替换为其定义的内容,,并将参数代入。floatradius=5.0;floatarea=(3.14*(5.0)*(5.0));printf("半径为%.2f的圆的面积是%.2f\n",radius,area);带参宏的优势带参宏在预处理阶段完成文本替换,避免了函数调用的开销,适合高频简单计算和日志格式化等场景。可变与空参:调试开关的优雅姿势可变参宏的定义利用__VA_ARGS__和##__VA_ARGS__,可定义可变参宏,如DEBUG_PRINT(fmt,...),实现灵活的调试信息输出。#include<stdio.h>#defineDEBUG

//定义DEBUG宏,启用调试打印intmain(){

#ifdefDEBUG

//定义带可变参数的调试打印宏,支持类似printf的格式

//使用##__VA_ARGS__解决无参数时的多余逗号问题

#defineDEBUG_PRINT(fmt,...)printf("[DEBUG]"fmt,##__VA_ARGS__)

#else

//未定义DEBUG时,调试打印宏为空(不执行任何操作)

#defineDEBUG_PRINT(fmt,...)

#endif

//修复:字符串需用双引号(单引号用于单个字符)

DEBUG_PRINT("调试开始...\n");

//示例:带参数的调试打印

intx=2025;

DEBUG_PRINT("当前x的值:%d\n",x);

DEBUG_PRINT("调试结束...\n");

return0;}去掉这一行,不会输出调试信息运行结果示例:根据需要启用或禁用调试信息的输出。空参宏的用途空参宏在条件编译中常用作调试开关,通过定义或取消定义宏,控制调试代码的编译与否,实现零成本调试。03条件编译条件编译#if家族:编译期分支语句#if家族的作用通过#if、#else、#elif、#endif等指令,预处理器可以在编译前根据条件判断保留或删除代码块,实现代码的灵活裁剪。#if家族的应用常用于根据硬件版本、配置开关或编译器定义的宏,选择性地编译特定功能代码,减少目标文件体积,提高代码的可维护性。#if常量表达式//代码块1#else//代码块2#endifHARDWARE_VERSION==1版本

1的功能代码版本2

的功能代码#ifdef卫士:头文件多重防护01头文件保护的必要性同一头文件被多次包含会导致类型重复定义而编译失败,头文件保护机制通过#ifndef、#define、#endif组合避免这一问题。02头文件保护的实现在头文件开头使用#ifndef宏名、#define宏名,在结尾使用#endif,确保结构体、函数原型等只定义一次。03头文件保护的优势保证代码的模块化清晰,便于多人协作开发,避免因头文件重复包含导致的编译错误。示例:"头文件保护"或"包含保护"。#ifndefUTILS_H#defineUTILS_H//头文件内容intadd(inta,intb);intsubtract(inta,intb);#endif//UTILS_H头文件utils.h当第一次包含utils.h时,UTILS_H宏还未定义,因此会定义UTILS_H宏并包含头文件的内容。再次包含utils.h,预处理器会跳过头文件的内容,从而避免重复定义。​跨平台休眠:条件编译实战跨平台代码的挑战不同操作系统可能提供不同的函数接口,如Windows的Sleep和Linux的sleep,直接编写跨平台代码会导致编译错误。条件编译的解决方案通过条件编译指令,根据操作系统预定义的宏,选择性地包含对应的头文件和函数调用,实现跨平台代码的无缝适配。#include<stdio.h>//条件编译:判断当前系统环境#ifdef_WIN32

//Windows系统的预定义宏(编译器自动定义)#include<windows.h>//Windows需包含的头文件#elifdefined(__linux__)//Linux系统的预定义宏#include<unistd.h>//Linux需包含的头文件#else#error"不支持当前操作系统!"//不匹配时报错#endifintmain(){

printf("程序休眠3秒...\n");#ifdef_WIN32

Sleep(3000);//Windows休眠函数(参数为毫秒)#elifdefined(__linux__)

sleep(3);

//Linux休眠函数(参数为秒)#endif

printf("休眠结束!\n");

return0;}04文件包含文件包含包含路径:尖括号与双引号秩序包含路径的区别#include<文件名>//标准库头文件#include"文件名"//用户自定义头文件优先从系统目录查找文件,适合标准库优先从当前项目目录查找文件,适合自定义头文件。AI辅助(包含路径的应用):通过合理使用包含路径,可以确保项目中正确引用本地头文件或系统头文件,避免文件查找错误。

声明与实现分离:加法模块示例#ifndefMATH_UTILS_H#defineMATH_UTILS_H//声明函数intadd(inta,intb);intsubtract(inta,intb);#endif//MATH_UTILS_H(1)创建一个头文件math_utils.h,

包含函数声明。//包含头文件#include"math_utils.h"//实现函数intadd(inta,intb){returna+b;}intsubtract(inta,intb){returna-b;}(2)创建一个源文件math_utils.c,

包含函数定义。#include<stdio.h>#include"math_utils.c"intmain(){intresult=add(5,3);printf("5+3=%d\n",result);result=subtract(5,3);printf("5-3=%d\n",result);return0;}(3)在主程序中使用这些函数。示例:“文件包含”。模块化的优点:提高代码的可维护性和可读性,便于多人协作开发,同时避免函数体在头文件中导致的多重定义问题。

结构体共享:多文件复用同一类型示例:“文件包含实现结构体共享”。//防止头文件重复包含(避免结构体重复定义报错)#ifndefSTUDENT_H#defineSTUDENT_H//定义学生结构体(仅写1次)structStudent{

intid;

charname[20];

floatscore;};#endif(1)创建student.h(存储公共结构体)(2)main.c包含头文件使用结构体#include<stdio.h>//包含头文件,直接使用结构体#include"student.h"intmain(){

structStudents={101,"张三",90.5};

printf("学号:%d,姓名:%s\n",s.id,);

return0;}(3)func.c包含头文件使用结构体#include<stdio.h>#include"student.h"//复用结构体定义,无需重复编写voidprintStu(structStudents){

printf("成绩:%.1f\n",s.score);}intmain(){

structStudents={102,"李四",98.5};

printStu(s);}结构体共享的需求:当多个源文件需要使用同一结构体时,直接复制粘贴会导致版本不一致和维护困难。结构体共享的解决方案:将结构体定义放入头文件并加卫式宏,任何源文件包含该头文件即可获得统一的类型描述,确保成员偏移一致。05本章小结预处理四板斧:宏·条件·包含·调试01预处理的核心机制预处理通过宏定义、条件编译、文件包含和调试支持等机制,实现代码的复用、裁剪和调试,为编译阶段提供清晰的输入。02预处理的应用价值掌握预处理命令后,可以在编译前阶段完成跨平台适配、调试信息注入和代码体积裁剪,提高代码的可移植性和可维护性。03预处理的后续衔接预处理是C语言编译过程的重要组成部分,为后续的Makefile、静态库、跨架构移植等高级主题打下坚实基础。感谢您的观看THANKYOUFORWATCHING汇报人:蒋亚平第10章

指针C语言的内存导航仪目录CONTENTS01指针的概念02指针变量03通过指针引用数组04字符串指针变量05指向指针的指针06指向函数的指针05指针综合编程案例06本章小结问题导入普通传参普通传参是把变量的“值副本”传入函数,函数修改的只是副本,不会影响原始变量指针传参指针传参传递的是变量的内存地址,函数能顺着地址直接操作原始变量的存储空间。那么,为何地址传递能突破“副本限制”?这两种传参方式的本质差异又该如何理解?01指针的概念指针的概念从门牌号到地址:指针生活化映射内存与指针的类比指针生活化映射将计算机内存比作宿舍楼,每个内存单元就像一个宿舍,拥有唯一的门牌号即地址。指针则相当于记录这些门牌号的纸条,通过它我们可以快速找到对应的内存单元。这种类比帮助初学者理解指针的基本概念,即指针是内存地址的代表。变量与指针的关系当定义一个变量a并赋值为10时,编译器会为其分配一个内存地址,例如1000。此时,指针p可以保存这个地址1000,通过指针p我们就能访问变量a的值。这种关系类似于通过门牌号找到对应的宿舍,从而获取或修改其中的内容。直接访问与间接访问直接访问变量就像亲自拿着钥匙打开宿舍门,而间接访问则先通过指针找到地址,再进行操作。例如,使用变量名a直接访问其值,或者通过指针p间接访问a的值。这两种访问方式在C语言中都很常见,理解它们的区别对于掌握指针至关重要。指针就是地址,地址指向内存单元从门牌号到地址:指针生活化映射内存用户数据区直接访问和间接访问的过程02指针变量指针变量指针变量定义与运算符指针变量的声明在C语言中,指针变量的声明形式为:取地址与解引用运算符取地址运算符'&':用于获取变量的内存地址。解引用运算符'*':用于通过指针访问其指向的内存单元中的数据。指针变量的声明必须明确其指向的数据类型,以便正确地进行内存操作。类型名*指针变量名示例:声明一个指向整型数据的指针变量p。inta=10;int*p;p=&a;//把变量a的地址赋值给指针变量p*表示这是一个指针示例:通过指针变量p来访问变量a的值。printf("%d",*p);//输出结果为10,与printf("%d",a);效果相同指针变量还可以在定义时进行初始化。int*p=&a;例如'&a'得到变量a的地址。例如'*p'访问指针p所指向的变量。指针变量定义与运算符给指针变量赋值p=&a;//

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

最新文档

评论

0/150

提交评论