目录
一、函数的定义和函数的分类
1. 函数的定义
说到函数,在数学中,我们会想到函数的自变量和因变量
其中在维基百科中对函数的定义为:子程序
(1)在计算机中,子程序,是一个大程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
(2)一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。
2. 函数的分类
(1)库函数
(2)自定义函数
二、库函数和自定义函数
1. 库函数
(1) 什么是库函数
在我们平时学习C语言时,经常会用到具有一些功能的函数,比如平时想要打印结果到屏幕上的 printf (格式化输出函数,最后一个字母f format),获取键盘的输入 scanf,计算字符串长度的 strlen.
像上面完成一定基础功能的,它们不是业务性的代码,都是平时我们每个程序员又经常使用的。
所以为了提高我们平时的工作效率,C语言基础库中提供了一些类似的库函数,方便程序员进行软件开发。
库函数一般是C语言标准规定好,由编译器厂家提供实现。
(2)如何学习库函数?
强烈推荐
首先,我们先进 www.cplusplus.com 进去后点击 REFERENCE 如图
在C Library 中 会看到,我们经常使用的 stdio.h 这样的头文件等
点击进去,在左侧栏中会看到一些常用的函数,
继续点击可以看到含函数的详解,如图
简单对库函数总结:其中常见的库函数:
- IO函数
- 字符操作符函数
- 字符串操作符函数
- 内存操作函数
- 数学函数
- 时间/日期函数
- 其他函数
注意:使用库函数,一定要包含 #include 对应的头文件 ,一定不要忘记了
2. 自定义函数
自定函数和函数一样,有函数名,返回值类型 ,函数参数
函数的定义形式
ret_type fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
类型说明符 函数名( 形参类型 参数1,形参类型 参数2,......)
{
stamens;
//语句项
}
(1)【举个例子】
写一个可以在两个数中找出最大值的函数。
#include <stdio.h>
//get_max函数的设计
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;
}
(2) 再来个例子【重点来了】
写一个可以交换两个整型变量的函数
//实现成函数,但是不能完成任务
void Swap1(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
注意代码有问题,存在对参数理解的错误,正确代码请看下方
#include <stdio.h>
//正确的版本
void Swap2(int* px, int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap2(&num1, &num2);
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
接下来函数的参数详细讲解参数的问题
三、函数的参数
1. 实际参数(实参)
实参:真实传给函数的参数(也就是数学中的自变量)。
参数可以是:常量,变量,表达式,函数等。
但是,在进行函数调用的时候,它们都必须有确定的值,以便传给形参。
在上述的求和代码
2. 形式参数(形参)
形式参数 是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。
形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
总结:形参实例化后相当于实参的的一份临时拷贝
四、 函数的调用
1. 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
当我们写一个函数的时候,写一个打印 两数相加函数 add(),那么函数调用需要两个数值,这里比如传 3和5的和 ,那么add(3,5)
2. 传址调用
(1)传址调用 是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
(2)这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操
作函数外部的变量。
所以上述 那个交换两个数的代码,函数需要传址调用,才能修改外部变量的值
(3)函数练习
1. 写一个函数可以判断一个数是不是素数。
#include <stdio.h>
int Is_prime(int n)
{
int i = 0;
for (i = 2; i < n;i++)
{
if (n%i == 0)
{
return 0;//不是素数返回0
}
}
return 1;//for循环完成后,如果是素数返回1
}
int main()
{
int n = 0;
scanf("%d",&n);
if (Is_prime(n))
printf("%d is prime", n);
else
printf("%d is not prime", n);
return 0;
}
素数就是只能被1和它本身整除的正整数,那么能被其他数(不包括1和它本身)整除就不是素数,拓展一下,一个数被其他数整除,
比如 16 ,16 = 2 * 8; 16 = 4 * 4; 16 = 8 * 2; 这里可以看出一个数不是素数要计算2,4,8
那木其实只需计算到 ,因为 2*8 与 8*2 一个满足 那么就不是素数。就 i <= 一个数的开平方,i<= 。根据此原理下方代码为
求素数方法二
#include <stdio.h>
#include <math.h>
int Is_prime(int n)
{
int i = 0;
for (i = 2; i <=sqrt(n); i++)
{
if (n % i == 0)
{
return 0;
}
}
return 1;
}
int main()
{
int n = 0;
scanf("%d", &n);
if (Is_prime(n))
printf("%d is prime", n);
else
printf("%d is not prime",n);
return 0;
}
2. 写一个函数判断一年是不是闰年。
#include <stdio.h>
int is_leap_year(int y)
{
if (y % 4 == 0 && y % 100 != 0 || y % 400 == 0)
return 1;
else
return 0;
}
int main()
{
int y = 0;
scanf("%d",&y);
if (is_leap_year(y))
printf("%d is leap year\n", y);
else
printf("%d is not leap year\n",y);
return 0;
}
普通闰年是指公历年份是4的倍数的且不是100的倍数,世纪闰年则必须是400的倍数。
3. 写一个函数,实现一个整形有序数组的二分查找。
#include <stdio.h>
int binary_search(int arr[],int num ,int sz)
{
int left = 0;
int right = sz - 1;
int mid = 0;
while (left<=right)
{
mid = (left + right) / 2; //可以优化为mid = left+(right-left)/2;
if (arr[mid]>num)
{
right = mid - 1;
}
else if (arr[mid]<num)
{
left = mid + 1;
}
else
{
return mid;
}
}
return -1;
}
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int num = 0;
int sz = sizeof(arr) / sizeof(arr[0]);//数组元素的个数
scanf("%d",&num); //输入一个要查找的数
int ret = binary_search(arr, num, sz);
if (ret != -1)
printf("找到了,该数下标为:%d",ret);
else
printf("没有找到 !");
}
4. 写一个函数,每调用一次这个函数,就会将 num 的值增加1。
#include <stdio.h>
void add(int * p)
{
*p = *p + 1;
}
int main()
{
int num = 0;
add(&num);//调用第一次,调用传的是地址
printf("%d\n",num);
add(&num);//第二次
printf("%d\n",num);
add(&num);//第三次
printf("%d\n",num);
return 0;
}
五、 函数的嵌套调用和链式访问
1. 函数的嵌套调用
函数与函数之间可以相互调用的,注意 函数可以嵌套调用,但是不能嵌套定义
#include<stdio.h>
void test3()
{
printf("666\n");
}
void test1()
{
int i = 0;
while (i < 3)
{
test3();
i++;
}
}
int main()
{
test1();
return 0;
}
2. 链式访问
把一个函数的返回值作为另一函数的参数
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
//注:printf函数的返回值是打印在屏幕上字符的个数
return 0;
}
结果是:4321
六、 函数的声明和定义
1. 函数声明
#include<stdio.h>
int add(int x, int y);
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数
声明决定不了。 - 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
2. 函数定义
函数的定义是指函数的具体实现,交待函数的功能实现。
//函数的实现
int add(int x,int y)
{
return x + y;
}
七、函数递归和迭代
1. 什么是递归?
递推+回归
程序调用自身的编程技巧称为递归
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接
调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略。只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。核心思想:大事化小
2. 递归的两个必要条件
- 有限制条件,条件满足限制条件时,递归停止
- 每一次递归后,都接近这个限制条件
【例】(1)接受一个整型值(无符号),按照顺序打印它的每一位
#include <stdio.h>
void print(int n)
{
if (n>9) //限制条件
{
print(n/10); //每次递归越来越接近这个限制条件
}
printf("%d ",n%10); //打印n 的每一位
}
int main()
{
int n = 0;
scanf("%d", &n);
print(n);
return 0;
}
图解 其中 5 -> 6 -> 7 -> 8 是打印顺序 2 0 2 3
【例】(2)编写函数不允许创建临时变量,求字符串的长度
#include <stdio.h>
int Strlen(char* str)
{
if (*str == '\0')
return 0;
else
return 1 + Strlen(str+1);//如果还有字符就继续递归判断下一个字符
}
int main()
{
char *str = "abc";
int len = Strlen(str);
printf("%d\n",len);
return 0;
}
【图解】按照箭头和步骤耐心看,会有很大收获
3. 递归与迭代
迭代也可以理解为循环
【例】求第n个斐波那契数(不考虑溢出)
斐波那契数 1 1 2 3 5 8 13 21 ...... 第1,2个数为1,后一项等于前两项的和
下方代码是 用递归方式实现
#include <stdio.h>
int fib(int n)
{
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);
return 0;
}
但是这样写会有一定的问题,那就是求第50以后的斐波那契数计算的时间很长,再去求一个非常大的数可能会发生 stack overflow 即栈溢出。
因为系统分配给程序的栈空间是有限的,容易出现死循环(死递归),这样有可能导致一
直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
解决上述问题可以 使用
- 递归改为非递归
- 使用static对象替代 nonstatic 局部对象。
下方是求 非递归 方式实现
#include<stdio.h>
int fib(int n )
{
int a = 1;
int b = 1;
int c = 1;
while (n>2)
{
c = a + b;
a = b;
b = c;
n = n - 1;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib(n);
printf("%d\n",ret);
return 0;
}
所以平时 如果递归很容易想到,没有明显的bug时,可以使用递归。