C 语言的函数

函数概述

函数的作用:

  • 执行某些动作
  • 计算得到一个值

一个函数可以同时或者单独具有上述两个作用。

函数是C程序的基本模块。

函数声明和函数原型

函数原型是一种声明形式,告知编译器正在使用某函数,因此函数原型也被称为函数声明。函数原型还指明了函数的属性。

ANSI C 之前的函数声明要求指明函数返回值的类型,不要求指明形参的类型,即:

函数返回值类型 函数名(); // 括号中为空即可。

ANSI C新增了函数原型,可以直接复制函数定义中的函数头,但需要用分号结尾,即:

函数返回值类型 函数名(形参类型 形参名1, 形参类型 形参名2, ... , 形参类型 形参名n);

函数原型中的形参名是假名,可以省略,也可以随意命名而与函数定义的函数头中的函数名不相同也是可以的,即:

函数返回值类型 函数名(形参类型, 形参类型 , ... , 形参类型);

或:

函数返回值类型 函数名(形参类型 形参名1', 形参类型 形参名2', ... , 形参类型 形参名n');

函数原型指明的信息有:函数返回值的类型,形参数量,每一个形参的类型。

在函数原型中使用变量名并没有创建变量。

函数原型提供了函数返回值的类型和函数形参变量的类型,这些信息被称为函数的签名(signature)。

使用函数原型的目的就是让编译器在第一次使用函数之前就知道怎么使用这个函数。如果将函数定义放在函数调用之前,就不需要函数原型,此时函数定义就相当于函数原型。

一般都把 main() 函数放在所有函数最前面,因为 main() 提供了程序的框架。C 程序的编译是自上向下的,如果没有函数原型作为函数的声明,那么编译到函数名时将无法识别这个标识符。如果将函数定义放在函数调用前面,当然也可以不写函数声明,但这是不符合编程规范的。

C 标准建议,要为程序中用到的所有函数提供函数原型。标准 include 文件(包含文件)为标准库函数提供库函数原型。

函数原型也可以放在主调函数的里面而不一定非要是在主调函数的外面,只要函数原型在函数调用之前就行。

main() 被称为主函数,其他函数称为子函数,子函数除了可以被主函数调用外,子函数之间也可以互相调用。主函数只能被操作系统调用。

函数传参时,函数原型会覆盖 C 语言数据类型的自动数据类型转换。比如,char 和 short 类型实参传递给形参变量时会自动转换为 int 类型,但形参变量定义的类型会覆盖这一自动类型转换。如:

函数原型:

void fun(float a , float b);

调用函数 fun:

char ch = 'A';
short sh = 8;
fun(ch , sh);

这时 ch 不会被转为 int 而是转为 float,sh 不会转为 int 而是转为 float。

函数声明告知编译器函数的类型,函数定义则提供函数的代码。

函数定义

函数分为函数头和函数体。

函数头:

函数返回值类型 函数名(形参类型 形参名1, 形参类型 形参名2, ... , 形参类型 形参名n)

函数体:

{
// 语句
}

函数返回值类型即函数类型。

函数头中,函数没有返回值时,函数返回值类型处应为 void。函数没有形参时圆括号内应为 void。圆括号内定义了一个或多个形参,如果有同类型的,也必须在每一个形参变量名前加类型名并用逗号隔开,不能多个同类型的形参变量共用一个类型名称(就行普通变量的定义那样:int a , b , c)。函数头后面不带分号。

定义带形参的函数时,并没有创建形参变量,直到调用这个函数时,才创建了形参变量,同时初始化,即将实参赋值给形参变量。在函数头中定义了形参就是形参的声明。

void fun(int a , int b , int c) // 正确的函数头
void fun(int a , b , c) // 错误的函数头

函数如果有返回值,那么类型以函数头中定义的类型为准。如:

int fun(void);
int main(void)
{
printf("%d\n" , fun());
return 0;
}
int fun(void)
{
float a = 1.0f;
int b = 2;
float c;
c = a / b;
return c;
}

结果:

0

这里 c 求出来被应该是 0.5,但被赋值给 int 类型,截断之后为 0。

函数之间的通信

带形参的函数要和主调函数建立通信,没有形参的函数不需要和主调函数之间建立通信。

一般而言,被调函数都是由主调函数调用的,被调函数如果有返回值,则该返回值返回给主调函数使用。但是 main 函数是由操作系统调用的,main 函数有返回值,该返回值返回给操作系统。

信息从主调函数到被调函数:传参

信息从被调函数到主调函数:return 语句

形参是局部变量,属于该函数私有。函数传参就是给形参赋值。

实参可以是常量、变量或表达式等等,首先要对实参求值,再将该值拷贝到形参中,形参中的值是实参的副本。所有无论被调函数对拷贝的数据做何种操作都不会影响主调函数中的原始数据。

如果函数没有返回值即函数头是 void,函数体可以没有 return 语句或者带这样的 return 语句:return;。这种 return 语句只有在 void 函数中才会用到。

return 语句只能传递一个值给主调函数,要想传回多个值,则需要用指针。

参数传递的机制根据系统的不同而不同。对于 PC,主调函数将实参拷贝一份,放进栈(stack)中,被调函数从栈中读取数据。栈是一个临时存储区。实参放进栈和形参从栈中读取数据这两个过程要匹配,否则出错。

实参根据自己的数据类型决定放进栈中的数据是什么类型。被调函数以形参的数据类型从栈中读取数据。

使用 ANSI C 之前的函数声明时,实参只会进行自动类型转换,如 short、char 变为 int,float 变为 double 等。这样实参拷贝之后保存在栈中的备份的类型未必和形参的类型一致,如果不一致,形参读取数据时将发生错误。也不会检查实参数量是否和形参相等。

函数原型提供的信息有:函数返回值的类型,形参数量,每一个形参的类型。使用函数原型时,实参将根据函数原型提供的信息检查实参数量和形参是否相等,并将实参的类型转换为形参的类型,于是栈中存储的实参的副本的类型未必和实参本身类型相同,但一定和形参的类型相同,于是形参从栈中读取数据时格式一定是匹配的。这里的数据类型转换属于自动类型转换。

对支持ANSI C的编译器来说,

int fun();

被视为没有使用函数原型,将不会检查参数。为明确表明确实函数没有参数,应该使用 void,即:

int fun(void);

此时编译器认为使用了函数原型,此函数没有参数,函数调用时会进行检查,以确保没有参数。

对于形参数目可变的函数,如 printf() 和 scanf() 等,ANSI C 允许使用部分原型,C 库通过 stdarg.h 头文件提供了一个定义形参数量不固定的函数的标准方法。

比如 printf() 函数的函数原型是:

int printf(const char* , ...);

此函数原型表明,第一个参数是字符串,还可能有其他未指定的参数。

程序示例:

#include<stdio.h>
void pound(int n);
int main(void)
{
int times = 8;
char ch = '!';
float f = 6.78;
pound(times);
pound(ch);
pound(f);
return 0;
}
void pound(int n)
{
while (n-- > 0)
{
printf("#");
}
printf("\n");
}

结果:

########
#################################
######

分析:

当 执行语句 pound(ch); 时, 是将 char 类型的值赋给 int 类型的值, 因为有函数原型的存在, 会指定要进行这一转换, 但是即便没有函数原型, 也会自动将 char 转换为 int. 这里的pound(ch);相当于pound((int)ch);,即传参时的类型转换相当于强制类型转换。

当 执行语句 pound(fl); 时, 是将 float 类型的值赋给 int 类型的值, 因为有函数原型的存在, 会指定要进行这一转换, 但是如果没有函数原型, 则会出错. 这里的pound(f);相当于pound((int)f);,即传参时的类型转换相当于强制类型转换。

没有函数原型或者函数原型没有指定形参的类型时, float 自动被转换为 double, char 和 short 自动转换为 int.

函数的递归

函数调用自己称为递归。递归是函数调用的特殊情况。

递归的代码中要有可以终止递归的条件测试部分,否则将会无限制的递归下去。

可以使用循环的地方通常都可以使用递归。

示例:

// 递归示例
#include<stdio.h>
void up_and_down(int n);
int main(void)
{
up_and_down(1);
return 0;
}
void up_and_down(int n)
{
printf("level %d location : %p\n" , n , &n);
if (n<4)
up_and_down(n+1);
printf("LEVEL %d location : %p\n" , n , &n);
}

结果:

level 1 location : 0061FF10
level 2 location : 0061FEF0
level 3 location : 0061FED0
level 4 location : 0061FEB0
LEVEL 4 location : 0061FEB0
LEVEL 3 location : 0061FED0
LEVEL 2 location : 0061FEF0
LEVEL 1 location : 0061FF10

程序解释:main() 调用 up_and_down() 称为第一级递归,up_and_down() 调用 up_and_down() 自己称为第二级递归,接着第二级递归调用第三级递归,第三级递归调用第四级递归,等等。于是,第四级递归的主函数是第三级递归,第三级递归的主函数是第二级递归,第二级递归的主函数是第一级递归,即主调函数调用被调函数,此处被调函数即递归函数。

第四级递归调用结束时,控制权转移到主函数即第三级递归调用,第三级递归调用继续执行递归代码部分后面的代码。程序必须按顺序逐级返回递归。

每级递归调用都创建变量n(包括第一级递归调用),每级递归调用的变量n都是这一级私有,名称相同但值不同。每级递归创建的变量都存放在栈中。

递归函数中,位于递归调用之前的语句按照被调函数的顺序执行,位于递归调用之后的语句按照被调函数的相反顺序执行。

尾递归指:递归调用在递归函数的末尾,即刚好在 return 语句之前。

posted @   有空  阅读(16)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
点击右上角即可分享
微信分享提示