云计算百科
云计算领域专业知识百科平台

C语言---函数详解

函数概念

        C语言的程序是由一个个函数(子程序)构成的,函数就是完成某项特定任务的一小段代码。C语言中有两类函数,库函数跟自定义函数。

库函数

标准库跟头文件

        C语言标准中规定了C语言的各种语法规则,C语言并不提供库函数;C语言的国际标准ANSI C规定了一些常用的函数的标准(例如函数的功能,返回值,参数等等),被称为标准库,那不同的编译器厂商根据ANSI提供的C语言标准就给出了一系列函数的实现。这些函数就被称为库函数。

        库函数所在的文件叫头文件,要使用这个库函数就要包含这个文件。具体可以去cplusplus网站上去看。

        一般来说,库函数的执行效率是最高的。

自定义函数

        自定义函数就是根据实际需求,我们自己写的函数。

   返回值 函数名(形式参数)

  {

        函数体
  }

        根据以上函数的定义概念,先来实现一个加法函数吧!

#include<stdio.h>

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

int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("%d\\n", ret);
return 0;
}

实参和形参

        实参就是真实传递给函数的参数,如Add函数里边的a和b。而形参就是函数里边完成加法操作的x和y。形参,顾名思义就是只在形式上存在,在调用函数之前,是不会在内存里边开辟空间的。此外,不可把形参跟实参混为一谈,两种参数在内存里边的地址是完全不一样的(如下图),形参只是实参的一份临时拷贝。改变形参是不会影响实参的。

return语句

        我直接用代码跟注释的方式来说明关于return语句的几个注意事项。

#include<stdio.h>

//函数功能:打印hehe函数
void Print()
{
//在函数里边只要遇到return,就直接退出,下边的代码不会再执行
//函数返回类型是void,可以不写return语句,除非以下特判场景,写return让函数返回
printf("hehe\\n");
int a = 5;
if (a % 2 == 1)
return;
//……
}

//函数功能:判断一个数是否是偶数,是就返回1,不是就返回0
int is_even(int num)
{
//遇到if分支语句,确保每条分支都有return语句
if (num % 2)
return 0;
else
return 1;
}

int test1()
{
//函数返回的值要跟其返回类型相同,不同则会隐式转成其返回类型
int n = 1;
if (n == 1)
{
return 1.1;//double -> int
}
else
{
return 1;
}
}

test2()
{
//当函数没有写返回类型的时候,默认返回的是int
printf("haha\\n");
}

int test3()
{
//函数有返回类型,但是没写return,这时候它的返回值是未知的
printf("hehe\\n");
printf("haha\\n");
}

int main()
{
Print();

int a = 10;
//这种写法叫函数的链式访问,就是一个函数的返回值作为另一个函数的参数
printf("%d\\n",is_even(a));

return 0;
}

数组做函数的参数

        在这里先补充一个概念叫标识符,标识符就是我们自己起的数组名,函数名,变量名等。关于标识符有2个命名规则,第一:它是可以由数字+字母+下划线组成的。第二:它是不能以数字开头的。

        关于数组作为函数参数,先来看一道例题:写一个函数将一个整型数组的内容,全部置为-1,再写一个函数打印数组的内容。

        要想将一个数组里边的值全部置为-1,必定需要便利数组,那就需要知道数组名跟数组长度。

#include<stdio.h>

//一维数组传参可以省略大小,二维数组只能省略行,但不能省略列
//形参跟实参是同一个数组,不会额外开空间
void set_arr(int a[], int len)
{
for (int i = 0; i < len; i++)
{
a[i] = -1;
}
}

void print_arr(int a[],int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", a[i]);
}
printf("\\n");
}

int main()
{
int arr1[5] = { 1,2,3,4,5 };
int sz = sizeof(arr1) / sizeof(int);
//数组传参:传数组名跟数组大小
set_arr(arr1, sz);
print_arr(arr1, sz);
return 0;
}

嵌套调用

        函数的嵌套调用其实就是函数之间的互相调用,这样可以让程序的功能更加强大。接下来直接看个例子:计算某年某月有多少天?

        我们可以设计一个函数来计算,另外,还需要一个函数,用来判断是否是闰年。

//计算某年某月有多少天
#include<stdio.h>
#include<stdbool.h>

bool is_leap_year(int y)
{
if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0))
{
return true;
}
else
{
return false;
}
}

