一篇理解c语言函数看不懂拿刀找我

一篇理解c语言函数看不懂拿刀找我,第1张

一篇理解c语言函数看不懂拿刀找我

目录

什么是函数?

函数的分类

库函数

库函数的分类

strlen

strcpy

易错点

memset

自定义函数

应用举例

函数的参数

 传值调用

传址调用:

传值调用和传址调用区分总结

函数调用的本质

函数的嵌套调用和链式访问

函数的嵌套调用

函数的链式访问

函数的声明和定义

函数的递归

应用举例1

应用举例2

应用举例2变式

应用举例3

应用举例4


什么是函数?

c语言函数定义:在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组 成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软 件库。

那为啥要出现函数这一概念呢?难道不可以只在主函数内部写呢?其实当然可以把所有代码都可以在主函数的内部使用,但是未免有些太复杂,大大降低程序员在读代码时候的效率,我们程序员在写代码不仅能让自己看懂而且要让别人看懂你的代码。你可以看看在我们写了这么多代码,函数是我们经常用到的,因为不用函数写代码会很复杂,如果不用函数写东西,那在写代码时也根本写不出来什么。

函数的分类

函数有两类一种是库函数一种是自定义函数。

库函数

什么是库函数呢?

库函数(Library function)是把函数放到库里,供别人使用的一种方式。.方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。调用的时候把它所在的文件名用#include>加到里面就可以了。一般是放到lib文件里的。

库函数优点:库函数在用户地址空间执行,系统调用是在内核地址空间执行,库函数运行时间属于用户时间,系统调用属于系统时间,库函数开销较小,系统调用开销较大。由于C语言的语句中没有提供直接计算sin或cos函数的语句,会造成编写程序困难;但是函数库提供了sin和cos函数,可以拿来直接调用。显示一段文字,我们在C语言中找不到显示语句,只能使用库函数printf。C语言的库函数并不是C语言本身的一部分,它是由编译程序根据一般用户的需要,编制并提供用户使用的一组程序。C的库函数极大地方便了用户,同时也补充了C语言本身的不足。在编写C语言程序时,使用库函数,既可以提高程序的运行效率,又可以提高编程的质量。

通俗的讲,库函数可以让原本复杂的东西不用让程序员去写,他自己创建一个函数库,把复杂的东西用简单的代码表示出来,这时程序员只要按照c语言编译器规定引头文件就可以实现。比如我们在用的printf,math.h,stdio.h,stdlib.h.....等等,这些只要写出来我们就不用写代码写出它的功能,编译器可以让我们实现。

库函数的分类

IO函数 字符串 *** 作函数 字符 *** 作函数 内存 *** 作函数 时间/日期函数 数学函数 其他库函数

我们需要怎么学这些头文件呢?

网站1:MSDN(Microsoft Developer Network)

www.cplusplus.com

http://en.cppreference.com(英文版)

http://zh.cppreference.com(中文版)

这些工具我们都可以去学习,参考,去应用起来

strlen

 

 

 

 这里告诉我们strlen这个函数是求字符串长度的,头文件是或者

 那怎么实现的?我们来尝试一下

#include
#include
int main()
{
	char str[10] = "abcdef";
	int len = strlen(str);
	printf("%dn", len);
	return 0;
}

strcpy

 

 这里的意思就是你想要把一个数组里的字符串拷贝到另外一个数组中,首先strcpy(目的地,来源),目的地就是你要复制到哪个数组里,来源是你想要复制的字符串在哪个数组里。其中引头文件#include

我们实现一下

#include
#include
int main()
{

	char str1[20] = {0};
	char str2[20] = "abcdef";
	strcpy(str1, str2);
	printf("%sn", str1);
	return 0;
}

这时就可以很好的打印了 

易错点

 当我们没有在[]就会报出这样的警告意思就是栈溢出。所以以后我们尽量在[]写上元素个数不要偷懒。

memset

 

 

void *memset(void *s, int ch, size_t n);

函数解释:将s中当前位置后面的n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。

 估计你可能看不懂我们代码实现一下你应该就明白了

#include
#include
int main()
{
	char str[20] = "hello,world";
	memset(str, 'o', 5);
	printf("%sn", str);
	return 0;
}

 这里的意思就是将str字符串里的前五个字符修改为字符 'o'

这会就用该明白库函数的作用了吧,这些库函数的复杂功能无需自己实现,这就是库函数的重要性。

自定义函数

什么是自定义函数呢?那当然是程序员自己定义出来的函数,如果只靠库函数也是什么都干不成的,剩下的编程魅力最后还得交给伟大的程序员。

