[C语言]第九节 函数一基础知识到高级技巧的全景探索

目录

9.1 函数的概念

9.2 库函数

9.2.1 标准库与库函数

示例:常见库函数

9.2.2 标准库与头文件的关系

 参考资料和学习工具

如何使用库函数

​编辑

9.3 ⾃定义函数

9.3.1 函数的语法形式

9.3.2函数的举例

9.4 实参与形参

9.4.1 什么是实参?

9.4.2 什么是形参?

9.4.3 实参和形参的关系

9.4.4 实参与形参的传递方式

9.5 return语句

9.5.1 return 后可以是数值或表达式

9.5.2 return 语句可以没有返回值

9.5.3 返回值类型自动转换

9.5.4 return 语句结束函数执行

9.5.5 分支语句中的 return 要确保每条路径都返回

9.5.6. 总结

9.6 数组做函数参数

9.6.1 传递数组的注意事项

9.6.2 设计 set_arr 和 print_arr 函数

9.7 嵌套调⽤和链式访问

9.7.1 嵌套调用

9.7.2 链式访问

9.8 函数的声明和定义

9.8.1 单文件中的函数声明和定义

示例:判断闰年

当函数定义在调用之后

函数声明的使用

总结

 9.8.2 多个文件

代码模块化的重要性

示例:多文件结构中的函数声明与定义

文件 1:add.c(源文件)

文件 2:add.h(头文件)

文件 3:test.c(主程序文件)

编译与链接

9.8.3 static 和 extern

作用域和生命周期

9.8.3.1 static 修饰局部变量

代码示例 1:未使用 static 修饰的局部变量

代码示例 2:使用 static 修饰的局部变量

9.8.3.2 static 修饰全局变量

代码示例 1:未使用 static 修饰的全局变量

代码示例 2:使用 static 修饰的全局变量

9.8.3.3 static 修饰函数

代码示例 1:未使用 static 修饰的函数

代码示例 2:使用 static 修饰的函数

小结


9.1 函数的概念

  在数学中,函数是一种根据输入值得到输出结果的关系,例如:一次函数 y = kx + b 中,kb 是常数,给定任意 x 值,我们可以计算出相应的 y 值。

   在C语言中,函数(function)的概念类似,也被称为子程序。函数是一小段代码,专门用于完成特定任务。C语言的程序其实就是由许多这样的函数组合而成的。通过将大任务拆分成小任务,每个小任务由一个函数完成,代码不仅更易管理,而且可以重复使用,提高了开发效率。

在C语言中,我们会遇到两类函数:

1.库函数

2.自定义函数

9.2 库函数

9.2.1 标准库与库函数

C语言的标准定义了一系列语法规则,但并不提供具体的库函数实现。为了让程序员能够方便地实现常见的功能,国际标准ANSI C规定了一些常用的函数,称为标准库。标准库中的函数由不同的编译器厂商根据ANSI C标准实现,这些函数统称为库函数

示例:常见库函数

我们之前学到的 printfscanf 就是典型的库函数。它们已经被实现好了,程序员只需学习并使用这些函数,而不必自己去实现相关功能。库函数不仅提升了开发效率,还保证了功能的质量和执行效率。

9.2.2 标准库与头文件的关系

 参考资料和学习工具

要深入了解库函数及其对应的头文件,可以参考以下资源:

  • C/C++ 官方文档 https://zh.cppreference.com/w/c/header
  • cplusplus.com: ssC library - C++ Reference

不同的库函数被根据其功能分配到不同的头文件中。每个头文件中声明了相关的函数、类型等信息。举例来说:

1.数学相关的库函数声明在 math.h 中。

2.字符串处理相关的库函数声明在 string.h

库函数相关头⽂件:https://zh.cppreference.com/w/c/header
如何使用库函数

要使用库函数,需要先包含相应的头文件。例如:

#include <math.h> // 记得包含对应的头文件 double sqrt(double x);
  • 函数名sqrt
  • 参数x,类型为 double,表示输入一个浮点数。
  • 返回值类型double,表示函数计算的结果也是一个浮点数。

在使用时,只需传入一个 double 类型的参数,函数会返回该数的平方根。

实践

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

9.3 ⾃定义函数

9.3.1 函数的语法形式

