初阶C语言-函数(7000字)

初阶C语言-函数(7000字),第1张

初阶C语言-函数(7000字)

目录

    函数的定义

库函数

自定义函数

实际参数

形式参数

函数的调用

嵌套调用

链式访问

函数声明

函数定义

函数递归

栈溢出

迭代


    函数的定义

函数是子程序,是一个大型程序中的某部分代码,由一个或多个语句块组成,它负责完成某项特定的任务,而且相较于其他的代码,具备相对的的独立性。
②一般会有输入参数并有返回值


库函数

C语言的基础库中提供了一系列库函数,方便程序员进行软件开发,使用库函数需要包括对应的头文件
IO函数 printf scanf getchar putchar(输入输出函数)
字符串 *** 作函数 strcmp(比较两个字符串是否相同) strlen(计算字符串长度)
字符 *** 作函数 toupper(小写转大写)
内存 *** 作函数 memcpy memcmp memset
时间/日期函数 time(时间戳)
数学函数 sqrt(开平方) pow
其他库函数
strcpy 字符串拷贝函数(string.h)
char * strcpy ( char * destination, const char * source );
即:strcpy(目标字符串,起始字符串)

memset 内存设置
void * memset ( void * ptr, int value, size_t num );
即memst(地址,值,修改的字节数)

学习库函数的网站:
cplusplus.com - The C++ Resources Network
cppreference.com


自定义函数
格式:
返回类型 函数名 (形参)
{
语句项; //大括号括起来的叫函数体
}

写一个自定义函数可以交换两个整形变量的内容:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

#include

//实现成函数,但是不能完成任务

void Swap1(int x, int y)

{

 int tmp = 0;

 tmp = x;

 x = y;

 y = tmp;

}

//正确的版本

void Swap2(int *px, int *py)

{

 int tmp = 0;

 tmp = *px;

 *px = *py;

 *py = tmp;

}

int main()