自定义函数和库函数同样也具有返回值,函数参数,返回类型,只不过函数名是程序员根据要实现自己的内容自己定义的。

我们实现一个判断大小函数让你具体知道怎么创建自定义函数

#include
#include
int get_max(int a, int b)
{
	return a > b ? a : b;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int ret = get_max(a, b);
	printf("%dn", ret);
	return 0;
}

应用举例

实现两个数的交换 ,输入12   23,输出23  12。

方法一:

#include
swap(int a, int b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	swap(a, b);
	printf("%d %dn", a, b);
	return 0;
}

打印结果:

为啥不是我想要的结果呢?

我明明将那两个数交换啦啊

接着我们调试一下来看看实际的情况

进入函数之前的

进入函数之后的

我们可以发现进入函数之前和进入函数之后的地址完全不一样,但你说a和b交没交换呢,从图二你肯定会发现a和b互换了啊,为啥打印又变回来了,真是个神奇的事。

这里又是怎么回事呢?

我们来仔细分析一下吧

我们在传一个值的时候你要将真实的它都传过去,这句话又是什么意思呢?我们来举个例子,比如你你要将一瓶雪碧和一瓶可乐交换一下,怎么交换呢?是不得找个空瓶子,先把可乐,倒入空瓶子中,再将雪碧倒入可乐瓶子中,然后再将可乐倒入雪碧瓶子里,而在让你调用的时候,你拿的原模原样的雪碧和可乐(你要交换的不是这两),进行交换,是不白忙活一场,这里的函数调用也是一样的,你要想交换两个值,你得把真实的它传过去,,那在c语言中怎么才能证明它是真正的它呢?这还用说那当然是它的地址了,也就得用我们的指针,好看下面一个方法。


方法二:

#include
#include
swap(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}
int main()
{
    int a = 0;
    int b = 0;
    scanf("%d %d", &a, &b);
    swap(&a, &b);
    printf("%d %dn", a, b);
    return 0;
}

打印结果:

调用函数之前

 

调用函数之后

 

 这回传的地址就一样了,这回才真正的做到把值交换啦

总结:当函数在调用时实参传给形参,形参是实参的一份临时拷贝,所以对形参的修改不影响实参,也就是第一个方法中,a和b与传进去的int a和int b是两个独立空间的数据,两者互不干扰。

函数的参数

函数的参数有形参和实参

实参:真实传给函数的参数,叫实参。 实参可以是:常量、变量、表达式、函数等。 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

形参:形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内 存单 元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

画图理解

 传值调用:

函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参

传址调用:

传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接 *** 作函数外部的变量。

传值调用和传址调用区分总结

当你实现一个函数时候,不需要改变某一变量的值就可以用传值调用

当你实现一个函数需要改变一个变量的值你就需要传地址(指针)传址调用

函数调用的本质

我们所写的代码实际上也被保存在一段连续的内存空间中,为了保证所写的一行行代码能够连续的执行下去,处理器必须具有某些手段来确定下一条指令的地址(程序计数器),程序计数器中永远存放着下一条指令的地址为cpu指明方向,如果想要使程序突然跳转至某个位置只需要修改程序计数器的值为目的地址,这就需要栈,栈,本意是储存货物或供旅客住宿的房屋,指数据暂时存储的地方,你可以将栈看做一个线性的桶,每当你需要数据储存的时候你都直接将数据丢入桶中,新丢入的数据会压在老数据之上,一层层盖起来,如果要去出老数据,不得不先把老数据之上的所有数据全部取出,每次只能取出最上层的数据,栈占据内存极大的空间,地址最大的地方叫做栈底,地址最小的地方叫做栈顶(最后压入栈中的数据存放的地址),并且有一个专门的指针——栈指针,使得你可以随时获取栈顶的那个数据,函数调用从一个函数跳转另一个函数中,此时应由底层修改程序计数器的值完成跳转,(不能一去不复返),跳转后变量及需要进行的下一条指令需要被妥善保管,以便跳转回来的值回来继续进行,同时跳转回来时,原来的函数所有内存都要被释放,假设有两个函数p和q,在函数p中你定义的部分局部变量都被依次压入栈中,当p调用q函数时,p也会把返回地址压入栈中,指明当q函数返回时,要从p函数的那个位置继续执行,我们把这个返回地址和p中的局部变量称做为栈帧,p的栈帧只属于p函数对其他函数不可见,当q函数执行时p以及所有直接或间接调用p的其他函数都是暂时被挂起的,当q运行时它只需要为他自己的局部变量分配新的存储空间,另一方面,当q返回时任何它所被分配的局部存储空间都可以被释放