int get_year_month_day(int y, int m)
{
//日期数组—数组的下标代表月份—里边的值代表当月日期
int day[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
// 0 1 2 3 4 5 6 7 8 9 10 11 12

//判断是否是闰年
if (m == 2 && is_leap_year(y))
{
return 29;
}

return day[m];
}

int main()
{
int year = 0;
int month = 0;
scanf("%d%d", &year, &month);
int day = get_year_month_day(year, month);
printf("%d\\n", day);
return 0;
}

函数的声明和定义

单个文件

        不知道大家有没有注意到,上边所有的示例代码,只要是自定义函数,都写在了main函数的上边。那如果它们写在main函数的下边行不行呢?答案是可以的。

        自定义函数具体的实现叫函数的定义,而如果仅将函数定义写在main函数的下边,其实编译就会报警告。原因就是没有函数声明,因为C语言在编译的时候是从上往下的,那一直编译到函数调用的部分,如果在这之前没有声明或者定义过这个函数,那编译器就不认识这个函数了,自然就会报警告。

        正确的处理办法就是加上函数声明,或者将函数的定义写在上边,因为定义是一种特殊的声明。

#include<stdio.h>

//函数声明—先声明后使用
int Add(int x, int y);

int main()
{
int a = 10;
int b = 20;
printf("%d\\n",Add(a, b));
return 0;
}

//函数定义
int Add(int x, int y)
{
return x + y;
}

多个文件

        在工程里边,分多个文件协同工作是最常见的情景,因为一个企业级项目有成千上万行代码,光靠一个程序员肯定是搞不定的,需要有多个程序员一起去实现这个项目,每个程序员完成相应的一部分功能,这些功能对应的函数分散在不同的.c源文件里边,最后只要包含函数对应所在的.h文件,这些文件就相当于串起来了。这就体现出分文件的作用了。分哪些文件如下。

        各个文件的作用已经在图片里标注,分了文件之后,.c文件里只要包含函数所在的.h文件就相当于声明了一下这个函数,就可以正常使用了。而且分了文件之后,函数定义跟声明分开之后也在一定程度上保护了这个函数的信息,只需关注函数的声明就可以知道怎么使用这个函数,至于函数的定义,无需关心。

static和extern

前置概念

        局部变量:可以理解为在大括号里的变量。

        全局变量:在大括号外的变量。

        作用域:变量能使用的范围。其中局部变量就是只能在大括号里使用,全局变量就是整个工程里都可以使用。

        生命周期:变量创建到销毁的一段时间。其中局部变量的生命周期就是它的作用域,进作用域开始,出作用域结束。全局变量的生命周期就是整个程序的生命周期,就是main函数的生命周期。

static

        static表示静态的意思,可以用来修饰全局变量,函数,局部变量。

修饰局部变量:

#include<stdio.h>

void test1()
{
int a = 1;
a++;
printf("%d ", a);
}

void test2()
{
//被static修饰的局部变量,会改变它在内存里存放的地方
//本来局部变量是存放在内存的栈区的,加上static就变成静态区了
//其生命周期跟全局变量一样,即出作用域不会销毁
//但其作用域不变
static int b = 1;
b++;
printf("%d ", b);
}

int main()
{
for (int i = 0; i < 5; i++)
{
test1();//会打印5个2
test2();//打印2 3 4 5 6
}
return 0;
}

修饰全局变量和函数:(是针对分文件的情况)

        正常在源文件里边要想用其他文件的全局变量,函数,只要用extren关键字声明一下就可以使用了,如下。原因是全局变量跟局部变量都有外部链接属性的,只要在同一个工程里,外部文件想使用它们,用extern声明一下即可。当然也可以通过在文件里包含自己写的.h头文件的方式声明。

        如果在它们前边加上static,它们的外部链接属性就会变成内部链接属性,这就导致它们只能在自己所在的源文件里使用。

函数递归

递归的概念

        递归就是函数自己调用自己。当遇到重复的子问题的时候就可以使用递归来解决。递归必有出口,不然就会变成无穷递归。递归出口就是不能继续拆分的子问题。

递归举例

题目一:求n的阶乘

        n的阶乘就是1*2*3*4*….*(n-1)*n。

  n! = (n – 1)! * n

(n – 1) ! = (n – 2)! * (n – 1)

…………..

0! = 1

由以上类推就会发现,求阶乘就是由重复的子问题组成

当n > 0时,n! = (n – 1)! * n;

当n == 0时,n! = 1;

#include<stdio.h>

int Fact(int n)
{
//出口
if (n == 0)
return 1;
else
return Fact(n – 1) * n;
}

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

题目二:顺序打印一个整数的每一位

        假设一个数是1234,顺序打印每一位的结果就是1 2 3 4

   对于一个数字num,获取到它的最后一位仅需要num % 10,去掉最后一位仅需要num / 10。因此可以从这里下手。

   打印1234,先打印123,再打印4

   打印123,先打印12,再打印3

   打印12,先打印1,再打印2

   打印1,仅只有一位的数字,直接打印(递归出口)

#include<stdio.h>

//对于一位以上数字,先打印除去最后一位的前边的数字,最后再打印最后一位
//对于一位数字,直接打印
void Print(int n)
{
if (n > 9)
Print(n / 10);
printf("%d ", n % 10);
}

int main()
{
int num = 0;
scanf("%d", &num);
Print(num);
return 0;
}

题目三:求第n个斐波那契数

        斐波那契数列就是1,1,2,3,5,8………..

   有明显的规律,前边两个数字是1,这是确定的,此后的每一个数都等于它前边的两个数之和。

#include<stdio.h>

int Fib(int n)
{
if (n == 1 || n == 2)
return 1;
else
return Fib(n – 1) + Fib(n – 2);
}

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

递归注意事项

        上边使用递归解决的问题,其代码是非常简单的,但是递归是反复在调用函数,这就意味着在不断的开辟函数栈帧,如果一个递归层次过深,就有能引发栈溢出的问题,同样的,递归次数过多,有可能同样的一个子问题在重复多次计算,导致程序的效率变低。

迭代

        递归虽好,代码简洁,但是也会有栈溢出跟重复计算的问题,这些都会导致程序效率变低。那迭代在有的时候就可以来提高效率,迭代其实就是循环,递归跟迭代在大部分情况下都是可以互相转化的。但迭代也有缺点,就是代码要比写递归复杂得多。

        总而言之,根据不同情景选择不同的方式即可。

赞(0)
未经允许不得转载:网硕互联帮助中心 » C语言---函数详解
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!