💖 技术宅,拯救世界!
🎁作者:@ecember
🎁专栏:《从0开始——C语言》
🎁 对读者的话:相信奇迹的人本身和奇迹一样伟大
🌹感谢大家的点赞 和关注 🌹,如果有需要可以看我主页专栏哟💖 |
⚡1. 前言
生命中最快乐的事情是拼搏,而非成功,生命中最痛苦的是懒散,而非失败。大家好,这里是ecember。今天ecmber来带大家认识函数,函数在整个计算机界占有及其重要的地位,函数就是将我们的大项目模块化,从而能让我们的团队成员能够分工合作完成,同时函数也为我们找到程序的bug提供了方便。
🌠1.1 什么是函数?
在数学中我们常常会使用函数,那么C语言中的函数呢?
维基百科中对函数的定义:子程序
<1>在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
<2>一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
⚡2. C语言中函数分类
1.库函数
2.自定义函数
🌠 2.1 库函数
为什么会有库函数?
- 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf)
- 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
- 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。
库函数的种类非常多,如果有需要可以看网站Cplusplus,此网站收录了所有C的库函数,并且还有详细介绍及例子,值得大家收藏。
总的来说C语言常用的库函数有:
1.IO函数
2.字符串操作函数
3.字符操作函数
4.内存操作函数
5.时间/日期函数
6.数学函数
7.其他库函数
由于库函数种类太多,这里就先给大家简单介绍几个。
🌀2.1.1 strcpy(拷贝)函数
char * strcpy ( char * destination, const char * source );
我们查阅网站可知strcpy可以将一串字符串拷贝到目标数组destination里面,并且拷贝的是字符串 \0(包含\0本身)之前的部分,该函数返回值即为目标字符串。同时我们根据网站最左侧得知该函数头文件为 <string.h> 。
OK,现在我们来试试这个函数。
int main()
{
char arr1[] = "abcdef";//a b c d e f \0
char arr2[20] = "XXXXXXXXXXX"; //XXXXXXXXXXX \0
strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
由此我们可知 \0 也被拷贝进去使arr2数组在 abcdef 处结束。
🌀2.1.2 memset——内存设置
void * memset ( void * ptr, int value, size_t num );
我们再次查看网站发现memeset函数可以通过访问指针或者数组首地址的方式将字符串指定位置所存放的值替换,并且还可以确定替换的个数,它一共有三个参数,第一个为目标指针(或者目标数组的首地址),第二个为操作者想要放进去的值,第三个为放进去值的个数。由最左侧得知使用此函数需要包含头文件<string.h>
老规矩,先上手热热身。
int main()
{
char arr[] = "hello bit";
memset(arr, 'X', 3);
printf("%s\n", arr);//XXXXXX bit
return 0;
}
果然,我们设置从数组首地址开始添加3个‘X’字符。那么既然我们知道memset函数内部第一个参数为数组首地址,那么如果我们通过让指针右移(即arr++),是不是就能改变插入位置了呢?
int main()
{
char arr[] = "hello bit";
memset(arr + 6, 'X', 3);
printf("%s\n", arr);//XXXXXX bit
return 0;
}
OK,perfect!
但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。这里对照网站来学习上面几个库函数,目的是掌握库函数的使用方法。那么我们是否要全部记住所有库函数呢?当时不是,我们只需要会查阅并使用就行了,除了上述网站以外,还有以下几个库函数查阅工具:
<1> MSDN(Microsoft Developer Network)
<2> www.cplusplus.com
<3> http://en.cppreference.com(英文版)
<4> http://zh.cppreference.com(中文版)
🌠 2.2 自定义函数
如果库函数能干所有的事情,那还要程序员干什么?所有更加重要的是自定义函数。自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
🌀2.2.1 自定义函数的形式
ret_type fun_name(para1, * )
{
statement;//语句项
}
//ret_type 返回类型
//fun_name 函数名
//para1 函数参数
练习——用函数实现求最大值
int get_max(int x, int y)
{
return (x > y ? x : y);
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int m = get_max(a, b);
printf("%d\n", m);
return 0;
}
🌀2.2.2 函数实现值的交换
如果不用函数的话,我们实现值的交换可能第一时间会想到引入一个辅助变量帮助进行值的交换。加入函数的话我们想到那不就是用函数传进去需要交换的两个参数,然后再在函数体内部实现值的交换,这样真的可行吗?
void exchange(int x, int y)//形参
{
int m = 0;
m = x;
x = y;
y = m;
}
int main()
{
int a = 0, b = 0;
scanf("%d %d", &a, &b);
printf("交换前:%d %d\n", a, b);
exchange(a, b);//传过去的叫实参
printf("交换后:%d %d\n", a, b);
return 0;
}
这里是为什么呢?明明我们已经在函数内部实现了a,b值的交换,但是依然输出的a,b值并没有交换。
小科普
实际参数:
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
一句话概括:
当实参传给形参的时候,形参是实参的一份临时拷贝,形参的改变并不影响实参。
由此我们可知,第一种方法将实参传过去给形参,实际上实参的值并没有受到影响,那么该怎么通过函数改变实参的值呢?我们调试一下代码并监视看能不能找到思路。
可以看到这里,x,y的确是把a,b的值拷贝过去了,但a,b本身的地址并未发生改变,也与x,y毫无关系,那么我们能不能通过改变a,b的地址实现值的交换呢?我们不妨一试,既然要操作我们的地址,那么就该我们的指针出马了,我们传入a,b的地址,然后用指针变量来接收,这时直接操作我们的指针变量就行了。
void exchange(int* x, int* y)//地址建立联系-远程操作
{
int m = 0;
m = *x;
*x = *y;
*y = m;
}
int main()
{
int a = 0, b = 0;
scanf("%d %d", &a, &b);
printf("交换前:%d %d\n", a, b);
exchange(&a, &b);//传过去的叫实参
printf("交换后:%d %d\n", a, b);
return 0;
}
值果然交换了,我们再监视看看。
我们将地址传过去时,我们直接操作的即是实参的地址,故对真正的实参产生了影响。
🌠 2.3 函数的调用
函数调用:传址调用和传值调用。
🌀2.3.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
🌀2.3.2 传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
🌠 2.4 函数小练习
本模块讲述四道经典题。
🌀2.4.1 利用函数判断一个数是否为素数
int is_prime(int n)
{
int i = 0, j = 0;
for (i = 2; i <= sqrt(n); i++)
{
if (n % i == 0)
{
return 0;//独立返回-函数结束
}
}
return 1;
}
int main()
{
int i = 0;
printf("%d ", 2);
for (i = 3; i <= 100; i += 2)
{
if (is_prime(i) == 1)
{
printf("%d ", i);
}
}
printf("\n");
return 0;
}
这里我们为了方便,直接使用素数求解的五大境界中的第三种境界(友友们有需要的话可以自行将五种境界改造成函数),只需要外部套个函数,当函数返回值为1是打印素数。
🌀2.4.2 输出闰年
这里我们利用函数输出1000-2000之间所有的闰年,天哪,我在做这道题之前天真的以为闰年只是单纯从1000开始+4,结果测试了很久依然通不过,直到我从百度搜才知道闰年是能被4整除但不能被100整除,或者能被400整除的数。哎,我们根据这个即可写出函数输出闰年。
int is_leap_year(int n)
{
return ((n % 4 == 0) && (n % 100 != 0) || (n % 400 == 0));
}
int main()
{
int i = 0, count = 0;
for (i = 1000; i <= 2000; i++)
{
if (is_leap_year(i) == 1)
{
printf("%d ", i);
count++;
}
}
printf("\n%d\n", count);
return 0;
}
这里我将函数体里面的语句糅合成了一个表达式进行返回,表达式结果为真,返回1,否则返回0。
🌀2.4.3 函数实现二分查找
在此之前我们需要了解:数组在传参的时候不是整个数组!而是数组首元素的地址,如果传递整个数组浪费空间,由于数组,所以数组在函数中时本质上一个指针*arr。故我们传进去函数的数组,即为地址,实际上相当于操作数组本身。
#define MAX 100//细节
int Seek(int parr[], int x, int n)
{
int min = 0, max = n;
int mid = 0;
while (min <= max)
{
mid = min + (max - min) / 2;//细节
if (x < parr[mid])
{
max = mid - 1;
}
else if (x > parr[mid])
{
min = mid + 1;
}
else
{
return mid;
}
}
return -1;//细节
}
int main()
{
int a = 0, b = 0;
int i = 0, j = 0, k = 0;
int arr[MAX] = { 0 };
printf("请输入查找范围:\n");
scanf("%d %d", &a, &b);
for (i = a; i <= b; i++)
{
arr[j] = i;
j++;
}
printf("请输入待查找的值:\n");
scanf("%d", &k);
int m = Seek(arr, k, j - 1);
if(m != -1)
{
printf("已找到,下标为:%d\n", m);
}
else
{
printf("查找数字超出范围!\n");
}
return 0;
}
二分查找我们已经在分支与循环语句练习下篇已经讲过原理,这里我们需要注意的是,如果查找值k合法,那么函数返回最终查找下标mid,否则返回-1(注意一定不能返回1或0,因为最终查找值下标可能是1或0)。
⚡3. 函数的嵌套调用与链式访问
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
🌠 3.1 嵌套调用
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for(i=0; i<3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0; }
这里的three_line函数就嵌套调用了另外一个函数new_line。但是我们需要注意:函数可以嵌套调用,但是不能嵌套定义。
🌠 3.2 链式访问
把一个函数的返回值作为另外一个函数的参数。
int main()
{
int len = strlen("abcdef");
printf("len = %d\n", len);
printf("len = %d\n", strlen("abcdef"));//嵌套调用
return 0;
}
上述例子即将strlen函数和printf函数嵌套调用在了一起。
小练习
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
//结果是啥?
//注:printf函数的返回值是打印在屏幕上字符的个数
return 0;
}
注意我们printf函数返回值为打印的字符个数,首先最内层先打印43,然后内层返回2(因为4,3为两个字符),继续打印2,同理返回1再打印。故结果为4321。
⚡4. 函数的声明和定义
🌠 4.1函数的声明
- 函数声明:告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
比如下列代码:
int Add(int x, int y)
{
return x + y;
}
我们在进入公司后都是分工完成一个大项目的开发,主要就是各个负责人完成哪个函数的内容,所以我们的函数都需要有一个头文件,将函数体声明放在头文件里。
add.h
int Add(int x, int y);
add.c
int Add(int x, int y)
{
return x + y;
}
把所有需要的函数头都放在头文件.h中,其它放在对应的.c文件中就实现了工程集成开发以及多文件编程。
🌠 4.2 函数的定义
我们的主要函数(例如main函数,菜单等函数)通常放在一个独立的.c文件中,其它功能函数放在另外一个.c文件中,对应的功能函数头放在对应的.h文件中,当我们需要调用功能函数时,直接加上 #include “ 功能函数头文件名”。例如
test.c
#include <stdio.h>
#include "add.h"
int main()
{
int a = 10;
int b = 20;
int sum = Add(a, b);
printf("%d\n", sum);
return 0;
}
add.h
int Add(int x, int y);
add.c
int Add(int x, int y)
{
return x + y;
}
我们来运行一波。
当我们接单子,开发某个软件或网页时,不想让自己编写的函数源码被别人看见,通常需要将我们开发的工程项编写成静态库(.lib)。等交付时我们只需要将函数头文件以及静态库交付即可。下面是编写静态库的方法:
右击项目文件,点击属性。
点击配置类型。
点击静态库。最后点击应用,此时我们的静态库就已生成。我们需要在存储工程的目录文件下找到静态库。这里我们以add为例。
找到初始建工程的文件,点进去。
点击X64。
点进Dubug文件。
此时我们的对应文件静态库.lib便显现了出了,我们只需要将此文件和函数头文件交给厂方即可。当我们想要应用静态库的文件时,这里以add.lib为例,只需要在文件头部添加 #pragma comment(lib, “add.lib”) 以及 #include “add.h” 即可。
⚡5. 函数递归
函数调用自身的编程技巧叫做递归。
🌠 5.1 函数递归的定义
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
🌠 5.2 递归的限制条件
这一节知识特别重要,因为博主本人老是忽略递归限制条件,导致程序一直报错。所以特别加粗。
<1>存在限制条件,当满足这个限制条件的时候,递归便不再继续。
<2>每次递归调用之后越来越接近这个限制条件。
我们使用递归的时候千万千万要注意这两个限制条件,不然编译器·就会报错。
int main()
{
printf("hehe\n");
main();
return 0;
}
这个main函数调用自身就是递归,但我们开始编译:
编译器报错了,提示是Stack overflow 即栈溢出,我们前面说过函数是定义在栈区上得,但这个程序由于没有终止条件,导致函数无限次调用,最终导致栈空间溢出。所以我们千万千万要注意使用递归时,一定要添加上述两个限制条件。
🌠 5.3 递归小练习
这里我整理了递归的几个小练习,我会一一为大家讲解,后续还会持续更新函数递归的相关题目。
🌀5.3.1 用递归将整数num的每一位打印出来
题目要求我们用递归的方法将num的每一位打印,即如果num = 1234,我们就要分别打印1 ,2,3,4。那么怎么用递归的方法实现呢?
首先·我们想想打印个位我们需要怎么搞?——%10是不是就取出了它的个位,依据这个思路,我们先看看下面的思路图。
由此我们的递归便有了雏形。我们的条件是形参n >0,形参每次==%10==以更接近递归条件。
void print(int n)
{
if (n > 0)
{
print(n / 10);
printf("%d ", n % 10);
}
}
int main()
{
unsigned int num = 0;
scanf("%d", &num);
print(num);
return 0;
}
打印成功,其实我们这里能稍微的改一改递归条件,递归次数更少,我们当n/10<9即可结束递归,因为个位数%1始终等于它自身。于是代码可改为
void print(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;
}
这里肯定会有一些小伙伴会疑惑,为什么是先打印1呢?一张图解决。
相信这样递归大家就能理解了吧。
🌀5.3.2 求字符串长度
大家可能看到这发现这题异常简单啊,求字符串长度还不容易?嘿嘿,这里我们要求自己写一个函数,并且不允许创建额外的临时变量求字符串长度。我们先来分析分析。假如允许创建临时变量呢?
int my_strlen(char* str)
{
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
我们大部分人应该都会这样做,直接定义一个count变量,然后用指针后移来求字符串长度,最后return这个count变量即为长度。那么我们如何不创建变量来实现求长度呢?——递归,这里递归不失为求解的好方法,我们每递归一次就加1,直到最后不满足条件时return的值是不是就是我们的长度了。
int my_strlen(char* str)
{
if (*str != '\0')
{
return my_strlen2(++str) + 1;
}
else
{
return 0;//注意空字符串的判定
}
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
在我们的递归函数my_strlen()函数中,我们用*str作为形参,实参传过去即为arr数组首元素的地址,判定条件是 *str != ’ \0 ',每递归一次则长度加1,注意递归限制条件还有一个++str (str先++,再参与递归)。
⚡6. 结语
到这,我们的 《大战函数》 已经接近尾声了,后续我还会持续更新C语言相关内容,学习永无止境,就会永远只会留给有准备的人。希望我的博客对大家有所帮助,如果喜欢的可以 点赞+收藏哦,也随时欢迎大家在评论区及时指出我的错误。