收集飞花令碎片——C语言(数组+函数)
C语言的
函数(Function)
是程序的基本构建块,用于封装一段可重用的代码
,完成特定任务。函数可以提高代码的模块化
、可读性
和复用性
。
目录
- 函数
- 库函数
- 标准库头文件
- 自定义函数
- (1)基本语法
- (2)函数的调用
- (3)函数的声明
- (4)形参和实参
- 实参
- 形参
- 形参和实参各自的内存空间
- (重点)数组作函数参数
- (1)数组作为函数参数的基本形式
- (2)数组传参的本质
- (3)数组大小信息的丢失
- (4)数组参数与const限定符
- (5)常见错误
- 总结
- 代码练习
- (5)return 语句
- 嵌套调用
- 链式访问
- 多个文件中定义函数
- 函数重要关键字
- 作用域和生命周期
- (1)static
- static修饰局部变量
- static修饰全局变量
- (2)extern
- 总结
- 如果你觉得这篇文章对你有帮助
- 请给个三连支持一下哦
函数
函数包括库函数和自定义函数
函数也被成为子程序,就是⼀个完成某项特定的任务的一小段代码
库函数
我们前⾯内容中学到的 printf 、 scanf 都是库函数,库函数也是函数,不过这些函数已经是现成的,我们只要学会就能直接使⽤了。有了库函数,⼀些常⻅的功能就不需要程序员⾃⼰实现了,⼀定程度提升了效率;同时库函数的质量和执⾏效率上都更有保证。
库函数相关头⽂件点这里
库函数是在标准库中对应的头⽂件中声明的,所以库函数的使⽤,务必包含对应的头⽂件,不包含是可能会出现⼀些问题的。
- 实践
#include <stdio.h>
#include <math.h>
int main()
{double d = 16.0;double r = sqrt(d);printf("%lf\n", r);return 0;
}
标准库头文件
C标准库的函数按功能分类在不同的头文件(.h)中,使用时需先包含对应头文件:
头文件 | 主要功能 | 常用函数示例 |
---|---|---|
<stdio.h> | 标准输入输出 | printf , scanf , fopen , fgets |
<string.h> | 字符串处理 | strcpy , strlen , strcat , strcmp |
<math.h> | 数学运算 | sin , cos , sqrt , pow |
<stdlib.h> | 内存管理、随机数、类型转换 | malloc , free , rand , atoi |
<time.h> | 时间和日期处理 | time , clock , strftime |
<ctype.h> | 字符分类和转换 | isalpha , tolower , isdigit |
自定义函数
自定义函数由 函数名
、参数列表
、返回类型
和函数体
构成
(1)基本语法
返回类型 函数名(参数列表) {// 函数体(代码逻辑)return 返回值; // 可选,取决于返回类型
}
- 返回值类型
-
函数可以返回一个值(如 int、float、char 等)。
-
如果不需要返回值,使用 void
- 函数名
-
遵循标识符命名规则(字母、数字、下划线,不能以数字开头)。
-
最好使用动词+名词形式(如 calculateSum、printArray)。
- 参数列表
-
可以是零个或多个参数,用
,
分隔。 -
参数可以是值传递(默认)或指针传递(用于修改实参)。
- 函数体
-
包含具体的执行代码。
-
如果返回类型不是 void,必须使用 return 返回值。
- 代码示例
int add(int a, int b) { // 返回类型:int | 函数名:add | 参数:int a, int breturn a + b; // 返回计算结果
}
(2)函数的调用
//函数定义后,可以通过 `函数名 + 参数` 调用:
int result = add(3, 5); // 调用 add(),传入 3 和 5
printf("3 + 5 = %d\n", result); // 输出: 3 + 5 = 8
//无返回值函数的调用
greet(); // 调用 greet(),输出 "Hello, World!"
//指针参数的调用
int x = 10, y = 20;
swap(&x, &y); // 传入地址,交换 x 和 y 的值
printf("x=%d, y=%d\n", x, y); // 输出: x=20, y=10
(3)函数的声明
如果函数定义在调用之后,需要先声明(告诉编译器函数的存在):
#include <stdio.h>// 函数声明(原型)
int add(int a, int b); int main() {int sum = add(3, 5); // 调用 add()printf("Sum: %d\n", sum);return 0;
}// 函数定义
int add(int a, int b) {return a + b;
}
(4)形参和实参
- 举例代码
#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); //17//输出printf("%d\n", r);return 0;
}
实参
在上⾯代码中,我们把第17行调用Add函数时,传递给函数的参数a和b,称为实际参数,简称实参
。
实际参数就是真实传递给函数的参数。
形参
在上⾯代码中,第2⾏定义函数的时候,在函数名 Add 后的括号中写的 x 和 y ,称为形式参数,简称形参
。
为什么叫形式参数呢?实际上,如果只是定义了 Add 函数,⽽不去调⽤的话, Add 函数的参数 x 和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在函数被调⽤的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形参的实例化
形参和实参各自的内存空间
当函数被调用时,形参在
栈(Stack)
中分配独立的内存空间。
函数执行结束后,形参的内存自动释放。
实参的内存空间在调用前已存在(可能是全局变量、栈变量或堆内存)。
实参的内存生命周期由定义它的作用域决定(如函数结束释放栈变量)。
在值传递时,实参的值会被拷贝给形参,二者占用不同内存空间。
在指针传递时,实参和形参共享同一内存地址(通过指针间接访问)。
特性 | 形参 | 实参 |
---|---|---|
内存位置 | 栈(函数调用时分配) | 由定义位置决定(栈/堆/全局) |
生命周期 | 函数执行期间 | 依赖原作用域 |
修改是否影响实参 | 值传递:否;指针传递:是 | 直接修改自身 |
本质 | 函数的局部变量 | 调用时传入的具体数据 |
(重点)数组作函数参数
在C语言中,数组作为函数参数传递是一个重要且需要特别注意的概念。下面我将从多个方面详细讲解数组作为函数参数的使用方法、原理和注意事项。
(1)数组作为函数参数的基本形式
// 写法一:使用数组形式声明
void func(int arr[], int size) {// 函数体
}// 写法二:使用指针形式声明
void func(int *arr, int size) {// 函数体
}
(2)数组传参的本质
C语言中数组作为函数参数传递时,实际上传递的是数组首元素的地址,而不是整个数组的副本。
- 代码展示
int main() {int arr[5] = {1, 2, 3, 4, 5};printArray(arr, 5); // arr在这里退化为指向首地址的指针return 0;
}void printArray(int a[], int size) {// 实际上a是一个指针,不是数组
}
(3)数组大小信息的丢失
由于数组参数退化为指针,函数内部无法直接获取数组的实际大小
因此,通常需要额外传递数组大小作为参数。
void printSize(int arr[]) {printf("%zu\n", sizeof(arr)); // 输出指针大小(如8字节),不是数组大小
}int main() {int a[10];printf("%zu\n", sizeof(a)); // 输出40(假设int为4字节)printSize(a); // 输出8(64位系统指针大小)return 0;
}
(4)数组参数与const限定符
如果不希望函数修改数组内容,可以使用const限定符
void printArray(const int arr[], int size) {for(int i = 0; i < size; i++) {printf("%d ", arr[i]);// arr[i] = 0; // 编译错误,不能修改const数组}
}
(5)常见错误
- 错误:试图计算数组参数的大小
void wrongFunc(int arr[]) {int size = sizeof(arr)/sizeof(arr[0]); // 错误!结果是1或2(指针大小/元素大小)
}
- 问题分析
- 当数组作为参数传递时,它会退化为指针
- sizeof(arr)返回的是指针大小(8字节),而不是数组大小
- 这种计算方式在函数内部完全不可靠
- 修正方法
必须显式传递数组大小作为额外参数:sizeof(a)在main函数中仍然是完整的数组大小
// 正确写法
void correctFunc(int arr[], size_t size) {for(size_t i = 0; i < size; i++) {printf("%d ", arr[i]);}
}int main() {int a[5] = {1, 2, 3, 4, 5};correctFunc(a, sizeof(a)/sizeof(a[0])); // 在调用处计算大小return 0;
}
总结
这里我们需要知道数组传参的⼏个重点知识:
• 函数的形式参数要和函数的实参个数匹配
// 正确:形参和实参个数匹配
void printArray(int arr[], int size) { ... }int main() {int a[5] = {1, 2, 3, 4, 5};printArray(a, 5); // 两个实参:数组名 + 数组大小return 0;
}
• 函数的实参是数组,形参也是可以写成数组形式的
// 以下两种写法等价
void func1(int arr[]) { ... } // 数组形式(推荐用于直观性)
void func2(int *arr) { ... } // 指针形式(推荐用于明确本质)
• 形参如果是⼀维数组,数组⼤⼩可以省略不写
// 以下三种声明完全等效
void funcA(int arr[]) { ... } // 省略大小
void funcB(int arr[10]) { ... } // 写了大小(但无效)
void funcC(int *arr) { ... } // 直接写指针
• 形参如果是⼆维数组,⾏可以省略,但是列不能省略
// 正确:列数必须明确
void printMatrix(int mat[][4], int rows) { ... }// 错误:列数未指定
void wrongFunc(int mat[][]) { ... } // 编译报错!
• 数组传参,形参是不会创建新的数组的
void modifyArray(int arr[]) {arr[0] = 100; // 修改会影响实参的数组
}int main() {int a[3] = {1, 2, 3};modifyArray(a);printf("%d", a[0]); // 输出100,原数组被修改return 0;
}
• 形参操作的数组和实参的数组是同⼀个数组
void clearArray(int arr[], int size) {for (int i = 0; i < size; i++) {arr[i] = 0; // 直接修改原始数组}
}int main() {int data[5] = {1, 2, 3, 4, 5};clearArray(data, 5); // data数组被清空return 0;
}
代码练习
#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;
}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");
}
另外在这里强调一下
数组首地址
和首元素地址
是一个概念
(5)return 语句
在函数的设计中,函数中经常会出现return语句,这⾥讲⼀下return语句使用的注意事项。
- return后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执⾏表达式,再返回表达式
的结果。 - return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况。
- return语句执行后,函数就彻底返回,后边的代码不再执⾏。
- return返回的值和函数返回类型不⼀致,系统会⾃动将返回的值隐式转换为函数的返回类型。
- 如果函数中存在if等分⽀的语句,则要保证每种情况下都有return返回,否则会出现编译错误。
- 函数的返回类型如果不写,编译器会默认函数的返回类型是int。
- 函数写了返回类型,但是函数中没有使用return返回值,那么函数的返回值是未知的。
嵌套调用
嵌套调⽤就是函数之间的互相调⽤,每个函数就像⼀个乐⾼零件,正是因为多个乐⾼的零件互相⽆缝的配合才能搭建出精美的乐⾼玩具,也正是因为函数之间有效的互相调⽤,最后写出来了相对⼤型的程序。
禁止函数嵌套定义
下面我们用一段代码来展示
计算某年某月某日有多少天
is_leap_year()
:根据年份确定是否是闰年get_days_of_month()
:调⽤is_leap_year确定是否是闰年后,再根据月计算这个⽉的天数
#include <stdio.h>// 判断闰年函数
int IsLeapYear(int year) {if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {return 1;}else {return 0;}
}// 获取月份天数函数
int get_days_of_month(int year, int month) {int arr[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // 修正数组顺序int day = arr[month];// 如果是闰年且2月,天数加1if (IsLeapYear(year) && month == 2) {day += 1;}return day;
}int main() {int y = 0;int m = 0;printf("请输入年份和月份:");scanf("%d %d", &y, &m); // 使用标准scanfint d = get_days_of_month(y, m);printf("%d年%d月有%d天\n", y, m, d);return 0;
}
链式访问
所谓链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数,像链条⼀样将函数串起来就是函数的链式访问。
#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是打印屏幕上的字符个数
我们就第一个printf打印的是第二个printf的返回值,第二个printf打印的是第三个printf的返回值。
第三个printf打印43,在屏幕上打印2个字符,再返回2
第二个printf打印2,在屏幕上打印1个字符,再放回1
第一个printf打印1
所以屏幕上最终打印:4321
多个文件中定义函数
- 基本概念
⼀般在企业中我们写代码时候,代码可能比较多,不会将所有的代码都放在⼀个文件中;我们往往会根据程序的功能,将代码拆分放在多个文件中。
声明: 告诉编译器"这个函数存在"(放在.h头文件)
定义: 实际实现函数功能(放在.c源文件)
一般情况下,函数的声明、类型的声明放在头文件(.h)中,函数的定义与功能的实现放在源文件(.c)当中
调用函数时候记得包含头文件————用"头文件名.h"
- 标准做法示例
文件结构
project/
├── main.c # 主程序
├── utils.h # 函数声明(头文件)
└── utils.c # 函数定义
函数重要关键字
在讲解关键字之前,我们先讲讲
作用域
和生命周期
作用域和生命周期
- 作用域指变量/函数在代码中的
可见范围
。
- 局部作用域(块作用域)
void func() {int x = 10; // 局部变量,只在func内可见if (1) {int y = 20; // 只在if块内可见}// y 这里不可用
}
- 全局作用域
int global = 100; // 全局变量,从定义处到文件末尾都可见void func1() {global++; // 可以访问
}void func2() {global--; // 可以访问
}
- 文件作用域(static全局变量)
static int file_scope = 50; // 只在当前.c文件可见void func() {file_scope = 60; // 可以访问
}// 其他文件无法访问file_scope
- 生命周期指变量
存活的时间
。
- 自动存储期(局部变量)
void func() {int auto_var = 10; // 函数调用时创建,函数结束时销毁
}
- 静态存储期()
int global_var; // 程序启动时创建,结束时销毁(初始化为0)
static int static_var; // 同上,但作用域受限void func() {static int local_static = 0; // 只会初始化一次,之后就保持值local_static++;
}
- 动态存储期(malloc分配)
void func() {int *p = malloc(sizeof(int)); // 手动分配*p = 100;free(p); // 手动释放
}
- 动态存储期(malloc分配)后面讲指针的时候会说
void func() {int *p = malloc(sizeof(int)); // 手动分配*p = 100;free(p); // 手动释放
}
- 总结
- 局部变量的⽣命周期是:进⼊作⽤域变量创建,生命周期开始,出作⽤域⽣命周期结束。
- 全局变量的⽣命周期是:整个程序的⽣命周期。
(1)static
static修饰局部变量
我们先来看下面的两端代码
- 代码一
#include <stdio.h>void test()
{int i = 0;i++;printf("%d ", i);
}int main()
{int i = 0;for (i = 0; i < 5; i++){test();}return 0;
}
代码1的test函数中的局部变量i是每次进⼊test函数先创建变量(⽣命周期开始)并赋值为0,然后++,再打印,出函数的时候变量⽣命周期将要结束(释放内存)。
- 代码二
#include <stdio.h>void test()
{// static修饰局部变量static int i = 0;i++;printf("%d ", i);
}int main()
{int i = 0;for (i = 0; i < 5; i++){test();}return 0;
}
代码2中,我们从输出结果来看,i的值有累加的效果,其实 test函数中的i创建好后,出函数的时候是不会销毁的,重新进⼊函数也就不会重新创建变量,直接上次累积的数值继续计算。
- 结论
static修饰局部变量改变了局部变量的生命周期,生命周期的改变本质上就是改变了变量的存储类型
本来局部变量是存储在内存的栈区的,但是被static
修饰后存储到了静态区。
存储在静态区的变量的生命周期和全局变量的生命周期是一样,直到程序结束,变量才销毁,内存才收回
- 使用建议
未来⼀个变量出了函数后,我们还想保留值,等下次进入函数继续使用,就可以使用
static
修饰。
static修饰全局变量
//test.c
#include <stdio.h>
extern int g_val;
int main()
{
printf("%d",g_val);
return 0;
}
//add.c
int g_val = 2018;
使用建议:如果⼀个全局变量,只想在所在的源⽂件内部使用,不想被其他文件发现,就可以使用static 修饰。
static对函数也同样适用
(2)extern
extern
是C语言中用于声明变量或函数的外部链接性的关键字,它告诉编译器"这个标识符的定义在其他文件中。
- 基本用法
- 声明外部变量:
extern int globalVar; // 声明globalVar在其他文件中定义
- 声明外部函数(函数声明默认就是extern的,所以通常省略):
extern void someFunction(); // 等同于 void someFunction();
- 主要作用
- 跨文件访问变量:
在一个源文件中定义变量:
// file1.c
int globalVar = 10; // 定义
在另一个源文件中使用:
// file2.c
extern int globalVar; // 声明
void foo() {printf("%d\n", globalVar); // 使用
}
- 在头文件中声明变量:
// globals.h
extern int globalVar;
- 与const变量一起使用:
extern const int MAX_SIZE; // 声明在别处定义的const变量
总结
函数的主要内容就到此为止了,下一章我们将结合知识点,编写一款扫雷游戏