从零到一彻底掌握C语言函数:入门·进阶·实战·优化

函数是C语言的灵魂,掌握它,你就掌握了模块化编程的钥匙。本文将从概念到实战,从语法到优化,带你彻底吃透C语言函数,写出更优雅、高效的代码。

你是否曾想过,为什么C语言程序总是由一个个函数组成?为什么printfscanf可以直接调用而不用自己实现?为什么有时候修改函数参数的值,外部的变量却“无动于衷”?

函数是C语言中最基础也最重要的概念之一。它就像乐高积木,是构建程序的基本模块。无论是简单的加减乘除,还是复杂的系统调用,背后都离不开函数的设计与调用。

本文将带你系统学习C语言函数的方方面面,包括:

  • 函数的基本概念与分类

  • 库函数的使用与自定义函数的编写

  • 形参与实参的区别与关系

  • 数组作为函数参数的正确用法

  • 嵌套调用与链式访问的技巧

  • 使用staticextern控制函数的作用域

每一部分都配有可运行的代码示例详细注释实战场景,无论你是初学者还是希望进阶的开发者,都能从中获益。

目录

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 语句

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=kx+b(k和b为常数),给定任意x值都能得到对应的y值。

这个概念在C语言中同样适用,称为"function",有时也译为"子程序"——这个译名更能准确体现其本质。C语言的函数实际上就是为完成特定任务而编写的一段独立代码,具有特定的语法结构和调用方式。

C语言程序本质上是由众多小函数组合构建而成的。换句话说,我们可以将复杂的大型计算任务分解为若干个相对简单的函数(对应子任务)来完成。这种模块化设计不仅使程序结构更清晰,还能实现函数复用,显著提升软件开发效率。

在C语言中,函数主要分为两大类:

  • 库函数
  • 自定义函数

2. 库函数

2.1 标准库和头文件

C语言标准规定了语法规则,但不包含库函数。ANSI C国际标准定义了一系列常用函数的标准,称为标准库。不同编译器厂商根据这个标准实现了这些函数,即库函数。

例如我们学过的printf、scanf都属于库函数。这些现成的函数可以直接调用,避免了重复开发常见功能,既提高了效率又保证了质量和性能。

标准库按功能将库函数分门别类,声明在不同的头文件中(如数学函数、字符串处理、日期操作等)。每个头文件包含相关函数和类型的定义。学习库函数不必急于求成,可以循序渐进地掌握。详细库函数信息可参考:C标准库头文件

2.2 库函数的使用方法

常用的库函数查询工具包括:

举例: sqrt
double sqrt (double x);
//sqrt 是函数名
//x 是函数的参数,表⽰调⽤sqrt函数需要传递⼀个double类型的值
//double 是返回值类型 - 表⽰函数计算的结果是double类型的值

2.2.1 功能

Compute square root 计算平方根
Returns the square root of x.(返回平⽅根)

2.2.2 头文件包含

使用库函数时必须包含对应的标准库头文件,否则可能导致程序运行异常。这些函数声明均位于标准库的头文件中。

2.2.3 实践

#include <stdio.h>
#include <math.h>
int main()
{
	double d = 16.0;
	double r = sqrt(d);
	printf("%lf\n", r);
	return 0;
}
运行结果:

2.2.4 库函数文档通用格式规范

  1. 函数原型
  2. 函数功能介绍
  3. 参数和返回类型说明
  4. 代码举例
  5. 代码输出
  6. 相关知识链接

3. 自定义函数

掌握了库函数之后,我们应当重点研究自定义函数。自定义函数不仅更为关键,还能为程序员提供更大的创造空间

3.1 函数的语法形式

其实⾃定义函数和库函数是⼀样的,形式如下:
ret_type fun_name(形式参数)
{

}

ret_type:函数返回类型
fun_name:函数名称
( ):包含形式参数
{ }:界定函数体

我们可以将函数类比为一个微型加工厂。就像工厂需要输入原材料,经过加工才能生产出产品一样,函数也需要输入值(可以是零个或多个),经过函数内部的计算处理后得出结果。

  • ret_type:表示函数返回结果的类型。当返回类型为void时,表示函数不返回任何值
  • fun_name:相当于函数的标识符。就像人名便于称呼一样,有意义的函数名能更直观地体现函数功能,方便调用
  • 参数列表:相当于工厂的原材料。参数可以是void,表示函数不需要参数。若有参数,需明确其类型、名称和数量
  • 函数体:由大括号{}包裹的部分,包含了实现具体功能的计算过程

3.2 函数的举例

举个例⼦:
写⼀个加法函数,完成2个整型变量的加法操作。
#include <stdio.h>
int main()
{
	int a = 0;
	int b = 0;
	//输⼊
	scanf("%d %d", &a, &b);
	//调⽤加法函数,完成a和b的相加
	//求和的结果放在r中
	//to do

	//输出
	printf("%d\n", r);
	return 0;
}

我们基于功能需求,将函数命名为Add。该函数接收两个整型参数,并返回整型计算结果。根据上述分析,我们编写函数如下:

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

