C语言超详细讲解——函数

前言

本文围绕C语言函数展开,介绍函数概念、库函数、自定义函数等,还阐述参数、语句、调用方式及声明定义等知识要点。

函数

    • 前言
    • 正文
      • 1. 函数的概念
      • 2. 库函数
        • 2.1 标准库和头文件
        • 2.2 库函数的使用方法
        • 2.2.1 功能
        • 2.2.2 头文件包含
        • 2.2.3 实践
        • 2.2.4 库函数文档的一般格式
      • 3. 自定义函数
        • 3.1 函数的语法形式
        • 3.2 函数的举例
      • 4. 形参和实参
        • 4.1 实参
        • 4.2 形参
        • 4.3 实参和形参的关系
      • 5. return语句
      • 6. 数组做函数参数
      • 7. 嵌套调用和链式访问
        • 7.1 嵌套调用
        • 7.2 链式访问
      • 8. 函数的声明和定义
        • 8.1 单个文件
        • 8.2 多个文件
        • 8.3 `static`和`extern`
        • 8.3.1 `static`修饰局部变量
        • 8.3.2 `static`修饰全局变量
        • 8.3.3 `static`修饰函数
    • 总结

正文

1. 函数的概念

在数学中,我们接触过函数的概念,例如一次函数 y = k x + b y = kx + b y=kx+b k k k b b b为常数),给定任意 x x x值,就能得到相应的 y y y值。在C语言中,同样引入了函数(function)的概念,也可称为子程序,这个翻译更为贴切。C语言中的函数是完成特定任务的一小段代码,有独特的写法和调用方式。C语言程序由众多小函数组合而成,即一个大的计算任务可分解为若干小函数来完成。而且,若一个函数能完成特定任务,便可以复用,从而提高软件开发效率。

在C语言中,常见的函数类型有两类:

  • 库函数
  • 自定义函数

2. 库函数

2.1 标准库和头文件

C语言标准规定了语法规则,但不提供库函数。ANSI C规定了一些常用函数标准,形成标准库。不同编译器厂商依据该标准实现了一系列函数,这些就是库函数,像printf、scanf都是库函数。库函数已编写好,我们学会后可直接使用,这提升了开发效率,其质量和执行效率也更有保障。

各种编译器标准库中的库函数按功能划分,声明在不同头文件中。可通过链接https://zh.cppreference.com/w/c/header查看库函数相关头文件,其中包含数学、字符串、日期等各类相关函数和类型信息。学习库函数不用急于求成,可逐步学习。

2.2 库函数的使用方法

学习和查看库函数的工具众多,例如:

  • C/C++官方链接:https://zh.cppreference.com/w/c/header
  • cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/

以sqrt函数为例:

double sqrt (double x); 
  • sqrt是函数名。
  • x是函数参数,调用sqrt函数时需传递一个double类型的值。
  • double是返回值类型,表示函数计算结果为double类型。
2.2.1 功能

计算平方根,返回x的平方根。

2.2.2 头文件包含

使用库函数时,必须包含对应的头文件include,否则可能出现问题。

2.2.3 实践
#include  
#include  

int main() 
{
    double d = 16.0;
    double r = sqrt(d);
    printf("%lf\n", r);
    return 0;
} 

运行结果:

4.000000
2.2.4 库函数文档的一般格式
  1. 函数原型
  2. 函数功能介绍
  3. 参数和返回类型说明
  4. 代码举例
  5. 代码输出
  6. 相关知识链接

3. 自定义函数

自定义函数更为重要,能让程序员在编程时发挥更多创造性。

3.1 函数的语法形式

自定义函数和库函数形式相似:

ret_type fun_name(形式参数) 
{
    // 函数体
} 
  • ret_type是函数返回类型,有时可设为void,表示不返回任何值。
  • fun_name是函数名,应根据函数功能取有意义的名字,方便调用。
  • 括号中的形式参数可为void,表示函数无参数。若有参数,需明确参数类型、名字和个数。
  • {}括起来的部分是函数体,用于完成计算过程。
3.2 函数的举例

编写一个加法函数,实现两个整型变量相加:

