关注

【C语言】函数

前言:

函数是C语言编程的核心概念之一,也是构建复杂程序的基础。本文将系统讲解C语言的函数知识,助力大家快速掌握函数的使用方法。


一、函数的组成

C语言函数主要包含其中以下几个部分

C语言函数
函数的概念
库函数
形参和实参
return语句
数组做函数参数
嵌套调用和链式访问
函数的声明和定义

二、函数的基本概念

在 C 语言中,函数可看作实现特定功能的独立代码单元。正如数学中 y= f (x)的函数逻辑 —— 接收输入参数、经内部处理后输出结果,C 语言函数也遵循这一核心逻辑。

函数的主要优势:

代码复用​:一次编写,多次调用
模块化设计​:将大任务分解为小函数
​提高可维护性​:便于调试和修改

在C语言之中我们一般会见到两类函数
库函数
自定义函数


三、库函数

3.1 标准库和头文件

C语言标准仅规定了语法规则,本身并不提供库函数的实现;而国际标准ANSI C则对常用函数制定了统一规范,这些规范构成了C语言的“标准库”。各编译器厂商依据ANSI C标准,对这些函数进行了具体实现——这些现成的函数,就是我们常说的“库函数”。

比如printf、scanf等都是典型的库函数。库函数本质上也是函数,只不过它们已经预先实现完成,程序员无需重复编写,只需掌握用法就能直接调用。

库函数的存在意义显著:一方面,它省去了开发者反复实现常见功能的麻烦,直接提升开发效率;另一方面,其实现经过严格优化,在质量和执行效率上更有保障。标准库中的函数按功能划分,分别在不同的头文件中声明,使用时需通过**#include**指令引入对应的头文件。
库函数相关头文件: https://zh.cppreference.com/w/c/header/

3.2 库函数的使用方法

学习与查阅库函数的工具十分丰富,常见的有:
C/C++官方链接https://zh.cppreference.com/w/c/header
cplusplus.comhttps://legacy.cplusplus.com/reference/clibrary

我们以 C 语言数学库中用于计算平方根的 sqrt 函数为例,详细讲解库函数的具体用法:

其函数原型如下:

double sqrt (double x);
// sqrt 是函数名
// x 是函数的参数,表示调用sqrt函数需要传递一个double类型的值
// double 是返回值类型,表示函数计算的结果是double类型的值

功能:

Compute square root(计算平方根)
Returns the square root of .x(返回参数x的算术平方根)

头文件包含:

库函数的声明存于对应的头文件中,因此使用任何库函数前,都必须先通过 #include 指令包含其关联的头文件。sqrt 函数属于数学类库函数,对应的头文件是 <math.h>。若在使用 sqrt 前未包含这个头文件,编译时大概率会出现 “标识符未定义” 之类的错误或警告,导致代码无法正常编译。

示例代码
#include <stdio.h>   // 包含printf函数的头文件
#include <math.h>    // 包含sqrt函数的头文件

int main()
{
    double d = 16.0;  // 定义需计算平方根的变量(double类型)
    double r = sqrt(d);  // 调用sqrt函数,接收返回的平方根结果
    printf("%lf\n", r);  // 打印结果(%lf为double类型的格式化输出符)
    return 0;
}
输出结果

4.000000

库函数文档的一般格式:

在这里插入图片描述

在官方文档或学习资料中,库函数说明多遵循固定格式,以方便开发者快速获取关键信息。
具体信息包括:

  1. 函数原型:明确函数名、参数类型、返回值类型;
  2. 函数功能:说明函数核心作用;
  3. 参数与返回值:解释参数含义、要求及返回值意义;
  4. 代码示例:提供可运行的调用示例;
  5. 代码输出:展示示例运行结果;
  6. 相关知识:链接同类函数或相关概念。

四、自定义函数

库函数虽能覆盖多数常见需求,但实际开发中,往往需要实现更具个性化的功能 —— 这时就需要借助 “自定义函数” 来完成。
这类函数可由程序员自身根据具体业务需求自行设计、编写,不仅灵活性更高,更是 C 语言开发中实现复杂功能、提升代码可维护性的核心手段。

基本语法:

ret_type f un_name(形式参数)
{
// 函数体:完成特定任务的代码逻辑
}

  • ret_type(函数返回值类型):指定函数执行后返回结果的数据类型,无需返回结果时设为void(空类型);
  • fun_name(函数名):需遵循“见名知意”原则(如Add表加法、IsLeapYear表判断闰年),且符合C语言标识符规则(仅含字母、数字、下划线,不以数字开头,不与关键字重名);
  • 形式参数:函数的“输入接口”,用于接收调用时传递的数据。可无参数(括号内可写void);有参数时需明确类型、名称及个数,多个参数用逗号分隔;
  • 函数体:位于{}内的核心部分,包含完成任务的具体代码(如变量定义、计算、调用其他函数等)。
