一、什么是函数?
维基百科中对函数的定义:子程序
1.在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
2.一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
二、函数的分类
1.库函数
1)库函数介绍
我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
注意:C语言本身并不直接去实现这些库函数,而是提供了C语言的标准和库函数的约定,库函数的实现一般是由编译器去实现的(vs2022,gcc)。使用库函数前,必须用。包含 #include 对应的头文件
2)常见库函数
C语言常用的库函数都有:
IO函数 (输入输出函数):scanf printf getchar putchar…
字符串操作函数:strlen strcmp…
字符操作函数:islower isupper…
内存操作函数:meset memcmp…
时间/日期函数:time…
数学函数:sqrt pow…
其他库函数
3) 函数的参数
实际参数(实参):
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数(形参):
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
4)strcpy函数
格式:char * strcpy ( char * destination, const char * source );
这个函数的功能:将源指向的 C 字符串复制到目标指向的数组中,包括终止的 null 字符(并在该点停止)。
点此查看strcpy函数
例一:运行前的代码
(通过监视不难看出每一个元素都在自己应该在的地址上)
运行后:
(arr2中的元素逐一被替代成arr1中的元素,并且打印arr2也可以看出strcpy的功能)
例二:运行前可以发现只是把例一中的arr2改变了
运行前:
运行后:
通过arr[11]可以看出这个取代也会把 \0给替代,也可以说是再把\0替代过去后,这个程序才停止。
5)memset函数
格式:void * memset ( void * ptr, int value, size_t num );
功能介绍:
由图可以看出memset的作用(将指向的内存块的第一个字节数设置为指定值)。
6)strlen函数
结构:size_t strlen ( const char * str );
功能:打印字符串长度。
2.自定义函数
如果库函数能干所有的事情,那还要程序员干什么?
所有更加重要的是自定义函数。
自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
1)比大小函数
例:设计一个比大小的自定义函数
#include<stdio.h>
#include<string.h>
int add(int x, int y)
{
return (x > y ? x : y);
}
int main()
{
int a = 5;
int b = 10;
int c = add(a, b);
printf("较大的值是%d", c);
}
输出的结果:
2)交换函数
例:设计一个交换常量的自定义函数
按照正常思维设计的函数
#include<stdio.h>
#include<string.h>
int swap(int x, int y)
{
int c = 0;
c = x;
x = y;
y = c;
}
int main()
{
int a = 5;
int b = 10;
printf("交换前 a = %d b = %d\n",a, b);
swap(a, b);
printf("交换前 a = %d b = %d", a, b);
}
由结果可以看出没没有完成交换
执行前:
执行后:
通过执行前后也是可以看出a和b的地址和数值都没发生改变
这是因为当实参传给形参的时候,形参只是实参的一份临时拷贝,所以形参数值的改变并不会影响实参的数值。
x,y这俩形参和实参a,b对应都不是同一块地址,所以不会改变。
改正后(传地址):
int swap(int* x, int* y)
{
int c = 0;
c = *x;
*x = *y;
*y = c;
}
int main()
{
int a = 5;
int b = 10;
int* pa = &a;
int* pb = &b;
printf("交换前 a = %d b = %d\n",a, b);
swap(pa, pb);
printf("交换前 a = %d b = %d", a, b);
}
可以看出此时就可以了
更简便的代码风格:
int swap(int* pa, int* pb)
{
int c = 0;
c = *pa;
*pa = *pb;
*pb = c;
}
int main()
{
int a = 5;
int b = 10;
printf("交换前 a = %d b = %d\n",a, b);
swap(&a, &b);
printf("交换前 a = %d b = %d", a, b);
}
3)两种调用方式
传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
总结:如果只是需要利用他的返回值的时候,函数只需要传值(数值)。但是当需要利用操作函数影响实参的时候就要利用到传址(地址和指针)。
三、扩展:内存
函数只有在调用的时候才会分配给其内存空间,不调用自然也不会分配给其空间。
还有一点要注意就是:机器看内存是看地址的,变量只是给程序员自己看的。
四、练习(用函数实现)
1.判断100~200间的每一个数是否为素数
本题核心就在:需要将满足素数条件的数找出来,并且需要使用自定义函数。
代码1:直接通过函数来实现输入的数字是否是素数
#include<stdio.h>
#include<math.h>
int find(int x)
{
int i1 = 0;
for (i1 = 2; i1 < sqrt(x); i1++)
{
if (x % i1 == 0)
{
printf("%d不是素数\n", x);
break;
}
}
if (i1 >=sqrt(x) )
{
printf("%d是素数\n",x);
}
}
int main()
{
int i = 0;
for (i = 100; i <= 200; i++)
{
find(i);
}
}
代码2:通过函数的返回值来实现他是否是素数
#include<stdio.h>
#include<math.h>
int find(int x)
{
int i1 = 0;
for (i1 = 2; i1 < sqrt(x); i1++)
{
if (x % i1 == 0)
{
return 0;
}
}
return 1;
}
int main()
{
int i = 0;
for (i = 100; i <= 200; i++)
{
if (find(i) == 1)
{
printf("%d是素数\n", i);
}
else
{
printf("%d不是素数\n", i);
}
}
}
2.打印1000-2000之间的闰年(包括1000和2000)
这个方法和思路也是通过返回值来实现的
#include<stdio.h>
int judge(int a)
{
if (((a % 4 == 0) && (a % 100 != 0)) || (a % 400 == 0))
{
return 1;
}
return 0;
}
int main()
{
int year = 0;
for (int year = 1000; year < 2000; year++)
{
if (judge(year) == 1)
{
printf("%d\n", year);
}
}
}
3.二分查找函数
#include<stdio.h>
void binary_search(int arr2[], int y, int sz)
{
int left = 0;
int right = sz - 1 ;
int mid = 0;
while (left <= right)
{
mid = (left + right) / 2;
if (y > arr2[mid])
{
left = mid+1;
}
else if (y < arr2[mid])
{
right = mid - 1;
}
else
{
printf("数组中下标为%d的元素就是要找的数\n", mid);
break;
}
}
}
int main()
{
int arr1[] = { 1 , 2, 3, 4, 5, 6, 7, 8 };
int a = 0;
int sz = sizeof(arr1) / sizeof(arr1[0]);
scanf("%d", &a);
binary_search(arr1, a, sz);
return 0;
}
4.每使用一次函数,num就+1
方法一:(传址)
#include<stdio.h>
void add(int* num)
{
(*num)++;
}
int main()
{
int num = 0;
add(&num);
add(&num);
printf("%d", num);
}
方法二:(static修饰变量)
#include<stdio.h>
int add()
{
static int num = 0;
num++;
return num;
}
int main()
{
add();
int a = add(); //此处也算调用了函数
printf("%d", a);
}
方法三:(全局变量)
#include<stdio.h>
int num = 0;
void add()
{
num++;
}
int main()
{
add();
add();
printf("%d", num);
}
五、 函数的嵌套调用和链式访问
1.嵌套调用
#include<stdio.h>
int func1()
{
printf("haha\n");
}
int func2()
{
for (int i = 0; i < 10; i++)
{
func1(); //嵌套调用
}
}
int main()
{
func2();
}
运行结果:
注意:函数可以嵌套使用 但是不能嵌套定义(嵌套定义就是在自定义函数里面再次定义一个函数)
2.链式访问
定义:把一个函数的返回值作为另一个函数的参数。
1)例一
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello ";
int num = strlen( strcat(arr, "bit") );
printf("%d\n", num);
return 0;
}
运行结果:
strcat函数
结构:char * strcat ( char * destination, const char * source );
作用:将源字符串的追加到目标字符串。目标中的终止空字符被源的第一个字符覆盖,并且在目标中由两者串联形成的新字符串的末尾包含一个‘\0’。
strcat介绍
2)例二
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
运行结果;
这道例题有很多需要注意的点:
1.在于printf的返回值:printf函数的返回值是打印在屏幕上字符的个数。
2.printf函数不止执行最外面的,里面的也会执行。
3.这个结构会从里往外执行并注意打印printf输出值。
再看另一个代码,会学到更多的细节:
#include <stdio.h>
int main()
{
printf("%d ", printf("%d ", printf("%d ", 43)));
return 0;
}
运行结果:
这里可以看出明明看起来和上面一样的代码却出现了截然不同的结果,是因为这个代码也只是看起来一样,在%d后面多加一个空格,就会导致打印在屏幕上的字符多一个空格,所以返回值也会+1。
六、函数声明和定义
1.函数的定义
函数的定义是指函数的具体实现,交待函数的功能实现。
还是老样子,先看一段代码:
#include<stdio.h>
//函数的定义
int func1()
{
printf("haha\n");
}
int main()
{
//函数的调用
func1();
}
2.函数的声明
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数
声明决定不了。 - 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
例子:
#include<stdio.h>
//函数的声明
int fun1();
int main()
{
func1();
}
int func1()
{
printf("haha\n");
}
一般看起来就是先写主函数,然后再写函数,在使用前就要先声明一下,不然就算可以运行也会报一些警告。
在声明的时候,形参的名字甚至可以不用写,但是需要写参数的类型。
为了更为实现模块化的功能,也为了具有更好的移植性,代码的各个部分都是分开的(如下面这个代码):
七、函数递归
1.什么是递归
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
2.递归的条件
递归有两个必要的条件:
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2.每次递归调用之后越来越接近这个限制条件。
1)练习1
题目:接受一个整型值(无符号),按照顺序打印它的每一位。
使用递归:
#include <stdio.h>
void print(int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
int num = 1234;
print(num);
return 0;
}
2)练习2
题目:编写函数不允许创建临时变量,求字符串的长度。
#include<stdio.h>
int my_strlen(char* str)
{
//循环来写
while ((*str) != '\0')
{
return 1 + my_strlen(++str);
}
return 0;
//判断来写
//int Strlen(const char* str)
//{
// if (*str == '\0')
// return 0;
// else
// return 1 + Strlen(str + 1);
//}
}
int main()
{
char arr1[] = "hello world";
int len = my_strlen(arr1);
printf("%d", len);
}
运行结果:
3.递归与迭代
1)练习1
求n的阶乘。(不考虑溢出)
int factorial(a)
{
while (a > 1)
{
return a * factorial(a - 1);
}
return 1;
}
#include<stdio.h>
int main()
{
int a = 0;
int mem = 0;
scanf("%d", &a);
mem = factorial(a);
printf("%d", mem);
}
2)练习2
题目:求第n个斐波那契数。(不考虑溢出)
#include<stdio.h>
int fib(int a)
{
if ((a == 1) || (a == 2))
{
return 1;
}
return fib(a - 1) + fib(a - 2);
}
int main()
{
int n = 0;
printf("请输入你想打印的第n个斐波那契数:");
scanf("%d", &n);
fib(n);
//函数的返回值也可以当作参数! 再次提醒
printf("%d", fib(n) );
}
4.思考
大家可以尝试下,上面这个代码在运算fib(50)时候 会格外的慢,这是因为什么呢?
答:递归的好处就是让你的代码更加简洁,但坏处就是会重复计算很多不必要的数。
关于栈帧的一些讲解:
在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出)这样的信息。系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
为了避免这些情况 经常使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
递归和迭代的对比:
1.许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2.但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3.当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。