函数参数需要明确说明以下几点:

  1. 参数数量
  2. 每个参数的数据类型
  3. 形参名称

以上示例仅为说明,实际开发中我们可以根据需求灵活设计:

  • 函数名称
  • 参数配置
  • 返回值类型

4. 形参和实参

函数调用时,参数可分为实参和形参两种类型。

4.1 实参

在示例代码中,第2-7行定义了Add函数。随后在第17行调用了该函数。

当调用Add函数时,传递给函数的参数a和b被称为实际参数(简称实参)。实参即真正传递给函数的具体参数值。

4.2 形参

在代码中,第2行定义Add函数时,括号内的x和y被称为形式参数(简称形参)。

之所以称为"形式参数",是因为如果仅定义Add函数而不调用它,这些参数x和y仅存在于形式上,不会实际占用内存空间。只有在函数被调用时,形参才会为存储实参传递的值而申请内存空间,这个过程就是形参的实例化

4.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的地址不同。这表明形参实际上是实参的一份临时拷贝

5. return 语句

Return语句使用指南

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

  1. return后可跟数值或表达式,若为表达式则先执行计算再返回结果值
  2. 当函数返回类型为void时,可直接写return;不带返回值
  3. 若返回值类型与函数声明类型不符,系统会自动进行隐式类型转换
  4. 执行return后函数立即终止,后续代码不会被执行
  5. 对于存在条件分支的函数,必须确保所有执行路径都有返回值,否则会导致编译错误

这些规则能帮助开发者编写更规范、可靠的函数代码。

6. 数组做函数参数

在函数编程中,经常需要将数组作为参数传递给函数,并在函数内部对数组进行处理。例如,我们可以编写一个函数将整型数组的所有元素设为-1,再创建另一个函数来打印数组内容。

最基础的形式可以这样实现:

#include <stdio.h>

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

要实现对数组内容的设置,set_arr函数需要接收两个参数:数组本身及其元素个数。这样函数内部才能遍历数组并设置每个元素的值。同理,print_arr函数也需要这两个参数才能正确遍历并打印数组的每个元素。

#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);//设置数组内容为-1
	print_arr(arr, sz);//打印数组内容
	return 0;
}

以下是优化后的内容:


当数组作为参数传递给 set_arr print_arr 函数时,函数设计需要注意以下几点:

  • 确保函数的形式参数与实际参数个数匹配
  • 若实参是数组,形参可直接使用数组形式声明
  • 对于一维数组形参,可省略数组大小
  • 对于二维数组形参,行数可省略但列数必须保留
  • 数组作为参数传递时不会创建新数组
  • 形参操作的数组与实参数组是同一数组
根据上述的信息,我们就可以实现这两个函数:
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");
}

7. 嵌套调用和链式访问

7.1 嵌套调用

嵌套调用指的是函数之间的相互调用。就像乐高积木的各个零件需要完美配合才能搭建出精美的作品一样,函数之间也需要通过有效的相互调用才能构建出复杂的程序。

以计算某年某月有多少天为例,我们可以设计两个函数来实现:

  1. is_leap_year():判断指定年份是否为闰年
  2. get_days_of_month():先调用is_leap_year()判断闰年,再根据月份返回当月的天数
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 函数调用了 scanf、printf 和 get_days_of_month
  • get_days_of_month 函数调用了 is_leap_year 在更复杂的代码中,函数之间通常存在多层嵌套调用,但要注意函数定义本身是不能嵌套的。

7.2 链式访问

链式访问是指将一个函数的返回值直接作为另一个函数的输入参数,从而像链条一样将多个函数串联调用。例如:

#include <stdio.h>
int main()
{
	int len = strlen("abcdef");//1.strlen求⼀个字符串的⻓度
	printf("%d\n", len);//2.打印⻓度 
	return 0;
}

将代码优化为链式访问示例:直接把 strlen 的返回值作为 printf 函数的参数即可实现。

#include <stdio.h>

int main()
{
	printf("%d\n", strlen("abcdef"));//链式访问
	return 0;
}
在看⼀个 有趣的 代码,下⾯代码执行的结果是什么呢?
#include <stdio.h>
int main()
{
	printf("%d", printf("%d", printf("%d", 43)));
	return 0;
}
这个代码的关键是明⽩ printf 函数的返回是啥?
int printf ( const char * format, ... );

printf函数会返回输出到屏幕的字符数量。

在上面的例子中:

  1. 第三个printf打印"43",输出2个字符并返回2
  2. 第二个printf打印2,输出1个字符并返回1
  3. 第一个printf打印1

因此程序最终输出结果为:4321

8. 函数的声明和定义

8.1 单个文件

通常情况下,我们直接定义函数即可使用。例如:编写一个判断某年是否为闰年的函数。

在以上代码中,橙色部分为函数定义,绿色部分则是函数调用
这种函数定义先于调用的顺序是符合规范的。

