14-C语言多文件编程
一、各种变量
在学习多文件编程之前,先要了解清楚各种变量的作用范围以及生命周期。
1.普通变量
1.1普通局部变量
- 定义形式:在复合语句{}里面定义的变量为普通局部变量;
- 作用范围:在复合语句{}里面有效;
- 生命周期:进入复合语句{}时开始,复合语句结束,局部变量被释放;
- 内存区域:栈区;
- 注意事项:
- 局部变量不初始化,内容不确定;
- 局部变量如果同名,遵循就近原则。
1.2普通全局变量
- 定义形式:在函数外定义的变量;
- 作用范围:当前源文件以及其他源文件都有效;
- 生命周期:进程开始到运行到程序结束后才释放;
- 内存区域:全局区;
- 注意事项:
- 全局变量不初始化,内容为 0;
- 如果其他源文件要使用全局变量,必须在使用处加 extern 声明;
- 全局变量和局部变量同名时,优先选择局部变量。
2.static修饰的变量
2.1静态局部变量
- 定义形式:在复合语句{}里面定义,加 static 修饰的变量;
- 作用范围:在复合语句{}里面有效;
- 生命周期:进程开始到运行到程序结束后才释放;
- 内存区域:全局区;
- 注意事项:
- 静态局部变量不初始化,内容为 0;
- 静态局部变量只会定义一次。
- 代码演示
void func()
{
static int num = 10;
num += 10;
printf("%d\n", num);
}
int main()
{
func();
func();
func();
func();
return 0;
}
- 运行结果
20
30
40
50
- 说明:根据以前的结果,函数内部的临时局部变量,函数调用结束就释放了,会打印4次20;但加了 static 修饰以后,函数调用结束并未释放,而是到整个进程结束才释放。
2.2静态全局变量
- 定义形式:全局变量前加 static 修饰;
- 作用范围:只在当前源文件有效;
- 生命周期:进程开始到运行到程序结束后才释放;
- 内存区域:全局区;
- 注意事项:
- 不初始化为 0;
- 只在当前源文件有效。
- 说明:和普通全局变量相比,静态全局变量只是作用范围发生了改变,目的是为了定义一个全局变量,只在当前文件有效,不希望其它文件去修改。
3.static修饰的函数
3.1全局函数
我们之前定义的函数默认为全局函数,只要在其他源文件加 extern 声明,就可以在其他源文件中使用。
其特性和全局变量差不多,不过函数是存储在代码区的。
3.2静态函数
定义函数时,函数返回值类型前加 static 修饰,为静态函数。
和全局函数相比,静态函数不能被其他源文件使用,只能在当前文件使用。
二、gcc编译
1.编译过程
gcc
编译过程主要分为:预处理、编译、汇编、链接四个步骤。
-
预处理:主要进行头文件包含、宏替换、条件编译、删除注释 (不作语法检查);
- 代码演示
#define NUM 100 #include <stdio.h> int main() { // 打印 NUM 的值 printf("NUM = %d", NUM); return 0; }
gcc -E test.c -o test.i // 预处理
- 预处理后的代码
// ......省略头文件里的代码 int main() { printf("NUM = %d", 100); return 0; }
- 说明:预处理后,头文件的代码会被拷贝到当前文件,宏被直接替换成了数值常量,注释也被删除了。
-
编译:将预处理好的.i文件,编译成汇编文件.s (作语法检查);
gcc -S test.i -o test.s // 编译
-
汇编:将汇编文件.s,生成 二进制文件.o;
gcc -c test.s -o test.o // 汇编
-
链接:将各个独立的二进制文件+库函数+启动代码,生成可执行文件。
gcc test.o -o test // 链接
- 上面的编译过程是具体的编译步骤,我们在实际编译的过程中,基本上一条命令就解决了:
gcc 源文件 -o 可执行文件名
;gcc 源文件
,这种编译方式,默认生成的可执行文件名为a.out
。
2.头文件包含
前面提到,头文件包含在预处理阶段,会将头文件里的代码拷贝到当前文件,头文件包含有两种方式:
#include <stdio.h>
#include "func.h"
#include <stdio.h>
:只从系统指定目录去找头文件,一般用于包含系统头文件;#include "func.h"
:先从当前目录查找头文件,如果找不到,才从系统指定目录找头文件。
3.宏定义
3.1无参的宏
前面编译过程中已经提到了,宏在预处理阶段会被替换成宏所对应的常量数据,而在编译阶段才会进行语法检查;如果代码中宏的使用发生了错误,是没法定位到错误语句的;同时,也可以通过宏来定义数组。
- 代码演示
#define NUM 10
int main()
{
int nums[NUM];
printf("%zu\n", sizeof(nums));
return 0;
}
- 运行结果
40
- 之前我们在定义数组的时候,[]里只能传整数常量,这里通过宏也可以定义成功,因为宏的预处理阶段就替换成了对应的整数常量了,编译阶段进行语法检查不会有问题。定义宏的时候,不要在末尾加分号
;
。 - 同时还可以通过另外一种方法,在
linux
终端输入编译命令的时候定义宏:
gcc test -D NUM=10 // NUM=10等号两边没有空格
注意:命令里定义了宏,在代码文件里就不要定义同名的宏了。宏只在当前文件有效。
3.2有参的宏
有参的宏又叫宏函数。
3.2.1宏函数的特性
- 代码演示
#define MUL(a,b) a*b
int main()
{
printf("%d\n", MUL(3, 5)); // 3 + 5
printf("%d\n", MUL(3 + 2, 5 + 1)); // 3 + 2 * 5 + 1
printf("%d\n", MUL2(3 + 2, 5 + 1)); // (3 + 2) * (5 + 1)
return 0;
}
- 运行结果
15
14
30
- 说明:
- 定义宏的时候,宏名一般用大写字母;
- 宏函数定义格式:
#define 宏名(参数1,参数2...) 表达式
; - 宏的参数在传递的时候是整体替换的,替换后再按照相应运算符的优先级进行计算,无法保证参数的完整性;
- 为了保证参数完整性,可以在参数外面加上()。
3.2.2宏函数与普通函数对比
- 宏函数:
- 宏函数在预处理阶段展开,有大量重复代码,占空间,但没有函数调用带来的出入栈的开销,用空间换时间;
- 宏的参数没有类型,不能保证参数的完整性;
- 宏没有作用域的限制,不能作为结构体或类(类在c++阶段学习)的成员。
- 普通函数:
- 普通函数代码只有一份,节约空间,但调用需要出入栈的开销,消耗时间,用时间换空间;
- 函数的参数,有类型,可以保证参数的完整性;
- 有作用域的限制,能作为结构体或类的成员。
3.3取消宏
可以通过以下方式取消已经定义的宏:
#undef N // 取消宏定义
4.条件编译
条件编译可以分为三种情况。
4.1条件编译之ifdef
- 语法格式
#ifdef 宏
语句1;
#else
语句2;
#endif
-
说明:
- 如果定义了相应的宏,则执行语句1,如果没有定义相应的宏,则执行语句2;
- 这里的条件编译和前面学习的 if 条件语句是有区别的,条件编译在预处理阶段会将不满足条件的代码删除,而 if 条件语句不会。
-
这种写法通常用来分割代码:
#define ADD
int main()
{
int a, b;
printf("请输入两个整数:");
scanf("%d %d",&a, &b);
#ifdef ADD
int ret = a + b;
#else
int ret = a - b;
#endif
printf("计算结果:%d\n", ret);
return 0;
}
- 运行结果:
- 当定义了宏 ADD 时:执行加法运算;或者也可以不在代码里定义宏,在编译时定义:
gcc test.c -D ADD
; - 当未定义宏 ADD 时:执行减法运算。
- 当定义了宏 ADD 时:执行加法运算;或者也可以不在代码里定义宏,在编译时定义:
4.2条件编译之ifndef
- 语法格式
#ifndef 宏
语句1;
#else
语句2;
#endif
- 说明:如果没有定义相应的宏,则执行语句1,如果定义了相应的宏,则执行语句2。
- 这种写法一般用于防止头文件包含,如下面案例:
头文件:a.h
#include "b.h"
头文件:b.h
int num = 100;
主函数:main.c
#include "a.h"
#include "b.h"
int main()
{
printf("%d\n", num);
return 0;
}
上面的代码会报错:原因是变量重复定义了,通过预处理就能看出,包含头文件以后,num变量定义了两次:
// #include "a.h"
// #include "b.h"
int num = 100;
// #include "b.h"
int num = 100;
int main()
{
printf("%d\n", num);
return 0;
}
- 解决办法:通过 ifndef 条件编译:
头文件:a.h
#ifndef __A_H__ // 两个下划线+头文件名大写(文件名的.用一个_代替)+两个下划线
#define __A_H__
#include "b.h"
#endif
头文件:b.h
#ifndef __B_H__
#define __B_H__
int num = 100;
#endif
主函数和上面一样,包含头文件以后:
#ifndef __A_H__
#define __A_H__
#include "b.h"
#endif
#ifndef __B_H__
#define __B_H__
int num = 100; // 因为上面已经包含了 b.h 头文件,所以这里条件不满足,不会再包含一遍了
#endif
int main()
{
printf("%d\n", num);
return 0;
}
- 上面是 linux 环境下,win 环境下在头文件写上如下一句代码即可:
#pragma once
4.3条件编译之if
- 语法格式
#if 宏
语句1;
#else
语句2;
#endif
- 说明:如果宏的值为真(非0),则执行语句1,如果宏的值为假(0),则执行语句2。
三、多文件编程
多文件编程,即相似的功能函数写在一个文件里,main.c
只负责整个项目的主体框架和各种功能函数的调用,函数的声明放到同名的头文件里,同时头文件里还主要放一下结构体类型,类等。
- 代码演示
功能文件:my_func.c
int my_add(int a, int b)
{
return a+b;
}
int my_sub(int a, int b)
{
return a-b;
}
int my_mul(int a, int b)
{
return a*b;
}
int my_div(int a, int b)
{
return a/b;
}
头文件:my_func.h
#ifndef __MY_FUNC_H__
#define __MY_FUNC_H__
extern int my_add(int a, int b);
extern int my_sub(int a, int b);
extern int my_mul(int a, int b);
extern int my_div(int a, int b);
#endif
主函数:main.c
#include <stdio.h>
#include "my_func.h"
int main()
{
printf("%d\n", my_add(100,20));
printf("%d\n", my_sub(100,20));
printf("%d\n", my_mul(100,20));
printf("%d\n", my_div(100,20));
return 0;
}
- 说明:编译的时候两个文件要一起编译,
gcc main.c my_func.c
。
四、静态库与动态库
1.静态库和动态库的区别
静态链接:
- 将静态库的所有函数都链接到可执行文件中,即使库删除了也不影响以及链接的文件的运行;
- 优点:对库的依赖不大;
- 缺点:
- 可执行文件大;
- 如果库发生变化,需要重新链接。
动态链接:
- 在链接阶段,仅仅建立和所需库函数的链接关系,在运行阶段才将所需的库函数包含在可执行文件中;
- 优点:生成可执行文件小;
- 缺点:对库的环境依赖大,如果库被删除了,就无法执行了。
- 我们之前的编译方式就是动态链接:
gcc test.c
,静态链接:gcc test.c --static
。
2.制作静态库
2.1静态库的制作流程
- 将需要制作库的源文件生成二进制文件.o;
gcc -c test.c -o test.o
- 使用二进制文件生成库;
ar rc libmylib.a test.o
注意:以 lib 开头,.a 结尾,库名称是mylib
库名前一定要加lib
。
2.2使用静态库
使用静态库用三种方法。
2.2.1将库放入项目目录
即将静态库和项目文件放入同一级目录下,编译格式:gcc 项目文件.c lib库文件.o
,如:
gcc main.c libmylib.a // linux命令
2.2.2将库放入指定目录
即将静态库放入一个其它创建好的目录,这个目录里也可以放自定义的头文件。
编译格式:gcc 项目文件.c -I+指定文件目录 -L+指定文件目录 -l库文件文件名
,如:
gcc main.h -I./fun -L./fun -lmylib
- 说明:
- -I 指头文件的路径,-L 指库的路径,-l 指库的名称,它们和相应目录、库文件之间没有空格;
- 如果将头文件放入其它目录,不通过 -L 指定,那么包含头文件的时候需要包含路径一起,比较麻烦,因此还是推荐这种方式。
2.2.3将库放入系统指定目录
将头文件和库文件移动到下面指定的文件路径下:
-
系统默认的头文件路径:
/usr/include
; -
系统默认的库的路径:
/usr/lib
。
包含头文件的时候,直接和包含系统头文件一样就可以,链接库也只需要加上-l库名称就行
。
3.制作动态库
3.1动态库的制作
制作动态库的格式:gcc -shared 用于制作动态库的文件.c -0 lib动态库名.s0
,如:
gcc -shared test.c -o libmylib.so
3.2动态库的使用
和静态库一样,使用动态库也有三种方法。
3.2.1动态库在项目目录
即将动态库和项目文件放入同一级目录下,编译格式:gcc 项目文件.c lib库文件.so
。
LD_LIBRARY_PATH
是 linux
系统中的一个环境变量,用于指定动态链接的搜索路径,这里需要加上项目路径,如:
gcc main.c libmylib.so
export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
- 说明:
./
是当前路径,即项目路径;:
用于分割不同路径;$LD_LIBRARY_PATH
取出原本的路径。
3.2.2将动态库放入指定路径
和静态库操作一样,只不过这里也还需要修改LD_LIBRARY_PATH
环境变量,如:
gcc main.h -I./fun -L./fun -lmylib
export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
3.2.3将动态库放入系统目录
和静态库一样,直接将动态库移动到系统指定目录就行,然后编译格式也一样:gcc 项目文件.c -l动态库名
gcc main.c -lmylb
- 说明:
- 如果静态库和动态库同名,默认是使用动态链接,使用静态库需要加
-static
; - 放到指定系统目录,就不需要配置环境变量了,会从系统指定的默认路径查找动态库。