一.概念
数学中我们常见到函数的概念。但是你了解C语言中的函数吗?
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组 成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。说到底,函数就是为了我们完成特定的任务而由我们的加工与改造产生(写代码)。
二.分类
1.库函数
这类函数是C语言库里自带的,通常你在使用时,写出正确的函数名及对应的头文件,传入对应的参数就能使用它的功能。
在你要查找函数时及功能时,下面的网站能帮助你。
cplusplus.com - The C++ Resources Network
cppreference.com (英文版)
cppreference.com (中文版)
1.1为什么存在库函数
我们在开发的过程中每个程序员都可能用到的函数, 为了支持可移植性和提高程序的效率,C语言的基础库中提供了一系列类似的库函数,方便程序员 进行软件开发。
2.自定义函数
如果库函数能干所有的事情,那还要程序员干什么?
所以更加重要的是自定义函数。 自定义函数和库函数一样,有函数名,返回值类型和函数参数。 但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
2.1函数组成
ret_type fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型 fun_name 函数名 para1 函数参数
举例:
写一个函数可以找出两个整数中的最大值。
int get_max(int x, int y)
{
if(x > y)
return x;
else
return y;
}
三.函数的参数
3.1实际参数(实参)
真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时它们都必须有确定的值,以便把这些值传送给形参。
3.2形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内 存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁。因此形式参数只在函数中有效。
所以:形参的改变不影响实参,形参时实参的一份临时拷贝(在函数栈帧与销毁了解更深)。
四.函数的调用
1.传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
2.传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接改变函数外部的变量。(影响实参)。
3.例子
写出一个函数来交换两个数的值
#include<stdio.h>
//传值调用
void Swap1(int x, int y)
{
int ep = 0;
ep = x;
x = y;
y = ep;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
Swap1(&a,&b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
你猜能不能实现?
为什么会这样呢?
根据上面的介绍,你或许猜到了。因为在函数里,形参的改变(在Swap函数中交换x y)不影响实参(main函数的a b)。
那么,该传址调用登场了。
#include<stdio.h>
//传址调用
void Swap2(int* c, int* d)
{
int e = 0;
e = *c;
*c = *d;
*d = e;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a,&b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
这样就很好的完成交换了!
五.函数的嵌套调用与链式访问
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;
}
函数能嵌套调用,但不能嵌套定义!
void new_line()
{
printf("hehe\n");
three_line()
{
printf("haha\n");
}
//error
}
2.链式访问
把一个函数的返回值作为另外一个函数的参数。
int main()
{
char arr[20] = "hello";
printf("%d\n", strlen(strcat(arr,"bity"));
return 0;
}
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
//结果是啥?
//注:printf函数的返回值是打印在屏幕上字符的个数
六. 函数的声明和定义
1.函数的声明
1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3. 函数的声明一般要放在头文件中的。
2.函数的定义
函数的定义是指函数的具体实现,交待函数的功能实现。
在写函数时,函数要放在main函数前面(编译器时从上往下扫描)也可以在main函数前声明
如果不想这样,也可以用单独用头文件(xxx.h)来声明,在源文件(xxx.c)实现函数,在另一个文件(xxx.c)来测试函数。(写较大工程时用到这种写法)
3.函数的封装
静态库: 在程序编译的时候,将库编译进可执行程序中, 运行的时候不需要外部函数库
我们可以把函数的实现写在同一个文件(xxx.c)中,再把函数的声明写在同一个头文件(xxx.h)中,对(xxx.c)进行函数封装->
然后再给文件下会生成(xxx.lib)文件,将该文件与(xxx.h)文件给别人,别人能使用你所创造的函数,但看不到里面具体是怎么写出来的。(别人在使用时要加 #pragma comment(lib,"xxx.lib"))。
七.函数的递归
1.概念
程序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 将要解决的大问题分解为一个个小问题。大大减少了程序的代码量。(代码量少,思考量大)
递归的主要思考方式在于:把大事化小
2.必要条件
存在限制条件,当满足这个限制条件的时候,递归便不再继续。(通常以if来作限制)
每次递归调用之后越来越接近这个限制条件。
八.递归与迭代(非递归)
1.例子
求第n个斐波那契数。
如果不考虑溢出,可用递归来解决
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
计算小一点的第n值还好,但一旦到了成百上千时,该程序就顶不住了。在该程序中,如果你观察的仔细,会发现其实在递归的过程中有许多数字被重复递归(因为算的数是从后往前相加)。
那么,我们就用迭代(循环)来提高效率
int Facs(int N)
{
int a = 1;
int b = 1;
int c = 1;//N<=2是N都是1
while (N > 2)
{
c = a + b;
a = b;
b = c;
N--;
}
return c;
}
但N>2时,a b相加赋值给c后就往后跳一位,再依次进行相加
注:但N到一定限度时,计算结果一样是错的(超过int的最大值),但就算是错的,非迭代的效率还是比递归的快(你看我快不快就完了)
在实际情况中,不是一定非要用递归或者是迭代,哪种用着更顺手,更容易解决问题就用哪种。
九.练习
对于函数与递归,最缺少不了的是练习。(先做后看效果更好喔)
1.strlen函数的实现(函数实现)
2.steln函数的实现(递归实现)
//strlen 用函数实现
int Strlen(char arr[])
{
int count = 0;
while (*arr != '\0')
{
count++;
arr++;
}
return count;
}
//实现不能用临时变量
int Strlens(char arr[])
{
if (*arr == '\0')
return 0;
return 1 + Strlens(arr+1);
}
3.字符串的逆序(用循环)
4.字符串的逆序(用递归)
前提:都不能借助库函数实现
//用循环
void Reverse_String(char arr[])
{
int len = Strlens(arr);//Strlens的函数实现
int left = 0;
int right = len - 1;
while (left < right)
{
char tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
//字符串逆序(递归实现)(思路与上面一样)
void Swap(char* a, char* b)
{
char c = *a;
*a = *b;
*b = c;
}
void reverse_string(char arr[],int left,int right)//借助下标
{
if (left >= right)//left走到right的后面去也就停止
return;
Swap(&(arr[left]), &(arr[right]));//char类型
reverse_string(arr, left + 1, right - 1);
}
//第二种写法(想不到,很妙)
void reverse_strings(char arr[])
{
int len = Strlens(arr);
//1
char tmp = arr[0];
//2
arr[0] = arr[len - 1];
//不着急把a[0]的值传进去,保证逆序的strlen的长度不会把arr[0]算进去
//3
arr[len - 1] = '\0';
if(Strlens(arr+1) >= 2)
reverse_strings(arr+1);//起始位置更改arr[1]
//4
arr[len-1] = tmp;
}
关于递归的第二种,我们来画图来解释
能把函数处理结果的2个数据返回给主调函数,在下面的方法中不正确(A)
A.return这2个数-->函数返回值只有一个
B.形参用数组
C.形参用2个指针
D.用2个全局变量->共享单车 可以在各个函数里使用,但在函数多的时候不记得定义的全局变量
有可能被改变,造成结果的错误,在平时里尽量少用
5.汉诺塔问题
在一根柱子上放N个盘子,有3根柱子(A B C),保证盘子在柱子上的顺序(由上往下)从小到大
一次只能移动一个盘子。实现:将在A上的盘子移到C上。
想来假设N的(盘子)个数
N=1 A->C
N=2 A->B A->C B->C
N=3 A->C A->B C->B A->C B->A B->C A->C
.....
将N分为上面(N-1)个和下面最大的那1个,将N-1个盘子通过C移到B上,再将最大1个直接移到C上,在B上的N-1个盘子通过A全部移到C上就完成。
void Move(char position,char Position)
{
printf("%c->%c ", position, Position);
}
void Hanoi(int N, char begin, char middle, char end)
{
if (N == 1)
Move(begin, end);
else
{
//1.将begin(A)上面的N-1个盘子通过end(C)全部移到middle(B)上
Hanoi(N - 1, begin, end, middle);
//2.最后一个直接从begin(A)移到end(C)
Move(begin, end);
//3.将在millde(B)上的盘子通过begin(A)移到end(C)上就完成任务
Hanoi(N - 1, middle, begin, end);
}
}
最后
以上便是我所学习与记录有关函数的相关知识,希望对你有所帮助!