前言:本篇文章是对C语言的函数的进一步理解,博主在这里抛砖引玉,希望与各路大佬多多交流,如有不妥及错误,望大家能指正和提出意见
函数
1. 什么是函数
数学中我们常见到函数的概念。但是你了解C语言中的函数吗?
百度百科对函数定义:函数
计算机的函数,是一个固定的一个程序段,或称其为一个子程序,它在可以实现固定运算功能的同时,还带有一个入口和一个出口,所谓的入口,就是函数所带的各个参数,我们可以通过这个入口,把函数的参数值代入子程序,供计算机处理;所谓出口,就是指函数的函数值,在计算机求得之后,由此口带回给调用它的程序。
2. C语言中函数的分类
- 库函数
- 自定义函数
2.1 库函数:
百度百科对库函数定义:库函数
库函数(Library function)是将函数封装入库,供用户使用的一种方式。方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。调用的时候把它所在的文件名用#include<>加到里面就可以了。一般是放到lib文件里的。
为什么会有库函数?
1 . 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf)。
2 . 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
3 . 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。
C语言中把常用的功能进行了封装,封装成一个个函数,提供出来,大家都可以使用。
库函数举例:
scanf
printf
strlen,strcmp
rand,srand
time
那怎么学习库函数呢?
我们可以通过下列网站来学习:
cplusplus:https://legacy.cplusplus.com/
cppreference: https://en.cppreference.com/w/
我们用cplusplus来看看:
这么多库函数不需要全部记住,会学习方法就行,
我们可以看到这些网站都是全英文的,所以英语很重要!
简单的总结,C语言常用的库函数都有:
- IO函数
- 字符串操作函数
- 字符操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
C语言并不去直接实现库函数
而是提供了C语言标准和库函数的约定
scanf 功能、名字、参数、返回值
库函数的实现一般是由编译器去实现的。
我们参照文档,学习几个库函数:
strcpy
char * strcpy ( char * destination, const char * source );
void * memset ( void * ptr, int value, size_t num );
使用库函数,必须包含#include对应的头文件。我们可以通过对照文档来学习各种库函数。
来演示一下学习方式
在函数界面我们可以获取到这些信息
下面还有使用实例
通过学习我们就学会使用strcpy函数啦
代码示例:
#include <stdio.h>
#include<string.h>
int main()
{
char arr1[] = "hello world";//源头
char arr2[20] = "xxxxxxxxxxxxxx";//目的
//对于数组,数组名其实就是数组首元素的地址,也就是起始地址
strcpy(arr2, arr1);
printf("%s", arr2);
return 0;
}
通过调试,我们发现它把\0也复制了过来
打印hello world
2.2 自定义函数
如果库函数能干所有的事情,那还要程序员干什么?
所以自定义函数更加重要。
自定义函数和库函数一样,有函数名,返回值类型和函数参数。
但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间,能体现程序员的能力。
函数的组成
ret_type fun_name(para1, * )
{
statement;//语句项,函数功能实现
}
//ret_type 返回类型
//fun_name 函数名
//para1 函数参数
由上面库函数我们可以知道函数的组成,这些返回类型,函数名,函数参数都是设计好的
而我们自定义函数要求我们自己设计这三部分,并且设计函数的功能。
代码示例:
写一个函数可以交换两个整形变量的内容。
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
//输入
scanf("%d%d", &a, &b);
//计算
int t = 0;
t = a;
a = b;
b = t;
//输出
printf("交换后:%d %d",a,b);
return 0;
}
我们尝试用用函数来完成
#include <stdio.h>
//实现成函数,但是不能完成任务
//x,y是形式参数——形参
void Swap1(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
int main()
{
int num1 = 0;
int num2 = 0;
scanf("%d%d", &num1, &num2);
printf("交换前:%d %d", num1, num2);
//这里的num1,num2是实际参数 —— 实参
//当实参传递给形参的时候
//形参是实参的一份临时拷贝
//所以对形参的修改不会影响实参
Swap1(num1, num2);
printf("交换后:num1 = %d num2 = %d\n", num1, num2);
return 0;
}
发现没有进行交换
我们发现num1,num2确实给x,y传参了,x和y拥有自己的空间,x和y在函数内部确实发生交换了,但它们的交换与a,b无关
#include <stdio.h>
//正确的版本
void Swap2(int* px, int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 0;
int num2 = 0;
scanf("%d%d", &num1, &num2);
printf("交换前:num1 = %d num2 = %d\n", num1, num2);
Swap2(&num1, &num2);
printf("交换后:num1 = %d num2 = %d\n", num1, num2);
return 0;
}
我们简单地分析一下:
3. 函数的参数
上面我们做了一些铺垫,这里我们在详细地看一下。
3.1 实际参数(实参):
真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形
参。
3.2 形式参数(形参):
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内
存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数
中有效。
上面 Swap1和Swap2函数中的参数 x,y和px,py都是形参。
在main函数中传给Swap的num1 ,num2 和传给Swap2函数的&num1 ,&num2是实参。
从上图我们可以看到Swap2函数在调用的时候,x ,y 拥有自己的空间,同时拥有了和实参一模一样的内容。
所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。
4.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
4.2 传址调用
传址调用是把函数外部创建地变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外部的变量建立起真正的联系,也就是函数内部可以直接操
作函数外部的变量。
通过形参的指针就能够访问到函数外面的变量,并进行操作。
我们对上面的代码再进行一次分析
代码整理如下:
#include <stdio.h>
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 num1 = 0;
int num2 = 0;
scanf("%d%d", &num1, &num2);
printf("交换前:num1 = %d num2 = %d\n", num1, num2);
Swap1(&num1, &num2);
printf("交换后:num1 = %d num2 = %d\n", num1, num2);
printf("交换前:num1 = %d num2 = %d\n", num1, num2);
Swap2(&num1, &num2);
printf("交换后:num1 = %d num2 = %d\n", num1, num2);
return 0;
}
5. 函数的嵌套调用和链式访问
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。这就有了嵌套调用和链式访问
5.1 嵌套调用
#include <stdio.h>
void new_line()
{
printf("heihei\n");
}
void five_line()
{
int i = 0;
for(i=0; i<5; i++)
{
new_line();//嵌套调用
}
}
int main()
{
five_line();
return 0;
}
可以嵌套调用,不可以嵌套定义。
//错误示例
void new_line()
{
void five_line()//嵌套定义
{
int i = 0;
for(i=0; i<5; i++)
{
new_line();
}
}
printf("heihei\n");
}
//这样的代码是不可行的
5.2 链式访问
把一个函数的返回值作为另外一个函数的参数。
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
int ret = strlen(strcat(arr,"world"));
//这里就把strcat的返回值作为了strlen的参数
//这就是链式访问
printf("%d\n", ret);
return 0;
}
再来看一下这段代码:
#include <stdio.h>
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 42)));
//结果是什么?
//注:printf函数的返回值是打印在屏幕上字符的个数
//结果是先打印42再打印'42'这个字符个数是2,再打印'2'这个字符的个数就是1
//所以结果为4221
return 0;
}
6. 函数的声明和定义
6.1 函数声明:
1 . 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数
声明决定不了。
2 . 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3 . 函数的声明一般要放在头文件中。
当我们把函数写到主函数下面时,需要进行函数声明,否则代码不能编译
//这两种声明方式都可以
//int Mul(int, int);
//int Mul(int x, int y);
int main()
{
int a = 0;
int b = 0;
//输入
scanf("%d %d", &a, &b);
//乘法
int c = Mul(a, b);//函数调用
//打印
printf("%d\n", c);
return 0;
}
int Mul(int x, int y)
{
return x * y;
}
实际上函数的定义和声明通常并不是这样用的,函数的声明通常放在头文件中,这就涉及到分文件写程序了。
6.2 函数定义
函数的定义是指函数的具体实现,交待函数的功能实现。
.h文件的内容
放置函数的声明:
#ifndef __TEST_H__
#define __TEST_H__
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__
.c文件的内容
放置函数的实现:
#include "test.h"
//函数Add的实现
int Add(int x, int y)
{
return x + y;
}
分文件写示例:
在实际工作中我们完成一个程序是要分工合作的,分模块写可提高协作效率,都写在一个文件很难进行修改,所以我们要重视分文件处理程序的能力。
7. 函数递归
7.1 什么是递归
程序调用自身的编程技巧称为递归(recursion)。
递归作为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归算法(英语:recursion algorithm)在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。绝大多数编程语言支持函数的自调用,在这些语言中函数可以通过调用自身来进行递归。
递归的主要思考方式在于:把大事化小
7.2 递归的两个必要条件
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续,没有限制条件容易造成死递归。
2.每次递归调用后越来越接近这个限制条件。
死递归:
看下列代码:
#include <stdio.h>
void print()
{
printf("666\n");
print();
}
int main()
{
print();
return 0;
}
结果报错:
结论:死递归会造成栈溢出
7.2.1递归示例
接收一个整型值(无符号),按顺序打印它的每一位。
例如:
输入:1234,输出 1 2 3 4.
参考代码:
#include <stdio.h>
void print(unsigned int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
int num = 1234;
print(num);
return 0;
}
递归讲解:
其在内存的情况简单讲解:
7.3 递归和迭代
我们来看这个题目:
求第n个斐波那契数。(不考虑溢出)
斐波那契数列:1 1 2 3 5 8 13 21 34 55 …
特点:前2个的数的和是第三个数
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
但是我们发现有问题;
使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
使用 fib 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
为什么呢?
我们发现 fib 函数在调用的过程中很多计算其实在一直重复。
如果我们把代码修改一下:
#include <stdio.h>
int count = 0;//全局变量
int fib(int n)
{
if (n == 3)
count++;
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
我们输出count,发现计算40的阶乘时,3重复计算了很多次
#include <stdio.h>
int count = 0;//全局变量
int fib(int n)
{
if (n == 3)//计算3被重复算了多少次
count++;
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib(n);
printf("%d\n", ret);
printf("%d", count);
return 0;
}
我们用迭代的方式来解决这个问题
//迭代写法
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;//返回c时,若n为1或2,则c直接赋值为1可得结果
//从第三个斐波那契数开始
while (n >= 3)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
这个方式里我们采用了不同算法
总结:
- 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
- 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
- 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
————————————————————————————————————————————
后记:本篇文章是数组的提高篇,文章多有不足,请各位大佬多多担待。