int Q(int x, int y)
{
	return y;
}
int main()
{
	int x = 1;
	int y = 0;
	Q(1, 2);
	return 0;
}

首先程序先执行函数中声明的局部变量的语句,计算机在栈中开辟空间,栈指针减小依次将两个变量x和y压入栈中,随后执行调用函数的 *** 作,第一步就是把p中紧跟着调用下一条函数的指令的地址压入栈中作为返回地址,至此栈指针以下的main函数的栈帧归main函数所有,,第二步传递参数,函数q需要两个参数,我们向堆栈从右向左的顺序依次压入y和x的值当q中的x和y进行加法,并将计算结果保存在寄存器eax中进行保存,最后依次将x和yd出栈,取到程序的返回地址通过修改pc的值返回主函数中

这个具体可以看b站上的视频讲解这里只靠文字理解是不够的配上动画理解会更好哦

【动画详解】C语言 函数基础及其底层原理_哔哩哔哩_bilibili

函数的嵌套调用和链式访问 函数的嵌套调用

官方定义:C语言中不允许作嵌套的函数定义。因此各函数之间是平行的,不存在上一级函数和下一级函数的问题。但是C语言允许在一个函数的定义中出现对另一个函数的调用。这样就出现了函数的嵌套调用。即在被调函数中又调用其它函数。这与其它语言的子程序嵌套的情形是类似的。

通俗的讲:什么是嵌套调用呢?不就是套娃似的调用函数么,也就是一个函数在使用的过程中需要调用另一个函数的作用,而这个函数又要调用下一个函数的作用,这是就被我们称为嵌套调用。

此外嵌套调用是上下级关系,在我们生活中理解就是一个部门管着另外一个部门,这个部门又管理很多小部门。而这跟函数的嵌套定义是不同的,嵌套定义是同一级别的,两个同一级别的部门的干部当然谁也不能命令谁,这样就会出毛病的,在c语言函数也是这么控制的。

举个嵌套定义的例子

void test1()
{
	void test2()
	{

	}
}

这里在编译器,就会显示红波浪线就是报错 

那嵌套调用的代码长啥样呢?

int add1(int *a)
{
	a = a + 1;
	return a;
}
void add3(int *a)
{
	int i = 0;
	for (i = 1; i <= 3; i++)
	{
		add1(a);
	}
}
int main()
{
	int a = 0;
	add3(&a);
	//printf("%dn", 1);
	return 0;
}

这个代码虽然有些问题,这里就是给大家理解一下什么是上下级的调用,也就是嵌套调用。

总结:函数在调用的过程中不能嵌套定义但可以嵌套调用。

函数的链式访问

什么是链式访问呢?

挺招人这名很高大上,其实就是以不同的方式来访问某一个变量

#include
#include
int main()
{
	char arr[20] = "abcdef";
	int len = strlen(arr);
	//1.printf("%dn", len);
	//2.printf("%dn", strlen(arr));

	return 0;
}

平常我们都先定义一个变量,然后打印定义的变量(也就是第一种方法)。

那我们还可以直接打印你想要打的东西(也就是第二种方法)。

这里的就是链式访问把函数参数的返回值作为另外一个函数的参数。

链式访问应用举例

猜猜这会打印什么

#include 
int main()
{
    printf("%d ", printf("%d", printf("%d", 43)));
    return 0;
}

 惊不惊喜意不意外怎么会打印这些呢?

那我们就要聊一聊printf的作用有些啥?

 这里的意思是printf返回的是个数,当错误时返回的是负数。

那我们接下来解释一下为啥会打印出4321呢?

首先应该是最里面的printf先打印打印43,倒数第二个有%d,打印最里面的个数那就是2了,然后倒数第二个就打印出432了,那第一个就打印倒数第二个的个数(432),元素个数为1,接着就打印数字1,最后呈现的结果就为4321了

总结:这个题要告诉我们什么呢?第一:这是用函数的返回值当做另外一个函数的参数(也就是链式访问是咋回事?),第二:这就要告诉我们printf是怎么样理解的?,就是当函数没有打印的值时,我们就打印里面的元素个数,当出现错误时返回负值。

函数的声明和定义

通过举例来说明函数的声明

int Add(int a, int b)
{
	return a + b;
}
#include
int main()
{
	int a = 10;
	int b = 5;

	int ret =Add(a,b);
	printf("%dn", ret);
	return 0;
}

