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

目录

[9.1 函数的概念](#9.1 函数的概念)

[9.2 库函数](#9.2 库函数)

[9.2.1 标准库与库函数](#9.2.1 标准库与库函数)

示例:常见库函数

[9.2.2 标准库与头文件的关系](#9.2.2 标准库与头文件的关系)

参考资料和学习工具

如何使用库函数

​编辑

[9.3 ⾃定义函数](#9.3 ⾃定义函数)

[9.3.1 函数的语法形式](#9.3.1 函数的语法形式)

9.3.2函数的举例

[9.4 实参与形参](#9.4 实参与形参)

[9.4.1 什么是实参?](#9.4.1 什么是实参?)

[9.4.2 什么是形参?](#9.4.2 什么是形参?)

[9.4.3 实参和形参的关系](#9.4.3 实参和形参的关系)

[9.4.4 实参与形参的传递方式](#9.4.4 实参与形参的传递方式)

[9.5 return语句](#9.5 return语句)

[9.5.1 return 后可以是数值或表达式](#9.5.1 return 后可以是数值或表达式)

[9.5.2 return 语句可以没有返回值](#9.5.2 return 语句可以没有返回值)

[9.5.3 返回值类型自动转换](#9.5.3 返回值类型自动转换)

[9.5.4 return 语句结束函数执行](#9.5.4 return 语句结束函数执行)

[9.5.5 分支语句中的 return 要确保每条路径都返回](#9.5.5 分支语句中的 return 要确保每条路径都返回)

[9.5.6. 总结](#9.5.6. 总结)

[9.6 数组做函数参数](#9.6 数组做函数参数)

[9.6.1 传递数组的注意事项](#9.6.1 传递数组的注意事项)

[9.6.2 设计 set_arr 和 print_arr 函数](#9.6.2 设计 set_arr 和 print_arr 函数)

[9.7 嵌套调⽤和链式访问](#9.7 嵌套调⽤和链式访问)

[9.7.1 嵌套调用](#9.7.1 嵌套调用)

[9.7.2 链式访问](#9.7.2 链式访问)

[9.8 函数的声明和定义](#9.8 函数的声明和定义)

[9.8.1 单文件中的函数声明和定义](#9.8.1 单文件中的函数声明和定义)

示例:判断闰年

当函数定义在调用之后

函数声明的使用

总结

[9.8.2 多个文件](#9.8.2 多个文件)

代码模块化的重要性

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

[文件 1:add.c(源文件)](#文件 1:add.c(源文件))

[文件 2:add.h(头文件)](#文件 2:add.h(头文件))

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

编译与链接

[9.8.3 static 和 extern](#9.8.3 static 和 extern)

作用域和生命周期

[9.8.3.1 static 修饰局部变量](#9.8.3.1 static 修饰局部变量)

[代码示例 1:未使用 static 修饰的局部变量](#代码示例 1:未使用 static 修饰的局部变量)

[代码示例 2:使用 static 修饰的局部变量](#代码示例 2:使用 static 修饰的局部变量)

[9.8.3.2 static 修饰全局变量](#9.8.3.2 static 修饰全局变量)

[代码示例 1:未使用 static 修饰的全局变量](#代码示例 1:未使用 static 修饰的全局变量)

[代码示例 2:使用 static 修饰的全局变量](#代码示例 2:使用 static 修饰的全局变量)

[9.8.3.3 static 修饰函数](#9.8.3.3 static 修饰函数)

[代码示例 1:未使用 static 修饰的函数](#代码示例 1:未使用 static 修饰的函数)

[代码示例 2:使用 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 标准库与头文件的关系

参考资料和学习工具

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

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

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

2.字符串处理相关的库函数声明在 string.h
库函数相关头⽂件:https://zh.cppreference.com/w/c/header

如何使用库函数

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

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

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

实践

cs 复制代码
#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 函数的语法形式

cs 复制代码
ret_type fun_name(形式参数)
{
}

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

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

9.3.2函数的举例

写⼀个加法函数,完成2个整型变量的加法操作

cs 复制代码
#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个整型类型的参数,函数计算的结果 也是整型。
所以我们根据上述的分析写出函数:

cs 复制代码
#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 什么是实参?

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

让我们看下面的例子:

cs 复制代码
#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 什么是形参?

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

继续看上面的代码:

cs 复制代码
int Add(int x, int y) {
    int z = x + y;
    return z;
}

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

9.4.3 实参和形参的关系

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

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

cs 复制代码
#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语言中,函数的参数传递通常是值传递。也就是说,函数接收到的是实参的值,而不是实参本身。这种传递方式确保了实参的安全性,因为无论函数内部如何修改形参,实参的值都不会受到影响。

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

cs 复制代码
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 语句可以返回一个具体的数值 ,也可以返回一个表达式的结果。如果是表达式,首先会计算该表达式的值,然后将结果作为函数的返回值。例如:

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

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

9.5.2 return 语句可以没有返回值

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

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

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

9.5.3 返回值类型自动转换

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

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

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

9.5.4 return 语句结束函数执行

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

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

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

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

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

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

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

正确的写法是

cs 复制代码
int Max(int a, int b) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}

或者更加简洁的写法

cs 复制代码
int Max(int a, int b) {
    return (a > b) ? a : b;
}

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

9.5.6. 总结

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

9.6 数组做函数参数

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

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

cs 复制代码
#include <stdio.h>

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

9.6.1 传递数组的注意事项

为了能够操作数组,我们需要将数组作为参数传递给 set_arr 函数,同时为了遍历数组,还需要知道数组的元素个数。因此,我们需要向 set_arr 函数传递两个参数:一个是数组本身,另一个是数组的元素个数。对于 print_arr 函数,也是同样的道理。

cs 复制代码
#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;
}

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

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

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

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

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

打印数组内容的函数

cs 复制代码
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() 判断是否为闰年,再根据月份计算天数。

示例代码如下:

cs 复制代码
#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 链式访问

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

例如:

cs 复制代码
#include <stdio.h>

int main() {
    int len = strlen("abcdef");  // 计算字符串长度
    printf("%d\n", len);  // 打印长度
    return 0;
}

如果将 strlen() 的返回值直接作为 printf() 的参数,代码可以进一步简化,变为链式访问的形式:

cs 复制代码
#include <stdio.h>

int main() {
    printf("%d\n", strlen("abcdef"));  // 链式访问
    return 0;
}
复制代码
链式访问中的有趣现像来看一个有趣的例子:
cs 复制代码
#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 单文件中的函数声明和定义

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

示例:判断闰年

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

cs 复制代码
#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函数的后面,如下

cs 复制代码
#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函数调用时,并没有找到其定义,可能会抛出警告甚至错误提示。为了解决这个问题,我们需要在函数调用之前声明函数,这样编译器就能提前知道函数的存在。

函数声明的使用

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

cs 复制代码
#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(源文件)
cs 复制代码
// 函数的定义
int Add(int x, int y) {
    return x + y;
}

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

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

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

文件 3:test.c(主程序文件)
cs 复制代码
#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 编译器为例,编译命令如下:

cs 复制代码
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文件,实现相应的代码功能

cs 复制代码
#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 修饰的局部变量
cs 复制代码
#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 修饰的局部变量
cs 复制代码
#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 文件:

cs 复制代码
int g_val = 2018; // 全局变量

test.c 文件:

cs 复制代码
#include <stdio.h>

extern int g_val; // 声明外部变量

int main() {
    printf("%d\n", g_val); // 输出2018
    return 0;
}

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

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

add.c 文件:

cs 复制代码
static int g_val = 2018; // 静态全局变量

test.c 文件:

cs 复制代码
#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 文件:

cs 复制代码
int Add(int x, int y) {
    return x + y;
}

test.c 文件:

cs 复制代码
#include <stdio.h>

extern int Add(int x, int y); // 声明外部函数

int main() {
    printf("%d\n", Add(2, 3)); // 输出5
    return 0;
}

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

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

add.c 文件:

cs 复制代码
static int Add(int x, int y) {
    return x + y;
}

test.c 文件:

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

相关推荐
向宇it1 小时前
【从零开始入门unity游戏开发之——unity篇01】unity6基础入门开篇——游戏引擎是什么、主流的游戏引擎、为什么选择Unity
开发语言·unity·c#·游戏引擎
Schwertlilien1 小时前
图像处理-Ch5-图像复原与重建
c语言·开发语言·机器学习
仰望大佬0071 小时前
Avalonia实例实战五:Carousel自动轮播图
数据库·microsoft·c#
糖朝1 小时前
c#读取json
c#·json
程序员buddha2 小时前
C语言从入门到放弃教程
c语言·开发语言
向宇it6 小时前
【从零开始入门unity游戏开发之——C#篇26】C#面向对象动态多态——接口(Interface)、接口里氏替换原则、密封方法(`sealed` )
java·开发语言·unity·c#·游戏引擎·里氏替换原则
AAA.建材批发刘哥6 小时前
Linux快速入门-Linux文件系统管理
linux·运维·服务器·c语言·学习方法
Kisorge8 小时前
【C语言】指针数组、数组指针、函数指针、指针函数、函数指针数组、回调函数
c语言·开发语言
Java Fans10 小时前
C# 中串口读取问题及解决方案
开发语言·c#
盛派网络小助手10 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#