目录
一、C语言中函数的分类
1.1 库函数
那我们应该如何学习C语言呢?这里给大家一个网站(以往也提到过),可以帮我们深入了解库函数:点击跳转
但是库函数必须知道的一个秘密就是:使用库函数,必须包含#include
对应的头文件。
1.2 自定义函数
例如写一个自定义函数来找出两个整数之间的最大值。
#include <stdio.h>
int find_max(int x, int y)
{
if (x > y)
return x;
else
return y;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int Max = find_max(a, b);
printf("最大值是:%d", Max);
return 0;
}
再举一个例子:写一个自定义函数可以交换两个整型变量的内容。
我想大部分初学者一定会这么写:
#include <stdio.h>
void swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
printf("a = %d\nb = %d\n", a, b);
return 0;
}
但是结果却和我们想的不太一样,当输入10和20,会发现交换后和交换前一模一样
我们可以通过监视来找程序错误:
大家注意看a
,x
,b
和y
的地址,a
对应x
和b
对应y
的地址都不一样怎么可能交换的了值呢?
官方说法:a,b叫做实际参数,x和y叫做形式参数,当函数调用时,实参传递给形参,形参就会有自己的空间(地址),所以形参的修改不会影响实参。
因此,可以用指针来建立联系,代码修改后为:
#include <stdio.h>
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 10;
int b = 20;
swap(&a, &b);
printf("a = %d\nb = %d\n", a, b);
return 0;
}
【程序结果】
代码详解:把a
的地址交给x
,b
的地址交给y
,此时x
中存的就是a
的地址,y
中存的是b
的地址,所以*x
就是a
,*y
就是b
,这里运用到指针中的解引用。对于指针不熟的同学们可以看看这篇博客:点击跳转
1.3 void在自定义函数中的应用
#include <stdio.h>
void tmp(void)
{
printf("hello world!\n");
}
int main()
{
tmp();
return 0;
}
代码详解:假设自定义函数tmp
,但是我不给它传任何参数,所以可以在定义函数括号里写上void
,表明这个函数在调用时不能传参,现在只希望函数内部打印hello world!
,因为不需要任何返回,所以它的返回类型为void
。
二、简单介绍两个库函数
2.1 实现字符串拷贝(strcpy)
【文档描述】
【代码样例】
#include <stdio.h>
#include <string.h> //使用strcpy需要包含的头文件
int main()
{
char arr1[20] = { 0 };
char arr2[] = "hello world!";
strcpy(arr1, arr2); //数组名就是指针
printf("%s\n", arr1); //%s用来打印字符串
return 0;
}
【程序结果】
2.2 设置内存(memset)
【文档描述】
【代码样例】
#include <stdio.h>
#include <string.h> //memset需要包含头文件
int main()
{
char arr1[20] = "welcome to China";
memset(arr1, 'x', 7); //'x'对应的是ASCII码,是整型
printf("%s\n", arr1);
return 0;
}
【程序结果】
三、函数的参数
3.1 实际参数(实参)
3.2 形式参数(形参)
四、函数的调用
4.1 传值调用
在【2.1自定义函数】写了一个自定义函数来找出两个整数之间的最大值,就是传值调用。
4.2 传址调用
那【2.1自定义函数】写了自定义函数可以交换两个整型变量的内容,则就是传址调用。
五、函数的嵌套调用和链式访问
5.1 嵌套调用
什么是嵌套调用呢?来看看下面的代码:
#include <stdio.h>
void new_line()
{
printf("hello world\n");
}
void three_line()
{
for (int i = 0; i < 3; i++)
{
new_line();
}
}
int main()
{
three_line();
return 0;
}
【程序结果】
注意,函数可以嵌套调用,但不能嵌套定义!
那什么是嵌套定义呢?举个例子大家就明白了
#include <stdio.h>
int main()
{
int Add(int x, int y)
{
return x + y;
}
return 0;
}
main
函数也是函数,main
函数只能调用Add
函数,而不能定义在函数内部。
5.2 链式访问
#include <stdio.h>
#include <string.h>
int main()
{
printf("%d\n", strlen("abcdef"));
return 0;
}
5.3 笔试题
以下代码输出的结果是什么?
#include <stdio.h>
#include <string.h>
int main()
{
printf("%d",printf("%d", printf("%d",43)));
return 0;
}
【解析】
首先,我们可以在cplusplus这个网站搜索printf
来查看它的返回值:
接下来我们从里向外分析,printf要打印一个整型,而这个整型是来自printf(“%d”, printf(“%d”, 43))的返回值,接下来的printf又要打印printf("%d", 43)
的返回值。
所以首先程序会先打印43,打印完43之后就会打印printf("%d", printf("%d", 43))
的返回值,而43的字符总数是2,所以会在屏幕中打印2,最后再打印整个printf("%d", printf("%d", printf("%d", 43)))
的返回值,也就是1,所以最后会在屏幕上打印4321。
六、函数的声明和定义
6.1 函数的声明
有的初学者可能会把函数定义写在main
函数后面,但我们要知道编辑器扫描代码是从上往下扫的,当扫到Add(a, b)
发现前面没有见过Add
函数,所以就会报错。
那如何纠正错误呢,只要在main
函数前声明就行了。
6.2 函数的定义
像上面刚刚写的代码,可以直接把函数定义放到main
函数前,是不是更加简洁。
6.3 声明和定义的拓展
其实我想告诉大家,实际上函数的声明和定义不是这样用的,上面的定义和声明只是语法展示。真正一个工程中,函数的定义和声明又是如何写的呢?我们接着往下看
比方还是求两个数的和
先新建一个头文件Add.h
,然后再定义一个源文件Add.c
,接下来我把函数的定义放到Add.c
中,对于函数的声明,我放到Add.h
中。如果想使用Add
函数,只需在text.c
中加上#include "Add.h"
即可。(函数的声明一般都放在头文件中,函数的定义(实现)放在源文件中)。注意#include
只包含头文件,库里提供的函数用尖括号,自己写的头文件用双引号。
- 拆成三个文件的好处(了解)
最后,为什么一个.c
文件就可以写完这些代码,而要把它拆成3个文件呢?其实它是有好处的。
①模块化开发(分工)
假设要写一个计算器程序,A程序员写加法,B程序员写减法,C程序员写乘法,D程序员写除法,如果没有多个文件设计,这些程序员都要在text.c中完成,这根本实现不了。有了多文件的设计,能够有效提高效率。
②有利于代码的隐藏
七、递归
7.1 什么是递归
为了了解递归,首先写一个史上最简单的递归(会发生错误的递归):
main
函数在自己调用自己。但调用着就会发现,程序崩了,它不会一直死递归下去。
同样可以按F10来观察程序
接下来程序就会弹出下面的窗口,stack overflow
:栈溢出。
这就要牵扯到内存中的栈区、堆区和静态了
每调用函数,都会为本次函数,在内存的栈区上开辟一块内存空间。
接下来回到刚刚的代码,每调用一次main
函数就会在栈区开辟一块内存空间,一直开辟总会有一天把栈区给“榨干”了,这时栈就溢出了。
这里为大家推荐两个问答社区网站:
- 这个网站(国外)相当于一个程序员的问答社区:点击跳转
- 思否是国内的程序员问答社区:点击跳转
7.2 递归的两个必要条件
7.3 递归练习
- 接收一个整型值(无符号),按照顺打印它的每一位。输入:1234,输出:1 2 3 4
【解题思路】
【代码实现】
#include <stdio.h>
void Print(unsigned int x)
{
if (x > 9) //判断两位数
{
Print(x/10);
}
printf("%d ", x % 10);
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);//%u - 输入无符号值
//写一个函数打印num的每一位
Print(num);
return 0;
}
【画图解释递归】
首先大家要知道,递归其实是两个词,递:递推,归:回归。(很重要!!!)
先递推(黑色),后回归(红色)
- 编写函数不允许创建临时变量,求字符串长度。
首先如果能创建临时变量的话,我们应该怎么写呢?
#include <stdio.h>
int ch_len(char* arr)
{
int count = 0;//计时器
while (*arr != '\0')
{
count++;
arr++;
}
return count;
}
int main()
{
char arr[] = "hello";
int ret = ch_len(arr);
printf("%d\n",ret);
}
【代码详解】
那不允许创建临时变量该怎么写呢?既然讲到了递归,就得用递归来解决。
【解题思路】
求ch_len(“hello”)
,如果第一个字符不是'\0'
,是不是就能转化成1+ch_len(“ello”)
,接下来ch_len(“ello”)
的第一个字符又不是'\0'
,是不是又能转化成1+1+ch_len(“llo”)
,接下来以此类推直到'\0'
。
【代码实现】
int ch_len(char* arr)
{
if (*arr != '\0')
return 1 + ch_len(arr + 1);
else
return 0;//当碰上第一个字符为\0,就返回0;
}
int main()
{
char arr[] = "hello";
int ret = ch_len(arr);
printf("%d\n",ret);
}
【画图解释递归】
先递推(黑色),后回归(红色)
八、迭代及练习
- 求n的阶乘(不考虑溢出)
【解题思路】
首先看看递归实现
【代码实现】
#include <stdio.h>
int Fac(int n) //形参的名字可以和实参一样
{
if (n <= 1)
{
return 1;
}
else
{
return n * Fac(n - 1);
}
}
int main()
{
int n = 0;
//输入n的值
scanf("%d", &n);
int ret = Fac(n);
printf("%d\n", ret);
return 0;
}
【画图分析递归】
递推(黑),回归(红)
【非递归法】
#include <stdio.h>
int Fac(int n)
{
int i = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret = ret * i;
}
return ret;
}
int main()
{
int n = 0;
//输入n的值
scanf("%d", &n);
int ret = Fac(n);
printf("%d\n", ret);
return 0;
}
- 求第n个斐波那契数
【递归解题思路】
【代码实现】
#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;
//输入n的值
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
【递归图】
实际上,当输入50时,编辑器的光标还在闪烁,有的人可能会认为程序挂掉了,其实并没有,程序此时此刻还在长时间计算第50个斐波那契数
所以这题使用递归还存在局限性,那么接下来我们来尝试迭代(循环)
【迭代解题思路】
【代码实现】
#include <stdio.h>
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while (n>2) //当n = 1或者2时可以不用算,因为结果都是1,
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
//输入n的值
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}