那如果我们将函数的定义放在函数的调⽤后边,如下:
#include <stdio.h>

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环境下编译时会出现以下警告信息:

C语言编译器在编译源代码时是从上至下逐行扫描的。当扫描到第7行调用is_leap_year函数时,由于尚未发现该函数的定义,因此会报出警告。

解决方法是在函数调用前先声明is_leap_year函数。函数声明只需明确以下三点:

  1. 函数名
  2. 返回值类型
  3. 参数类型 例如:int is_leap_year(int y); 注意:函数声明中参数名可以省略,只保留类型也是允许的。

修改后的代码就能顺利通过编译了。

#include <stdio.h>

int is_leap_year(int y); //函数声明

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

为确保程序正确执行,函数调用必须遵循"先声明后使用"的原则。需要注意的是,函数定义本身也属于声明的一种特殊形式,因此将函数定义置于调用语句之前同样符合该规则。

8.2 多个文件

在企业开发中,我们通常会将代码按功能模块拆分到多个文件中,而不是将所有代码都集中在一个文件里。通常的做法是:

  1. 将函数声明和类型定义放在头文件(.h)中
  2. 将函数实现放在源文件(.c)中
如下:
add.c
//函数的定义
int Add(int x, int y)
{
	return x + y;
}
add.h
//函数的声明
int Add(int x, int y);
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;
}

理解函数声明和定义后,编写代码会更加得心应手。

8.3 static 和 extern

staticextern是C语言中的两个重要关键字。

static关键字用于:

  • 修饰局部变量
  • 修饰全局变量
  • 修饰函数

extern关键字则主要用于声明外部符号。

在深入理解staticextern之前,需要先明确两个基本概念:作用域和生命周期。

作用域(scope)指程序中变量或函数名有效的代码范围:

  1. 局部变量的作用域仅限于其所在的代码块
  2. 全局变量的作用域跨越整个工程/项目

生命周期指变量从创建(内存分配)到销毁(内存回收)的时间段:

  1. 局部变量的生命周期始于进入作用域,止于离开作用域
  2. 全局变量的生命周期等同于整个程序的运行周期

8.3.1 static 修饰局部变量:

对比代码1和代码2的执行效果,有助于理解 static 修饰局部变量的作用。

在代码1中,test函数内的局部变量i会在每次函数调用时:

  1. 创建变量(生命周期开始)
  2. 初始化为0
  3. 执行自增操作
  4. 打印当前值 函数退出时,变量生命周期结束(内存被释放)

而在代码2中,观察输出结果可以发现:

  1. i的值呈现出累加效果
  2. test函数中的i在首次创建后不会被销毁
  3. 后续函数调用时不会重新创建变量
  4. 直接基于上次的计算结果继续操作

结论:static修饰局部变量会改变其生命周期,本质上是改变了变量的存储类型。原先存储在栈区的局部变量,在被static修饰后将转移到静态区存储。静态区的变量与全局变量具有相同的特性,其生命周期与程序一致,只有当程序结束时才会被销毁并回收内存。需要注意的是,变量的作用域不会因此改变。

使用建议:若需在函数退出后保留变量值,以供下次调用时继续使用,可通过static修饰符实现。

8.3.2 static 修饰全局变量

extern用于声明外部符号。当一个全局符号在A文件中定义,若要在B文件中使用该符号,可以使用extern进行声明后即可调用。

代码1可以正常编译,而代码2在编译时会出现链接错误。

结论: 使用static修饰全局变量会限制其作用域仅在当前源文件内有效,无法被其他源文件访问。这是因为全局变量默认具有外部链接属性,只要进行适当声明即可在其他文件中使用;而static修饰会将其链接属性改为内部链接,从而将可见性限制在当前文件范围内。

建议:当某个全局变量仅需在当前源文件中使用时,推荐使用static修饰以隐藏其实现细节,避免被其他文件误用。

8.3.3 static 修饰函数

static修饰函数和全局变量具有相同效果:默认情况下函数具有全局可见性(外部链接属性),可以被整个工程访问;而使用static修饰后,函数将变为文件作用域(内部链接属性),仅能在当前源文件内使用。

当需要限制函数仅在当前源文件内使用时,建议使用static修饰该函数,这样能有效避免被其他文件链接调用。

✅ 结尾总结

函数是C语言编程的基石,掌握好函数的使用,不仅能提升代码的复用性和可读性,还能为后续学习更复杂的编程范式(如面向对象、函数式编程)打下坚实基础。

通过本文的学习,希望你能够:

  • ✅ 理解函数的定义、声明与调用机制

  • ✅ 熟练使用库函数和自定义函数

  • ✅ 掌握数组、作用域等进阶用法

  • ✅ 避免常见错误,写出更健壮的代码

编程是一门实践的艺术,光看不够,一定要动手去写、去调试、去优化。如果你在实践过程中遇到问题,欢迎在评论区留言交流,也欢迎关注我的专栏,获取更多C语言开发相关内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

0xCode 小新

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

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

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

打赏作者

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

抵扣说明:

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

余额充值