ret_type fun_name(形式参数)
{
}
ret_type 是 函数返回类型                                                         fun_name 是 函数名  
括号中放的是 形式参数                                                              {}括起来的是 函数体

我们可以把函数想象成⼩型的⼀个加⼯⼚,⼯⼚得输⼊原材料,经过⼯⼚加⼯才能⽣产出产品,那函数也是⼀样的,函数⼀般会输⼊⼀些值(可以是0个,也可以是多个),经过函数内的计算,得出结果。
ret_type 是⽤来表⽰函数计算结果的类型,有时候返回类型可以是 void ,表⽰什么都不返回
fun_name 是为了⽅便使⽤函数;就像⼈的名字⼀样,有了名字⽅便称呼,函数有了名字⽅便调
⽤,所以函数名尽量要根据函数的功能起的有意义。
函数的参数就相当于,⼯⼚中送进去的原材料,函数的参数也可以是 void ,明确表⽰函数没有参
数。如果有参数,要交代清楚参数的类型和名字,以及参数个数。
{}括起来的部分被称为函数体,函数体就是完成计算的过程。

9.3.2函数的举例

写⼀个加法函数,完成2个整型变量的加法操作
#include <stdio.h>
int main()
{int x = 0;int y = 0;int r;scanf("%d%d", &x, &y);r = x + y;printf("x+y=%d", r);return 0;}

我们根据要完成的功能,给函数取名:Add,函数Add需要接收2个整型类型的参数,函数计算的结果 也是整型。
所以我们根据上述的分析写出函数:
#include <stdio.h>
int Add(int x, int y)
{return x + y;
}
int main()
{int x = 0;int y = 0;int r;scanf("%d%d", &x, &y);r = Add(x, y);printf("x+y=%d", r);return 0;}

9.4 实参与形参

在编写和使用函数的过程中,参数的概念至关重要。我们将参数分为两类:实际参数(实参)形式参数(形参)。了解它们的区别和联系是掌握函数调用机制的关键。

9.4.1 什么是实参?

实参指的是在函数调用时,传递给函数的实际值。这些值可以是变量,也可以是常量。实参是真实存在的,它们占用内存空间,并且在函数调用时将值传递给形参。

让我们看下面的例子:

#include <stdio.h>int Add(int x, int y) {int z = x + y;return z;
}int main() {int a, b;scanf("%d %d", &a, &b);int result = Add(a, b);printf("Result: %d\n", result);return 0;
}

在这个例子中,ab 是实参。当我们调用 Add(a, b) 时,ab 的值(由用户输入)被传递给 Add 函数。这里的 ab 是实际参与计算的数值。

9.4.2 什么是形参?

形参指的是在函数定义中,用于接收实参的变量。形参只是一个占位符,表示将来在函数调用时,实参的值将传递给它们。形参在函数定义中不会实际占用内存,只有当函数被调用时,形参才会被实例化,占用内存以存放实参的值。

继续看上面的代码:

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

 这里的 xy 就是形参。它们在函数被调用之前只是名义上的变量,并没有具体的值。当我们调用 Add(a, b) 时,ab 的值分别被传递给 xy,此时形参才真正生效,开始参与计算。

9.4.3 实参和形参的关系

实参和形参的关系类似于值的拷贝。实参的值传递给形参,但它们在内存中是独立的。这意味着,形参只是实参的副本,函数内部对形参的修改不会影响到实参的值。

我们可以通过调试工具清晰地看到,形参 xy 的地址与实参 ab 的地址是不一样的。这说明形参和实参分配了不同的内存空间。例如:

#include <stdio.h>int Add(int x, int y) {printf("Address of x: %p\n", (void*)&x);printf("Address of y: %p\n", (void*)&y);return x + y;
}int main() {int a = 5, b = 10;printf("Address of a: %p\n", (void*)&a);printf("Address of b: %p\n", (void*)&b);Add(a, b);return 0;
}

 运行结果中会显示 xy 的地址不同于 ab,这验证了形参与实参是独立的

9.4.4 实参与形参的传递方式

在C语言中,函数的参数传递通常是值传递。也就是说,函数接收到的是实参的值,而不是实参本身。这种传递方式确保了实参的安全性,因为无论函数内部如何修改形参,实参的值都不会受到影响。

