C语言基础(函数)

函数的概述

  • 函数:实现一定功能的,独立的代码模块。对于函数的使用,一定是先定义,后使用。
  • 使用函数的优势:

① 我们可以通过函数提供功能给别人使用。当然我们也可以使用别人提供的函数,减少代码
量。
②借助函数可以减少重复性的代码。
③ 实现结构化(模块化) 程序设计思想。
关于 结构化设计思想 :将大型的任务功能划分为相互独立的小型的任务模块来设计。

  • 函数是C语言程序的基本组成单元:

C语言程序是由一个(必然是 main 函数)或多个函数组成。

函数的分类

  • 从函数实现的角度:

库函数:C语言标准库实现的并提供使用的函数,比如说: scanf() 、 printf() 、 fgets() 、
fputs() 、 strlen()
自定义函数:需要程序员自行实现,开发中大部分函数都是自定义函数。

  • 从函数形式的角度:

无参函数:函数调用时,无需传递,可有可无返回值,举例:show_all();
有参函数:函数调用时,需要参数传递数据,经常需要配套返回值来使用,举例:
printf("%d\n",12);

  • 从函数调用的角度:

主调函数:主动去调用其他函数的函数。(main函数只能作为主调函数)
被调函数:被其他函数调用的函数。

举例:

cs 复制代码
// 主调函数
int main()
{
// 被调函数
printf("hello world!\n");
}

很多时候,尤其是对于自定义函数来说,一个函数有可能既是主调函数,又是被调函数。

函数的定义

定义

语法:

cs 复制代码
[返回类型] 函数名([形参列表])    -- 函数头 | 函数首部
{
    函数体语句;                 -- 函数体,整个{}包裹的内容包括返回值都属于函数体,{}不能省略
}

函数头:

  • 返回类型:函数返回值的类型

  • 函数名:函数的名称,遵循标识符命名(不能以数字开头,只能包含字母、数字、下划线,建议:小写+下划线命名,举例:show_all())

  • 形参列表:用于接收主调函数传递的数据,如果有多个参数,使用","分隔,且每一个形参都需要指明类型。

小贴士:

形参列表(被调函数):主调函数给被调函数传递数据,主调函数 → 被调函数

返回类型(被调函数):被调函数给主调函数返回数据,被调函数 → 主调函数

说明:

  • 函数的返回类型:就是返回值的类型,两个类型可以不同,但是必须能够进行转换。举例:
cs 复制代码
double fun_a() // 函数的返回类型是double
{
    return 12; // 函数的返回值是int
}

以上代码可以理解为:将 int 类型的 12 赋值给一个 double 类型的匿名变量( int → double ),此
时属于隐式转换。

cs 复制代码
int fun_a() // 函数的返回类型是double
{
    return 12.5; // 函数的返回值是int
}
  • 在C语言中还可以定义无类型(void类型)的函数,这种函数不返回函数值(没有返回值),只 是完成某种功能,举例:
cs 复制代码
void test()// 此时这个函数,没有返回值,也就是它的返回值是 return;
{
    printf("hello\n");
}
// 下面写法等价于上面写法
void test()
{
    return; // 一般,这个return是省略不写的。
}
  • 在C语言中,函数的返回类型标识符是可以省略的,如果省略,默认返回int,举例:
cs 复制代码
// 写法1:main的返回类型是int类型,默认的返回值是0,等价于 写法2
main()
{
    ...
}
// 写法2:main的返回类型是int类型,返回值是0
int main()
{
    return 0;// 0:逻辑真,-1:逻辑假,支持非0表示
}
  • 函数中返回语句的形式为 return(表达式) 或者 return 表达式 。
cs 复制代码
// 写法1
int main()
{
    return(0);
}
// 写法2 完全等价于写法1
int main()
{
    return 0;
}
  • 如果 参数列表 中有多个形式参数,则它们之间要用","分隔;即使它们的类型相同,在形式参数 中只能逐个进行说明,举例:
cs 复制代码
// 正确示例
int avg(int x, int y, int z)
{
    ...
}
// 错误示例
int avg(int x, y, z)
{
    ...
}
  • 如果 形参列表 中没有参数,我们可以不写,也可以用void标识,举例:
cs 复制代码
// 写法1 推荐
int main()
{
    ...
}
// 写法2
int main(void)
{
    ...
}

形参和实参

函数定义时指定的参数,形参是用来接收数据的,函数定义时,系统不会为形参申请内存,只有当
函数调用时,系统才会为形参申请内存。主要用于存储实际参数,并且当函数返回时(执行
return),系统会自动回收为形参申请的内存资源。

实参(实际参数)

定义

  • 函数调用时主调函数传递的数据参数(字面量、符号常量、表达式...,只要有确定的值),实参 就是传递的数据。
  • 实参和形参必须类型相同,如果不同时,按赋值规定进行类型转换,比如隐式转换。
  • 在C语言中,参数传递必须遵循 单向值传递 (通过实参给形参赋值),实参只是将自身的值传递 给了形参,而不是实参本身。形参的值的改变不会影响实参。