示例代码

实现两个整型变量的加法操作

编写main主体框架

#include <stdio.h>

int main()
{
    int a = 0;
    int b = 0;
    // 输入两个整数
    scanf("%d %d", &a, &b);
    // 调用加法函数,将结果存入r(待实现)
    int r = Add(a, b);
    // 输出结果
    printf("%d\n", r);
    return 0;
}

根据功能需求,给函数命名为 Add(需接收 2 个int整型参数,计算结果的返回值也为int整型)

#include <stdio.h>

// 加法函数的定义
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);  // 调用Add函数,传递a和b作为参数
    printf("%d\n", r);
    return 0;
}

Add函数也可进一步简化:

int Add(int x, int y)
{
    return x + y;  // 直接返回两数之和
}

五、实参和形参

在函数的使用过程中,可将函数的参数分为实参形参
完整代码回顾:

#include <stdio.h>

// Add函数定义:x和y是形参
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);
    // 调用Add函数:a和b是实参
    int r = Add(a, b);
    printf("%d\n", r);
    return 0;
}

5.1 实际参数(简称实参)

实参是调用函数时传递的具体数据
例如上述代码中,main 函数第 17 行(int r = Add(a, b);)里的a和b就是实参。

5.2 形式参数(简称形参)

形参是定义函数时,函数名后括号内声明的参数。
例如上述代码中,第 4 行Add 函数定义时括号里的int x, int y就是形参。

5.3 实参和形参的关系

形参是实参的一份临时拷贝

虽然实参负责向形参传递数据,二者存在关联,但形参和实参拥有各自独立的内存空间。
二者内存独立的这一特点,可通过调试直接观察到。

#include <stdio.h>

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);
    //调用加法函数,完成a和b的相加
    //求和的结果放在r中
    int r = Add(a, b);  
    //输出
    printf("%d\n", r);
    return 0;
}
监视

在这里插入图片描述
调试时能观察到:x、y 确实接收了 a、b 的值,但 x、y 的地址与a、b的地址完全不同。这则说明,形参本质是实参的一份临时拷贝


六、return语句

在我们函数设计的过程中常需用到return语句,其使用需注意以下几点:

return后可跟数值或表达式:若为表达式,会先执行表达式,再返回其计算结果;

return后也可无内容(直接写return):这种写法仅适用于函数返回类型为void的场景;

返回值与函数类型需匹配:若return返回值与函数声明的返回类型不一致,系统会自动将返回值隐式转换为函数返回类型,但建议主动保证类型一致以避免隐患;

return执行即函数结束return语句一旦执行,函数会立即返回调用处,其后续的代码不再执行;

分支场景需全覆盖:若函数包含if等分支语句,必须保证所有分支路径都有return返回(或返回类型为void且无需返回),否则会导致编译错误。


七、数组做函数参数

我们有时在用函数解决问题时,常会将数组作为参数传给函数,并在函数内部对其操作。
以有如:定义一个整型数组,写两个函数分别完成两件事 :
1、将数组所有元素设置为 - 1
2、打印数组的所有元素
如果不用函数的话,我们可以在main函数里直接写循环处理,但这样代码会显得杂乱无章,尤其是数组操作逻辑复杂时,可读性和可维护性会很差。但用函数封装后,main函数只需专注于 “调用”,逻辑会显得更清晰
初步思路的代码框架可能是这样的:

#include <stdio.h>

int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    // 调用函数设置数组元素为-1
    set_arr(); 
    // 调用函数打印数组
    print_arr(); 
    return 0;
}

但显然,这样的set_arrprint_arr函数 “什么都不知道”—— 它们不知道要操作哪个数组,也不知道数组有多少个元素,根本无法完成任务。因此,我们必须将数组本身和数组的元素个数作为参数传递给函数。
要让函数能操作数组,需传递个关键信息:
1、要操作的数组告诉函数 “操作谁”
2、数组的元素个数告诉函数 “操作到哪”
修改后的main函数如下:

#include <stdio.h>

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;
}

这里的sz是数组元素个数,通过sizeof计算(仅在数组定义的作用域内有效)。接下来的问题是:set_arrprint_arr函数该如何定义?这就需要掌握数组传参的核心规则。

