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

14-C语言多文件编程

一、各种变量

在学习多文件编程之前,先要了解清楚各种变量的作用范围以及生命周期。

1.普通变量

1.1普通局部变量

  1. 定义形式:在复合语句{}里面定义的变量为普通局部变量;
  2. 作用范围:在复合语句{}里面有效;
  3. 生命周期:进入复合语句{}时开始,复合语句结束,局部变量被释放;
  4. 内存区域:栈区;
  5. 注意事项:
    • 局部变量不初始化,内容不确定;
    • 局部变量如果同名,遵循就近原则。

1.2普通全局变量

  1. 定义形式:在函数外定义的变量;
  2. 作用范围:当前源文件以及其他源文件都有效;
  3. 生命周期:进程开始到运行到程序结束后才释放;
  4. 内存区域:全局区;
  5. 注意事项:
    • 全局变量不初始化,内容为 0;
    • 如果其他源文件要使用全局变量,必须在使用处加 extern 声明;
    • 全局变量和局部变量同名时,优先选择局部变量。

2.static修饰的变量

2.1静态局部变量

  1. 定义形式:在复合语句{}里面定义,加 static 修饰的变量;
  2. 作用范围:在复合语句{}里面有效;
  3. 生命周期:进程开始到运行到程序结束后才释放;
  4. 内存区域:全局区;
  5. 注意事项:
    • 静态局部变量不初始化,内容为 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静态全局变量

  1. 定义形式:全局变量前加 static 修饰;
  2. 作用范围:只在当前源文件有效;
  3. 生命周期:进程开始到运行到程序结束后才释放;
  4. 内存区域:全局区;
  5. 注意事项:
    • 不初始化为 0;
    • 只在当前源文件有效。
  • 说明:和普通全局变量相比,静态全局变量只是作用范围发生了改变,目的是为了定义一个全局变量,只在当前文件有效,不希望其它文件去修改。

3.static修饰的函数

3.1全局函数

我们之前定义的函数默认为全局函数,只要在其他源文件加 extern 声明,就可以在其他源文件中使用。

其特性和全局变量差不多,不过函数是存储在代码区的。

3.2静态函数

定义函数时,函数返回值类型前加 static 修饰,为静态函数。

和全局函数相比,静态函数不能被其他源文件使用,只能在当前文件使用。

二、gcc编译

1.编译过程

gcc编译过程主要分为:预处理、编译、汇编、链接四个步骤。

  1. 预处理:主要进行头文件包含、宏替换、条件编译、删除注释 (不作语法检查);

    • 代码演示
    #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;
    }
    
    • 说明:预处理后,头文件的代码会被拷贝到当前文件,宏被直接替换成了数值常量,注释也被删除了。
  2. 编译:将预处理好的.i文件,编译成汇编文件.s (作语法检查);

    gcc -S test.i -o test.s // 编译
    
  3. 汇编:将汇编文件.s,生成 二进制文件.o;

    gcc -c test.s -o test.o // 汇编
    
  4. 链接:将各个独立的二进制文件+库函数+启动代码,生成可执行文件。

    gcc test.o -o test // 链接
    
  • 上面的编译过程是具体的编译步骤,我们在实际编译的过程中,基本上一条命令就解决了:
    1. gcc 源文件 -o 可执行文件名
    2. gcc 源文件 ,这种编译方式,默认生成的可执行文件名为 a.out

2.头文件包含

前面提到,头文件包含在预处理阶段,会将头文件里的代码拷贝到当前文件,头文件包含有两种方式:

#include <stdio.h>
#include "func.h"
  1. #include <stdio.h>:只从系统指定目录去找头文件,一般用于包含系统头文件;
  2. #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
  • 说明:
    1. 定义宏的时候,宏名一般用大写字母;
    2. 宏函数定义格式:#define 宏名(参数1,参数2...) 表达式
    3. 宏的参数在传递的时候是整体替换的,替换后再按照相应运算符的优先级进行计算,无法保证参数的完整性;
    4. 为了保证参数完整性,可以在参数外面加上()。