然而,如果我们希望函数能够直接修改实参的值,可以使用指针进行参数传递。指针传递的是变量的内存地址,这使得函数可以访问并修改原始数据。例如:

void Add(int* x, int* y) {*x += *y;
}int main() {int a = 5, b = 10;Add(&a, &b);printf("New value of a: %d\n", a);return 0;
}

 在这个例子中,通过传递 ab 的地址,Add 函数能够修改 a 的值。

9.5 return语句

9.5.1 return 后可以是数值或表达式

return 语句可以返回一个具体的数值,也可以返回一个表达式的结果。如果是表达式,首先会计算该表达式的值,然后将结果作为函数的返回值。例如:

int Add(int x, int y) {return x + y;  // 返回表达式x + y的结果
}

在上面的例子中,x + y 是一个表达式,它的结果会先被计算出来,然后通过 return 返回给调用函数。表达式的使用使得代码更加简洁灵活,支持动态计算和条件返回。

9.5.2 return 语句可以没有返回值

返回类型为 void 的函数中,return 语句后面可以什么都不写。这种用法表明函数不需要返回任何值,仅仅是提前结束函数的执行流程:

void PrintMessage() {printf("Hello, World!\n");return;  // 直接结束函数,不返回值
}

当函数的返回类型为 void 时,return 可以省略,也可以写 return; 明确结束函数。这种用法常见于处理控制流程的函数,比如显示消息、执行某些操作但不需要反馈结果的函数。

9.5.3 返回值类型自动转换

当函数的返回值类型与 return 语句返回的类型不一致时,编译器会进行隐式类型转换。例如,如果函数的返回类型是 double,但 return 返回一个 int 值,系统会自动将 int 转换为 double。虽然这种隐式转换可以避免类型不匹配的编译错误,但程序员应该谨慎使用,以避免潜在的精度损失或不必要的性能消耗。

double GetArea(int radius) {return 3.14 * radius * radius;  // radius是int类型,但返回类型是double,自动转换
}

尽管这种转换通常不会引发错误,但如果数据类型的差异较大(例如从 double 转换为 int),可能会丢失重要的信息或精度,因此推荐确保返回值类型与函数的声明一致。

9.5.4 return 语句结束函数执行

一旦 return 语句被执行,函数将立即停止运行,后续的代码将不再执行。对于需要根据条件终止函数的场景,return 是一种非常有效的手段。例如:

int CheckPositive(int num) {if (num < 0) {return -1;  // 如果num为负数,提前返回}return 1;  // 否则返回正数
}

在这个例子中,return -1 使得函数在检测到负数时立刻返回,而不执行后续的代码。这种逻辑控制方式在避免不必要的计算和提高效率方面非常有效。

9.5.5 分支语句中的 return 要确保每条路径都返回

在使用 ifswitch 等条件分支时,应该确保函数在每种可能的情况下都有返回值。否则,编译器可能会抛出编译错误,因为某些路径可能导致函数未返回任何值。

例如,下面的代码会出错:

int Max(int a, int b) {if (a > b) {return a;}// 如果a <= b,没有返回值,编译器会报错
}

正确的写法是

int Max(int a, int b) {if (a > b) {return a;} else {return b;}
}

或者更加简洁的写法

int Max(int a, int b) {return (a > b) ? a : b;
}

这种写法确保在所有情况下都有返回值,避免潜在的编译错误。

9.5.6. 总结

  • return 可以返回数值或表达式的结果,返回前会先计算表达式。
  • 对于 void 类型的函数,return 可以没有返回值,或简单结束函数执行。
  • 返回值类型不一致时,系统会自动进行隐式类型转换,但应注意潜在的精度损失。
  • return 语句一旦执行,函数的剩余代码不再运行。
  • 在分支语句中,确保所有路径都有返回值,避免编译错误。

9.6 数组做函数参数

在使用函数解决问题时,通常会将数组作为参数传递给函数,从而可以在函数内部对数组进行操作。比如,写一个函数将整型数组的所有元素设置为 -1,再写一个函数打印数组的内容。

下面是这个程序的基本结构:

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

9.6.1 传递数组的注意事项

为了能够操作数组,我们需要将数组作为参数传递给 set_arr 函数,同时为了遍历数组,还需要知道数组的元素个数。因此,我们需要向 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);   // 设置数组内容为 -1print_arr(arr, sz); // 打印数组内容return 0;
}