#include  

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

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

实际编程中,可根据需求灵活设计函数的名字、参数和返回类型。

4. 形参和实参

在函数使用过程中,函数参数分为实参和形参。

#include  

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

int main() 
{
    int a = 0;
    int b = 0;
    scanf("%d %d", &a, &b);
    int r = Add(a, b);
    printf("%d\n", r);
    return 0;
} 
4.1 实参

上述代码中,调用Add函数时传递的参数ab是实际参数,即真实传递给函数的参数。

4.2 形参

定义函数时,Add函数名后的括号中的xy是形式参数。若仅定义函数而不调用,xy仅在形式上存在,不会占用内存空间;只有在函数被调用时,才会为存放实参传递的值申请内存空间,这一过程称为形参的实例化。

4.3 实参和形参的关系

实参传递给形参,但形参和实参拥有各自独立的内存空间。通过调试可观察到,虽然xy获取了ab的值,但它们的地址不同,因此可将形参看作实参的一份临时拷贝。

5. return语句

在函数设计中,return语句使用需注意以下几点:

  • return后可接数值或表达式,若是表达式,先执行表达式再返回结果。
  • return后也可无内容,直接写return;,适用于函数返回类型为void的情况。
  • return语句执行后,函数立即返回,后续代码不再执行。
    举例:
void test()
{
	int i = 0;
	for (i = 0; i < 6; i++)
	{
		if(i== 5)
		return;
		//break;
		printf("%d ",i);
	}
	printf("hehe\n");
}
int main()
{
	test();
	return 0;
}

这个代码不会打印heheC语言超详细讲解——函数_第1张图片
但如果将return改为break,就会打印hehe,要理解两者的差异,return比break更加彻底,return彻底结束这个程序,break仅仅跳出循环。

  • return返回的值若与函数返回类型不一致,系统会自动隐式转换为函数的返回类型。

  • 若函数中有if等分支语句,需确保每种情况都有return返回值,否则会出现编译错误。报错

  • 若函数未写返回类型,编译器默认其返回类型为int

  • 若函数有返回类型但未使用return返回值,函数的返回值是未知的。

6. 数组做函数参数

使用函数解决问题时,常需将数组作为参数传递给函数并在内部操作。例如,编写函数将整型数组内容全部置为-1,以及打印数组内容的函数。

#include  

void set_arr(int arr[], int sz) 
{
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        arr[i] = -1;
    }
}

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

int main() 
{
    int arr[] = {1,2,3,4,5,6,7,8,9,10};
    int sz = sizeof(arr)/sizeof(arr[0]);
    set_arr(arr, sz);
    print_arr(arr, sz);
    return 0;
} 

数组传参时需注意:

  • 函数的形式参数要与实参个数匹配。
  • 函数实参是数组,形参可写成数组形式。
  • 形参若是一维数组,数组大小可省略不写。
  • 形参若是二维数组,行可省略,但列不能省略。
  • 数组传参时,形参不会创建新数组,操作的是实参的数组。

7. 嵌套调用和链式访问

7.1 嵌套调用

嵌套调用指函数之间相互调用。以计算某年某月的天数为例,可设计两个函数:

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

int get_days_of_month(int y, int m) 
{
    int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int day = days[m];
    if (is_leap_year(y) && m == 2)
    {
        day += 1;
    }
    return day;
}

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

这段代码中,main函数调用了scanfprintfget_days_of_monthget_days_of_month函数又调用了is_leap_year。在较大规模的代码中,常使用函数嵌套调用,但函数不能嵌套定义。

7.2 链式访问

链式访问是将一个函数的返回值作为另一个函数的参数,串联函数。例如:

#include  

int main() 
{
    printf("%d\n", strlen("abcdef")); 
    return 0;
} 

再看一个有趣的代码:

#include  

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

printf函数返回值是打印在屏幕上的字符个数。在上述代码中,第三个printf打印43,返回2;第二个printf打印2,返回1;第一个printf打印1,所以屏幕最终显示4321

8. 函数的声明和定义

8.1 单个文件

通常使用函数时,直接编写并使用。例如判断一年是否为闰年的函数:

