往期文章
- C语言:初识C语言
- C语言:分支语句和循环语句
目录
- 往期文章
- 前言
- 1. 函数是什么
- 2. 库函数
- 3. 自定义函数
- 4. 函数的参数
- 5. 函数的调用
- 6. 函数的嵌套调用和链式访问
- 6.1 函数嵌套调用
- 6.2 函数的链式访问
- 7. 函数的声明和定义
- 8. 函数的递归
- 后记
前言
今天来继续更新我们的C语言博客,目前还处于一鼓作气的阶段,干劲十足哈。希望能够把C语言的博客坚持更新完成。
今天我们介绍C语言的函数部分,闲言少叙,我们开始。
1. 函数是什么
数学中我们常见到函数的概念。但是在C语言中,我们也有函数(子程序)的概念。
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
C语言中函数的分类:
- 库函数
- 自定义函数
2. 库函数
为什么会有库函数?
- 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf)。
- 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
- 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
在这个网站中,我们可以学习库函数的用法。
cplusplus
但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。
关于库函数,我们先介绍到这里。接下来,我们介绍自定义函数。
3. 自定义函数
我们有了库函数,但库函数不是万能的,很多时候需要我们的自定义函数。
自定义函数和库函数一样,有函数名,返回值类型和函数参数。 但是不一样的是这些都是我们自己来设计。
我们来看一个自定义函数的基本结构:
我们来设计一个函数,使其可以找到两个数的最大值:
#include <stdio.h>
int get_max(int x, int y)
{
return (x>y) ? (x) : (y);
}
int main()
{
int num1 = 10;
int num2 = 20;
int max = get_max(num1, num2);
printf("max = %d\n", max);
return 0;
}
函数名,返回类型、参数、和函数体是一个函数的要素。参数可以没有,返回类型可以为空。
我们再来看一个交换两个数字的例子:
#include<stdio.h>
void Swap(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap(num1, num2);
printf("Swap::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
我们发现,我们的代码按道理是可以实现两个数字的交换,但是却没有实现数字交换,这是为什么呢?
但是,如果我们将代码修改一下:
#include<stdio.h>
void Swap(int *px, int *py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap(&num1, &num2);
printf("Swap::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
我们再来看一下运行结果:
交换成功。
这是为什么捏?这就需要我们了解一下函数的参数。
4. 函数的参数
函数的参数分为实参和形参两种。
实际参数(实参):
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数(形参):
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
在第一份代码中,我们传递的xy是形参,在第二份代码中,我们传递的&x&y是实参。形参实例化之后其实相当于实参的一份临时拷贝,并不能改变原来的内容。
如果我们把传参比作让张三到李四家去,传形参就好比我克隆了一个张三,到李四家后,李四把克隆的张三打了一顿破相了,但实际的张三并没有受伤。而传递实参,就是张三亲自到李四家去了,李四把张三打破相了,张三真受伤了。
5. 函数的调用
函数调用分为传值调用和传址调用。
传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
所以,如果我们不想要改变原来的参数,我们可以选择传值,如果我们要改变,我们可以选择传地址。两种调用是C语言考试中常考的知识点,也经常会马虎做错,大家一定要注意哦。(我搞错过好多次)
6. 函数的嵌套调用和链式访问
6.1 函数嵌套调用
虽然我们生活中禁止套娃,但是有些时候,套娃还是挺有意思的。在函数的调用中,我们是允许嵌套调用的。我们来看一个例子:
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for (i = 0; i<3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
嵌套调用成功。但是哈,我们在使用嵌套调用的时候,不要太张狂太随意的使用,有时候你套着套着自己就懵逼了,所以,仔细一点,按需使用。
但是大家一定要注意,函数可以嵌套调用,但不可以嵌套定义,一个函数定义在另一个函数里面是绝对不允许的。
6.2 函数的链式访问
链式访问指的是把一个函数的返回值作为另外一个函数的参数。我们来看一个例子:
#include<stdio.h>
int Add(int a, int b)
{
return a + b;
}
int Mul(int c, int d)
{
return c*d;
}
int main()
{
int a = 2;
int b = 3;
int c = 4;
int k = Mul(c, Add(a, b));
printf("%d\n", k);
return 0;
}
我们可以看到add函数的返回结果作为了mul函数的参数。
我们可以发现函数的链式访问是基于返回值实现的。
我们再来看一个有趣的例子:
#include<stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
这个很链式,那输出结果是什么呢?来看一下:
4321,如果你第一次接触这个问题,对这个结果一定会感到非常意外。
为什么会是这样呢?我们来分析一下:
我们知道,printf是库函数。其返回值是写入的字符总数,我们把这个问题搞清楚之后再来分析:
根据链式调用,最先打印出来的是43,而43是两个字符,所以接下来打印出的是2,而2是一个字符,所以接下来打印出来的是1.所以这道题屏幕上打印出来的是4321,其实如果我们用逗号分割一下的话是,43,2,1.
7. 函数的声明和定义
函数声明:
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,无关紧要。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
函数定义:
函数的定义是指函数的具体实现,交待函数的功能实现。
当我们用代码实现比较复杂的功能的时候,我们会采用分文件的形式,此时,我们通常在.h文件中声明函数,在.c中定义函数。
在一个文件中,函数的定义和声明也可以分离,要先声明,在定义,记住,声明函数时后面要加;
我们来看一下:
#include<stdio.h>
void Swap(int *px, int *py);
int main()
{
int num1 = 1;
int num2 = 2;
Swap(&num1, &num2);
printf("Swap::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
void Swap(int *px, int *py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
8. 函数的递归
什么是递归?
程序调用自身的编程技巧称为递归( recursion)。 递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在于:把大事化小。
递归的两个必要条件:
存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件。
递归是一个好东西,说白话就是不断重复调用自身直到得到想要的。
关于递归有一个十分经典的问题,斐波那契数列:
#include<stdio.h>
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
int main()
{
printf("%d\n",fib(8));
return 0;
}
递归其实也是一种函数的嵌套调用。
但是递归函数消耗是很大的。如果我们要求n很大的时候,时间会很长,我们可以采用如下两种方法解决这样的情况:
- 将递归改写成非递归。
- 使用static对象替代nonstatic局部对象。在递归函数设计中,可以使用static对象替代nonstatic局部对象(即栈对 象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
后记
好的,这篇博客就到这里了,这篇博客好像没有之前那么事无巨细了,博主这两天有些懒惰。博主将努力克服懒惰,请各位督促。