9.6.2 设计 set_arrprint_arr 函数

要实现这两个函数,首先需要了解数组传参的几个重点知识:

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

根据这些要点,我们可以实现如下两个函数:

设置数组内容为 -1 的函数

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

打印数组内容的函数

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

这段代码展示了如何将数组作为参数传递给函数,并在函数内部对数组进行操作。

9.7 嵌套调⽤和链式访问

在编程中,函数之间的互相调用就像积木拼接一样,多个函数组合起来可以实现复杂的功能。这种互相调用可以分为嵌套调用和链式访问。接下来,我们来详细探讨这两个概念。

9.7.1 嵌套调用

嵌套调用指的是一个函数内部调用另一个函数。通过多个函数的协同工作,可以解决较为复杂的问题。比如,我们可以设计两个函数来计算某一年某月的天数:

  • is_leap_year():根据年份判断是否为闰年。
  • get_days_of_month():调用 is_leap_year() 判断是否为闰年,再根据月份计算天数。

示例代码如下:

#include <stdio.h>int is_leap_year(int y) {if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)) {return 1;  // 闰年返回1}return 0;  // 平年返回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;  // 如果是闰年的2月,多加1天}return day;
}int main() {int y, m;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()。通过函数嵌套调用,我们可以逐步解决问题。

注意:虽然函数可以嵌套调用,但 C 语言中不允许函数嵌套定义。

9.7.2 链式访问

链式访问是指一个函数的返回值直接作为另一个函数的参数。通过这种方式,可以将多个函数像链条一样连接起来,简化代码逻辑。

例如:

#include <stdio.h>int main() {int len = strlen("abcdef");  // 计算字符串长度printf("%d\n", len);  // 打印长度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() 函数的返回值是成功打印的字符个数。

分析这个例子:

  1. 最内层的 printf("%d", 43) 打印了数字 43,字符数为 2,因此返回值是 2
  2. 中间的 printf("%d", 2) 打印返回的字符数 2,字符数为 1,因此返回值是 1
  3. 最外层的 printf("%d", 1) 打印返回的字符数 1,字符数为 1。

最终屏幕上会打印 4321

通过嵌套调用和链式访问,我们可以编写更加灵活、高效的代码,同时也增强了代码的可读性与扩展性。这两者的结合让函数在程序设计中如同乐高积木,能够创造出复杂而精妙的程序结构。

9.8 函数的声明和定义

在C语言中,函数的声明和定义是编写可维护代码的基础之一。我们常见的情况是将函数的定义直接写在函数调用之前,这种方式能够确保编译器在编译过程中可以顺利找到该函数。但在更复杂的场景下,我们需要将函数的声明和定义分开,这不仅能够提升代码的可读性,还能让我们更灵活地组织代码。

9.8.1 单文件中的函数声明和定义

函数声明函数定义是两个密切相关的概念。函数定义提供了函数的完整实现,包括逻辑和功能的具体代码,而函数声明则提前告诉编译器函数的名称、返回类型和参数类型。这样做的好处是,无论函数定义在文件中的什么位置,编译器都能够识别并正确处理函数调用。

示例:判断闰年

以下是一个函数用于判断给定年份是否为闰年:

#include <stdio.h>// 判断一年是否是闰年(函数定义)
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;
}

在上面的代码中,橙色部分为函数的定义,绿色部分为函数的调用。函数的定义位于调用之前,编译器能够顺利找到is_leap_year函数,并正常编译运行。

当函数定义在调用之后

如果我们将函数定义放在main函数的后面,如下

#include <stdio.h>int main() {int y = 0;scanf("%d", &y);int r = is_leap_year(y);  // 调用is_leap_year函数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;}
}

在编译过程中,编译器在处理到is_leap_year函数调用时,并没有找到其定义,可能会抛出警告甚至错误提示。为了解决这个问题,我们需要在函数调用之前声明函数,这样编译器就能提前知道函数的存在。

函数声明的使用

函数声明的格式非常简单,只需要告知编译器函数的返回类型、函数名以及参数的类型(参数名可以省略)。下面是改进后的代码:

