什么是函数?
函数(function)是完成特定任务的独立程序代码单元。语法规则定义了函数的结构和使用方式。虽然C中的函数和其他语言中的函数、子程序、过程作用相同,但是细节上略有不同。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
C语言中函数的分类
1、库函数:
我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁地使用一个功能:将信息按照一定的格式打印到屏幕上(printf);
在编程的过程中我们会频繁地做一些字符串的拷贝工作(strcpy);
在编程时我们也计算,总是会计算n的k次方(pow)。
为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
C语言的库函数非常的多,我们的目的是学会查询工具来掌握库函数的使用。下面,正是官网来帮助我们来学习。
https://cplusplus.com/reference/clibrary/
2、自定义函数
更加重要的是自定义函数,使我们自己来设计,这给程序员一个很大的发挥空间。自定义函数和库函数一样,有函数名,返回值类型和函数参数。
典型的ANSI C 函数的定义形式为:
返回类型 名称 (形参声明列表)
函数体
形参声明列表是用逗号分隔的一系列变量声明。除形参变量外,函数的其他变量均在函数体的花括号之内声明。
ret_type fun_name(para1, * )
{
statement; //语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
下面,从一个简单的程序示例来深入学习:
#include
#define NAME "GIGATHUNK, INC."
#define ADDRESS "101 Megabuck Plaza"
#define PLACE "Megapolis, CA 94904"
#define WIDTH 40
void starbar(void); //函数原型
int main(void) //使用函数
{
starbar();
printf("%s\n", NAME);
printf("%s\n", ADDRESS);
printf("%s\n", PLACE);
starbar();
return 0;
}
void starbar(void) //定义函数
{
int count;
for (count == 1; count <= WIDTH; count++)
putchar('*');
putchar('\n');
}
该程序由main()和starbar()组成。
1、函数原型(function prototype)负责告诉编译器函数 starbar( )的类型; void starbar (void) ;
圆括号表明 starbar 是一个函数名。第1个 void 是函数类型,void 类型表明函数没有返回值。第2个void(在圆括号中的)表明该函数不带参数。分号表明这是在声明函数,不是定义函数。也就是说,这行声明了程序将使用一个名为starbar()、没有返回值、没有参数的函数,并告诉编译器在别处查找该函数的定义。
一般而言,函数原型指明了函数的返回值类型和函数接受的参数类型。这些信息称为该函数的签名(signature)。
声明函数时必须声明函数的类型。带返回值的函数类型应该与其返回值类型相同,而没有返回值的函数应声明为void类型。要正确的使用函数,通常的做法是提前声明函数,把函数的信息告知编译器。
2、调用函数
接下来,在main()中,调用了starbar()函数;
starbar( );
这是调用void类型函数的一种形式。当计算机执行到starbar();语句时,会找到该函数的定义并执行其中的内容。执行完starbar()中的代码后,计算机返回主调函数继续执行下一行。
3、定义函数
首先函数头包括函数类型、函数名和圆括号,接着是左花括号、变量声明、函数表达式语句,最后以右花括号结束。注意,这里的starbar()后面没有分号。
同样,通过一个例子来学习函数参数中形式参数和实际参数。
#include
void Swap1(int x, int y) //错误版本
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
void Swap2(int *px, int *py) //正确版本
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int x = 1;
int y = 2;
Swap1(x, y);
printf("Swap1:x = %d y = %d", x, y);
Swap2(&x, &y);
printf("Swap2:x = %d y = %d", x, y);
return 0;
}
很显然,这是一个交换两个整型变量的函数,那为什么Swap1是行不通的呢?
函数的参数分为实际参数(实参)和形式参数(形参)
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因为形式参数只有在函数中有效。
上面Swap1和Swap2函数中的参数x, y, px, py,都是形式参数。在main函数中传给Swap1的x,y和传给Swap2函数的&x, &y是实际参数。
调用带实参的函数
Swap1(x , y);
这里,实参是x=1 和y=2.这两个值被赋给void Swap1(int x, int y)中相应的形参。和定义在函数中的变量一样,形参也是局部变量,属该函数私有。这意味着在其他函数中使用同名变量不会引起名称冲突。简而言之,形参是被调函数中的变量,实参是主调函数赋给被调函数的具体值。
这里Swap1函数在调用的时候,x , y 拥有自己的空间,同时拥有了和实参一模一样的内容。这里进行的就是传值调用,函数的形参和实参分别占有不同内存快,实参传递给形参的时候,形参是实参的一份临时拷贝,对形参的修改不会影响实参。
Swap2(&x, &y);
而这里是进行了传址调用,传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外部的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
这就是为何第二个函数可以完成交换,远程交换了地址。
函数的嵌套调用和链式访问
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用。函数可以嵌套调用,但是不能嵌套定义。
把一个函数的返回值作为另一个函数的参数就是链式访问。
#include
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
想要看懂上面的代码,还得先知道printf函数的返回值是打印在屏幕上字符的个数。
先打印第三个printf,打印的是43,43是两个字符,返回2,于是第二个printf打印的2,2是一个字符,于是返回1,接着打印1。于是最后呈现的就是4321了。
这就是把printf函数的返回值作为下一个printf函数的参数的运用。
C允许函数调用它自己,这种调用过程称为递归。只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。结束递归是使用递归的难点,因为如果递归代码中没有终止递归的条件测试部分,一个调用自己的函数会无限递归。递归的主要思考方式在于:把大事化小。
递归的两个必要条件:
存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件。
求n的阶乘,就可以用非递归和递归的两种方法来实现
int factorial(int n)
{
int result = 1;
while (n > 1)
{
result *= n;
n -= 1;
}
return result;
}
int factorial(int n)
{
if (n <= 1)
return 1;
else
return n * factorial(n - 1);
}
第二个代码就是用递归来求n的阶乘,除了1以外n!=1,n!都是等于n*(n-1)。那么,就可以让函数调用自己来相乘。
提示:
许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。