cs 复制代码
int fun(int n) // n 是形参
{
    printf("%d\n",n); // n = 10 n = 12
}
int main()
{
int a = 10; // a = 10
fun(a); // a 是实参,此时传递的数据是 10 实参是变量
fun(12); // 字面量12就是实参,此时传递的数据是 12 实参是常量
fun(a + 12); // 此时传递的数据是 22 实参是表达式
  • 实参与形参在内存中占据不同的内存空间。

函数的返回值

  • 若不需要返回值,函数可以没有return语句。
cs 复制代码
// 如果返回类型是void,return关键字可以省略
void fun1
{
}
// 这种写法,return关键字也可以省略,但是此时默认返回是 return 0
int fun2()
{
}
// 这种写法,return关键字也可以省略,但是此时默认返回是 return 0
fun3() // 如果不写返回类型,默认返回int
{
}
  • 一个函数中可以有多个return语句,但任一时刻只有一个return语句被执行。
cs 复制代码
int eq(int num)
{
    if (num % 2 == 0)
    {
        printf("%d是偶数\n",num);
        return 0;// 第一次出现
    }
    printf("%d是奇数\n",num);
    return 0; //第二次出现
}
  • 返回值类型一般情况下要和函数中return语句返回的数据类型一致,如果不一致,以函数定义 时指定的返回值类型为标准。

函数的调用

调用的方式

①函数语句

cs 复制代码
test();                //对于无返回值的函数,直接调用
int res=max(2,4);     //对于有返回值的函数,一般需要在主调函数中接收被调函数的返回值

在一个函数中调用另一个函数具备以下条件: ①被调用的函数必须是己经定义的函数。

②若使用库函数,应在本文件开头用#include包含其对应的头文件。

③若使用自定义函数,自定义函数又在主调函数的后面,则应在主调函数中对被调函数进行声明。声明的作用是把函数名、函数参数的个数和类型等信息通知编译系统,以便于在遇到函数时,编译系统能正确识别函数,并检查函数调用的 合法性。

函数的声明

函数调用时,往往要遵循先定义后使用,,但如果我们对函数的调用操作出现在函数定义之前,则需要对函数进行声明。

定义:

完整的函数分为三部分:

  • 函数声明
cs 复制代码
int max(int x,int y);   //指明参数名称
int max(int,int);       //省略参数名称

函数声明如果是在同一个文件,一定要定义在文件中所有函数定义的最前面。如果有对应的山文件,可以将函数的 声明抽取到h中。

  • 函数定义
cs 复制代码
int max(int x,int y,double z)
{
    return x >y x : y > z (int) ? y : (int) z;
}

函数定义的时候,不能省略参数的数据类型,参数个数,参数名称,位置要和函数声明完全一致。

  • 函数调用
cs 复制代码
int main()
{
    printf("%d\n",max(4,5,6));
}

函数声明的作用:

  • 是把函数名、函数参数的个数、函数参数的类型和返回类型等信息通知给编译系统,以便于在遇到函数调用时,编译系统能正确识别函数,并检查函数调用的合法性(C语言规范)。

使用

错误演示:被调函数写在主调函数之后

cs 复制代码
//主调函数
int main()
{
	printf("%d\n",add(12,13));  //编译会报错
}
//被调函数
int add(int x,int y)
{
    return x y;
}

注意:如果函数的调用比较简单,并且被调函数写在主调函数之前,此时是可以省略函数声明的。

正确演示:被调函数和主调函数无法区分前后,需要增加被调函数的函数声明

cs 复制代码
//函数声明
//int add(int x, int y);
int add(int, int);

//主调函数
int main()
{
	printf("%d\n",add(12,13));  //编译会报错
}

//被调函数
int add(int x,int y)
{
    return x y;
}

注意:如果涉及函数的相互嵌套调用,或者复杂嵌套调用,此时是无法区分函数的前后的,这就需要函数声明。

声明的方式:

  • 函数头加上分号
cs 复制代码
int add(int a,int b);
  • 函数头加上分号,可省略形参名称,但不能省略参数类型
cs 复制代码
 int add(int,int);

函数的嵌套调用

函数不允许嵌套定义,但允许嵌套调用

  • 正确:函数嵌套调用:
cs 复制代码
void a(){..}
void b()
{
	a(0;
}
  • 错误:函数嵌套定义
cs 复制代码
void a()
{
	void b()
	{
	}
}

嵌套调用:在被调函数内又主动去调用其他函数,这样的函数调用形式,称之为嵌套调用。

函数的递归调用

递归调用的含义:在一个函数中,直接或间接调用了函数自身,就称之为函数的递归调用。本质上还是函数的嵌套调用。

递归调用的本质

是一种循环结构,它不同于我们之前学的while、for、do.while这样的循环结构,这些循环结构是借助于循环变量;而递归调用时利用函数自身实现循环结构,如果不加以控制,很容易产生死循环。

递归调用的注意事项

①递归调用必须要有出口,一定要想办法终止递归(否则就会产生死循环)

②对终止条件的判断一定要放在函数递归之前(先判断,再执行)

③进行函数的递归调用。

④函数递归的同时一定要将函数调用向出口逼近。

数组做函数参数

定义

当用数组做函数的实参时,则形参应该也要用数组或者指针变量来接收(函数实参是数组,形参一

定是数组或者指着),注意的是,此次传递的并不代表传递了数组中所有的元素数据,而是传递了

第一个元素的内存地址(数组首地址),形参接收到这个地址后,则形参和实参就代表了同一个内

存空间,则形参的数据修改会改变实参。这种数据传递方式称之为地址传递。

如果用数组作为函数的形参,那么我们提供另一个形参表示数组的元素个数。原因是数组形参代

表的仅仅是实际数组的首地址。也就是说形参只获取到了实参数组的第一个元素的地址,并不确定

传递了多少个数据。所以提供另一个形参表示数组元素的容量,可以防止在被调函数对实际数组访

问时产生的下标越界。
但有一个例外,如果是用字符数组做形参,且实参数组中存放的是字符串数据(形参是字符数组,
实参是字符串常量)。则不用表示数组元素个数的形参,原因是字符串本身会添加自动结束标志
\n ,举例 :

cs 复制代码
#include <stdio.h>
// 定义一个函数,传递一个字符串
void fun(char arr[])
{
    char c;
    int i = 0;
    while((c = arr[i]) != '\0')
    {
        printf("%c",c);
        i++;
    }
    printf("\n");
}
void main()
{
fun("hello");
}

变量的作用域

变量的作用域

概念:变量的作用范围,也就是说变量在什么范围有效。

变量的分类

根据变量的作用域不同,变量可以分为:

全局变量

举例:

cs 复制代码
int num1; // 定义在所有函数之外的变量,称之为全局变量,num1能被fun1,fun2,main共同访问
void fun1(){}
int num2; // 定义在所有函数之外的变量,称之为全局变量,num2能被fun2,main共同访问
void fun2(){}
void main(){}
int num3; // 定义在所有函数之外的变量,称之为全局变量,num3不能被任何函数访问

局部变量:

使用全局变量的优缺点:
优点:

  1. 利用全局变量可以实现一个函数对外输出的多个结果数据。
  2. 利用全局变量可以减少函数形参的个数,从而降低内存消耗,以及因为形参传递带来的时间消
    耗。
    缺点:
  3. 全局变量在程序的整个运行期间,始终占据内存空间,会引起资源消耗。
  4. 过多的全局变量会引起程序的混乱,操作程序结果错误。
  5. 降低程序的通用性,特别是当我们进行函数移植时,不仅仅要移植函数,还要考虑全局变量。
  6. 违反了 " 高内聚,低耦合 " 的程序设计原则

总结:我们发现弊大于利,建议尽量减少对全局变量的使用,函数之间要产生联系,仅通过实参

  • 形参的方式产生联系。

作用域举例:

注意:
如果全局变量(外部变量)和局部变量同名,程序执行的时候,就近原则(区分作用域)

cs 复制代码
int a = 10;// 全局变量
int main()
{
    int a = 20;// 局部变量
    printf("%d\n",a); // 20 就近原则
    for (int a = 0; a < 5; a++) // 这里的a是块作用域,一旦出了for循环,就不能再访问
    {
        printf("a=%d ",a); // 0 1 2 3 4 就近原则
    }
    printf("%d\n",a); // 20
}

变量的生命周期

定义:

概念:变量在程序运行中的存在时间(内存申请到内存释放的时间)。
根据变量存在的时间不同,变量可分为静态存储方式和动态存储方式。


变量的存储类型

cs 复制代码
变量的完整定义格式:[存储类型] 数据类型 变量列表;
相关推荐
癞皮狗不赖皮6 分钟前
WEB攻防- PHP反序列化&属性权限特征&原生类 TIPS&字符串逃逸&CVE 绕过漏洞
开发语言·php
UpUpUp……6 分钟前
C++继承与组合完结
开发语言·c++·笔记
EasyCVR8 分钟前
EasyRTC嵌入式音视频通信SDK:WebRTC技术下的硬件与软件协同演进,开启通信新时代
linux·运维·服务器·c语言·音视频·webrtc
淘小欣8 分钟前
10分钟打造专属AI助手:用ms-swift实现自我认知微调
开发语言·人工智能·ai·swift·模型微调
搬砖工程师Cola1 小时前
<C#> 详细介绍.net 三种依赖注入:AddTransient、AddScoped、AddSingleton 的区别
开发语言·c#·.net
画个逗号给明天"5 小时前
C#从入门到精通(1)
开发语言·c#
JavaPub-rodert5 小时前
golang 的 goroutine 和 channel
开发语言·后端·golang
lly2024065 小时前
Matplotlib 柱形图
开发语言
_Matthew6 小时前
JavaScript |(四)正则表达式 | 尚硅谷JavaScript基础&实战
开发语言·javascript·正则表达式
Vitalia7 小时前
⭐算法OJ⭐二叉树的后序遍历【树的遍历】(C++实现)Binary Tree Postorder Traversal
开发语言·c++·算法·二叉树