形参和实参
形参(形式参数):
函数定义时指定的参数,形参是用来接收数据的,函数定义时,系统不会为形参申请内存,只有当函数调用是,系统才会为形参申请内存,用于存储实际参数,并且当函数返回,系统会自动回收为形参申请的内存资源。(本质上所有函数都有return,只不过当我们的函数返回类型是void类型的时候,return是作为隐式的)
实参(实际参数):
函数调用时主调函数传递的数据参数(常量、符号常量、变量、表达式.,只要有确定的值),实参是传递的数据。
实参和形参必须类型相同。如果不同时,按赋值规定自动进行类型转换。
在C语言中,参数传递必须遵循单向值传递 ,实参只是将自身的值传递给形参,而不是实参本身。形参的值的改变不会影响实参。
实参与形参在内存中占据不同的内存空间,(尽可能实参和形参名称是一样的)
c
double fun(double a,double b);
{
return a + b;
}
int main()
{
int x = 12,y = 13;
int c = (int)fun(x,y);
// 通过案例:传参时-我们将int类型赋值给double类型,此时程序不报错,因为此时会发生自动类型转换(隐
式转换)
// 通过案例:返回时-我们将double类型赋值给int类型,此时将满足强制类型转换条件,需要我们手动转换
}
案例:
c
#include <stdio.h>
/**
* 需求:函数案例-输入两个整数,要求用一个函数求出最大值,并在主调函数输出次数
*/
int max(int x,int y)
{
return x > y ? x : y;
}
int main(int argc,char *argv[])
{
int a,b,c;
printf("请输入两个整数:\n");
scanf("%d,%d",&a,&b);
// 调用函数求最大值
c = max(a,b);
printf("%d,%d中的最大值是%d\n",a,b,c);
return 0;
}
函数的返回值
若不需要返回值,函数中可以没有return语句。
一个函数中可以有多个return语句,但任一时刻只有一个return语句被执行。
被调函数返回给主调函数的结果数据(可以是变量、常量、表达式.,只要是确定值就可以了)。
返回值类型一般情况下要和函数中return语句返回的数据类型保持一致,如果不一致,以函数定义时指定的返回类型为标准。
也就是返回值类型和实际返回值可以存在自动类型或者强制类型转换的关系。
案例1:
c
#include <stdio.h>
/**
* 需求:函数案例-输入两个整数,要求用一个函数求出最大值,并在主调函数输出次数
*/
int max(int x,int y)
{
if(x > y)
{
return x;// 一旦return,return后的代码将不再执行
}
return y;
}
int main(int argc,char *argv[])
{
int a,b,c;
printf("请输入两个整数:\n");
scanf("%d,%d",&a,&b);
// 调用函数求最大值
c = max(a,b);
printf("%d,%d中的最大值是%d\n",a,b,c);
return 0;
}
案例2:
c
/**
* 需求:函数案例-输入两个整数,要求用一个函数求出最大值,并在主调函数输出次数
*/
double max(int x,int y)
{
if(x > y)
{
return x;// 一旦return,return后的代码将不再执行
}
return y;
}
int main(int argc,char *argv[])
{
int a,b,c;
printf("请输入两个整数:\n");
scanf("%d,%d",&a,&b);
// 调用函数求最大值
c = (int)max(a,b);
printf("%d,%d中的最大值是%d\n",a,b,c);
return 0;
}
案例3
c
#include <stdio.h>
/**
* 需求:函数案例-输入两个整数,要求用一个函数求出最大值,并在主调函数输出次数
*/
int max(int x,int y)
{
double z;
z = x > y ? x : y;
return (int)z;// 将double类型转换为int类型,此时会强制转换,如果为了增加代码的可读性,我们可以
手动强转。
}
int main(int argc,char *argv[])
{
int a,b,c;
printf("请输入两个整数:\n");
scanf("%d,%d",&a,&b);
// 调用函数求最大值
c = max(a,b);
printf("%d,%d中的最大值是%d\n",a,b,c);
return 0;
}
函数的调用
函数的调用
① 函数语句:test(); int res = max(2,4);
② 函数表达式:4 + max(2,4);
③ 函数参数:printf("%d",max(2,4));
在一个函数中调用另一个函数具备以下条件
① 被调用的函数必须是已经定义的函数。
② 若使用函数库,应在本文件开头用#include包含。
③ 若使用自定义函数,自定以函数又在主调函数的后面,则应在主调函数中对被调用函数进行声明。
声明的作用是把函数名、函数参数的个数和类型等信息通知编译系统,以便于在遇到函数时,编译系统能正确识别函数,并检查函数调用的合法性。
函数声明
函数调用时,往往要遵循需要对函数进行声明。
完整的函数使用分为三部分:
先定义后使用 ,但如果我们对函数的调用操作出现在函数的定义之前,则
[函数声明]
函数定义
函数调用
函数声明的作用:
是把函数名、函数参数的个数和返回类型等信息通知给编译系统,以便于在遇到函数时,编译系统能正确识别函数,并检查函数调用的合法性。
错误演示(被调函数写在了主调函数之后):
c
// 主调函数
int main()
{
int c = add(12,13); // 此时编译会报编译错误,因为函数没有经过声明,编译器无法对此进行检查(无法
正确识别函数)
printf("%d\n",c);
}
// 被调函数
int add(int x,int y)
{
return x + y;
}
正确演示(被调函数和主调函数不分先后,需要增加被调函数的函数声明)
c
int add(int,int);
// 主调函数
int main()
{
int c = add(12,13); // 此时编译通过,
printf("%d\n",c);
}
// 被调函数
int add(int x,int y)
{
return x + y;
}
声明的方式:函数首部后加上分号;
c
void fun(int a);
函数首部后加上分号,可省略形参名,但不能省略参数类型。
c
void fun(int);
函数的嵌套调用
函数不允许嵌套定义,但允许嵌套调用。
c
void funa()
{
void funb() {}
}
// C语言不支持嵌套定义
嵌套调用:在被调函数内又主动去调用其他函数,这样的函数调用方式,称之为嵌套调用。
c
funa() {}
funb() {funa(); }
main() {funb();}
// 嵌套调用
案例1:
c
/**
* 需求:编写一个函数,判断给定的3~100正整数是否是素数,若是返回1,否则返回0
*/
#include <stdio.h>
// 定义一个函数,求素数
int sushu(int n)
{
int k,i,flag = 1;
// 素数:只能被1和自身整除的数,需要校验的是2~n-1
for(i = 2; i < n-1;i++)
{
if(n % i == 0)
{
flag = 0;
break;
}
}
return flag;
}
// 主函数
int main()
{
for(int i = 3; i <= 100;i++)
{
if(sushu(i)==1)
{
printf("%d是素数\n",i);
}
}
printf("\n");
return 0;
}
案例2:
c
#include <stdio.h>
// 函数声明
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); // 求a,b的最大值
// max = max_2(max,c); // 求a,b,c的最大值
// max = max_2(max,d); // 求a,b,c,d的最大值
// return max;
// 写法2
return max_2(a,b) > max_2(c,d) ? max_2(a,b) : max_2(c,d);
}
int main(int argc,char *argv[])
{
int a=12,b=44,c=33,d=16,result;
result = max_4(a,b,c,d);
printf("%d,%d,%d,%d中的最大值是%d\n",a,b,c,d,result);
return 0;
}
函数的递归调用
递归调用的含义:在一个函数中,直接或者间接调用了函数本身,就称之为函数的递归调用。
c
// 直接调用
a() → a();
// 间接调用
a() → b() → a();
a() → b() → .. → a();
递归调用的本质:
是一种循环结构,他不同于之前所学的while、do.while、for这样的循环结构,这些循环结构是
借助循环变量;而递归是利用函数自身实现循环结构,如果不加以控制,很容易产生死循环。
递归调用的注意事项:
- 递归调用必须要有出口,一定要终止递归(否则就会产生死循环)。
- 对终止条件的判断一定要放在函数递归之前(先判断,再执行)。
- 进行函数的递归调用。
- 函数递归的同时一定要将函数调用向出口逼近。
案例1:
c
/* 需求:递归案例-有5个人坐在一起,问第5个人多少岁?他说比第4个人大2岁。
问第4个人岁数,他说比第3个人大2岁。
问第3个人,又说比第2个人大2岁。
问第2个人,说比第1个人大2岁。
最后问第1个人,他说是10岁。请问第5个人多大。
> Created Time: 2024年12月06日 星期五 14时15分02秒
************************************************************************/
#include <stdio.h>
/**
* 求年龄的递归函数
* @param n 第几个人
*/
int age(int n)
{
// 存放函数的返回值,也就是年龄
int c;
if(n == 1)
{
c = 10;// 第1个人的年龄
}
else if(n > 1)
{
c = age(n-1)+2; // 当前这个人的年龄 = 上一个人的年龄 + 2
}
return c;
}
int main(int argc,char *argv[])
{
printf("%d\n",age(5));
return 0;
}
案例2:
c
#include <stdio.h>
/**
* 编写一个函数,用来求阶乘
* @param n 上限
*/
long fac(int n)
{
// 定义一个变量f,用来接收乘积
long f;
// 出口校验
if(n < 0)
{
printf("n的范围不能是0以下的数\n");
return -1;
}
else if(n == 0 || n == 1)
{
f = 1; // 出口
}
else
{
f = fac(n-1)*n;
}
return f;
}
int main(int argc,char *argv[])
{
int n;
printf("请输入一个整数:\n");
scanf("%d",&n);
printf("%d的阶乘结果是%ld\n",n,fac(n));
return 0;
}
关于递归:一般是倒着进(进的时候,由上限(n)到下限(1)),正着出(出的时候,由下限
(1)到上限(n)),
数组做函数参数
注意:
当用数组做函数的实际参数时,则形参应该也要用数组/指针变量来接收,但请注意,此次并不代表传递了数组中所有的元素数据,而是传递了第一个元素的内存地址(数组首地址),形参接收这个地址后,则形参和实参就代表了同一块内存空间,则形参的数据修改会改变实参的。这种数据传递方式我们可以称之为"引用传递"。
如果用数组做函数形式参数,那么我们提供另一个形参表示数组的元素个数。原因是数组形参
代表的仅仅是实际数组的首地址。也就是说形参只获取到了实际数组元素的开始,并未获取元素的
结束。所以提供另一个形参表示数组的元素个数,可以防止在被调函数对实际数组元素访问的越
界。
但有一个例外,如果是用字符数组做形参,且实际数组中存放的是字符串数据(形参是字符数组,实参是字符串)。则不用表示数组元素的个数的形参,原因是字符串本身会自动结束符\0。
案例
c
/**
* 需求:数组为参数案例-有两个数组a和b,各有10个元素,将它们对应元素逐个地相比(即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数组,并分别统计出两
个数组相应元素大于、等于、小于的个数。
*
*/
#include <stdio.h>
{11,22,33,44}
{05,11,22,54}
/* 定义一个函数,实现两个数的比较 */
int large(int x,int y)
{
}
int flag;// 用来存放比较结果
if(x > y) flag = 1;
else if(x < y) flag = -1;
else flag = 0;
return flag;
int main()
{
// 比较用的两个数组,循环变量,最大,最小,相等
int a[10],b[10],i,max=0,min=0,k=0;
printf("请给数组a添加十个整型数据:\n");
for(i = 0;i < sizeof(a)/sizeof(int);i++)
{
scanf("%d",&a[i]);
}
printf("\n");
printf("请给数组b添加十个整型数据:\n");
for(i = 0;i < sizeof(b)/sizeof(int);i++)
scanf("%d",&b[i]);
printf("\n");
// 遍历
for(i = 0;i < sizeof(a)/sizeof(int);i++)
{
if(large(a[i],b[i])==1)
{
max++;
}
else if(large(a[i],b[i])==0)
{
k++;
}
else
{
min++;
}
}
printf("max=%d,min=%d,k=%d\n",max,min,k);
return 0;
}
案例2:
c
#include <stdio.h>
/* 定义一个函数,用来求平均分 */
float 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 score_1[5] = {66,34,46,37,97};
float score_2[10] = {77,88,66,55,65,76,87,98,75,34};
printf("这个班的平均分是:%6.2f\n",avg(score_1,sizeof(score_1)/sizeof(float)));
printf("这个班的平均分是:%6.2f\n",avg(score_2,sizeof(score_2)/sizeof(float)));
return 0;
}
变量的作用域
引入问题
我们在函数设计的过程中,经常要考虑对于参数的设计,换句话说,我们需要考虑函数需要几个参
数,需要什么类型的参数,但我们并没有考虑函数是否需要提供参数,如果说函数可以访问到已定
义的数据,则就不需要提供函数形参。那么我们到底要不要提供函数形参,取决于什么?答案就是
变量的作用域(如果函数在变量的作用域范围内,则函数可以直接访问数据,无需提供形参)
变量作用域
概念:变量的作用范围,也就是说变量在什么范围有效。
变量的分类
根据变量的作用域不同,变量可以分为:
全局变量
局部变量
全局变量
局部变量
建议在全局变量定义时初始化,如果不初始化,系统回家昂全局变量初始化为
0(0 | \0 | 0.0)
使用全局变量的优缺点:
优点:
- 利用全局变量可以实现一个函数对外输出的多个结果数据。
- 利用全局变量可以减少函数形参的个数,从而降低内存消耗,以及因为形参传递带来的时间
消耗。
缺点:
- 全局变量在程序的整个运行期间,始终占据内存空间,会引起资源消耗。
- 过多的全局变量会引起程序的混乱,操作程序结果错误。
- 降低程序的通用性,特别是当我们进行函数移植时,不仅仅要移植函数,还要考虑全局变
量。 - 违反了"高内聚,低耦合"的程序设计原则。
总结:我们发现弊大于利,建议尽量减少对全局变量的使用,函数之间要产生联系,仅通过实
参+形参的方式产生联系。
作用域举例:
注意:
如果全局变量(外部变量)和局部变量同名,程序执行的时候,就近原则
c
int a = 10;
int main()
{
int i = 20;
printf("%d\n",a); // 10
for(int i = 0;i < 5; i++)
{
printf("i=%d ",i); // 0 1 2 3 4 就近原则
}
}
变量的生命周期
概念:变量在程序运行中的存在时间。
根据变量存在的时间不同,变量可分为静态存储方式和动态存储方式。
变量的存储类型
c
变量的完整定义格式:[存储类型] 数据类型 变量列表;
存储类型
auto
auto存储类型只能修饰局部变量,被auto修饰的局部变量是存储在动态存储区的。auto也是局
部变量默认的存储类型。
c
int a = 10; 等价于 auto int a = 10;
static
修饰局部变量:局部变量会被存储在静态存储区。局部变量的生命周期被延长,但是作用域不
发生改变。
修饰全局变量:全局变量的生命周期不变,但作用域被衰减。一般限制全局变量只能在本文件
内。
demo01.c
c
#include "demo01.h"
// 全局变量
static int fun_a = 10;
int fun1()
demo02.c
c
#include "demo01.h"
main()
{
// 此时fun_a就不能被其他文件访问
fun_a = 20;
}
c
#include <stdio.h>
void counter() {
static int count = 0;
printf("Count: %d\n", count);
count++;
}
int main() {
counter(); // 输出 Count: 0
counter(); // 输出 Count: 1
counter(); // 输出 Count: 2
return 0;
}
extern
外部存储类型:只能修饰全局变量,此全局变量可以被其他文件访问。相当于扩展了全局变量
的作用域。
extern修饰外部变量,往往是外部变量进行声明,声明该变量是在外部文件中定义的;不是变
量定义。
demo01.c
c
#include "demo01.h"
int fun_a = 10;
int fun1(){..}
demo02.c
c
#include "demo01.h"
// 声明外部文件的变量
extern int fun_a;
// 声明外部文件的函数
extern int fun1();
main()
{
fun_a = 20;
fun1();
}
register
寄存器存储类型:只能修饰局部变量,用register修饰的局部变量会直接存储到CPU的寄存器
中,往往将循环变量设
置为寄存器存储类型。
c
register int a = 10;
面试题
static
关键字的作用
- static修饰局部变量,延长其生命周期,但不影响局部变量的作用域。
- static修饰全局变量,不影响全局变量的生命周期,会限制全局变量的作用域仅限本文件内使用
(私有化); - static修饰函数:此函数就称为内部函数,仅限本文件内调用(私有化)。
{...}
值传递与指针传递
值传递
:单向传递,基本数据类型默认是通过值传递的,也就是传递的是数值,也就是内存空
间只能被当前变量独享。
指针传递
:通过指针可以实现类似引用传递的效果,即允许函数内部修改实参的值,传递的是
地址,也就是内存空间可以被多个变量共享。
引用传递
:C语言本身不支持引用传递的语法,但可以通过指针来实现类似的功能。 引用传递
意味着在函数调用时,会将实参的引用(即一个别名)传递给形参。这样,函数内部对形参的
任何修改都会直接影响到实参。然而,C语言本身并不支持引用传递这种语法。在其他一些编程
语言(如C++、Java、Python等)中,引用传递是原生支持的。
案例:
c
// 值传递(整型、浮点型、字符型..)
fun(int x)
{
printf("%d\n",x); // x = 10
x = 20;
}
// x = 20
main()
{
int a = 10; // a = 10
fun(a);
printf("%d\n",a);// a = 10
}
}------------------------------------------------------------------------------------
// 地址传递(数组、指针、结构体..)
fun(int x[10])
{
printf("%d\n",x[9]);// x[9] = 0
x[9] = 20;
// x[9] = 20
main()
{
int a[10] = {1,2,3};
fun(a);
printf("%d\n",a[9]);// a[9] = 20
}
内部函数和外部函数
内部函数:使用static修饰的函数,称作内部函数,内部函数只能在当前文件中调用。
外部函数:使用extern修饰的函数,称作外部函数,extern是默认的,可以不写(区分环境),
也就是说本质上我们缩写的函数基本上都是外部函数,建议外部函数在被其他文件调用的时
候,在其他文件中声明的时候,加上extern关键字,主要是提高代码的可读性。