3.2.2宏函数与普通函数对比
  • 宏函数:
    1. 宏函数在预处理阶段展开,有大量重复代码,占空间,但没有函数调用带来的出入栈的开销,用空间换时间;
    2. 宏的参数没有类型,不能保证参数的完整性;
    3. 宏没有作用域的限制,不能作为结构体或类(类在c++阶段学习)的成员。
  • 普通函数:
    1. 普通函数代码只有一份,节约空间,但调用需要出入栈的开销,消耗时间,用时间换空间;
    2. 函数的参数,有类型,可以保证参数的完整性;
    3. 有作用域的限制,能作为结构体或类的成员。

3.3取消宏

可以通过以下方式取消已经定义的宏:

#undef N // 取消宏定义

4.条件编译

条件编译可以分为三种情况。

4.1条件编译之ifdef

  • 语法格式
#ifdef 
	语句1#else
    语句2#endif        
  • 说明:

    1. 如果定义了相应的宏,则执行语句1,如果没有定义相应的宏,则执行语句2;
    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;
}
  • 运行结果:
    1. 当定义了宏 ADD 时:执行加法运算;或者也可以不在代码里定义宏,在编译时定义:gcc test.c -D ADD
    2. 当未定义宏 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.静态库和动态库的区别

静态链接:

  1. 将静态库的所有函数都链接到可执行文件中,即使库删除了也不影响以及链接的文件的运行;
  2. 优点:对库的依赖不大;
  3. 缺点:
    • 可执行文件大;
    • 如果库发生变化,需要重新链接。

动态链接:

  1. 在链接阶段,仅仅建立和所需库函数的链接关系,在运行阶段才将所需的库函数包含在可执行文件中;
  2. 优点:生成可执行文件小;
  3. 缺点:对库的环境依赖大,如果库被删除了,就无法执行了。
  • 我们之前的编译方式就是动态链接:gcc test.c,静态链接:gcc test.c --static

2.制作静态库

2.1静态库的制作流程

  1. 将需要制作库的源文件生成二进制文件.o;
    • gcc -c test.c -o test.o
  2. 使用二进制文件生成库;
    • 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
  • 说明:
    1. -I 指头文件的路径,-L 指库的路径,-l 指库的名称,它们和相应目录、库文件之间没有空格;
    2. 如果将头文件放入其它目录,不通过 -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_PATHlinux系统中的一个环境变量,用于指定动态链接的搜索路径,这里需要加上项目路径,如:

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
  • 放到指定系统目录,就不需要配置环境变量了,会从系统指定的默认路径查找动态库。

http://www.kler.cn/a/450538.html

相关文章:

  • c++ [spdlog 配置与使用]
  • 大语言模型学习工具及资源总结和落地应用
  • Rust 在前端基建中的使用
  • 谷歌浏览器的网络连接问题解决方案
  • Spring常见面试题总结
  • JavaScript中的Set、Map、WeakSet和WeakMap
  • CES Asia(赛逸展)有哪些科技创新产品?
  • 静态路由配置包括DHCP
  • 华为ensp--BGP路由反射器
  • 数据库操作【JDBC HIbernate Mybatis】
  • C#变量作用域详解
  • uni-app 统一请求处理 请求拦截器 响应拦截器 请求封装
  • 熊军出席ACDU·中国行南京站,详解SQL管理之道
  • 大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
  • OpenGL 笔记(2)
  • 家用无线路由器的 2.4GHz 和 5GHz
  • flink+kafka实现流数据处理学习
  • VSCode 性能优化指南:提高编码效率,减少资源占用
  • [机器学习]sklearn入门指南(1)
  • 量子模拟器市场的增长潜力与未来趋势
  • Windows内核开发环境配置
  • 51c大模型~合集95
  • 基于微信小程序的乡村政务服务系统springboot+论文源码调试讲解
  • 2024 写写 歇歇
  • 构建一个rust生产应用读书笔记7-确认邮件2
  • 机器人加装电主轴【铣削、钻孔、打磨、去毛刺】更高效