C语言从入门到入土——函数

🥳🥳🥳大家好啊,我是爱吃甜品的泡泡牛奶啊😘😘,这个时间想必也都差不多都放假了吧,可以乘着这段时间好好的学一些新东西,或者巩固一下所学知识,俗话说的好“宁可累死自己也要卷死别人”😅😅

今天,小编给大家带来的是——C语言中函数是什么,以及其如何使用

让我们废话不多说,立刻开始今天的内容吧

🎄1. C语言中函数的分类

  1. 库函数
  2. 自定义函数

🚀1.1 库函数

举例:

  1. 当我们想打印一个数的时候会用到 printf 这个函数,这个函数就是属于 <stdio.h> 里的库函数

  2. 当我们想对字符串进行拷贝的工作时,可以使用 strcpy ,这属于 <string.h> 的库函数

库函数有很多,当我们想包含库函数的时候需要使用 < > (尖括号) 来引用头文件,< > 会先在头文件库里进行搜索。

了解了这么多,我们应该怎样去学库函数呢🤔?

小编在这里给大家推荐一个网站 👉cplusplus

这里面介绍了很多有关函数的介绍。

当然,作为一名程序员有一定的英语能力,能更方便的阅读别人写的库函数

1.1.1 C语言常用库函数

  • IO函数
  • 字符串操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其它函数

image-20220628185210443

注: 在使用库函数时,必须包含#include对应的头文件

🚀1.2 自定义函数

虽然有了库函数,但并不是能完成所有的事,所以更加重要的就是自定义函数

自定义函数需要有以下成分

  1. 函数名
  2. 返回类型
  3. 函数的参数