#include <stdio.h>// 函数声明
int is_leap_year(int y);int main() {int y = 0;scanf("%d", &y);int r = is_leap_year(y);  // 调用is_leap_year函数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;}
}

通过在函数调用前添加声明,编译器在处理函数调用时,就能够识别该函数的存在,即使实际的定义在后面,这样就能避免编译器的报错。

总结

  1. 在函数调用之前进行函数声明是确保编译器能够顺利编译代码的关键。
  2. 函数声明只需包含函数的返回类型、名称和参数类型,参数名可以省略。
  3. 将函数定义放在调用之前也可以,但如果定义在调用之后,则一定要在调用前进行声明。

 9.8.2 多个文件

 在实际的企业开发中,程序规模通常较大,不可能将所有代码都集中在一个文件中。为了提高代码的可维护性和可扩展性,我们常常根据功能对代码进行模块化处理,将其拆分到多个文件中。函数的声明、类型定义等通常存放在头文件.h),而具体的函数实现则存放在源文件.c)。这种分离有助于代码的复用、维护与管理。

代码模块化的重要性

在复杂系统中,代码模块化不仅有助于功能的分离,还可以使团队协作更加顺畅。通过合理地将代码分散在多个文件中,开发者可以专注于各自负责的模块,减少了代码冲突和维护困难。通常,我们会将函数的声明和类型定义集中到头文件(.h)中,而将函数的具体实现保留在源文件(.c)中。这样一来,其他文件只需要通过包含头文件,就能轻松调用相关功能,而不必关心函数的具体实现细节。

示例:多文件结构中的函数声明与定义

假设我们有一个简单的加法函数,该函数的实现与调用分别位于不同的文件中:

文件 1:add.c(源文件)
// 函数的定义
int Add(int x, int y) {return x + y;
}

在这个源文件中,函数 Add 实现了两个整数相加的功能。源文件中只包含函数的具体实现细节。

文件 2:add.h(头文件)
// 函数的声明
int Add(int x, int y);

头文件 add.h 中只包含函数的声明,它告诉编译器该函数存在,并提供了函数的名称、返回类型和参数类型。头文件起到了接口的作用,方便其他文件引用。

文件 3:test.c(主程序文件)
#include <stdio.h>
#include "add.h"  // 包含头文件int main() {int a = 10;int b = 20;// 调用Add函数int c = Add(a, b);printf("%d\n", c);return 0;
}

test.c 是我们的主程序文件,它通过包含头文件 add.h,成功调用了 Add 函数。此时,主程序并不需要关心 Add 函数的具体实现,而是依赖于头文件提供的声明。编译器在链接阶段会将 add.c 中的实现与 test.c 中的调用结合起来。

编译与链接

在这种多文件的结构中,编译过程分为多个步骤:

  1. 编译:每个 .c 文件分别编译为目标文件(.o.obj)。
  2. 链接:编译器将这些目标文件链接在一起,生成最终的可执行文件。

以常见的 gcc 编译器为例,编译命令如下:

gcc -c add.c  // 将add.c编译为目标文件add.o
gcc -c test.c  // 将test.c编译为目标文件test.o
gcc add.o test.o -o program  // 将目标文件链接为可执行文件program

 通过这种方式,我们可以轻松管理多个文件之间的依赖关系。

多文件同时还可以是适当的隐藏代码,若我们完成一个代码功能的实现,现在要被其其他人使用,我们可以通过静态库的方式,使他人只能使用其功能,而不能看到源代码

例如下面是一个Add函数,我么可以将加法函数的代码转换成静态库文件

点击项目名称

右键选择属性 

 

在常规中选择配置类型,选择静态库 

 在项目文件中会生成一个X64文件,点击里面的debug,里面后有一个Add.lib

 在代码中可以就直接引用Add.lib文件,实现相应的代码功能

#include <stdio.h>
#include "add.h"  // 包含头文件
#pragma comment(lib,"Add.lib")
int main() {int a = 10;int b = 20;// 调用Add函数int c = Add(a, b);printf("%d\n", c);return 0;
}

9.8.3 static 和 extern

在C语言中,staticextern 是两个非常重要的关键字,分别用于控制变量和函数的作用域(scope)与链接属性(linkage)。理解这两个关键字的作用,对于编写高质量、模块化的代码至关重要。

