1.函数是什么
子程序,是一个程序中一部分代码,由一个或多个语句块组成,负责完成某项特定的任务,相比与其他代码,具有相对的独立性。eg strlen( )只负责求字符串长度 strcmp只负责比较两个字符串的大小 ...(每个函数的功能都是独立,使用需要调用,各函数之间本身是没有联系的)
函数一般会有输入参数和返回值
2.C语言中函数的分类
2.1 库函数
为什么要有库函数?
早期,C语言是没有库函数的,是需要自己去写程序实现一些基础功能,使用C语言时,发现一些功能是常用的,为了提升代码书写效率,减少重复书写一些基础的代码,降低出现bug的概率,就出现了一系列库函数。
C语言中的库函数都有:IO函数 字符串操作函数 字符操作函数 内存操作函数 时间/日期函数 数学函数 其他库函数
文档阅读
例:验证文档阅读的memset用法
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = {"hello world"};
memset(arr, 'x', 5);
printf("%s\n", arr); //输出:xxxxx world
memset(arr + 6, 'y', 3);//当想改中间的字符:改指针(让指针指向要改的第一个字符) 此处要改wor,w的位置是arr+6
printf("%s\n", arr); //输出:xxxxx yyyld
return 0;
}
例:验证文档阅读的strcpy用法
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[20] = {0};
char arr2[] = "hello world";
strcpy(arr1, arr2);
printf("%s\n", arr1); //输出:hello world
return 0;
}
注意:库函数的使用必须包含对应头文件
如何学习使用库函数?
查询工具的使用:
(en—>zh中文版)
2.2 自定义函数
自己创造的函数,自定义函数与库函数都有函数名,返回值类型和函数参数,不一样的是自定义函数都是自己设计的,自己定义函数的实现
函数的组成:
注意:函数参数可以有0,1或多个
例:写一个函数可以找到两个整数中的较大值
#include <stdio.h>
int getmax(int x, int y) //定义整型的参数x,y接收整型变量a,b的值
{ // return的是整型,函数的返回类型为int
return (x > y ? x : y);
}
int main()
{ //用函数求两个数的较大值
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int m = getmax(a, b);
printf("%d\n", m);
return 0;
}
例:写一个函数交换两个整型变量的内容
错误示例:
#include <stdio.h>
void Swap(int x, int y)
//调试窗口&x,&y与&a,&b完全不相同 说明x,y和a,b都是独立的变量,有单独的空间
{
int z = 0;
z = x;
//借助z 确实将x和y交换了,但是不会影响a和b,(因为是在不同的空间下)没有达到交换的效果
x = y;
y = z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
//实参:a、b 形参:x、y
//实参传递给形参时,形参将是实参的一份临时拷贝,对形参的修改不会影响实参
//(形参将数据拷贝了一份,但是有独立的空间)
printf("交换前:a=%d b=%d\n", a, b);
Swap(a, b); //有问题的代码
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
上面的代码有点问题,输出时没有交换
实参a,b与形参x,y没有联系,各自有相互独立的空间,互不影响
正确做法:
#include <stdio.h>
void Swap(int *px, int *py) //将地址所指向的内容的值进行交换
{
int z = *px; //z=a
*px = *py; //a=b
*py = z; //b=z
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap(&a, &b); //用地址将实参与形参建立联系
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
运行输出:
例:函数实现相加
#include <stdio.h>
int add(int x, int y)
{
return (x + y);
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int c = add(a, b);
printf("%d\n", c);
return 0;
}
注意:
- 传地址:形参不仅要得到实参的值,还要对实参进行修改,就需要传地址;
- 传值:形参只需要得到实参的值,不需要对实参进行修改,只需要传值,不需要传递地址
3.函数的参数
3.1 实际参数
真实传给函数的参数,叫实参,实参可以是常量,变量,表达式eg add(a+3,b),函数 eg: add(add(a,3),b)
无论实参是何种类型的量,在进行函数调用时,都必须有确定的值,以便把这些值传给形参,
注意:用函数做实参时函数需要有返回值
3.2 形式参数
形式参数是指函数名后括号中的变量,形参只有在函数被调用的过程中才实例化(分配内存单元),定义好函数不调用函数时,形式参数不分配空间(不调用的时候不占用内存空间),调用函数时,形参会占用内存空间,形式参数在不调用函数时,只是形式上的一种存在,不会真实的在内存中存在,因此叫形参,形参当函数调用完成后自动销毁,因此形参只有在函数中有效。(形参实例化之后相当于实参的一份临时拷贝)
注意:形参的名字和实参的名字可以相同也可以不同。
4.函数的调用
4.1 传值调用
调用函数时传的是变量本身:实参和形参分别占用不同内存块,形参实例化之后相当于实参的一份临时拷贝,对形参的修改不会影响实参
4.2 传址调用
调用函数时传的是变量地址:函数内部与函数外的变量(main函数中的变量)建立联系(让实参与形参建立了联系),即函数内部可以直接操作函数外部变量
4.3 练习
1.写一个函数,可以判断一个数是不是素数
#include <stdio.h>
#include <math.h>
int is_prime(int n) //是素数返回1 不是返回0
{
int j = 0;
for (j = 2; j <= sqrt(n); j++)
{
if (n % j == 0)
{
return 0;
}
}
return 1;
}
int main()
{
int i = 0;
int count = 0;
for (i = 101; i <= 200; i += 2)
{ //判断i是否为素数(是:打印) 只是判断不修改实参,没有必要传地址,只需要传数值
if (is_prime(i))
{
count++;
printf("%d ", i);
}
}
printf("\ncount = %d\n", count);
return 0;
}
2.写一个函数判断一年是不是闰年
#include <stdio.h>
int is_leap_year(int y) //是闰年:返回1 否 返回0
{ //函数功能尽量单一 被复用才方便(所以我们不在函数内部打印输出)
if (y % 4 == 0 && y % 100 != 0 || y % 400 == 0)
return 1;
else
return 0;
}
int main()
{
//打印1000~2000年的闰年
//闰年判断:1 能够被4整除,但是不能被100整除 2 能够被400整除
int year = 0;
for (year = 1000; year <= 2000; year++)
{
//判断闰年
if (is_leap_year(year)) //传值 不修改实参传值
{
printf("%d ", year);
}
}
return 0;
}
3.写一个函数,实现一个整型有序数组的二分查找
#include <stdio.h>
int binary_search(int arr1[], int k1, int sz1)
//int arr1[]中arr1本质上是个指针变量 内部存放arr首元素的地址,通过地址就能找到arr
//数组在内存中连续存放的,只要有首元素地址就能访问整个数组
//main函数和binary_search函数访问数组都是在访问定义在main函数中的arr
{
int left = 0;
int right = sz1 - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (arr1[mid] > k1)
{
right = mid - 1;
}
else if (arr1[mid] < k1)
{
left = mid + 1;
}
else
{
return mid; //找到了返回下标
}
}
return -1; //找不到 返回-1
}
int main()
{
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int k = 7;
int sz = sizeof(arr) / sizeof(arr[0]);
//数组元素个数 二分查找需要左下标和右下标 right=sz-1 需要传元素个数给函数
//找到了返回下标 找不到返回-1
int ret = binary_search(arr, k, sz);//传递数组时只写数组名(数组名就是地址)
if (ret == -1)
{
printf("找不到\n");
}
else
{
printf("找到了,下标是%d\n", ret);
}
//先写函数怎么用,再写函数
return 0;
}
err:错误示例:不在main函数中秋数组大小,而是放到binary_search中求数组大小
将数组元素个数放到binary_search()中,查找数字7时运行后输出:找不到
为什么会出现这个问题?
调试观察:发现sz=1 arr1不是数组而是指针变量, sz:4/8 / 4/8 =1 (32位是4/4=1 64位是8/8=1)所以这里不能计算数组元素个数
一般情况下:
实参传递给形参时,形参是实参的一份临时拷贝(一般情况,变量传参是复制拷贝了一份,完全传递过去了)
数组实参形参传递:
如果还是按上面那种规则将实参的整个数组传递到形参中,在形参中重新创建一个跟实参一样的数组,会造成空间的浪费,所以数组传参不会采取这种规则。
数组传参实际上是传递数组首元素的地址,而不是整个数组,所以在被调用的函数内部计算一个函数参数部分的数组的元素个数是不可取(用形参来计算数组元素个数是错误的,因为此处形参不是数组,本质上是一个指针)。
4.写一个函数,每调用一次这个函数,就会将num的值增加1
#include <stdio.h>
void Add(int *p)
{
(*p)++;
}
int main()
{ //每调用一次这个函数,就会将num的值增加1
int num = 0;
Add(&num); //传地址就可以改变num
//改变函数外部的某个变量,可以将变量的地址传进去,通过指针找到变量进行修改
printf("%d\n", num);//1
Add(&num);
printf("%d\n", num);//2
return 0;
}
//写法二
#include <stdio.h>
int Add(int n)
{
return n + 1;//return ++n;不可以写成return n++;先返回后++没有意义
}
int main()
{ //每调用一次这个函数,就会将num的值增加1
int num = 0;
num = Add(num);
printf("%d\n", num); // 1
num = Add(num);
printf("%d\n", num); // 2
return 0;
}
5.函数的嵌套调用和链式访问
5.1 嵌套调用
函数之间是可以互相调用的
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for (i = 0; i < 3; i++)
{
new_line();//调用new_line打印三个hehe
}
}
int main()
{ //嵌套调用
three_line();//main() call three_line()
//three_line() call new_line
return 0;
}
注意:函数可以嵌套调用但是不可以嵌套定义
5.2 链式访问
链式访问依赖的是函数的返回值
#include <stdio.h>
#include <string.h>
void test()
{
printf("hehe\n");
}
//函数不写返回类型时,默认返回值类型是int 建议明确写出来,不要不写
//写了返回类型要写返回值 写了int(有返回值的类型)不写返回值return 一些编译器默认返回最后一条指令执行的结果 不建议这样做
//函数中无参数就不要传参 (拒绝传参可以将参数设置为void:明确说明此函数不需要参数)
// eg:int main(void)为什么外面通常不加上void main()本质是有参数的
// main()的三个参数
// int main(int argc,char* argv[],char *envp[])
int main()
{
int len = strlen("abcdef");
printf("%d\n", len); // 6
//链式访问 把一个函数的返回值作为另外其他函数的参数
//前提条件:函数要有返回值(返回类型)(没有返回值的函数,无法链式访问)
printf("%d\n", strlen("abcdef")); // 6
// strlen()的返回值是一个 size_t 类型的无符号整数 将返回值用%d打印
// strlen()的返回值作为printf()的参数 链式访问
printf("%d", printf("%d", printf("%d", 43))); // 4321
//先打印最内层的printf("%d", 43) 打印输出43 外面两个printf()依赖前面的返回值
// 返回值:打印字符的个数
// 43是两个字符 最内层返回值作为第二层的参数 打印输出 2
// 2是一个字符 第二层返回值作为最外层的参数 打印输出 1
//所以输出:4321
// int n=test();//err test()无返回值,不需要定义整型变量接收
return 0;
}
6.函数的声明和定义
6.1 函数声明
告知编译器有一个函数(函数名,返回类型,参数类型)具体函数存在与否函数声明决定不了(函数声明是假的也是可以的,发布了这个函数声明,但是这个函数未定义不存在)
函数声明一般出现在函数的使用之前,要满足先声明后使用
函数的声明一般放在头文件中
例题:函数声明和定义
#include <stdio.h>
//函数的声明
int add(int, int);
//函数名+参数类型+返回类型
// int add(int x,int y);
//这种写法也可以,x,y写不写都是对的,其实没有必要写
int main()
{ //函数的声明和定义
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int sum = add(a, b);
printf("%d\n", sum);
return 0;
}
//函数的定义
int add(int x, int y) //定义到后面报warning错:add未定义
{ //编译器处理代码时,从前往后扫描代码,进入main函数中前没有见过add函数,就会报一个warning
//解决处理:1.建议函数定义在main()前面 2.如果非要把函数定义放后面:前面进行函数的声明
return x + y;
}
注意:
函数声明一般放在头文件中的,如下图,将函数的声明放到add.h文件中,函数的定义单独放在一个add.c文件中,想在test.c中使用加法的功能:用#include “add.h”即可(add.c和add.h两个文件称为一个加法模块,其他函数想使用加法功能只要包含add.h的头文件即可 C语言中库提供头文件用<> 而自定义的头文件用"")
自定义的函数与库函数(printf...)的使用十分相似,都只需要包含他们的头文件就可以正常使用了(不管它的函数定义实际定义在哪里,只要包含它对应的头文件,就可以正常使用)
一个文件能搞定,分成三个文件,何必呢? 初学时可能会觉得将所有的代码放到一个文件中最方便,但是,①不方便协同合作②需要模块化设计,代码太长阅读体验差,将代码模块化,方便维护代码
函数声明add.h放到函数定义add.c中或者直接不进行函数的声明(移除工程中的add.h),工程下只有add.c和test.c文件(test.c文件中不加自定义头文件),直接在test.c中使用add()貌似也没有大问题(会报一个warning但是也可以用)为什么还要将一个加法模块拆分为.c和.h的组合?
1.test.c中包含#include “add.h”的效果是将头文件内容拷贝过来即int Add(int x,int y);就相当于实现函数声明,声明过编译时编译器就没有warning错误
2.add.c->add.lib(静态库)将函数如何实现的隐藏起来 add.h:函数如何使用可以不隐藏:在将代码变现时避免别人白嫖自己劳动成果,使用如下图 (需要写代码导入静态库)
注意:.h文件中#pragma once 意思是防止头文件被重复包含。
生成静态库的做法
用静态库(需要用代码导入)和.h文件实现加法模块
6.2 函数定义
真实的去创造一个函数,这个函数有没有存在取决于函数定义
函数定义是指函数的具体实现,交待函数的功能实现(如何运行)
7.函数递归
7.1 什么是递归
程序调用自身的编程技巧称为递归
一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,通常将一个大型复杂问题层层转化为与原问题相似的较小规模问题来求解,只需要少量代码就可描述解题过程中所需要的多次重复计算,大大减少了程序的代码量
递归的核心思想:大事化小
7.2 递归的两个必要条件
- 存在限制条件,当满足这个限制条件时,递归便不能继续
- 每次递归调用之后越来越接近这个限制条件
例题:接收一个整型值(无符号),按照顺序打印它的每一位
#include <stdio.h>
void print(unsigned int n)
//最容易得到的是最后一位 1234%10=4 递归核心思想:大事化小
// print(1234)->拆分成:print(123)(1234/10)+4(1234%10)->print(12)+3 4->print(1)+2 3 4->输出结果:1 2 3 4
//递:递延 归:回归
{
if (n > 9) //这个if语句如果不加,没有限制条件,函数一直自己调用自己,就会没有机会停下来 就会死递归
{ //会导致栈溢出 Stack overflow 栈区:局部变量 函数形参 函数返回值(寄存器) 每一次函数的调用都会在栈区上申请空间
//函数一直自己调用自己,就会在栈区中不断申请空间,栈区的空间有限,会导致栈溢出
print(n / 10);//写成print(n)也会死递归,导致栈溢出
}
printf("%d ", n % 10);
}
//
int main()
{ //接收一个整型值(无符号),按照顺序打印它的每一位 eg 1234
unsigned int num = 0;
scanf("%u", &num); //%d 打印有符号的整数(会有正负数) %u打印无符号整数(无负数)
/* while (num)
{
printf("%d \n", num % 10);// 倒序 可以用数组先存起来在按顺序输出
num = num / 10;
}*/
//递归实现 自己调用自己 前提:必须是函数 函数自己调用自己
print(num); //接收一个值顺序打印每一位
return 0;
}
注意:
- 递归需要停下来的条件,不能是死递归;每次递归调用逐渐接近这个条件直至不满足递归条件退出递归
- 这两个条件是必须要有的,没有这两个条件,递归一定是错的,但是两个条件都有递归也不一定对
引伸:函数栈帧的创建和销毁
例题:编写函数不允许创建临时变量,求字符串的长度
#include <stdio.h>
#include <string.h>
//编写函数不允许创建临时变量,求字符串的长度
// 1.求字符串的长度
// 2.模拟实现strlen
// 3.递归求解
int my_strlen(char *str) // 2.模拟实现strlen
// int my_strlen(char str[])
{
int count = 0; //计数 临时变量
while (*str != '\0')
{
count++;
str++; //找下一个字符
}
return count;
}
int my_strlen2(char *str1)
// 3. 递归实现 my_strlen2(“abc”)->1+my_strlen2(“bc")->1+1+my_strlen2("c")->1+1+1+my_strlen2("")->1+1+1+0
//第一个字符不是'\0'长度至少有一个字符
{
if (*str1 != '\0')
{
return 1 + my_strlen2(str1 + 1); //不推荐++str str=str+1 这个值会改变str的值 只写str+1是不会改变str
} //不能用 str1++ 先使用再++ 没有意义
else
return 0;
}
int main()
{
// int len = strlen("abc"); // 1.用strlen函数
// int len = my_strlen("abc");//2.自定义函数去模拟实现strlen函数的功能
//传递参数:传递的是首字符的地址 跟数组类似
//方便观察 用数组存起来
char arr[] = {"abc"};
int len = my_strlen(arr);
int len2 = my_strlen2(arr);
printf("%d\n", len);
printf("%d\n", len2);
return 0;
}
7.3 递归与迭代
迭代:重复 循环也是一种迭代(重复),迭代不仅仅是循环,我们写代码可以写成递归或者类似循环的方式
练习
例题:求n! 阶乘公式如下
#include <stdio.h>
// 1.递归 据公式
int fact(int n1)
{
if (n1 <= 1)
{
return 1;
}
else
return n1 * fact(n1 - 1);
}
// 2.迭代
int fac(int n2)
{
int ret1 = 1;
int i = 0;
for (i = 1; i <= n2; i++)
{
ret1 = ret1 * i;
}
return ret1;
}
int main()
{ // n! 1.递归 2.迭代
//有些时候用递归解决问题并不太好 fib
int n = 0;
scanf("%d", &n);
int ret = fact(n);
int ret2 = fac(n);
printf("%d\n", ret);
printf("%d\n", ret2);
return 0;
}
例题:求第n个斐波那契数 1 1 2 3 5 8 13 21 34 55......
#include <stdio.h>
int count;
// 1.递归实现
int fib1(int n1)
{
if (n1 == 3)//观察递归计算第40位fib需要计算多少次第3位fib
count++;
if (n1 <= 2)
return 1;
else
return fib1(n1 - 1) + fib1(n1 - 2);
}
// 2.迭代实现 效率高
//
int fib2(int n2)
{
int a = 1;
int b = 1;//前两位不用计算
int c = 1;
while (n2 >= 3)
{//第3个fib需要计算1次就设置进入循环1次 1+1=2
// 第4个fib需要计算2次就设置进入循环2次 1+1=2 1+2=3
c = a + b;
a = b;
b = c;
n2--;
}
return c;
}
int main()
{ //斐波那契 1.递归实现 计算第50个 发现计算机一直在计算画很长时间 代码效率低
//为什么时间长? 计算的复杂程度:n=3要求第3个fib;count++计算在整个计算(算第40个)中,函数计算了多少次第3个fib
//大量的重复计算 存在时间的大量浪费 效率太低
//这里不用递归解决,不好
int n = 0;
scanf("%d", &n);
int ret = fib1(n);
printf("%d\n", ret);
printf("%d\n", count);
// 2.迭代实现
int ret1 = fib2(n);
printf("%d\n", ret1);
return 0;
}
用递归求第40 个fib,发现仅第3个fib就求了好几万次,存在大量重复计算
写代码时,发现用递归写代码很简单,而且没有明显缺陷,就可以选择用递归解决问题,如果用递归有明显的缺陷就不用递归,可以选择用非递归方法:迭代。
递归层次太深有可能会出现栈溢出的现象。
例题:递归的栈溢出
#include <stdio.h>
void test(int n)
{
if (n < 10000)
test(n + 1);
}
int main()
{
test(1);
return 0;
}
调试观察:
解决:
- 递归改成非递归
- 使用static对象(静态变量不是放在栈上,是在静态区)代替nonstatic(非静态)局部变量(局部变量是在栈上)(不是静态区不会溢出,只是分配一些数据平衡一下减小栈溢出发生概率)
递归的经典问题:
1.汉诺塔问题
2.青蛙跳台阶问题(fib)