ret_type fname(paral pname)
{
    //内容}

ret_type  返回类型
fname     函数名
paral     函数参数

例如:

写一个函数交换两个数

#include <stdio.h>

//swap函数设计

//错误示范
void swap1(int x, int y)
{
    int tmp = x;
    x = y;
    y = tmp;
}

//正确版本
void swap2(int* p1, int* p2)
{
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}

int main()
{
    int a = 1;
    int b = 2;
    
    swap1(a, b);
    printf("%d %d\n", a, b);
    
    swap2(&a, &b);
    printf("%d %d\n", a, b);
    
    return 0;
}

🎄2.函数的参数

函数的参数大致可以分为两类

  1. 实参
  2. 形参

🤔2.1 什么是函数的实际参数(实参)

真实传递过去的交实参

实参可以是:常量、(指针)变量、表达式、函数等

实参无论是什么返回类型,函数调用时,参数必须要有确定的值,以便把这些值传递给实参

实参通常时通过指针来访问,上面的swap2函数就是通过指针来操作实参

image-20220628175832660

通过指针访问地址,我们可以直接对原来的值进行修改

image-20220628180011256

🤔2.2 什么是函数的形式参数(形参)

形式参数是指,在函数名括号后面临时变量

因为只有在函数调用的时候才能被实际用到(开辟内存单元),所以叫形式参数。

形参只在函数中有效,当函数调用完成之后,自动销毁。

image-20220628171555678

image-20220628184126130

传入值的时候,将值拷贝过去给了 xy ,在出 swap1 函数的时候,xy 自动销毁,而 ab 原本的值并没有改变。

通过以上例子,我们可以简单的认为——形参是实参的一份临时拷贝

🎄3. 函数的调用

🎁3.1 传值调用

  • 传值调用是将参数的形参传给函数的一种调用方式。

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

    上方的 swap1 就是传值调用

🎁3.2 传址调用

  • 传址调用是将函数外部的内存地址传给函数参数的一种调用方式。
  • 通过这种方法可以利用函数直接对原本的参数进行改变

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

🛸4.1 嵌套调用

函数和函数之间可以根据实际需求进行组合调用了,例如

求1~10之间的素数

分析:

素数是指只能被1或自己整除的数,我们可以反过来思考,当其中有其它数可以将它整除的时候,这个数就不是素数,我们只需要寻找一个数的开平方根内的数字,若其中有一个数i可以整除n,那么n就不是质数

#include <stdio.h>
#include <math.h>

int is_prime(int n)
{
    int i = 0;
    for (i = 2; i <= sqrt(n); ++i)//函数sqrt需要用到math.h的库
    {
        if (n % i == 0)//表示n能被i整除
        {
            return 0;//若有一个满足则不是质数
        }
    }
    return 1;//否则为质数
}

int main()
{
    int i = 0;

    for (i = 1; i < 10; ++i)
    {
        if (is_prime(i))
        {
            printf("%d ", i);
        }
    }
    return 0;
}

image-20220629025742910

在这其中我们自定义了一个函数,而在自定义函数中,我们还调用了一个 sqrt 函数(这个函数需要配合 math.h 的库函数进行使用)

注意: 函数可以嵌套调用,但是不能嵌套定义

🛸4.2 链式访问

把一个数的返回值作为另一个函数的参数

#include <stdio.h>
#include <string.h>

int main()
{
    char ch[] = "abcdef";
    printf("%u", strlen(ch));//%u打印无符号整型
    return 0;
}

输出结果为:

image-20220629030215986

我们首先看看strlen函数

image-20220629030718799

在这之中有一个size_t,我们可以在编译器中选中这个函数,右击鼠标 -> 选择转到定义

image-20220629031044884 image-20220629031206049

选中size_t ,继续选择转到定义

image-20220629031342144

可以看到,size_t 本质上就是 unsigned int ,则 strlen 的返回值就是 unsigned int

🎄5. 函数的声明和定义

🎈5.1 函数的声明

  1. 函数声明首先要函数存在才能声明,不存在的函数无法进行函数声明
  2. 函数声明要在函数使用之前,满足先声明后使用
  3. 函数声明一般放在头文件中
image-20220629171737175

image-20220629173016032

举个例子🍐

#include <stdio.h>

void test();//函数声明

int main()
{
    test();//调用test函数
    return 0;
}

void test()//函数的实现
{
    printf("haha");
}

但是在有些地方会遇到下面这样的情况(通常在学校会遇到😅)

#include <stdio.h>

int main()
{
    void test(int x);//函数的声明
    test(1);
    return 0;
}

void test(int x)
{
    printf("%d", x);
}

🎈5.2 函数的定义

函数的定义就是指,函数的具体实现

在进行函数声明的时候,只要满足 声明 or 定义 使用的前面 就行,所以也出现了这样一种写法

#include <stdio.h>

void test()//test 函数的具体实现
{
    printf("haha");
}

int main()
{
    test();
    return 0;
}

这样的写法,因为函数定义在使用之前,声明的同时也把函数给定义了。

通常情况下,我们写项目的时候应该怎样去规范写法呢?(参考《高质量的C/C++编程》)

test.h的内容

//版权和版本声明

#ifndef TEST_H   //防止test.h被重复引用
#define TEST_H	 //使用全大写,符号 .  改成  _

//包含头文件
#include <stdio.h>
...
#include <math.h>
...
#include "myhander.h"
...

//函数声明
void Add(int x, int y);
void test2();
...
    
//类结构声明
struct Color
{
  ...  
};

#endif

test.c的内容

#include <test.h>
//函数的实现

int Add(int x, int y)
{
    return x+y;
}

...
    

这样的写法通常是用于需要分模块来完成的,也就是需要实现很多功能来实现,具体可以看一下🚀🚀🚀🎄递归实现扫雷游戏🎄

🎄6.函数的递归

🎉6.1 什么是递归?

程序自己调用自己的编程技巧被称为递归。

这种方法,主要是把一个大型复杂的问题层层转化成一个与原问题相似较小规模的问题来解决,这就是递归的策略。

递归的主要思考方式在于:大事化小

🎉6.2 递归的两个必要条件

  1. 存在一个限制条件,能使递归不再继续
  2. 每次递归调用都会更逼近于限制条件

🎉6.3 递归与迭代

我们刚刚认识什么是递归,那么我们接下来就认识一下,递归的具体运用吧😏😏😏

Q1求解n阶汉诺塔问题

将A柱子上所有圆盘移动到C柱子上,一次只能移动一个 圆盘,且大圆盘不能在小圆盘的上方

#include <stdio.h>

void hanoi(int n, char a, char b, char c)
{
	if (n == 1)
	{
		printf("%c -> %c\n", a, c);
	}
	else
	{
		hanoi(n - 1, a, c, b);
		printf("%c -> %c\n", a, c);
		hanoi(n-1, b, a, c);
	}
}

int main()
{
	int n = 0;//表示n阶汉诺塔
	scanf("%d", &n);
	hanoi(n, 'A', 'B', 'C');
	return 0;
}

解析:

根据递归的思维,需要把原问题层层递进,把它化简成最简单且与原问题相似的问题。

image-20220629235026027

当A柱子上只剩下1个圆盘的时候,直接将它移动到C柱子上即可

image-20220629235848829

当 A 柱子上有 n 个圆盘的时候,本质上就是把上方 n-1 个圆盘全部移动到 工具柱(B柱) 上,再把最下面的柱子移动到 C 柱,最后把 B 柱子上的圆盘 全放回 A 柱,继续重复以上过程就好了

image-20220630000547815

Q2 求斐波那契数列的第n个数 (不考虑溢出)

斐波那契数列指的是这样一串数列:1,1,2,3,5,8,13,21,34,55,89…

从第三项开始,每一项第一等于前两项之和

#include <stdio.h>

int Fibrec(int n)
{
	if (n > 2)
		return Fibrec(n - 1) + Fibrec(n - 2);
	else
		return 1;
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	printf("%d\n", Fibrec(n));
	return 0;
}

image-20220630023239764

思考:

当我们要计算第50个甚至第100个斐波那契数的时候就会显得十分耗费时间

为什么会有这样的事发生呢?

  • 我们不妨对原函数进行修改
#include <stdio.h>

int count = 0;//定义全局变量

int Fibrec(int n)
{
    ++count;//每调用一次,count+1
	if (n > 2)
		return Fibrec(n - 1) + Fibrec(n - 2);
	else
		return 1;
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	printf("%d\n", Fibrec(n));
    printf("%d\n", count);
	return 0;
}

image-20220630023857178

可以发现,当计算到第40个斐波那契数的时候,函数已经调用了204668309次。

原因是

  1. 在计算一个较大的斐波那契数的时候,中间会出现很多重复的数据,这也大大降低了运算的速率

  2. 每调用一次函数就会向内存申请一块空间,当函数调用过多的时候,也有可能会造成 栈溢出 的现象stack overflow

    (系统分配的空间是有限的,对于一直在栈区开辟空间,最终导致空间耗尽的情况,我们称之为栈溢出)

如何解决上面的问题呢?

  1. 将递归写成非递归的形式(多数都可以用迭代来解决(也就是循环)
  2. 使用 static 代替 nonstatic 局部对象,这样可以减少每次递归调用和返回时的产生和释放 nonstatic 局部对象的开销

我们将上面的问题转化成非递归的形式

#include <stdio.h>
int Fibloop(int n)
{
	int i = 1, j = 1, k = 0;
	if (n > 2)
	{
		n -= 2;
		while (n--)
		{
			k = i + j;
			i = j;
			j = k;
		}
		return k;
	}
	else
		return 1;
}

int main()
{
	int n = 0;
	scanf("%d", &n);
	printf("%d\n", Fibloop(n));
	return 0;
}

image-20220630031650425

显然速度快了很多,虽然结果可能不太对,(主要是超出 int 的范围

当我们将 int 改成 unsigned int 就正常很多了

image-20220630032030712

提醒:

  • 递归的优点是大事化小,将问题简单化,同时也能使代码的形式更加清晰,缺点就是可能会导致运行速度较慢或着栈溢出
  • 循环解决起来非常麻烦的事可以使用递归,递归难以解决的事可以使用循环

🥳小结

好啦!ヾ(^▽^*)))本期的内容到这里就结束了,对于函数你有更深的了解了吗?那就赶紧来挑战一下自己吧ヾ(≧▽≦*)o

如果喜欢小编的话,就请支持一下,你的支持就是我最大的动力😘😘😘

  • 28
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 27
    评论
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

泡泡牛奶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值