当前位置: 首页 > news >正文

[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 是编写高效、模块化代码的重要基础。

相关文章:

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

目录 9.1 函数的概念 9.2 库函数 9.2.1 标准库与库函数 示例&#xff1a;常见库函数 9.2.2 标准库与头文件的关系 参考资料和学习工具 如何使用库函数 ​编辑 9.3 ⾃定义函数 9.3.1 函数的语法形式 9.3.2函数的举例 9.4 实参与形参 9.4.1 什么是实参&#xff1f; 9…...

1.1 计算机网络基本概述

欢迎大家订阅【计算机网络】学习专栏&#xff0c;开启你的计算机网络学习之旅&#xff01; 文章目录 前言一、网络的基本概念二、集线器、交换机和路由器三、互连网与互联网四、网络的类型五、互连网的组成1. 边缘部分2. 核心部分 六、网络协议 前言 计算机网络是现代信息社会…...

Linux环境基础开发工具使用(gcc/g++与makefile)

1.Linux编译器-gcc/g使用 1. 背景知识 接下来的操作&#xff0c;我以gcc为例&#xff0c;因为两者选项都是通用的&#xff0c;所以也就相当于间接学习了 1.预处理&#xff08;进行宏替换) 2.编译&#xff08;生成汇编) 3.汇编&#xff08;生成机器可识别代码&#xff09;…...

PointNet++改进策略 :模块改进 | EdgeConv | DGCNN, 动态图卷积在3d任务上应用

目录 介绍核心思想及其实现核心思想实现步骤 如何改进PointNet**局部几何结构的处理****动态图的引入****特征聚合的灵活性****全局和局部特征的结合** 论文题目&#xff1a;Dynamic Graph CNN for Learning on Point Clouds发布期刊&#xff1a;TOG作者单位&#xff1a;麻省理…...

FFmpeg源码:skip_bits、skip_bits1、show_bits函数分析

GetBitContext结构体和其相关的函数分析&#xff1a; FFmpeg中位操作相关的源码&#xff1a;GetBitContext结构体&#xff0c;init_get_bits函数、get_bits1函数和get_bits函数分析 FFmpeg源码&#xff1a;skip_bits、skip_bits1、show_bits函数分析 一、skip_bits函数 skip…...

加密

一、加密 加密运算需要两个输入&#xff1a;密钥和明文 解密运算也需要两个输入&#xff1a;密钥和密文 密文通常看起来都是晦涩难懂、毫无逻辑的&#xff0c;所以我们一般会通过传输或者存储密文来保护私密数据&#xff0c;当然&#xff0c;这建立在一个基础上&#xff0c;…...

Kibana:如何使用魔法公式创建具有影响力的可视化效果?(第 1 部分)

作者&#xff1a;来自 Elastic Vincent du Sordet 我们将看到 Kibana Lens 编辑器中的神奇数学公式如何帮助突出显示高值。 简介 在上一篇博文《作为非设计师设计直观的 Kibana 仪表板》中&#xff0c;我们强调了创建直观仪表板的重要性。它展示了简单的更改&#xff08;分组…...

【C++】多态and多态原理

目录 一、多态的概念 二、多态的定义及实现 &#x1f31f;多态的构成条件 &#x1f31f;虚函数 &#x1f31f;虚函数的重写 &#x1f320;小贴士&#xff1a; &#x1f31f;C11 override 和 final &#x1f31f;重载、重写&#xff08;覆盖&#xff09;、重定义&#xf…...

C# 实现二维数据数组导出到 Excel

目录 功能需求 范例运行环境 Excel DCOM 配置 设计实现 组件库引入 ​编辑​ 方法设计 生成二维数据数组 核心方法实现 调用示例 总结 功能需求 将数据库查询出来的数据导出并生成 Excel 文件&#xff0c;是项目中经常使用的一项功能。本文将介绍通过数据集生成二维…...

nlohmann::json中有中文时调用dump转string抛出异常的问题

问题描述 Winodows下C开发想使用一个json库&#xff0c;使用的nlohmann::json&#xff0c;但是遇到json中使用中文时&#xff0c;转成string&#xff0c;会抛出异常。 nlohmann::json contentJson;contentJson["chinese"] "哈哈哈";std::string test con…...

Unity中InputField一些属性的理解

先看代码&#xff1a; using UnityEngine; using UnityEngine.UI;public class TestInput : MonoBehaviour {[SerializeField]InputField inputField;void Start(){Debug.Log(inputField.text);Debug.Log(inputField.text.Length);Debug.Log(inputField.preferredWidth);Debug…...

【webpack4系列】webpack构建速度和体积优化策略(五)

文章目录 速度分析&#xff1a;使用 speed-measure-webpack-plugin体积分析&#xff1a;使用webpack-bundle-analyzer使用高版本的 webpack 和 Node.js多进程/多实例构建资源并行解析可选方案使用 HappyPack 解析资源使用 thread-loader 解析资源 多进程并行压缩代码方法一&…...

从零开始搭建 PHP

&#x1f6e0;️ 从零开始搭建 PHP 环境&#xff1a;详细教程 PHP&#xff08;Hypertext Preprocessor&#xff09;是最流行的后端脚本语言之一&#xff0c;广泛用于构建动态网站和 Web 应用程序。在开始 PHP 开发之前&#xff0c;首先需要搭建 PHP 运行环境。无论你使用的是 …...

【数据结构】8——图3,十字链表,邻接多重表

数据结构8——图3&#xff0c;十字链表&#xff0c;邻接多重表 文章目录 数据结构8——图3&#xff0c;十字链表&#xff0c;邻接多重表前言一、十字链表结构例子 复杂例子 二、邻接多重表&#xff08;Adjacency Multilist&#xff09;例子 前言 除了之前的邻接矩阵和邻接表 …...

eth-trunk 笔记

LACP&#xff1a;Link Aggregation Control protocol 链路聚合控制协议 将多条以太网物理链路捆绑在一起成为一条逻辑链路&#xff0c;从而实现增加链路带宽的目的。同时&#xff0c;这些捆绑在一起的链路通过相互间的动态备份&#xff0c;可以有效地提高链路的可靠性 一、配…...

通信工程学习:什么是接入网(AN)中的TF传送功能

接入网&#xff08;AN&#xff09;中的TF传送功能 在通信工程中&#xff0c;TF&#xff08;Transfer Function&#xff09;传送功能是指为接入网&#xff08;AN&#xff09;不同位置之间提供通道和传输介质&#xff0c;以实现数据的有效传输。以下是关于TF传送功能的详细解释&a…...

【JavaEE】IO基础知识及代码演示

目录 一、File 1.1 观察get系列特点差异 1.2 创建文件 1.3.1 delete()删除文件 1.3.2 deleteOnExit()删除文件 1.4 mkdir 与 mkdirs的区别 1.5 文件重命名 二、文件内容的读写----数据流 1.1 InputStream 1.1.1 使用 read() 读取文件 1.2 OutputStream 1.3 代码演示…...

安卓13系统导航方式分析以及安卓13修改默认方式为手势导航 android13修改导航方式

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改5.彩蛋1.前言 系统导航方式默认一般是按键的,如果要改成手势的话,我们来看看用户怎么修改的: 设置=>系统=>手势=>系统导航,在这里进行修改。我们来分析下这个流程,并且将其修改为…...

[技术杂谈]暗影精灵8plus电竞版台式机安装和使用注意

最近买回二手台式机准备做深度学习训练模型使用。由于个人不是十分有钱&#xff0c;因此下血本入手一台&#xff0c;不然深度学习玩不转。配置&#xff1a;i9-12900K / 64G d4 3733频率 / 1TSSD2TB机械 / RTX3090 24G显卡 旗舰版 机箱45L超大机箱。买回来后整体不错&#…...

【加密算法基础——AES解密实践】

AES 解密实践 AES 解密是对使用 AES 加密算法加密的数据进行恢复的过程。 常用的解密方式有三种&#xff1a; 在线解密工具&#xff1a;格式比较好控制&#xff0c;但是有些在线工具兼容性不好&#xff0c;有时候无法解出&#xff0c;不知道是自己的密文密钥没找对&#xff0…...

Git 常见操作

目录 1.git stash 2.合并多个commit 3. git commit -amend (后悔药) 4.版本回退 5.merge和rebase 6.cherry pick 7.分支 8.alias 1.git stash git-stash操作_git stash 怎么增加更改内容-CSDN博客 2.合并多个commit 通过git bash工具交互式操作。 1.查询commit的c…...

Facebook接入说明

Facebook 原生 Messenger 聊天消息接入到一洽对话中 1、创建 Facebook 主页 进入 https://www.facebook.com/pages/create 页面根据提示创建主页&#xff08;如果已经有待用主页&#xff0c;可跳过&#xff09; 2、授权对话权限 1、向您的一洽负责人获取 Facebook 授权链接 2、…...

从C到C++语法过度1

从C到C语法过度1 文章目录 从C到C语法过度11. 字符串string2. 引用3. 类型转换3.1 新式转换 const_cast3.2 新式转换 static_cast 4. 关键字auto 1. 字符串string C语言从本质上来说&#xff0c;是没有字符串这种类型的&#xff0c;在C语言中如果要表达字符串&#xff0c;只能…...

音频剪辑软件少之又少好用

我们平时见到的图片以及视频编辑工具非常多&#xff0c;但是音频剪辑软件却是少之又少&#xff0c;更不用说有没有好用的&#xff0c;今天&#xff0c;给大家带来一款非常专业的音频剪辑软件&#xff0c;而且是会员喔。 软件简介 一款手机号登录即可以享受会员的超专业音频剪…...

机器学习:聚类算法及实战案例

本文目录&#xff1a; 一、聚类算法介绍二、分类&#xff08;一&#xff09;根据聚类颗粒度分类&#xff08;二&#xff09;根据实现方法分类 三、聚类流程四、K值的确定—肘部法&#xff08;一&#xff09;SSE-误差平方和&#xff08;二&#xff09;肘部法确定 K 值 五、代码重…...

分布式爬虫代理IP使用技巧

最近我们讨论的是分布式爬虫如何使用代理IP。在我们日常的分布式爬虫系统中&#xff0c;多个爬虫节点同时工作&#xff0c;每个节点都需要使用代理IP来避免被目标网站封禁。怎么解决代理IP问题显得尤为重要。 我们知道在分布式爬虫中使用代理IP是解决IP封禁、提高并发能力和实…...

FPGA定点和浮点数学运算-实例对比

在创建 RTL 示例时&#xff0c;经常使用 VHDL 2008 附带的 VHDL 包。它提供了出色的功能&#xff0c;可以高效地处理定点数&#xff0c;当然&#xff0c;它们也是可综合的。该包的一些优点包括&#xff1a; 有符号和无符号&#xff08;后缀和后缀&#xff09;定点向量。轻松将定…...

Python Excel 文件处理:openpyxl 与 pandas 库完全指南

在数据处理和分析过程中&#xff0c;Excel 文件是最常见的数据存储格式之一。Python 提供了多个库来处理 Excel 文件&#xff0c;其中 openpyxl 和 pandas 是最常用的两个库。它们各自有独特的优势&#xff0c;适用于不同的需求。本文将详细介绍如何使用这两个库来处理 Excel 文…...

python版若依框架开发:前端开发规范

python版若依框架开发 从0起步,扬帆起航。 python版若依部署代码生成指南,迅速落地CURD!项目结构解析前端开发规范文章目录 python版若依框架开发新增 view新增 api新增组件新增样式引⼊依赖新增 view 在 @/views文件下 创建对应的文件夹,一般性一个路由对应⼀个文件, 该…...

Oracle 用户名大小写控制

Oracle 用户名大小写控制 在 Oracle 数据库中&#xff0c;用户名的默认大小写行为和精确控制方法如下&#xff1a; 一 默认用户名大小写行为 不引用的用户名&#xff1a;自动转换为大写 CREATE USER white IDENTIFIED BY oracle123; -- 实际创建的用户名是 "WHITE"…...