在深入讨论 staticextern 之前,我们需要先了解两个重要的概念:作用域生命周期

作用域和生命周期
  • 作用域(scope):定义了变量或函数在程序中可见的范围,即在哪些代码区域可以访问到该变量或函数。
    • 局部变量的作用域仅限于其所在的代码块或函数内部。
    • 全局变量的作用域则扩展至整个程序,即所有源文件都能访问到它。
  • 生命周期(lifetime):指的是变量从创建(内存分配)到销毁(内存回收)之间的时间段。
    • 局部变量的生命周期在进入其作用域时开始,离开作用域时结束。
    • 全局变量的生命周期贯穿整个程序的执行过程,直到程序结束。

9.8.3.1 static 修饰局部变量

通过 static 关键字,我们可以改变局部变量的生命周期。来看下面的两个代码示例:

代码示例 1:未使用 static 修饰的局部变量
#include <stdio.h>void test() {int i = 0; // 每次进入函数时重新创建并初始化i++;printf("%d ", i);
}int main() {for (int i = 0; i < 5; i++) {test(); // 调用5次}return 0;
}

 输出结果:

 在这个例子中,test 函数中的局部变量 i 在每次进入函数时都会重新创建并初始化为0,因此每次调用函数时,i 的值都会重新开始累加。

代码示例 2:使用 static 修饰的局部变量
#include <stdio.h>void test() {static int i = 0; // 仅在第一次调用时初始化i++;printf("%d ", i);
}int main() {for (int i = 0; i < 5; i++) {test(); // 调用5次}return 0;
}

输出结果 

在这个例子中,i 变量被 static 修饰,生命周期被扩展到整个程序的执行期间。即使离开 test 函数,i 也不会被销毁,下一次进入函数时,i 的值将保留并继续累加。

结论static 修饰局部变量后,变量的存储位置从栈区转移到静态存储区,生命周期从局部函数的作用域扩展到整个程序执行期。这样我们可以保留变量的值,即使函数多次调用,也能继续使用上次的计算结果。

使用建议:当需要局部变量在函数退出后保持其值,下次进入函数时继续使用时,建议使用 static 修饰该变量。

9.8.3.2 static 修饰全局变量

全局变量默认具有外部链接属性,可以在其他源文件中通过 extern 关键字声明并使用。但是,使用 static 修饰全局变量后,该变量的链接属性会变为内部链接属性,只能在定义它的源文件中使用。

代码示例 1:未使用 static 修饰的全局变量

add.c 文件:

int g_val = 2018; // 全局变量

test.c 文件:

#include <stdio.h>extern int g_val; // 声明外部变量int main() {printf("%d\n", g_val); // 输出2018return 0;
}

在这个例子中,全局变量 g_val 可以在 test.c 文件中通过 extern 关键字进行引用。

代码示例 2:使用 static 修饰的全局变量

add.c 文件:

static int g_val = 2018; // 静态全局变量

test.c 文件:

#include <stdio.h>extern int g_val; // 尝试声明外部变量int main() {printf("%d\n", g_val); // 链接错误return 0;
}

在这个例子中,由于 g_valstatic 修饰,其链接属性被限制为内部链接,因此无法在其他源文件中通过 extern 声明使用,编译时会出现链接错误。

结论static 修饰全局变量后,该变量只能在定义它的源文件中使用,其他文件无法通过 extern 进行访问。

使用建议:当一个全局变量只需要在定义它的源文件中使用时,可以使用 static 修饰,以避免其他文件误用该变量,确保数据的封装性和安全性。

9.8.3.3 static 修饰函数

与全局变量类似,函数默认具有外部链接属性,可以在其他源文件中通过 extern 声明调用。然而,当函数被 static 修饰后,链接属性变为内部链接属性,该函数只能在定义它的源文件中使用。

代码示例 1:未使用 static 修饰的函数

add.c 文件:

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

test.c 文件: 

#include <stdio.h>extern int Add(int x, int y); // 声明外部函数int main() {printf("%d\n", Add(2, 3)); // 输出5return 0;
}

在这个例子中,Add 函数可以在 test.c 文件中通过 extern 关键字进行引用。

代码示例 2:使用 static 修饰的函数

add.c 文件:

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

test.c 文件: 

