trinity C programming Slide 4
Slide3 比较简单跳过
1. 全局变量
避免使用全局变量,建议把任何变量的范围限制在尽可能小的范围内
2. inline关键字
C语言中,inline关键字是用于提示编译器将一个函数的调用直接替换为该函数的代码主体,从而避免常规函数调用时的开销(如参数传递、栈帧管理等)。这是为了提高函数调用的效率,特别是当函数很短或经常被调用时,inline可以减少开销并提高程序的执行速度。下面是对C语言中inline关键字的详细介绍:
2.1. inline的基本概念
inline是C语言的一种优化机制,它告诉编译器可以将函数“内联”(inline)。内联函数并不是通过常规的函数调用机制来执行的,而是通过直接将函数的代码插入到调用点来避免调用函数的开销。
示例:
inline int add(int a, int b) {
return a + b;
}
在这个例子中,add函数被标记为inline。如果在程序中调用这个函数,编译器可能会将这个函数的代码直接嵌入调用点,而不是生成一个实际的函数调用。
2.2. 内联函数的优点
- 减少函数调用开销:普通函数调用需要保存上下文(例如函数的局部变量和调用地址)、参数传递、跳转到函数代码并在返回时恢复上下文。这些开销对小函数来说是非常显著的。使用
inline可以避免这些开销。 - 提高执行效率:对于简单的、短小的函数,内联可以减少程序运行时的开销,并提高性能,特别是在频繁调用的情况下。
- 代码优化机会:编译器可以更好地进行优化。例如,当编译器将函数内联后,它可以消除一些不必要的操作,比如死代码消除、常量折叠等。
2.3. 内联函数的缺点
- 代码膨胀(Code Bloat) :如果内联函数非常大,且在多个地方调用,会导致函数体的代码被重复插入多个调用点,增大可执行文件的大小。
- 调试困难:内联函数并没有实际的函数调用栈信息,因此在使用调试工具(如gdb)时,跟踪和调试这些内联函数变得更加困难。
- 递归函数不能内联:递归函数由于需要重复调用自己,编译器无法将其内联化。
2.4. inline的实现机制与编译器行为
使用inline只是编译器的建议,它不保证函数一定会被内联。现代编译器会根据函数的复杂性、代码的大小、调用次数等因素决定是否真正将函数内联化。编译器可能会忽略inline关键字,并且有时即使没有显式声明inline,编译器也可能自行决定内联一些非常短小的函数。
编译器的决定基于以下因素:
- 函数体的大小:小型的、简单的函数更容易被内联化。
- 调用频率:频繁调用的函数更有可能被内联。
- 编译器优化级别:在更高的优化级别(如
-O2或-O3),编译器更倾向于内联函数。
2.5. 使用inline的限制与注意事项
-
inline函数的定义:inline函数通常需要与函数定义在同一个文件中,否则编译器无法在调用点内联函数。为了解决这一问题,可以将inline函数的定义放在头文件中,但这样做可能会导致重复定义错误。 -
与
extern的结合:为了确保inline函数在多个文件中可以正常使用,可以结合extern关键字。在使用时,如果想将inline函数的定义放在头文件中,同时又希望在多个文件中使用,可以这样做:在头文件中:
inline int add(int a, int b) { return a + b; }在源文件中:
extern inline int add(int a, int b);
2.6. C99与C11中的inline规则
C语言在C99标准中引入了inline关键字,并且在C99及之后的标准(如C11)中,inline关键字的使用有了更为明确的定义。
-
C99中的
inline规则:inline:表示该函数的实现可以被内联,但在没有其他定义时,仍需要一个外部定义。extern inline:告诉编译器该函数是内联的,但也可以有一个外部定义,用于非内联调用。static inline:表示该函数只在当前文件中内联,不能从其他文件中访问。
2.7. 示例说明
-
普通
inline函数:inline int square(int x) { return x * x; }使用这个函数时,编译器会尝试将
square函数的代码直接嵌入到调用点。 -
static inline函数:static inline int multiply(int a, int b) { return a * b; }这个函数只能在当前文件中使用,不能在其他文件中引用。编译器会内联这个函数,并且不生成函数的外部链接。
-
extern inline函数:extern inline int add(int a, int b) { return a + b; }这个函数提示编译器如果可以,将其内联化。但在需要时,也可以提供非内联的外部定义。
2.8. 何时使用inline?
- 当函数很短且调用频繁时,使用
inline可能带来显著的性能提升。 - 避免将大型函数标记为
inline,因为这会导致代码膨胀,并且可能降低程序的性能。 inline适合在性能关键的代码中使用,但要谨慎,确保不会因此导致可执行文件过大。
2.9. 现代编译器的优化能力
值得注意的是,现代编译器已经非常智能,即使不使用inline关键字,它们也会根据代码的情况自动决定是否进行内联。因此,在许多情况下,编译器优化选项(如-O2或-O3)已经足够处理内联需求,开发者不一定需要手动指定inline。
总结:
inline关键字用于提示编译器将函数直接嵌入到调用点,以提高执行效率。- 过度使用
inline可能导致代码膨胀,增加可执行文件的大小。 - 使用
inline时,最好应用于小型且频繁调用的函数。 - 现代编译器的优化能力很强,在大多数情况下能自动决定是否内联函数,因此不一定需要手动使用
inline。
3. extern
extern 关键字用于声明变量或函数的外部链接性。它表示该变量或函数是定义在当前文件之外的其他地方,并且可以在当前文件中使用。
3.1 主要用途:
-
变量的外部声明:
-
如果一个变量在另一个文件中定义,而你想在当前文件中使用它,可以使用
extern来声明这个变量。 -
例如:
// file1.c #include<stdio.h> int count = 10; // 变量定义 // file2.c #include<stdio.h> extern int count; // 变量声明 void someFunction() { printf("%d", count); // 使用变量 } int main() { someFunction(); return 0; } -
在这个例子中,
count在file1.c中定义,而在file2.c中使用了extern来声明它,从而使它可以被引用。
-
-
函数的外部声明:
-
函数在C语言中默认具有外部链接性,因此即使不加
extern关键字,函数也可以被其他文件引用。不过,使用extern明确声明函数有助于代码的可读性。 -
例如:
// file1.c void myFunction() { // 函数实现 } // file2.c extern void myFunction(); // 函数声明 void anotherFunction() { myFunction(); // 调用定义在 file1.c 的函数 }
-
3.2 特点和作用:
-
声明而不是定义:
extern只是声明变量的存在,而不是定义或分配内存空间。实际的定义必须在另一个地方。
-
多文件共享:
extern允许在多个文件中共享变量或函数,从而实现模块化编程。
-
全局变量的访问:
- 当一个全局变量需要在多个文件之间共享时,可以使用
extern进行声明,但该变量必须在一个文件中有且仅有一次定义。
- 当一个全局变量需要在多个文件之间共享时,可以使用
例子:
// main.c
#include <stdio.h>
extern int globalVariable; // 声明外部变量
int main() {
printf("Global Variable: %d\n", globalVariable);
return 0;
}
// other.c
int globalVariable = 42; // 定义全局变量
在上面的例子中,globalVariable 定义在 other.c 中,而在 main.c 中通过 extern 关键字来声明它,从而可以在 main.c 中访问。
注意事项:
- 如果在
extern声明之后没有对应的变量定义,编译器会报错,提示找不到定义。 extern关键字常用于多文件项目中,帮助管理全局变量和函数的链接和共享。
总结来说,extern 关键字是C语言中用来声明一个变量或函数的外部可访问性,使得可以在多个源文件中共享它们。
4. -lm标志
在编写C语言程序时,数学库(math library)提供了各种常用的数学函数,如三角函数、对数函数、指数函数等。-lm 选项用于在编译时链接数学库,以使这些数学函数能够在程序中正常使用。
4.1 数学库概述
- 数学库在C标准库中通常被称为
libm,其中包含了像sqrt()、sin()、cos()、log()等函数。 - 由于这些数学函数不是默认包含在基本C标准库中,因此需要显式地将数学库与程序链接。
4.2 使用 -lm 链接数学库
- 当编写包含数学函数的C程序时,例如
sqrt()或sin(),需要在编译时使用-lm选项来链接数学库。 -l表示链接一个库,m表示libm(数学库),即-lm命令告诉编译器要链接数学库。
4.3 编译示例
假设有一个程序文件 main.c,内容如下:
#include <stdio.h>
#include <math.h>
int main() {
double value = 16.0;
double result = sqrt(value);
printf("Square root of %.2f is %.2f\n", value, result);
return 0;
}
在上面的代码中,使用了数学库中的 sqrt() 函数。如果我们要编译这个程序,需要执行以下命令:
gcc main.c -o main -lm
解释:
gcc:GNU Compiler Collection,用于编译C程序。main.c:待编译的源文件。-o main:指定输出的可执行文件名为main。-lm:链接数学库libm,以确保sqrt()函数正确链接。
如果不加 -lm 选项,编译器会报错,提示未定义引用(undefined reference)到数学库中的函数。
需要链接数学库的常用函数
一些常用的数学库函数包括:
sqrt(double x):返回x的平方根。sin(double x)、cos(double x)、tan(double x):返回x的正弦、余弦和正切。log(double x):返回x的自然对数。exp(double x):返回e^x。pow(double base, double exponent):返回base的exponent次幂。
注意事项
- 需要包含
<math.h>头文件:在使用数学库函数时,必须包含头文件#include <math.h>。 - 链接顺序:在使用
gcc编译时,-lm必须放在源文件之后,因为链接器处理命令行参数的顺序。如果把-lm放在前面,可能会找不到符号的定义。