函数的声明是啥,也就是在你使用一个函数是我们要传给函数的值,返回类型,函数名这样才能精准在main函数调用。这里经常在头文件里声明,

如果声明函数一定要先声明后使用,这个应该很好理解,你得先告诉编译器有这个函数,他才能具体发挥它的作用。

上面这个代码是啥意思呢?

我们要实现两个数的加法,首先创建两个变量a和b,然后创建函数实现两个数的相加,那我们把他的和放在ret里,我们打印ret就好了,那函数传参,传Add的是啥呢,那就是两个整形,返回类型呢也是整形,最后return返回两个数的之后就可以算出结果。

上面的代码是在主函数前面声明的,那我们在后面声明命可以不?

我们看一下

 这时候就会报错(函数的声明就是(int Add(int a, int b))

总结:函数的声明就是告诉你一个函数的名字,返回的类型,返回的值

函数的声明一般放在头文件中

函数一定要先声明后使用

那函数的定义又是啥呢?

函数的定义也就是在函数体里的具体实现

也可以参考上面的代码在函数里进行加法的解释

int Add(int a, int b)
{
	return a + b;
}

这里就是函数的实现(也就是a+b)。

在庞大的工程中,我们通常也会在其他的源文件和头文件中声明和定义,比如我们写个很大的项目,你要在一个源文件中实现的话,你哪一个函数写错得一个一个寻找,而这时你如果要实现多文件,每个文件都写得非常清楚写的是哪一个环节代码的可读性就非常好,也利于其他人看懂你的代码的实现,那我们怎样实现多文件呢?给大家演示一下

主文件test.c

 头文件add.h

函数具体实现add.c

 等我们具体实现一个大的项目就更好的说明了,敬请期待吧!!!

函数的递归

官方定义:

C 语言支持递归,即一个函数可以调用其自身。但在使用递归时,程序员需要注意定义一个从函数退出的条件,否则会进入死循环。递归函数在解决许多数学问题上起了至关重要的作用。

递归实现执行图解

 说白了,就是自己调用自己,一个函数进行多次调用,大量了简化代码,多次实现直到得到结果为止

函数递归的优点:递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接 调用自身的 一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解, 递归策略 只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在于:把大事化小

应用举例1

我们写一个题目来验证一下递归是如何实现的

接受一个整型值(无符号),按照顺序打印它的每一位。 例如: 输入:1234,输出 1 2 3 4

#include
 print(int n)
{
	//print(123)  4
	//print(12) 3 4
	//print(1)2 3 4 
	if (n > 9)
	{
		print(n / 10);
	}
	printf("%d ", n % 10);
}
#include
int main()
{
	//接受一个整型值(无符号),按照顺序打印它的每一位。 例如: 输入:1234,输出 1 2 3 4
	int n = 0;//创建一个变量(1234)
	scanf("%d", &n);
	print(n);
	return 0;
}

看到这个题我们该怎么想呢(用递归实现)?

他既然要正着打印1 2 3 4,要打印一个整数的每一位,那我们肯定想的是除法了

我们知道1234/10=123,1234%10=4,我们可以这么想,我们将这样一个大的事件分解成一个个小的事情,我们先将4分离出来,接着在执行print(123)再将3分离出来,像这样我们就可以一个一个打印出来了

我们来具体画一下图解来理解一下函数递归的实现

 我们解释一下红的是传递蓝色是回归(递归么有传递就有回归)

首先我们输入1234然后传入print函数里,进行if判断,成立执行print函数(此时传入下一个print函数值n=123),接着又进入print(此时传入下一个print函数值n=12),继续进入print函数(此时传入下一个print函数值n=1),再次进入print函数不成立,则打印printf(打印1),(记住当函数回归的时候从哪来回哪去),这回我们就回到上回那个print里有不成立,就打印2,继续回归回到print不成立打印3,最后再次执行print(不成立打印4),我们整个递归就完成了,输出结果为1234。

明白了么同志们!!,

总结:我们进行递归时先进行传递,然后再逆回来回归打印,当我们回归时,记住上次从那个函数出来的,这回回归时就返回去(从哪来回哪去)。这回递归就可以准确的完成了。

应用举例2

编写函数,求字符串的长度。输入abcdef        输出结果:6

int my_strlen(char*str)
{
    int count = 0;
    while (*str != '')
    {
        count++;
        str++;
    }
    return count;
}
#include
int main()
{
    //编写函数,求字符串的长度。输入abcdef        输出结果:6
    char arr[20];
    scanf("%s", arr);
    int len = my_strlen(arr);
    printf("%dn", len);
    return 0;
}

 这个代码是怎样实现的呢?

首先我们创建一个自定义函数用来模仿strlen来求字符串长度,我们知道字符串结尾应该是();那我们就一个一个字符计算大小,每一个字符都+1,同时计算一个字符结束时就计算下一个字符,当字符为()时我们就停止计算长度,最后返回计算出来的结果就可以了。

应用举例2变式

编写函数不允许创建临时变量,求字符串的长度。输入abcdef        输出结果:6

分析一下题目,他说不可以创建变量,其实意思就是让你用别的方法来实现,这是我们就可以用递归,思路还是一样的就是用递归来实现,那我们如何实现呢,是不也应该按照递归思想大事化小的思想进行,还是当指针指向时结束,第一次我们肯定识别第一个字符所以计数为1,接着我们是不要识别下一个字符看是不是,如果是就结束循环,如果不是就继续识别下一个字符。这样我们的代码就可以实现了

将我上面的话转化为图应该就更好理解了

                         计算结果

strlen(a,b,c,)       0
strlen a (b,c,)      1
strlen a b (c,)      2
strlen a b c ()      3

结束

int my_strlen(char* str)
{
	while (*str != '')
	{
		return 1 + my_strlen(str + 1);
	}
	return 0;
}
#include
int main()
{
	//编写函数,求字符串的长度。输入abcdef        输出结果:6
	char arr[20];
	scanf("%s", arr);
	int len = my_strlen(arr);
	printf("%dn", len);
	return 0;
}

画图理解

 其实这个和上面的代码思路是一样的,为了更好理解递归我在给大家讲解一下

首先我们输入字符'abc',进入my_strlen函数然后指针指向字符串的第一个字符‘a’,判断不是,接着继续调用函数指针指向第二个字符‘b’,继续调用函数指针指向第三个字符‘c’,再次调用函数发现此时指针指向的事,所以跳出循环返回0,接着就开始回归了(还记得么从哪来的就回归到哪里去),这是我们进入上面的函数进入return,由于刚才返回的是0,这是就返回1,继续向上回归return1+1=2;再次向上回归1+2=3;然后结束最后返回值结果为2。

应用举例3

计算n的阶乘(递归实现和非递归实现)

#include
int main()
{
	//计算n的阶乘(非递归实现)
	int n = 0;
	scanf("%d", &n);
	int ret = 1;
	int i = 0;
	for (i = 1; i <=n; i++)
	{
		ret *= i;
	}
	printf("%dn", ret);
	return 0;
}
#include
int Fac(int n)
{
      //递归实现
	if (n <= 1)
		return 1;
	else
		return n * Fac(n - 1);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret =Fac(n);
	printf("%dn", ret);
	return 0;
}

 

画图理解

首先输入 3,进入判断部分,走else进入fac,走下一个fac(此时n=2),再继续调用Fac,此时n=1,这是符合if,然后return 1,(这时该回归了),进入上面的return 2*1,继续回归到上面的return 3*2,最后返回ret=6;

应用举例4

求第n个斐波那契数(递归和非递归(迭代)实现)

这个数列从第3项开始,每一项都等于前两项之和。

F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(≥ 2,∈ N*)

int fib(int n)
{
	if (n >2 )
	{
		return fib(n - 1) + fib(n - 2);
	}
	else
	{
		return 1;
	}
}
#include
int main()
{
        //递归实现
	int n = 0;
	scanf("%d", &n);
	int ret = fib(n);
	printf("%dn", ret);
	return 0;
}

迭代实现

#include
int fib(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("%dn", ret);
	return 0;
}

虽然都是算斐波那契数但是递归进行这个计算效率非常低,那为啥效率低呢?

 因为斐波那契数是前两个数相加,看这幅图,每个数都重复出现多回,所以计算非常的慢,造成栈溢出(之前我们讲过栈,我们把栈想做一个空间,每当我们调用时就会在栈上申请空间,当我们重复多次调用时,栈就会溢出),导致报错。

而这时我们选择迭代,效率就会很高

总结 1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。 2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。 3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开 销。

所以我们也要合适的选择递归它既有优点也有缺点,但递归也有条件,1是必须要有限制条件,防止重复调用导致栈溢出,2是每次随着递归调用,递归调用次数就减少。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5156384.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-18
下一篇 2022-11-18

发表评论

登录后才能评论

评论列表(0条)

保存