#include <stdio.h>extern int Add(int x, int y); // 声明外部函数int main() {printf("%d\n", Add(2, 3)); // 链接错误return 0;
}

 

由于 Add 函数被 static 修饰,其链接属性变为内部链接,因此无法在其他源文件中通过 extern 声明调用,编译时会出现链接错误。

结论static 修饰函数后,该函数只能在定义它的源文件中调用,其他文件无法引用该函数

使用建议:当一个函数只需要在定义它的源文件中使用时,可以使用 static 修饰,以避免函数暴露给外部文件,确保代码模块化和安全性。

小结

staticextern 关键字在C语言中用于控制变量和函数的作用域与链接属性。通过合理地使用这些关键字,我们可以有效地控制代码的可见性与数据的封装性,提升程序的安全性和可维护性。在实际开发中,理解并合理应用 staticextern 是编写高效、模块化代码的重要基础。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/427380.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

求Huffman树及其matlab程序详解

#################本文为学习《图论算法及其MATLAB实现》的学习笔记################# 算法用途 求Haffman树 算法思想 根据定理4.17,给出求Huffman树的算法步骤如下: ①对给出的所要求的叶子顶点的权进行从小到大排序,写出的权重向量 ; ②根据定理4.17,写出兄弟的权重分别为…

《Pyramid Vision Transformer》论文笔记

原文笔记 What 为了解决VIT在视觉任务上的局限性并且探究Transformer模型在视觉任务上的应用&#xff0c;这项工作提出了一种纯 Transformer 主干&#xff0c;称为 Pyramid Vision Transformer (PVT)&#xff0c;它可以作为 CNN 主干在许多下游任务中的替代方案&#xff0c;包…

全网最适合入门的面向对象编程教程:50 Python函数方法与接口-接口和抽象基类

全网最适合入门的面向对象编程教程&#xff1a;50 Python 函数方法与接口-接口和抽象基类 摘要&#xff1a; 在 Python 中&#xff0c;接口和抽象基类&#xff08;Abstract Base Classes, ABCs&#xff09;都用于定义类的结构和强制子类实现特定的方法&#xff0c;Python 没有…

基于ExtendSim的 电子制造 仿真模型

说明&#xff1a; 此模型表示电路板制造设施。该过程有4个步骤&#xff1a; *焊料制备 *组件放置 *烤箱 *检查 详情&#xff1a; *烤箱的容量为10张卡&#xff0c;但如果烤箱循环开始时仅能处理5张卡&#xff0c;则最多只能处理5张。 *如果检查员发现问题&#xff0c;他们将修理…

【STM32系统】基于STM32设计的SD卡数据读取与上位机显示系统(SDIO接口驱动、雷龙SD卡)——文末资料下载

基于STM32设计的SD卡数据读取与上位机显示系统 演示视频&#xff1a; 基于STM32设计的SD卡数据读取与上位机显示系统 简介&#xff1a;本研究的主要目的是基于STM32F103微控制器&#xff0c;设计一个能够读取SD卡数据并显示到上位机的系统。SD卡的数据扇区读取不仅是为了验证存…

考研数据结构——C语言实现无向图邻接矩阵

首先&#xff0c;定义了一些基本的数据结构和常量&#xff1a; VertexType 和 EdgeType 分别用于表示图中的顶点和边的权重。MAXVEX 定义了图中最大顶点数为100。INFINITY 用于表示顶点之间没有直接的边相连&#xff0c;这里用65535作为无穷大的表示。 定义了一个图的结构体 MG…

react + antDesign封装图片预览组件(支持多张图片)

需求场景&#xff1a;最近在开发后台系统时经常遇到图片预览问题&#xff0c;如果一个一个的引用antDesign的图片预览组件就有点繁琐了&#xff0c;于是在antDesign图片预览组件的基础上二次封装了一下&#xff0c;避免重复无用代码的出现 效果 公共预览组件代码 import React…

网站建设的服务器该如何选择?

服务器的选择对于网站的稳定运行、性能表现以及成本控制至关重要。以下是一些关键的考虑因素&#xff0c;帮助你选择适合的服务器&#xff1a; 明确需求&#xff1a;你需要先明确网站的需求和目标。这包括确定服务器将用于托管什么样的应用&#xff08;如Web前端、应用服务器、…