#include  

// 判断一年是不是闰年
int is_leap_year(int y) 
{
    if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

int main() 
{
    int y = 0;
    scanf("%d", &y);
    int r = is_leap_year(y);
    if (r == 1)
    {
        printf("闰年\n");
    }
    else
    {
        printf("非闰年\n");
    }
    return 0;
} 

若函数定义在调用之后,如:

#include  

int main() 
{
    int y = 0;
    scanf("%d", &y);
    int r = is_leap_year(y);
    if (r == 1)
    {
        printf("闰年\n");
    }
    else
    {
        printf("非闰年\n");
    }
    return 0;
}

// 判断一年是不是闰年
int is_leap_year(int y) 
{
    if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
    {
        return 1;
    }
    else
    {
        return 0;
    }
} 

在VS2022上编译会出现警告,因为编译器从第一行开始扫描,遇到函数调用时未找到定义。解决方法是在函数调用前声明函数,如int is_leap_year(int y);,声明时参数可省略名字。函数调用需先声明后使用,函数定义也是一种特殊的声明,若定义在调用前则无需额外声明。

8.2 多个文件

企业开发中,代码量较大,通常会根据功能将代码拆分到多个文件中。一般函数声明、类型声明放在头文件(.h)中,函数实现放在源文件(.c)中。例如:
add.c

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

add.h

int Add(int x, int y); 

test.c

#include  
#include "add.h" 

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

staticextern是C语言关键字。static表示静态,可修饰局部变量、全局变量和函数;extern用于声明外部符号。在讲解它们之前,先了解作用域和生命周期。

作用域(scope)是程序设计概念,指一段程序代码中名字有效的代码范围。局部变量作用域是其所在的局部范围,全局变量作用域是整个工程(项目)。

生命周期指变量从创建(申请内存)到销毁(收回内存)的时间段。局部变量进入作用域时创建,生命周期开始,离开作用域时结束;全局变量生命周期是整个程序的生命周期。

8.3.1 static修饰局部变量
#include  

void test() 
{
    static int i = 0;
    i++;
    printf("%d ", i);
}

int main() 
{
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        test();
    }
    return 0;
} 

对比未用static修饰的情况,使用static修饰后,局部变量i出函数时不会销毁,下次进入函数也不会重新创建,而是基于上次累积的值继续计算。static修饰局部变量改变了其生命周期,将存储类型从栈区改为静态区,与全局变量一样,只有程序结束才销毁,但作用域不变。若希望变量出函数后保留值,下次进入函数继续使用,可使用static修饰。

8.3.2 static修饰全局变量

add.c

// 代码1
int g_val = 2018;
// 代码2
static int g_val = 2018;

test.c

#include  
extern int g_val;

int main() 
{
    printf("%d\n", g_val);
    return 0;
} 

extern用于声明外部符号,若一个全局符号在A文件定义,在B文件使用时需用extern声明。代码1正常,代码2编译会出现链接性错误。因为全局变量默认有外部链接属性,可在外部文件声明使用;被static修饰后,变为内部链接属性,只能在本源文件内使用。若只想在所在源文件内部使用全局变量,可使用static修饰。

8.3.3 static修饰函数

add.c

// 代码1
int Add(int x, int y) 
{
    return x + y;
}
// 代码2
static int Add(int x, int y) 
{
    return x + y;
}

test.c

#include  
extern int Add(int x, int y);

int main() 
{
    printf("%d\n", Add(2, 3));
    return 0;
} 

代码1能正常运行,代码2出现链接错误。static修饰函数和修饰全局变量类似,函数默认有外部链接属性,整个工程中声明后可使用;被static修饰后变为内部链接属性,只能在本文件内部使用。若一个函数只想在所在源文件内部使用,可使用static修饰。

总结

函数的各类知识,包括概念、库函数与自定义函数、参数、调用等,是我们学习C语言十分重要的部分,希望可以详细学习了解,为之后学习做铺垫,希望点赞、关注、收藏、评论支持一下,谢谢。

你可能感兴趣的:(C语言,c语言,开发语言)