数组作为参数时,形参的设计有特殊规则:
形参与实参个数必须匹配
实参传递了 “数组 + 元素个数” 两个参数,形参也必须定义两个参数,顺序和类型要对应。
形参可以写成数组形式
实参是数组时,形参可以直接写成数组的形式(比如int arr[ ]),看起来和实参的定义一致,便于理解。
一维数组形参的大小可以省略
形参如果是一维数组(比如int arr[ ]),方括号里的大小(如int arr[10])可以省略,因为编译器不会检查形参数组的大小(实际传递的是数组首地址)。
二维数组形参的 “列” 不能省略
若传递二维数组,形参可以省略 “行” 的大小(如int arr[ ][3]),但 “列” 的大小必须写(比如3),因为二维数组在内存中按行存储,编译器需要 “列数” 计算元素位置。
形参不会创建新数组
数组传参时,形参不会像普通变量那样创建一份临时拷贝,而是直接指向实参数组的首地址。
形参与实参操作的是同个数组
正因为形参指向实参数组的首地址,所以在函数内部修改形参数组的元素,会直接影响实参数组(这和普通变量 “值传递” 完全不同)。

根据上面的规则,我们来实现set_arrprint_arr函数。
实现set_arr:将数组元素全部置为 - 1
函数需要接收数组和元素个数,通过循环遍历每个元素并赋值为 - 1:

// 形参写成int arr[](大小省略),接收数组首地址;sz接收元素个数
void set_arr(int arr[], int sz) {
    int i = 0;
    for (i = 0; i < sz; i++) {
        arr[i] = -1; // 直接修改数组元素,会影响实参
    }
}

实现print_arr:打印数组所有元素
同样接收数组和元素个数,循环打印每个元素:

void print_arr(int arr[], int sz) {
    int i = 0;
    for (i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n"); // 打印完换行
}
完整代码
#include <stdio.h>

// 函数声明
void set_arr(int arr[], int sz);
void print_arr(int arr[], int sz);

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); // 输出:-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 
    return 0;
}

// 函数实现
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");
}

八、嵌套调用与链式访问

8.1 嵌套调用

嵌套调用就是函数间的互相调用。每个函数就像一个乐高零件 —— 多个乐高零件通过无缝配合能搭建出精美的玩具,同理,函数通过有效的互相调用,最终能编写出相对大型的程序。

示例代码

计算某年某月的天数
思考编写关键信息:
判断年份是否为闰年(闰年2月29天,平年2月28天);
根据闰年判断结果和月份,计算该月天数
设计其两个函数:
is_leap_year:判断年份是否为闰年,返回1(闰年)或 0(平年)
get_days_of_month:调用is_leap_year,结合月份返回该月天数

#include <stdio.h>

// 判断是否为闰年
int is_leap_year(int y)
{
    // 闰年规则:能被4整除且不能被100整除,或能被400整除
    if (((y%4==0)&&(y%100!=0)) || (y%400==0))
        return 1;
    else
        return 0;
}

// 计算某年某月的天数
int get_days_of_month(int y, int m)
{
    // 平年各月天数(索引0 unused,1~12对应1~12月)
    int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int day = days[m];
    
    // 闰年2月天数+1
    if (is_leap_year(y) && m == 2)
        day += 1;
    
    return day;
}

int main()
{
    int y = 0;
    int m = 0;
    scanf("%d %d", &y, &m);
    
    // 校验月份合法性
    if (m < 1 || m > 12)
    {
        printf("月份无效(需为1~12)!\n");
        return 1;
    }
    
    int d = get_days_of_month(y, m);  // main调用get_days_of_month
    printf("%d年%d月有%d天\n", y, m, d);
    
    return 0;
}
函数嵌套调用关系
  • main调用get_days_of_month
  • get_days_of_month调用is_leap_year
示例运行

比如输入2024 2(2024年2月):

  1. main接收y=2024m=2,调用get_days_of_month(2024, 2)
  2. get_days_of_month先取days[2]=28,然后调用is_leap_year(2024)
  3. is_leap_year判断2024%4==0且2024%100!=0,返回1
  4. get_days_of_month满足“闰年且2月”,day=28+1=29,返回29。
  5. main输出29

8.2 链式访问

所谓链式访问,就是将一个函数的返回值直接作为另一个函数的参数,像链条一样把函数串联起来。

示例代码

常规写法(分两步)

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

int main()
{
    int len = strlen("abcdef");  // 1. 计算长度,存入len
    printf("%d\n", len);         // 2. 打印len
    return 0;
}

链式访问(一步到位)

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

int main()
{
    // 将strlen的返回值直接作为printf的参数
    printf("%d\n", strlen("abcdef"));
    return 0;
}
运行结果