{

 int num1 = 1;

 int num2 = 2;

 Swap1(num1, num2);//传值调用

 printf("Swap1::num1 = %d num2 = %dn", num1, num2);

  Swap2(&num1, &num2);//传址调用

 printf("Swap2::num1 = %d num2 = %dn", num1, num2);

 return 0;

分析

为什么Swap1不能完成交换数值的任务:在调用过程中,我们将num1和num2的数值传给了x,y。然后在函数内将x,y进行数值交换,但没有改变原来的num1,num2。因为x,y是形参,用完了就销毁的东西,仅仅是用于临时储存原来的数值,所以根本不能改变num1,num2的值。(即swap1在被调用的时候,实参传给形参,而形参是实参的一份临时拷贝,改变形参,不能改变实参)
为什么Swap2能完成交换数值的任务:在调用过程中,传的是地址,将地址传给指针*px,*py。*px即num1的地址,*py即num2的地址,变量储存在地址中,那么就可以根据指针定位到原来的变量。


实际参数

①定义:真实传给函数的值叫实参。
②实参可以是常量,变量,表达式,函数等。在函数调用的时候,它们都必须有确定的值,以便把这些值传给形参


形式参数

①定义:形参指的是函数名后括号内的变量
形参只有在函数被调用的时候才会分配内存单元,当函数完成调用后,它就自动销毁了,所以形参只在函数中有效。

ps:上面 Swap1 和 Swap2 函数中的参数 x,y,px,py 都是形式参数。在main函数中传给 Swap1 的 num1 ,
num2 和传给 Swap2 函数的 &num1 , &num2 是实际参数。


函数的调用

传值调用:函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参
传址调用:传址调用是把函数外部创建变量的内存地址传给函数参数的一种调用方式。使得函数和函数外边的变量建立起真正的联系,也就是函数内部可以 *** 作函数外部的变量


嵌套调用

函数和函数之间可以根据实际的需求进行互相调用
函数可以嵌套调用,但是不能嵌套定义(即在一个函数体内不能包含另一个函数的定义)

举例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

#include

void new_line()

{

 printf("hehen");

}

void three_line()

{

    int i = 0;

 for(i=0; i<3; i++)

   {

        new_line();

   }

}

int main()

{

 three_line();

 return 0;

}

分析

main函数中调用了three_line函数,three_line函数每次执行循环都会调用一次new_line函数,new_line函数的作用是打印hehe。所以这个程序的最终结果就是打印3个hehe。这就是嵌套调用。


链式访问

定义:把一个函数的返回值作为另外一个函数的参数

举例1:

1

2

3

4

5

6

7

8

9

#include

#include

int main()

{

    char arr[20] = "hello";

 int ret = strlen(strcat(arr,"xxs"));

 printf("%dn", ret);

 return 0;

}

分析

①该函数定义了一个20长度的数组,在里面存放了字符串hello。
②strcat库函数的头文件是string.h,它的作用是将两个字符串连接在一起,然后返回合并字符串
③strlen是计算字符串长度的。
④stract将hello和xxs合并了,然后返回helloxxs给strlen,strlen计算长度返回整形8。

举例2: 

1

2

3

4

5

6

#include

int main()

{

    printf("%d", printf("%d", printf("%d", 43)));

    return 0;

}

分析

printf函数的返回值是打印在屏幕上字符的个数
②第一层的printf要打印第二层的printf的返回值,第二层的printf要打印第三层printf的返回值。那么就由第三层开始返回。
③第三层的printf打印完43以后,计算出字符个数为2,返回整形2给第二层的printf;第二层printf打印2,计算出字符个数为1,返回整形1给第一层的printf,打印1,返回1。
④最终打印结果为4321


函数声明


1.定义:告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。具体存不存在,无关紧要
2.函数声明一般出现在函数使用之前,要满足先声明后使用。函数如果放在int main()函数后面,则调用的时候需要声明。
3.函数的声明一般放在头文件中
4.补充:在VS的项目属性中,可以选择把配置类型exe改成lib,对自定义的函数与头文件进行封装,生成lib静态库,这样就对源码进行二进制加密了。然后把lib静态库跟头文件导入文件夹,如果想要调用这个函数,就得在源码中先包括头文件,然后在源码开头部分加入#pragma comment(lib,”函数文件名.lib”),这样就成功导入lib静态库了,就可以根据头文件的说明使用自定义的函数了


函数定义


函数的定义是指函数的具体实现,交待函数的功能实现。
所以可以在比如test.h里放函数的声明,在test.c中放函数的定义,然后实现分模块调用自定义函数。


函数递归

定义:程序调用自身的编程技巧称为递归。它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思考方式在于:把大事化小
递归的两个必要条件:1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。2.每次递归调用之后越来越接近这个限制条件。补充:递归不能太深,否则会栈溢出

举例1:接受一个整型值(无符号),按照顺序打印它的每一位。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

#include

void print(int n)

{

 if(n>9)

 {

 print(n/10);

 }

 printf("%d ", n%10);

}

int main()

{

 int num = 1234;

 print(num);

 return 0;

}

分析

①将1234传给定义的print函数
②我们很容易可以取出1234数最后一位,只需要%10就可以了。print函数的作用是取出函数的最后一位,那么我们的思路就可以是这样的:
print(1234)→print→(123)+4→(12) 3 4→(1)2 3 4→1 2 3 4
图示如下:

举例2:不允许创建临时变量,用递归模拟strlen求字符串的长度。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

#include

int my_strlen(char *pa)

{

    if(*pa == '')

    {

        return 0;

    }

    else

    return 1+my_strlen(pa+1);

}

int main()

{

    char arr[] = "nmsl";

    printf("%d",my_strlen(arr));

    return 0;

分析:

①定义了一个字符数组arr,然后将它的第一个字符的地址传给了my_strlen函数中的*pa
②如果*pa为转义字符,则说明字符串结束了,就返回一个0
③否则说明字符还没有结束,字符个数就要加1,然后还要继续判断下一个字符,于是就把pa+1,即把下标+1。于是就有了1+my_strlen(1+下一次判断的结果)


栈溢出

内存在使用的时候,划分为以下几个区域:
栈区(栈,堆栈):局部变量,函数形参,函数调用的时候返回值等临时的变量
堆区(堆):动态内存分配的malloc/free
静态区:全局变量,static修饰的静态变量
每一个函数调用,都要在栈区为自己分配一块空间。倘若递归比较深,则会导致栈溢出Stack overflow。

举例:

1

2

3

4

5

6

7

8

9

10

11

12

void test(int n)//栈溢出

{

    if (n < 10000)

    {

        test(n + 1);

    }

}

int main()

{

    test(1);

    return 0;

}

分析:

①在主函数部分把实参1传给函数test的形参n
②test中进行判断,若n<10000,就执行test(n+1)
③这个函数递归虽然符合两个基本条件,但是递归太深,要调用数万次函数,而内存的栈区就只有这么大,就导致栈溢出了


迭代

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

举例1:使用递归求n的阶乘。(不考虑溢出)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

#define _CRT_SECURE_NO_WARNINGS 1;

#include

int fuc(int x)

{

    if (x > 1)

        return  x * fuc(x - 1);

    else

        return 1;

}

int main()

{

    int n;

    scanf("%d",&n);

    printf("%d的阶乘=%d",n,fuc(n));

    return 0;

}

分析:

①公式:如果n<=1,fac=1;如果n>1,fac=fac(n-1)*n。
②对公式进行分析:如果输入一个3,函数值=3*2的阶乘,2的阶乘=2*1的阶乘,1的阶乘=1;从而递归求解
③如果我们输入10000,程序就直接崩溃了,因为进行了数以亿计的计算

举例2:求第n个斐波那契数列(不考虑溢出)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

#define _CRT_SECURE_NO_WARNINGS 1;

#include

int count = 0;

int fuc(int x)

{

    if (x == 3)

        count++;//记录x==3调用了多少次

    if (x <= 2)

        return 1;

    else

        return fuc(x - 2) + fuc(x - 1);

}

int main()

{

    int n;

    printf("请输入一个数");

    scanf("%d",&n);

    printf("斐波那契数列的第%d项的值为%d", n, fuc(n));

    printf("调用次数为%d", count);

    return 0;

}

分析

①斐波那契数列:1 1 2 3 5 8…,从第三项开始,n项=(n-1)项+(n-2)项
②我们对fuc的定义是,用来求前两项的和。如果我们输入一个4,就把实参4传给形参x,x返回fuc(x-2)+fuc(x-1),fuc(x-1)求x的前一项的值,fuc(x-2)求x的前二项的值,即求第3项和2项的值。第三项进入函数后,求2项与1项的和,于是返回了2,第二项则直接返回1,于是把2和1同时返回给第四项,得出3。
③但是如果我们输入一个比较大的值,比如50,程序运行要很久时间才出结果。因为进行了数以亿计的计算

为什么两个程序输入较大一点数就运行异常了呢?

因为它们在调用的时候,很多计算其实在一直重复。
如何解决这个问题呢?只需要将递归改写成非递归。

改写后的阶乘:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

#define _CRT_SECURE_NO_WARNINGS 1;

#include

int fuc(int n)

{

    int result = 1;

    while (n > 1)

    {

        result *= n;

        n -= 1;

    }

    return result;

}

int main()

{

    int n;

    printf("请输入n=>");

    scanf("%d",&n);

    printf("%d的阶乘为%d",n,fuc(n));   

    return 0;

}

分析

①将输入的数值传给fuc的n,然后进行判断
②若n>1,就把n的值累乘到result里面,然后n递减,继续累乘,如此循环,直到n<=1了,就返回结果。
如果我们输入5,result=1*5=5→result=5*4=20→20*3=60→60*2=120
这种写法虽然也会溢出,但我们不考虑。至少我们执行程序输入10000不会报错了

改写后的斐波那契数列:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

#define _CRT_SECURE_NO_WARNINGS 1;

#include

int fuc(int x)

{

    int a = 1,b = 1, c = 1;

    int i;

    while (x>2)

    {

            c = a + b;

            a = b;

            b = c;

            x--;

    }

        return c;

    }

int main()

{

    int n;

    printf("请输入一个数");

    scanf("%d", &n);

    printf("斐波那契数列的第%d项的值为%ld", n, fuc(n));

    return 0;

}

分析:

1 1 2 3 5
a b
把输入值传给形参x,先定义第一项和第二项a=1,b=1,如果x>3,那么就先把a+b赋值给第三项c,然后a和b同时向右移动一位,即a=b,b=c,x为循环变量,x每次循环后都减少1,直到x<=2为止。
输入50,虽然溢出了,但是很快就得出了结果,就问你快不快就完事

所以要根据不同的情况使用不同的方法,才能使得问题更高效地解决。

函数递归和迭代部分要真正掌握还有很长的路要走   

本章完,祝好。

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

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

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

发表评论

登录后才能评论

评论列表(0条)

保存