系列文章目录
速通C语言系列
速通C语言第一站 一篇博客带你初识C语言 http://t.csdn.cn/K43IN
速通C语言第二站 一篇博客带你搞定分支循环 http://t.csdn.cn/O9ISr
又是你在卷是吧
感谢佬们阅读支持!
文章目录
前言
上篇博客我们详细地带大家剖析了分支循环,这篇博客我们来搞另一个重要的章节----函数。
一、函数是什么?
大型程序中的部分代码,由一个/多个语句块组成。完成某项特定任务,相对独立,有输入参数、返回值。
二、库函数
库函数的分类
IO函数(输入输出函数) | 如printf、scanf、putchar |
字符串操作函数 | 如strcmp、strlen |
字符操作函数 | 如toupper(小写转大写) |
内存操作函数 | 如memcmp |
时间、日期函数 | 如time |
数学函数 | 如sprt、pow |
其他库函数 |
举例
我们举两个函数为例
strcpy
字符串拷贝函数
char*strcpy(char*destnation,char*source);
//目的地 //源头
例:
#include<stdio.h>
int main()
{
char arr1[20] = { 0 };
char arr2[] = { "hello yigang"};
//将arr2中的字符放入一中
strcpy(arr1, arr2);
printf("%s", arr1);
}
memset
mem指的是memory,所以是对内存操作的。
memset 内存设置。
void*memset(void *ptr,int value,size_t num);
//把ptr所指的那块内存的前num个字节设置为想要的value值
例:
#include<stdio.h>
int main()
{
char arr[] = "hello yigang";
//把hello的前5个字节换成x
memset(arr, 'x', 5);
printf("%s\n", arr);
return 0;
}
三、自定义函数
自定义函数与库函数一样,有函数名、返回值类型和函数参数,但不一样的是这些都需要我们自己来实现。
函数的组成
ret_type fun_name(para1, ……);
//函数返回类型 函数名 参数 如果无需传参,直接在括号中写void
{
//函数体
statement;
}
例:写一个函数找出两个数的较大值
int get_max(int x, int y)
{
int z = 0;
if (x > y)
{
z = x;
}
else
{
z = y;
}
return z;
}
int main()
{
int a = 10;
int b = 20;
//调用函数
int max = get_max(a, b);
printf("^%d ", max);
return 0;
}
例:写一个函数交换两个整形变量的内容。
void swap(int x, int y)
{
//临时变量
int z = 0;
z = x;
x = y;
y = z;
}
int main()
{
int a = 10;
int b = 20;
//交换前
printf("%d %d", a, b);
swap(a, b);
//交换后
printf("%d %d", a, b);
}
但是运行之后,并未得到我们想要的结果。
这是为什么呢?
因为其地址没变,我们将a、b叫做实参,x、y叫做形参,x、y有各自独立的空间,所以交换x、y,并不会影响a、b。
所以为了能改变其地址,我们在传参时就直接传地址过去。
#include<stdio.h>
void swap(int* pa, int* pb)
{
int z = 0;
z = *pa;
*pa = *pb;
*pb = z;
}
int main()
{
int a = 10;
int b = 20;
printf("%d %d\n", a, b);
swap(&a, &b);
printf("%d %d\n", a, b);
return 0;
}
结果成功了:
思考:为什么我们的第一个例子不用传地址呢?
因为get_max只是找出两个数中的最大值,并不会改变值。
四 函数的参数
从上个例子我们知道,函数的参数分为实参、形参。而形参是实c参的一份临时拷贝,对形参的修改不影响实参,所以第一个swap函数没能成功;而当我们传的参数变成指针时,就成功了。
五、函数调用
传值调用、传址调用
我们将第一种直接传值的函数调用方式称为传值调用,将第二种传指针的函数调用方式称为
传址调用。
函数 | swap1为传值调用 | swap2为传址调用 |
形参、实参 | 分别占有不同内存块 | 可以使函数和函数外边的变量建立真正的联系,也就是函数内部可以直接操作外部的变量 |
示例
用函数判断一个数是否为素数
(带注释版本)
#include<stdio.h>
int is_prime(int n)
{
int j = 0;
//从2至n-1当除数试
for (j = 2; j < n; j++)
{
if (n % j == 0)
{
return 0;
}
}
//找到了返回1
return 1;
}
int main()
{
int i = 0;
//判断100-200间的素数
for (i = 100; i <= 200; i++)
{
if (is_prime(i) == 1)
{
printf("%d", i);
}
}
return 0;
}
(不带注释版本)
#include<stdio.h>
int is_prime(int n)
{
int j = 0;
for (j = 2; j < n; j++)
{
if (n % j == 0)
{
return 0;
}
}
return 1;
}
int main()
{
int i = 0;
for (i = 100; i <= 200; i++)
{
if (is_prime(i) == 1)
{
printf("%d", i);
}
}
return 0;
}
注:由于有两种情况,所以对应了两个return。
再例:写一个函数,每调用一次,num的值加一。
num的值加一,意味着num会改变,所以我们用传址调用。
void Add(int* p)
{
(*p)++;
}
int main()
{
int num = 0;
Add(&num);
return 0;
}
六 函数的嵌套调用和链式访问
嵌套调用
嵌套定义非常简单且好理解,我们直接给一个例子。
示例
void test3()
{
printf("hehe");
}
int test2()
{
test3();
return 0;
}
int main()
{
test2();
return 0;
}
注:函数间可嵌套调用,不可嵌套定义。
示例
int main()
{
int ADD()
{
……
}
}
//不行
链式访问
把一个函数的返回值当作另一个函数的参数
示例:求一个字符串的长度
//一般我们会这么写
int main()
{
int len = strlen("abc");
printf("%d", len);
return 0;
}
其实我们可以通过链式访问直接:
printf("%d",strlen("abc));
两种写法都可得到正确答案
七 函数的声明及定义
规则
对声明及定义,我们有以下规则
1 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么;但是具体是不是存在,无关紧要。
2 一般为先声明,后使用。
3 函数的声明一般放在头文件(.h)中,函数的定义一般放在(.c)文件中。
示例
我们实现一个有加、减、乘、除的计算器。
先实现一下加法:我们建立3个文件:ADD.c ADD.h test.c
在ADD.h中我们进行声明
int ADD(int x, int y);
在ADD.c中我们具体定义这个函数,当然,在定义之前,我们需引我们写的ADD.h。由于这个头文件是我们自己实现的,所以声明时要用双引号。
#include"ADD.h"
int ADD(int x, int y)
{
return x + y;
}
在主函数中我们调用它求加法
#include"ADD.h"
int main()
{
int a = 10;
int b = 20;
int c = ADD(a, b);
printf("%d ", c);
return 0;
}
其他运算同理,我们可以分别建立多个.c文件和头文件来表示不同算法。
也可以在一个头文件中声明所有函数,在一个.c文件中实现这些函数。
后期我们实现的项目中,这些都是常规操作。
-八、函数递归
我们把程序调用自身的编程技巧称为递归。
特点是将大型的复杂问题层层化为一堆与原问题相似的小问题。
说起来太抽象了,我们给一个例子
小例
#include<stdio.h>
int main()
{
printf("张一钢");
main();
return 0;
}
main函数调用了main函数(自己),结果就是循环打印。
小例仅仅是给大家初步介绍下递归,并未涉及到解决复杂问题的思想,接下来,我会举具体的例子带大家学习递归“大化小”的思想。
举例
接受一个整形值(无符号),依照顺序打印其每一位。如:输入1234,打印1 2 3 4.。
思考
如果是仅仅打印1234中的每一位,我们是有办法的。
就是对1234先取余10得到4,再把1234除以10得到123;
再对123取余10得到3,再把123 除以10得到12 ;
再对12 取余10得到2,再把12 除以10得到1
最后1 取余10得到1.
最后确实能得到每一位,但顺序是反的,即 4 3 2 1.
我们将上面的思路进行一波分解
具体思路:
1 刚开始为1234 > 9, 所以1234/10=123先留下,不打印;
2 上次的 123存起来,123>9 123/10=12 留下, 不打印;
3 上次的 12 存起来, 12>9 12/10=1留下, 不打印;
4 上次的 1 存起来 1<9 所以打印1。
5 打印了1以后,再回到上一层打印2,3,4。
代码实现
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void print(unsigned int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf("%d", &num);
print(num);
return 0;
}
我们输入1234
示例 :求n的阶乘
n的阶乘就是从n乘n-1……一直到乘到1.所以易得出
int Fac(int n)
{
if (n <= 1)
{
return 1;
}
else
{
return n * Fac(n - 1);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fac(n);
printf("%d\n", ret);
return 0;
}
示例
求斐波那契数
科普:斐波那契数列
由科普中的递推公式,我们很容易有思路。
我们用代码实现一波
int Fib(int j)
{
if (j <= 2)
{
return 1;
}
else
{
return Fib(j - 1) + Fib(j - 2);
}
}
int main()
{
int j = 0;
scanf("%d", &j);
int n = Fib(j);
printf("%d", n);
return 0;
}
当我们输入40时,过了几秒才输出结果,如果你尝试更大的数,时间会更长。
这个时候我给大家科普一下递归对内存的使用原理。
递归所进行的调用并不是左右同时进行的,而是调用完左边后,再开始重新调用右边。
这就导致了调用时会进行一堆重复操作,加长了时间。
递归的两个必要条件
1 存在限制条件,当满足这个限制条件时,递归便不在继续。
2 每次递归调用之后会越来越接近。
即便如此,使用递归时依然可能出现错误。
void test(int n)
{
if (n < 10000)
{
test(n + 1);
}
}
int main()
{
test(1);
return 0;
}
这段代码看似没什么问题,但是当我们运行之后。
程序直接退出了,这是为什么呢?
系统分配给程序的栈空间是有限的,但是如果出现了死循环/死递归,这样有可能导致一 直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
再加上递归在调用时会进行一堆重复操作,时间太长。
所以在后期,类似功能能用循环实现的,我们一般不考虑递归。
总结
做总结,今天这篇博客带大家简单的剖析了函数的相关要点,函数是C语言非常重要的模块,在后期会经常使用,希望大家好好学习函数模块。水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。