[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.xdnf.cn/news/1536022.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

影刀RPA实战:网页爬虫之CSDN博文作品数据

今天我们使用影刀来采集网页数据&#xff0c;影刀RPA是一款功能强大的自动化办公软件&#xff0c;它可以模拟人工的各种操作&#xff0c;帮助企业自动处理大量重复性、有逻辑规则的工作。影刀RPA在网页数据采集方面表现出色&#xff0c;能够实现对任何桌面软件、Web程序的自动化…

NeMo Curator 整理用于 LLM 参数高效微调的自定义数据集

目录 概述 预备知识 定义自定义文档构建器 下载数据集 解析和迭代数据集 将数据集写入 JSONL 格式 使用文档构建器加载数据集 使用现有工具统一 Unicode 格式 设计自定义数据集过滤器 编辑所有个人识别信息 添加指令提示 整合管线 概述 出于演示目的&#xff0c;本…

6芯7芯可旋转电连接器航空插头

概述 可旋转电航空插头是一种能够在旋转或相对运动的部件间稳定传输电气信号或电源的装置&#xff0c;广泛应用于航空航天、自动化设备、医疗设备等多个领域。它的核心在于精密的接触系统&#xff0c;由旋转端和固定端两部分组成&#xff0c;通过金属触点或导电环实现电气连接。…

哪些网站用python开发

国内的话&#xff0c;知乎&#xff0c;网易&#xff0c;腾讯&#xff0c;搜狐&#xff0c;金山&#xff0c;豆瓣这些属于用Python比较知名的。大型的项目的话&#xff0c;网易的许多游戏&#xff0c;腾讯的某些网站&#xff0c;搜狐的邮箱&#xff0c;金山的测试框架等等都是或…

实习期间git的分枝管理以及最常用的命令

各位找工作实习的友友在工作之前一定要把git的相关知识掌握呀&#xff0c;我实现期间被leader说过关于git规范的相关问题了 目前已更新系列&#xff1a; 当前&#xff1a;:实习期间git的分枝管理以及最常用的命令 Redis高级-----持久化AOF、RDB原理 Redis高级---面试总结5种…

【JavaEE初阶】多线程(4)

欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 目录 线程安全的 第四个原因 代码举例: 分析原因 解决方法 方法1 方法2 wait(等待)和notify(通知) wait和sleep区别 线程安全的 第四个原因 内存可见性,引起的线程安全问…

springboot3.X版本集成mybatis遇到的问题

由于我本地springboot为3.x版本&#xff0c;如下图所示&#xff0c;最新版本 当我参照如下搜索的内容去集成mybatis的时候&#xff0c;会出现各种各样的报错 最根本的原因是搜出来的配置是参照springboot2.X版本&#xff0c;当我们使用springboot3.x版本之后&#xff0c;需要配…

JVM 垃圾回收机制和GC案例分析

1. 引言 Java 虚拟机&#xff08;JVM&#xff09;的垃圾回收&#xff08;Garbage Collection, GC&#xff09;机制&#xff0c;是自动内存管理的重要组成部分。它通过回收不再使用的对象&#xff0c;避免手动释放内存的麻烦。然而&#xff0c;随着系统复杂性的增加&#xff0c…

4.C_数据结构_队列

概述 什么是队列&#xff1a; 队列是限定在两端进行插入操作和删除操作的线性表。具有先入先出(FIFO)的特点 相关名词&#xff1a; 队尾&#xff1a;写入数据的一段队头&#xff1a;读取数据的一段空队&#xff1a;队列中没有数据&#xff0c;队头指针 队尾指针满队&#…

劳特巴赫ICD调试器CMM调用烧录框架固件研究之C语言版本

接到客户一个项目是基本GD32F301C8XX的,尝试用手上的劳特巴赫仿真器对它进行开发操作,发现总是提示“FLASH algorithm did not execute completely” 怀疑是底层调用用烧录固件“~~/demo/arm/flash/word/stm32f300.bin”与芯片不兼容造成的,于是有了这编研究文档,多的不说直…

Spring4-IoC2-基于注解管理bean

目录 开启组件扫描 使用注解定义bean Autowired注入 场景一&#xff1a;属性注入 场景二&#xff1a;set注入 场景三&#xff1a;构造方法注入 场景四&#xff1a;形参注入 场景五&#xff1a;只有一个构造函数&#xff0c;无注解 场景六&#xff1a;Autowired和Quali…

Tcl lnit error: Can’t find a usable init.tcl in the following directories 问题解决

这个问题出现在我用py2exe打包了一个包含tkinter的图形化界面&#xff0c;在当前电脑上运行无问题&#xff0c;在移动到新电脑上后提示报错、 这里吐槽一下&#xff0c;新电脑上报错信息一闪而过&#xff0c;我用的土法子解决的&#xff0c;就是录视频然后0.25倍速度暂定找到报…

Acrobat 9 安装教程

软件介绍 Adobe Acrobat 是由Adobe公司开发的一款PDF&#xff08;Portable Document Format&#xff0c;便携式文档格式&#xff09;编辑软件。借助它&#xff0c;可以以PDF格式制作和保存文档&#xff0c;以便于浏览和打印&#xff0c;同时还可以使用一些高级工具来创建、编辑…

Qt 菜单栏、工具栏、状态栏、标签、铆接部件(浮动窗口) 设置窗口核心部件(文本编辑控件)的基本使用

效果 代码 #include "mainwindow.h" #include "ui_mainwindow.h" #include<QToolBar> #include<QDebug> #include<QPushButton> #include<QStatusBar> #include<QLabel> #include<QDockWidget> #include<QTextEdi…

将事物分为三教九流?不妨通过logistic回归

和多元线性回归一样&#xff0c;逻辑回归也是建立“多对一”型变量之间的线性关系——也即找出线性方程的近似解。有所不同的是&#xff0c;逻辑回归的解只能出现0~1之间&#xff08;亦或就是0/1两种结果&#xff09;&#xff0c;这倒是有点像bool型和int型之间的区别了。实际上…

S32K3 工具篇7:如何使用VScode编译EB MCAL工程

S32K3 工具篇7&#xff1a;如何使用VScode编译EB MCAL工程 1. VScode工具与配置2. 使用VScode编译RTD MCAL工程2.1 使用EB tresos生成配置2.2 VScode 打开工程2.3 修改mk文件2.4 编译文件2.5 debug生成好的elf文件 对于EB配置的MCAL代码&#xff0c;通常是基于RTD去做&#xff…

GEO IGEO MEO介绍 和 北斗导航系统使用三轨道原因

GEO IGSO MEO基本轨道知识 中地球轨道&#xff08;MEO&#xff1a;Middle Earth Orbit&#xff09; 轨道高度2000-36000kmGPS、GLONASS都属于此类轨道 地球同步轨道&#xff08;或称对地静止轨道&#xff09;[同步转动] 轨道高度约为36000 km&#xff1b;此轨道上卫星运行方…

情感识别系统源码分享

情感识别检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vision …

发工资-python

题目要求&#xff1a; 代码&#xff1a; import random from random import randintmoney 10000 for i in range(1, 21):performance randint(1, 10)if performance < 5:print(f"员工{i},绩效分{performance},低于5,不发工资&#xff0c;下一位")continueif m…

每日学习一个数据结构-倒排表

文章目录 示意图倒排表的基本概念倒排表的数据结构示例 倒排表的优点应用场景 倒排表&#xff08;Inverted Index&#xff09;&#xff0c;也称为反向索引或倒排文件&#xff0c;在信息检索系统中是一种重要的数据结构。它主要用于快速搜索文档中的关键词&#xff0c;并找到包含…