4

进阶示例

下面代码执行的结果是什么呢?

#include <stdio.h>

int main()
{
    // 链式访问:三个printf嵌套调用
    printf("%d", printf("%d", printf("%d", 43)));
    return 0;
}

这个程序的核心是理解printf函数的返回值规则,下面逐步解析执行过程:

关键知识printf的返回值
printf函数执行成功时,返回的是它实际打印到屏幕上的字符个数
程序执行步骤

代码是嵌套的printf调用:printf(“%d”, printf(“%d”, printf(“%d”, 43))),执行顺序是从最内层向外层

  1. 最内层printf("%d", 43)

    • 打印内容:43(共2个字符)。
    • 返回值:2(因为打印了2个字符)。
  2. 中间层printf("%d", 外层返回的2)

    • 打印内容:2(共1个字符)。
    • 返回值:1(因为打印了1个字符)。
  3. 最外层printf("%d", 中间层返回的1)

    • 打印内容:1(共1个字符)。

最终输出
内层到外层的打印内容依次是4321,所以屏幕上最终显示的是:4321


九、函数的声明和定义

9.1 单个文件

函数的定义,就是函数的完整实现—— 告诉编译器 “这个函数叫什么、返回什么类型、需要什么参数、具体做什么”。简单说,就是把函数的 “身体” 完整写出来。
比如判断闰年的函数,完整定义如下

#include <stdio.h>

// 这部分就是函数的定义:完整实现了“判断闰年”的逻辑
int is_leap_year(int y)
{
    // 闰年判断规则:能被4整除且不能被100整除,或能被400整除
    if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
        return 1;  // 是闰年,返回1
    else
        return 0;  // 非闰年,返回0
}

int main()
{
    int y = 0;
    scanf("%d", &y);
    // 这里是函数的调用:使用is_leap_year判断输入的年份
    int r = is_leap_year(y);
    
    if (r == 1)
        printf("闰年\n");
    else
        printf("非闰年\n");
    return 0;
}

这种情况下,函数定义在调用之前,编译器从上到下扫描时,先 “认识” 了is_leap_year函数,后续调用时就不会报错,能正常编译运行。

如果我们把函数定义放到main函数(调用处)后面,代码会变成这样:

#include <stdio.h>

int main()
{
    int y = 0;
    scanf("%d", &y);
    // 问题:这里调用is_leap_year,但编译器还没见过这个函数
    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 等编译器编译,会出现 “未定义标识符 is_leap_year” 的警告 —— 原因很简单:C 语言编译器是从上到下逐行扫描代码的,当执行到int r = is_leap_year(y);时,编译器还没扫描到后面的函数定义,根本不知道is_leap_year是什么,自然会报错。

要解决这个问题,只需在函数调用前,加一句函数声明。函数声明的作用很简单:告诉编译器 “后面会有一个叫 XX 的函数,返回类型是 XX,需要 XX 类型的参数”—— 不用讲函数具体做什么(不用写函数体),只要提前 “报备” 关键信息就行。

函数声明的两种写法
函数声明的核心是 “返回类型、函数名、参数类型”,参数名可写可不写,两种写法都有效:
带参数名:int is_leap_year(int y);(更直观,推荐新手用)
不带参数名:int is_leap_year(int);(只保留类型,编译器也能识别)
修正后的代码(可正常编译)
在main函数前加一句声明,代码就没问题了:

#include <stdio.h>

// 这就是函数声明:提前告诉编译器is_leap_year的关键信息
int is_leap_year(int y);

int main()
{
    int y = 0;
    scanf("%d", &y);
    // 现在编译器知道is_leap_year存在,不会报错了
    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;
}

9.2 多个文件

在企业开发场景中,代码量通常较大,不会将所有代码集中在单个文件;通常会按程序功能,将代码拆分为多个文件分别存放。
在 C 语言开发的常规规范中,函数声明与类型声明通常置于头文件(.h),函数实现则置于源文件(.c)。

各文件代码示例:

add.c(源文件,函数定义):

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

add.h(头文件):

//函数的声明 
int Add(int x, int y);

test.c(源文件,调用函数):

test.c
#include <stdio.h>
#include "add.h"
int main()
{
	int a = 10;
	int b = 20;
	//函数调⽤ 
	int c = Add(a, b);
	printf("%d\n", c);
	return 0;
}

以上就是本期博客的全部内容了,感谢各位的阅读以及关注。如有内容存在疏漏或不足之处,恳请各位技术大佬不吝赐教、多多指正。

转载自CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2301_78257800/article/details/153979623

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--