基于vue框架的宠物寻回小程序8g7el(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;发布人,宠物分类,宠物信息,接取人,接取信息,完成信息 开题报告内容 基于Vue框架的宠物寻回小程序开题报告 一、研究背景与意义 随着城市化进程的加快和人们生活水平的提高&#xff0c;宠物已成为许多家庭不可或缺的一员。它们不仅为生…

RK3568平台(网络篇)MAC地址烧录

一.max地址简介 MAC地址(Media Access Control Address)也称为硬件地址或物理地址(Physical Address),它是一个用来确认网络设备位置的位址。在OSI模型中,第二层数据链路层则负责MAC位址 。MAC地址用于在网络中唯一标示一个网卡,一台设备若有一或多个网卡,则每个网卡都…

[数据集][目标检测]俯拍航拍森林火灾检测数据集VOC+YOLO格式6116张2类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;6116 标注数量(xml文件个数)&#xff1a;6116 标注数量(txt文件个数)&#xff1a;6116 标注…

Mamba YOLO World

论文地址&#xff1a;https://arxiv.org/pdf/2409.08513v1 代码地址&#xff1a; GitHub - Xuan-World/Mamba-YOLO-World: Mamba-YOLO-World: Marrying YOLO-World with Mamba for Open-Vocabulary Detection 开集检测&#xff08;OVD&#xff09;旨在检测预定义类别之外的物体…

gma 2.0.13 (2024.09.16) 更新日志

安装 gma 2.0.13 pip install gma2.0.13网盘下载&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1P0nmZUPMJaPEmYgixoL2QQ?pwd1pc8 提取码&#xff1a;1pc8 注意&#xff1a;此版本没有Linux版&#xff01; 编译gma的Linux虚拟机没有时间修复&#xff0c;本期Linux版…

基于SpringBoot+Vue的企业会议室预定管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于JavaSpringBootVueMySQL的…

【iOS】——JSONModel源码

JSONModel用法 基本用法 将传入的字典转换成模型&#xff1a; 首先定义模型类&#xff1a; interface Person : JSONModel property (nonatomic, copy) NSString *name; property (nonatomic, copy) NSString *sex; property (nonatomic, assign) NSInteger age; end接…

相亲交易系统源码详解与开发指南

随着互联网技术的发展&#xff0c;越来越多的传统行业开始寻求线上转型&#xff0c;其中就包括婚恋服务。传统的相亲方式已经不能满足现代人快节奏的生活需求&#xff0c;因此&#xff0c;开发一款基于Web的相亲交易系统显得尤为重要开发者h17711347205。本文将详细介绍如何使用…

FinGPT金融大模型

FinGPT仓库https://github.com/AI4Finance-Foundation/FinGPT 功能&#xff1a; Adviser。根据新闻判断市场情绪&#xff08;积极、消极、中性&#xff09;&#xff0c;给出投资建议。Quantitative Trading。定制属于自己的金融助手。叫它关注某几个股票、监测消息等。可以直…

Comsol 多孔弹性波应用三:吸声器(超宽频带)

超宽频带吸声材料&#xff08;Ultra-wideband absorbing materials&#xff09;是指能够在非常宽的频率范围内吸收声波的材料。传统的吸声材料通常只能在较窄的频率范围内有效吸收声波&#xff0c;而超宽频带吸声材料可以在更广泛的频率范围内实现高效的吸声效果。这使得超宽频…

光伏业务管理系统:全流程管理成重点

一、光伏业务管理的挑战 光伏业务管理涉及项目规划、设计选型、施工建设、运营维护、数据分析等多个环节&#xff0c;每一个环节都直接关系到项目的经济性、安全性和可持续性。传统的管理方式往往存在信息不对称、流程不透明、响应速度慢等问题&#xff0c;难以适应光伏产业快…

有毒有害气体检测仪的应用和性能_鼎跃安全

随着现代工业的不断发展和扩张&#xff0c;越来越多的企业涉及到有毒有害气体的生产、使用和处理。工业规模的扩大导致有毒有害气体的排放量增加&#xff0c;同时也增加了气体泄漏的风险。在发生火灾、爆炸或危险化学品泄漏等紧急事件时&#xff0c;救援人员需要迅速了解现场的…