【前言】本系列(初阶)适用于初学者课前预习或者课后复习资料,包含了大部分基础知识点梳理与代码例方便看官理解,有问题请指出。本人邮箱地址:[email protected]
可爱捏
目录
1.什么是函数
1.1库函数
1.2自定义函数
2.函数调用
2.1传值调用
2.2传址调用
2.3小练习
3.函数的嵌套调用和链式访问
3.1嵌套调用
3.2链式访问
4.函数声明与定义
4.1函数声明
4.2函数定义
5.递归
5.1什么是递归
5.2递归练习
6.递归与迭代
我们在数学中经常会遇到函数,高考压轴也常常和函数拖不了干系。函数在数学中是不可或缺的一环。
那么在c语言中,函数同样发挥着不可或缺的重要力量。在维基百科里,函数被称为子程序。子程序通常指一个大型程序中的部分代码,并且负责完成某项特定的任务,同时具备一定的独立性。当你需要再次使用时,则可以直接调用,节省精力和时间,也能使代码看起来更加的简洁美观。
在c语言中通常分成两种函数。库函数和自定义函数。
在编写程序时,小伙伴们总是会频繁的使用某些功能,比如往屏幕上打印一些字符,又或者输入一些东西,这些被我们看作是基础功能的东西,也是函数,只不过为了支持可移植性和提高程序效率,c语言对其进行了一系列的封装,方便我们直接使用。比如scanf,printf,极大的提高了我们编写的方便性。
库函数的学习可以前往www.cplusplus.com或者一些其他的网站自行了解。包括不限于IO函数,字符串操作函数,内存操作函数等。
简单的介绍一个吧
【strcpy】也就是copy string,复制字符串。
其定义为
char *strcpy(char *destination const char* source);
该函数返回一个指向最终的目标字符串 destination的指针。
int main()
{
char arr1[20] = "hello,world";
char arr2[20] = { 0 };
//把arr1拷贝到arr2中
char *p = strcpy(arr2,arr1);
printf("%s", arr2);
return 0;
}
注意,需要头文件
更多详细的点可以在www.cplusplus.com上找到。需要注意的是库函数的功能,返回值,还有书写格式以及头文件等。
使用库函数时,必须有包含#include对应的头文件哦。
还有两个网站可以很好的解决库函数问题。
cppreference.com (英文版)
cppreference.com (中文版)
库函数无法在这里进行一个完全的介绍,需要在不断的学习中不断积累。
库函数及时可以帮助程序员解决很多问题,但对于这个瞬息万变的世界,还是太微不足道了。很多其他的功能都需要人工的去根据实际情况书写。
函数基本组成
ret_tpye fun_name(para)
{
statement;
}
ret_type是函数的返回类型
fum_name是函数名
para是参数
statement是语句项。
例如,我们编写函数实现一个比较大小并返回较大值的程序
int main()
{
int num1 = 10;
int num2 = 50;
int max = getmax(num1, num2); //我们定义函数名为getmax
printf("%d\n", max);
}
接下来就是对函数的建立。(一般放在main函数前,方便查看)
int getmax(int x, int y)
{
return (x > y) ? x : y;
}
函数名我们取为getmax,其中的参数是两个整型,负责接收主函数里的num1和num2,同时函数的返回类型为int,正好对应return的值,返回了较大的那个数。
如果没有返回值,就置为void,没有参数也置为void
例如
void test (void)
{
printf("hello");
}
此函数只是简单的打印一个hello,并不需要返回一个值到主函数里,所以第一个置为void,同时函数不需要调用参数,所以第二个也是void
需要注意的是,函数中的x与y接受了num1与num2的值后,他所划分的空间与num不是同一片!
这一点非常重要,就像大家都住在一样的别墅区里,但是却是不一样的位置,虽然房子看起来一样,但可能你在东,我在西。
来看例子深刻的理解。
写一个函数交换两个整型变量
void exchange1(int x, int y)
{
int temp = 0;
temp = x;
x = y;
y = temp;
}
int main()
{
int num1 = 1;
int num2 = 2;
exchange1(num1, num2);
printf("num1=%d,num2=%d", num1, num2);
return 0;
}
运行后发现并未交换成功。
这也就是上文所提到的,大家虽然都是一样的值,x接受了num1的值,但是也只是房子相同,其地址是不同的。num1被称为实参,x则被称为形参,其具备各自独立的空间,是互不影响的。
形参实例化之后就相当于是实参的一份临时拷贝。对形参的任何改动都无法影响实参。并且形参在函数调用完成后便自行销毁。(详情可见专栏第一章对生命周期的讲解)
那我们该如何实现呢?答案是指针。
一个在主函数,一个在自定义函数里,就像是一个人在中国,一个人在美国,想要建立联系就需要借助网络的力量,指针就充当着如此的作用。网络有ip地址,c语言有指针。借助指针就能跳出实参形参的限制,这也是指针的魅力所在。指针也是c语言经久不衰的原因。
void exchange1(int *p, int *q)
{
int temp = 0;
temp = *p;
*p = *q;
*q = temp;
}
int main()
{
int num1 = 1;
int num2 = 2;
exchange1(&num1, &num2);
printf("num1=%d,num2=%d", num1, num2);
return 0;
}
我们向exchange中传入的是num1与num2的地址(&为取值符),那么函数里也改为*p,*q,分别接收这俩个值。*p与*q不同于x与y,他们始终都是num1和num2的“门牌号”,并不会拿了值就另起炉灶了。所以这样程序就会运行成功。
综上我们对函数调用做出一个总结。
函数的形参与实参占据着不同的模块,即对形参修改并不会影响到实参。
这也在上文解释的非常详细了。
传址调用就是将函数外部创建的变量的内存地址传递给函数参数的一种调用函数方式。
这种传参方式使得函数内外可以产生真正的联系,也就是在函数内部的操作会直接的影响到函数外部的变量。
毕竟函数内外的门牌号都是一样的,总不至于走错了房子。
问题描述:书写一个函数,每次调用此函数,就将num值加1
#include
int add(int *x)
{
(*x)++;
}
int main()
{
int num = 0;
num = add(&num);
printf("%d", num);
return 0;
}
应该也是没有什么问题,程序运行正确就显示为1,需要注意的点就在于*和++的优先顺序啦。
++是优于*的,所以我们必须先把*x括起来。
还有需要注意的是不要省略作为函数返回类型的int哦,哪怕是省略掉函数也是会默认返回int类型的。不是我们所想的void类型。这点也是需要牢记的。
需要记住的是,函数与函数间可以进行相互调用。
比如往屏幕上打印三个三行hello,用嵌套函数来实现。
#include
void one_hello()
{
printf("hello\n");
}
void three_hello()
{
for (int i = 0; i < 3; i++)
{
one_hello();
}
}
int main()
{
three_hello();
return 0;
}
也是非常简单,需要记住的是
函数可以嵌套调用,但不可以嵌套定义
例如在main函数里进行一个其他函数的定义,是不被支持的。
链式访问用一句话来说,就是将一个函数的返回值当作另一个函数的参数。
有一个关于链式访问的非常经典的例子,分享在这里。
#include
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
}
答案是4321
小知识点:printf的返回值是打印在屏幕上的字符个数。
那么就是先打印43,再返回2,将2打印后返回1,最终打印1
声明的作用就是告诉编译器有一个函数,他的函数名叫什么,他的参数是什么,他的返回类型是什么。
并且声明需要满足的是先声明后使用。
比如我们先前所写的所有自定义函数,都是放在了main函数之前,所以哪怕没有对其进行声明也无伤大雅。
但一旦我们将函数放在了main函数之后,也不进行声明,就会出现问题了。编译器从第一行开始查看,查看到main函数里你需要用到的函数,再转过头去并未发现你的声明或定义,就会有问题。所以有两种处理方式
1.在main函数前定义
2.在main函数前声明,在main函数后定义
函数定义就是写明函数的具体实现方式。
一般而言我们将函数声明放置在.h文件中,将函数定义放在.c文件中,这样拆开的好处就在于可以分工实现,进行模块化开发。
分文件的进行书写,在后期编写例如扫雷等大型代码的时候就非常有用,一目了然,且结构清晰。
递归,逆向思维解决问题。
也就是调用自身的一种编程技巧。
递归最大的特点就在于可以用极少量的程序,去进行需要的大量计算,极大的减少所需要的代码量。所以其主要思考方式就是以大化小。
递归有两个非常重要的条件。
且是必须要有的。
1.递归存在一个限制的条件,当满足限制条件时,递归将不再进行。
2.在每一次的递归调用后都会越来越接近这个限制的条件。
看一个例子
#include
int main()
{
printf("hello\n");
main();
return 0;
}
在main函数里接着调用main,又因为缺乏了限制条件,导致了递归的不断进行,屏幕上打印了大量的hello,并随着栈溢出而停止。
在递归里,每一次调用都会为本次函数在内存栈区中开辟空间。但栈是有限度的。
来看一个例子理解递归的用法。
题例:输入1234 输出1 2 3 4
之前我们见过类似的例子,1234%10得到4,1234/10得到123,再用123%10得到3,123/10得到12,再以此反复。
递归其实就是进行一个逆向的思考,首先得去思考如何让其停下来。也就是找到这个限制的条件。
上面这个例子往后面进行的话,最后一步就是1%10=1,1/10=0,也就是当分离的只剩下1的时候就得到了最后一个目标值。
不妨令限制条件为n>9,那么递归可以写成
#include
void print(int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d", n%10);
}
int main()
{
int num = 1234;
print(num);
return 0;
}
如果n>9的话,就会调用print函数里的print,进入后以此类推,会一直进行到为n=1,此时不满足限制条件,则打印1,并返回上一级的print函数。
也就是不断的进行调用,直到底层,得到了一个值后,再有序的往前逐层返回。
递归,分开来看,递代表着递推,在不断的调用中寻找着终止条件
归代表着回归,一旦底层有了值,便可以逐层进行返回。
再比如在不允许创建临时变量的情况下,求解一个字符串的长度。
int strlen(char *arr)
{
if (*arr == '\0')
{
return 0;
}
else
{
return 1 + strlen(arr + 1);
}
}
#include
int main()
{
char arr[] = { "abcdef" };
strlen(arr);
printf("%d", strlen(arr));
}
我们寻找到的限制条件很容易就会想到是字符串的最后一个字符等于'\0'了。
还需要提的是arr其实就是数组第一个元素的地址。所以不断的加1进行遍历直到遇到/0,也就是字符串的终止条件。
写到这,看官也算是领略到了递归的魅力了把,当你再去研究数据结构中的一些例子时,你会对递归有更深的感触。递归在数据结构中也会扮演重要的角色。
但递归就一定无敌了吗?答案是否定的。递归也具有其致命的缺点。
题例:求n的阶乘
int factorial(int n)
{
if (n <= 1)
{
return 1;
}
else
{
return factorial(n - 1) * n;
}
}
在这里简单提一下栈帧。
在每次函数调用时都会为本次调用分配一个内存空间(在内存栈区),也被称为栈帧空间。这个空间会存在到递结束,归开始的时候。
也就是当n=3时,会调用factorial,此时会为n=3分配一个栈帧,当递归再次返回到这里时,空间会被销毁。
栈帧的概念非常重要,对算法等都有很大的帮助。可以自行了解。
题例:求第n个斐波那契数
int fib(int n)
{
if (n <= 2)
{
return 1;
}
else
{
return fib(n-2) + fib(n - 1);
}
}
用递归确实很快就能完成,但是当你开始运行时,你输入的数字越来越大,你会发现运算需要的时间越来越多。可以试着求40和50的斐波那契数。你会发现运行时间是天壤之别。
当n=50,我们需要去调用49与48,调用49和48有需要调用47,48,47,46,以这样的方式递归下去,其时间复杂度会以指数倍增长。因为在调用过程中,进行了许多的重复计算。
可以粗略的统计一下当n等于40时,第三个斐波那契数被计算的次数
int count = 0;
int fib(int n)
{
if (n == 3)
{
count++;
}
if (n <= 2)
{
return 1;
}
else
{
return fib(n-2) + fib(n - 1);
}
}
会发现仅仅是n=40,count的值就已经足够恐怖了。更别提50,其增长以指数倍进行。
所以递归并非万能用法,当遇到这样的情况,还是请使用非递归的办法。,递归的操作稍有不慎,就会陷入栈溢出的尴尬境地。
改为非递归
int factorial(int n)
{
int a = 1;
int b = 1;
int c = 0;
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
这样就会极大的减少时间复杂度,运行也就更加的快捷。省略了大量的重复计算。
所以请记住:
如果递归参数过大,可能会报错,例如:stack overflow(栈溢出)
系统分配给程序的栈空间是有限的,如果出现死递归,会导致一直开辟空间直到栈空间耗尽,这样的现象就是栈溢出。
结束语:函数初阶的运用大概就是这些内容,想要熟练掌握还是要多手撕代码,比如递归的经典题型:青蛙爬台阶,汉诺塔,都是很值得深入研究的内容。