五、创建函数时的一些错误和导致的结果
1.函数未写返回类型,默认返回int类型
有些人在写函数时,就不写返回类型,尤其是不需要返回值的函数,因为他们认为不写返回类型就是返回void类型的,但这是一个经典错误,典型的0分。
函数不写返回类型,默认返回int类型。
如以下代码所示,test返回int类型,这是c语言的规定。
test()
{
printf("haha");
}
int main()
{
test();
return 0;
}
2.函数不写形参,但是传递了实参
如以下代码所示,函数中不写形参,但是却传递了实参,这样的操作是不会报错的,不就是你传的参数这个函数不接收罢了,对代码毫无影响。
但是不推荐这样写,容易误导人,降低代码的可读性。这也是c语言不严谨的一个小地方
int test()
{
printf("haha");
}
int main()
{
test(1000);
return 0;
}
3.函数形参写了void,但非要传实参
如以下代码所示,函数已经明确规定不需要参数了,但是非要传参,那这时编译器就会报警告
#include<stdio.h>
int test(void)
{
printf("haha");
}
int main()
{
test(1000);
return 0;
}
4。函数里没有return,但强行要求返回一个值,那大多数编译器返回最后一个语句的执行结果
如以下代码所示,函数里没有return,但强行要求返回一个值,那大多数编译器返回最后一个语句的执行结果
#include<stdio.h>
int test(void)
{
printf("haha\n");
}
int main()
{
int ret=test(1000);
printf("%d\n", ret);
return 0;
}
运行结果如下,printf的返回值是他打印出的字符数(hehe\n),
在库函数文件中我们可以看到
但只最好不要这样写,因为这只是一般情况,在其他的编译器中可能会报错。
5.声明函数时省略函数形参,但不会忽略类型
int Add(int, int);
这样的代码在声明函数时是没错的,因为声明函数可以忽略形参的变量,但是不可以忽略类型。
但这样的代码在定义函数是是错误的,
我们不建议这样声明函数时这样写,因为声明和定义函数一般都是一起的,容易出错。
5. 函数的嵌套调用和链式访问
我们可以通过下面这段代码来理解什么是嵌套调用。
three_line 中调用了new_line这个函数,一个函数调用另一个函数,这就是嵌套调用。
> 注意函数可以嵌套调用,但是绝对不可以嵌套定义。函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
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;
}
2.链式访问
我们通过下面这段代码来理解一下什么是链式访问。
顾名思义,链式访问就是它们之间的关系像一条链子一样串起来,把一个函数的返回值作为另一个函数的参数。
这里是将strlen的返回值作为printf的参数
我们再来看一个经典例子
printf的返回值是它打印出的字符个数。
这里先打印出43,
然后将43作为第二个printf的参数,打印出2,
再将2作为最外面那个printf的参数,打印出1 ,
所以最后的结果是4321
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
int ret = strlen(strcat(arr,"bit"));//这里介绍一下strlen函数
printf("%d\n", ret);
return 0;
}
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
//结果是啥?
//注:printf函数的返回值是打印在屏幕上字符的个数
return 0;
}
七、、函数的声明和定义
1. 函数声明:
告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数 声明决定不了。
函数的声明一般出现在函数的使用之前。要满足先声明后使用。
2. 函数定义:
函数的定义是指函数的具体实现,交待函数的功能实现。
test.h的内容 放置函数的声明
test.c的内容 放置函数的实现
函数的声明一般要放在头文件中的。
我们看下下面这段代码,有什么错误?
#include<stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
int ret = Add(a, b);
printf("%d", ret);
return 0;
}
int Add(int x, int y)
{
return x + y;
}
由于计算机编译时是按照从上到下的顺序编译的。当我们将将函数放在调用它的后面,调用是就不知道函数的定义是什么,就会报错。
这是我们为了防止报错,在前面加一个函数的声明,表明这个函数的存在,这样就可以防止报错了。 此时上面的Add是声明,下面的Add是定义。
#include<stdio.h>
int Add(int x, int y);//声明
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
int ret = Add(a, b);
printf("%d", ret);
return 0;
}
int Add(int x, int y)//定义
{
return x + y;
}
当然我们可以按照之前的方法将Add放在调用前,这时Add就是定义,同样这时的定义是一个特殊的声明·。
#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 ret = Add(a, b);
printf("%d", ret);
return 0;
}
将定义和申明分开的做法大家肯定会觉得麻烦,为什么要这样做呢?为什么不能将像上面的定义在去调用函数前,此时定义是特殊的声明?
这种定义方式在工程文件不是这样使用的,而是通过分文件使用的,我们将声明放在头文件中,我们在三子棋就是这样使用的。
那这样定义,分文件使用有什么好处呢?
这样我们就可以隐藏头文件,从而隐藏函数的声明,确保自己的代码不轻易被别人解读。
#include<stdio.h>
void Add(int* n);
int main()
{
int num = 0;
Add(&num);
printf("%d\n", num);
Add(&num);
printf("%d\n", num);
Add(&num);
printf("%d\n", num);
return 0;
}
void Add(int* n)
{
//*n = *n + 1;
(*n)++;
}
八、函数递归
1. 什么是递归?
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。个过程或函数在其定义或说明中有直接或间接 调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,
递归策略
只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
我们来看一个简单的递归
#include<stdio.h>
int main()
{
printf("hehe\n");
main();
return 0;
}
这份递归结果是这样的,最终程序挂了
我们调试时,就会发现错误:stack overflow栈溢出。
那栈溢出是什么意思呢?为什么会栈溢出呢?这就需要我们了解内存的分区使用了。内存粗略分为栈区、堆区、和静态区,如下图:
其中栈区存放着函数的局部变量、函数参数、临时的变量。每一次调用递归,都会为本次函数,在内存的栈区上开辟一块空间。而我们上面的调用递归,会使得main函数不断占用栈区,这个递归相当于一个无限递归,而我们的栈区是有限的。所以最终就会使得栈区内存被完全占用,也就是所谓的栈溢出了。
也就是栈区的空间被耗尽了,程序最终会崩溃,所以上面的简单递归就是一个错误递归。
那我们如何正确使用递归呢?我们首先要明白递归的条件
2.递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
我们可以通过下面这两个例子来理解一下。
(1).接受一个整型值(无符号),按照顺序打印它的每一位。
例如: 输入:1234,输出 1 2 3 4.
这道题我们一开始的思路就是利用/和%将每一位单独隔开,然后依次打印,但是这样打印的结果是4 3 2 1。
void Print(unsigned int num)
{
while (num != 0)
{
printf("%d ", num % 10);
num /= 10;
}
}
int main()
{
unsigned int num = 0;
scanf("%d", &num);
Print(num);
return 0;
}
那我们如何来实现顺序打印?我们可以来尝试一下递归的思想,
如果想要将1234按顺序打印出来,
那可不可以分为先打印123?然后再打印4呢?
想要将123打印出来,可不可以先打印12再打印3呢?
想要12按顺序打印出来,可以先把1打印出来,然后再打印2,
也就是说
那我们递归函数的表达式应该是这样,
也就是如果num>9,执行两条语句,如果不大于9,就只执行后一条语句。
我们按上面的思路来实现一下代码,
void Print(unsigned int num)
{
if (num > 9)
{
Print(num / 10);
}
printf("%d ", num % 10);
}
int main()
{
unsigned int num = 0;
scanf("%u", &num);
Print(num);
return 0;
}
看到这个代码肯定还是有点懵的,但是我们可以用画图来理解,
递归=递推+回归,先递推再回归
我们再来想一下之前使用递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
我们发现我们建立的函数完全符合这两个条件
其中num>9是递归的结束条件,
num/10逐渐逼近递归的结束条件
(2).编写函数,不允许创建临时变量,求字符号长度
我们已经知道在库函数中有一个strlen函数可以求字符串的长度,也就是这样使用的。
#include<stdio.h>
#include<string.h>
int main()
{
char a[] = "hello";
printf("%d\n", strlen(a));
}
我们也可以根据strlen函数来模拟实现一下,我们先找到首字符的地址,判断是不是\0,如果不是则指针+1,找到下一个地址,再继续判断。直到找到\0,我们可以用一个计数器来记录判断不是\0的次数,也就是字符串的地址。
#include<stdio.h>
int my_strlen(char* str)
{
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
int main()
{
char a[] = "hello";
printf("%d\n", my_strlen(a));
}
这个模拟实现的方法是通过创建临时变量来实现的,那我们可以不创建临时变量来实现吗?当然可以。
我们可以使用递归的思想。将字符串一点点减少。
我们想要求my_strlen(“abcd”)的长度,不就是strlen(“abc”)+1吗?
想要求my_strlen(“abc”)的长度,不就是(strlen(“ab”)+1)+1吗?
想要求my_strlen(“a”)的长度,不就是((strlen(“ab”)+1)+1)+1吗?
不就是1+1+1+1+my_strlen(“\0”)的大小吗?也就是4
表达式就是这样:
递归函数就是这样的。
int my_strlen(char* str)
{
if (*str != '\0')
{
return 1 + my_strlen(str + 1);
}
else
{
return 0;
}
}
int main()
{
char a[] = "hello";
printf("%d\n", my_strlen(a));
}
3.递归与迭代
迭代是指用非递归的方式解决问题
我们可以通过这两个题来理解一下
(1).求n的阶乘。(不考虑溢出)
非递归的方式,也就是迭代的方式,相信大家都可以很快写出来,代码如下:
#include<stdio.h>
int Factorial(int n)
{
int i = 0;
int s = 1;
for (i = 1; i <= n; i++)
{
s = s * i;
}
return s;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Factorial(n);
printf("%d\n", ret);
return 0;
}
用递归的方法也简单,因为n的阶乘=n*n-1
#include<stdio.h>
int Factorial(int n)
{
if (n > 1)
{
return n * Factorial(n - 1);
}
else
{
return 1;
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Factorial(n);
printf("%d\n", ret);
return 0;
}
这道题,递归和迭代都可以解决,但是递归可能会造成栈溢出,程序崩溃,因此,这个题更推荐迭代。
(2). 求第n个斐波那契数。(不考虑溢出)
我们首先需要知道斐波那契数的递推式
知道了递归式,我们可以写出代码了
#include<stdio.h>
int Fib(int n)
{
if (n > 2)
{
return Fib(n - 1)+ Fib(n - 2);
}
else
{
return 1;
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
但是我们发现有问题; 在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
我们可以定义一个全局变量count计算一下,在计算fib(40)的过程中fib(3)被重复计算了多少次
#include<stdio.h>
int count = 0;
int Fib(int n)
{
if (n == 3)
{
count++;
}
if (n > 2)
{
return Fib(n - 1)+ Fib(n - 2);
}
else
{
return 1;
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
printf("%d\n", count);
return 0;
}
光是fib(3)就被计算了将近4千多次,可见这个程序的计算量之大,运行速度就不快。
那么有什么更好一些的办法呢?其实我们可以使用循环,循环也是迭代的一种
已知斐波那契数字是前两个数之和,已知第一个和第二个数,就可以求出第三个数。
这时我们将后两个数放在前两个的位置,求出下一个数,这样循环下去。
判断条件是n>2,n<=2直接返回即可,由于我们返回的是c,c要初始化为1才行
#include<stdio.h>
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出) 这样的信息。
系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一
直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
那如何解决上述的问题:
将递归改写成非递归。
使用static对象替代 nonstatic 局部对象。
在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(即栈对象),这不 仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保存递归调用的中间状态,并且可为 各个调用层所访问。许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开 销。
总结
本篇介绍了创建函数时的一些错误和导致的结果、函数的嵌套调用和链式访问、函数的声明和定义和函数递归。
重点是函数的声明和定义和函数递归,一个容易混淆,一个不易理解,我们需要在今后的练习中好好理解掌握。
希望这篇文章对你有所帮助。