目录
- 前言
- 软件工程的基本原则
- 程序的模块化开发和代码重用技术
- 开发自己的头文件
- 定义实现自己的头文件
- 编写实现文件(源文件)
- 编译代码
- 链接目标文件到可执行文件
- 实现类似标准库效果的几种方法
- 实际使用的开发方法
- 头文件库
- 尝试自动链接静态库(好像每次都还是要指定库的名字)
- 生成静态库
- 配置环境变量(可选)
- 编译链接程序
- 使用命令行链接静态库并编译
- 配置构建任务task.json文件编译并链接
- 配置编译器设置
- 对应文件
- 尝试自动链接动态库
- 配置环境变量(非常重要)
- 在用户环境变量下的Path变量里放入自己的库的路径
- 系统环境变量也是如此操作
- LD_LIBRARY_PATH
- 生成动态库文件
- 配置构建任务task.json
- 效果
- 待续
前言
温馨提示:前面都是一些介绍性的内容,为的是今后可以继续拓展类似的技术,具体技术可以看第四节开发自己的头文件
在了解GUI的时候发现好像可以使用链接库
的技术实现自己定义、实现自己的库函数
,并且在代码中包含头文件即可。于是.开始了解这方面的内容。发现静、动态链接库属于程序的模块化开发和代码重用
的内容,后者又属于软件工程的基本原则
。
软件工程的基本原则
软件工程是一门应用工程原则、方法和技术来开发、运行和维护软件的学科。它包括一系列的原则和实践,旨在提高软件的质量和生产效率。以下是一些软件工程的基本原则:
-
模块化:将软件分解成独立的、可管理的模块,以便于开发、测试和维护。
-
抽象
:通过抽象隐藏复杂性,只暴露必要的接口。 -
信息隐藏:隐藏模块内部的实现细节,只公开必要的接口。
-
可维护性:设计易于理解和修改的软件。
-
可测试性:确保软件可以被有效地测试。
-
重用性
:尽可能重用已有的代码和组件,以减少开发时间和成本。 -
配置管理:管理软件的版本和变更,确保软件的一致性和可追溯性。
-
持续集成:频繁地将代码集成到主分支,以尽早发现和解决问题。
-
风险管理:识别、评估和缓解项目风险。
-
质量保证:确保软件满足质量标准和用户需求。
-
用户参与:在整个开发过程中,让用户参与决策和测试。
-
迭代开发:通过多次迭代逐步完善软件,每次迭代都包括需求分析、设计、编码、测试和部署。
-
文档化:编写清晰、完整的文档,以支持软件的开发、使用和维护。
-
性能优化:确保软件在性能上满足用户的需求。
-
安全性:设计安全的软件,以防止未授权访问和数据泄露。
-
可扩展性:设计能够适应未来需求变化的软件。
程序的模块化开发和代码重用技术
这篇文章只简要介绍一下动静态库的概念 ,主要还是实现功能,具体的功能作用后续会写
。
动态链接库
(Dynamic LInk LIbrary)
是一种微软公司在Windows操作系统中实现共享函数库的方式。DLL文件中包含了可由多个程序同时使用的代码和数据,这些程序不必包含实际的代码,而是在运行时调用DLL中包含的代码。
代码共享
:多个程序可以共享同一个DLL中的代码
,这样可以减少内存占用和磁盘空间。
便于维护和更新
:如果DLL中的代码需要更新或修复,只需更新DLL文件,所有使用该DLL的程序都可以获得更新,而不需要重新编译或更新每个程序
。
性能优化:DLL可以被设计为在需要时才加载,这样可以减少程序的启动时间。
本地化:DLL可以用于本地化应用程序,例如,可以有多个语言版本的DLL,应用程序根据用户的地区设置加载相应的DLL
。
Linux系统中
在Linux系统中,动态链接库通常以.so(Shared Object)作为扩展名这些库在程序运行时被动态加载,允许多个程序共享同一库文件,节省内存和磁盘空间。Linux使用ldconfig工具来管理动态链接库的搜索路径,并通过/etc/ld.so.conf和LD_LIBRARY_PATH环境变量来指定库文件的位置。可以使用gcc命令配合-shared和-fPIC选项来创建动态链接库。
Windows
Windows系统中的动态链接库以.dll(Dynamic Link Library)作为文件扩展名。Windows使用PE(Portable Executable)作为其可执行文件和动态链接库的格式。在Windows中,动态链接库的创建通常通过Microsoft Visual Studio进行,使用/LD选项来编译DLL。
静态链接库
(Static Libraries):
静态链接库在编译时将库代码直接集成到可执行文件中,这可以减少运行时的依赖,但会增加可执行文件的大小。
静态库(.a 文件在Unix-like系统中,或 .lib 文件在Windows系统中)
- 对象文件(Object Files):
对象文件包含编译后但未链接的代码。它们可以被重复使用,以避免重新编译源代码。
框架
(Frameworks):
在某些操作系统(如macOS)中,框架是一种特殊的库,它包含了代码、资源和头文件,用于支持应用程序的特定功能。
服务化
(Services):
将功能封装为服务,通过网络调用。这种模式允许跨平台和跨语言的服务重用。
微服务架构
(Microservices):
将应用程序分解为一组小服务,每个服务实现特定功能,并通过API进行通信。
- 插件系统(Plugin Systems):
允许用户或开发者通过插件来扩展应用程序的功能,插件可以在运行时动态加载。
- 组件化(Componentization):
将软件系统分解为可重用的组件,这些组件可以通过不同的方式组合来构建应用程序。
面向对象编程
(Object-Oriented Programming, OOP):
通过类和对象的概念,实现代码的封装、继承和多态,从而提高代码的重用性。
- 设计模式(Design Patterns):
设计模式是解决特定问题的通用方法,它们提供了一种模板,可以用来构建模块化和可重用的设计。
- 代码生成工具(Code Generation Tools):
这些工具可以根据模板或规则自动生成代码,减少手动编写重复代码的工作。
模板
(Templates):
在编程语言中,模板是一种用于创建通用数据结构或算法的方式,它们可以在不同的上下文中重复使用。
- 函数式编程(Functional Programming):
通过不可变数据和高阶函数,函数式编程鼓励代码的重用和模块化。
- 依赖注入(Dependency Injection):
一种设计模式,允许将模块间的依赖关系明确地传递给组件,而不是让组件自行查找或创建它们需要的资源。
- 软件包管理器(Package Managers):
如npm、Maven、pip等,它们允许开发者共享和重用代码库,同时管理项目依赖。
代码片段和代码库
(Code Snippets and Repositories):
开发者可以创建和分享代码片段,其他人可以在需要时复制和粘贴这些代码。
开发自己的头文件
头文件中通常包含了函数声明、宏定义、类型定义、模板声明等
,它们可以被多个源文件包含(#include),从而使得在不同的源文件中可以使用相同的函数、类型和宏。
头文件的使用是C语言模块化编程的基础,它们使得代码更加组织化和可重用
。通过将函数和宏的定义放在头文件中,可以在不同的源文件中多次使用而无需重复编写相同的代码。这是一种代码重用的简单而有效的方法,也是上面提到的“组件化”和“面向对象编程”
中封装原则的体现。
定义实现自己的头文件
我们的目的是能够像使用标准库那样可以通过包含头文件的方式就可以使用里面的库函数,而且里面的功能还要我们自己定制。
首先我们创建一个头文件并编写内容,在头文件中,声明你想要提供的函数、定义宏、声明类型等
。
#ifndef CONVERT_H
#define CONVERT_H
void printBinary(unsigned int num);
#endif
编写实现文件(源文件)
#include "convert.h"
#include <stdio.h>
void printBinary(unsigned int num) {
if (num == 0) {
printf("0");
return ;
}
if (num > 0) {
printBinary(num >> 1);
printf("%d", num&1);//取最低位打印
}
}
编译代码
gcc -c convert.c -o convert.o
这样做完之后,在其他源文件中就可以包含创建的头文件,并通过编译器将其链接到程序中从而升成可执行文件 main.o
链接目标文件到可执行文件
gcc main.c convert.o -o main
但是会发现,这样做的话,每次都需要将自己的库链接到自己的程序中,我觉得是比较麻烦的,我们可以发现标准库不需要每次都显示链接库,因为这些库是运行时环境的一部分,已经预先编译链接到系统中了。在C语言中,标准库函数大多数是由编译器提供,并且与编译器一起安装的。这些函数的实现通常包含在编译器的运行时库中,而不是由用户程序在编译时链接。
实现类似标准库效果的几种方法
那么如何让自己的库也能实现这样的效果呢,有以下几种方法:
- 静态库:
将你的代码编译成静态库(.a 文件在Unix-like系统中,或 .lib 文件在Windows系统中)。
在编译时,使用编译器的 -static 选项将库静态链接到你的可执行文件中。
- 创建共享(动态库)库:
将你的代码编译成共享库(.so 文件在Unix-like系统中,或 .dll 文件在Windows系统中)。
将共享库放置在系统的库搜索路径中,如 /usr/lib 或 /usr/local/lib。
使用 ldconfig(在Unix-like系统中)更新动态链接器的缓存。
- 安装到标准路径:
将你的库文件安装到操作系统的标准库路径下,如 /usr/lib 或 /usr/local/lib。
确保头文件位于标准包含路径下,如 /usr/include 或 /usr/local/include。
- 修改链接器配置:
修改系统的链接器配置文件,如 /etc/ld.so.conf(在Unix-like系统中),并运行 ldconfig 来更新链接器的缓存。
- 环境变量:
设置环境变量,如 LD_LIBRARY_PATH(在Unix-like系统中)或 PATH(在Windows系统中),以包含你的库文件所在的目录。
使用pkg-config:
- 创建 pkg-config 文件
这是一个帮助库维护者和开发者管理编译和链接标志的工具。
用户可以使用 pkg-config 工具来获取编译和链接你的库所需的标志。
- 集成到编译器:
如果你有能力,可以与编译器开发商合作,将你的库集成到编译器的运行时库中。
- 发布为系统软件包:
为你的操作系统创建软件包(如 .deb、.rpm、.pkg),这样用户可以通过包管理器安装你的库。
除了静态库和共享库之外,其他方法可能需要管理员权限或对系统有更深入的了解。此外,修改系统级别的文件和配置可能会对系统的稳定性和安全性产生影响,因此在进行这些操作时应格外小心。
实际使用的开发方法
头文件库
直接将头文件放到标准库路径下C:\Program Files\mingw64\x86_64-w64-mingw32\include
然后在头文件里面就写好对应的函数实现
这种方式通常称为“头文件库”或“内联库
”。这样做的好处是,用户在使用你的库时,只需要包含头文件即可,无需进行额外的链接步骤。这种方式在一些小型库或者模板库中非常常见。
我看stdio.h也是这样实现的
注意事项
代码膨胀:将实现放入头文件会导致每个包含该头文件的源文件都会有一份库函数的实现代码,这可能会导致编译后的二进制文件体积增大
。
宏定义:使用宏定义来防止头文件被重复包含是必要的,如示例中的#ifndef、#define和#endif。
内联函数:如果库函数很小,可以考虑使用inline关键字,这可以使得编译器尝试将函数内联到每个调用点,从而减少函数调用的开销
。
标准路径:确保你的头文件位于编译器的搜索路径中,这样编译器才能正确找到并包含它。
尝试自动链接静态库(好像每次都还是要指定库的名字)
对于标准库,是编译器自带的库,比如 C 标准库(libc),这些库在编译时会自动链接到的程序中,不需要显式指定链接这些库。
而对于静态库,是一组编译好的函数和数据的集合,它们在编译时被整合到程序中
。使用静态库时,需要在编译命令中指定静态库的路径和名称
,以便编译器知道如何链接这些库。
也就是说无论如何都要链接的
那么就有两种方式来编译链接了,一种是用命令行,一种是使用文件,其实本质上都是一样的,但是使用task.json文件配置会更加方便
生成静态库
ar rcs libdatatest.a file1.o file2.o file3.o
这里就是使用file1-file3目标文件生成libdatatest静态库
配置环境变量(可选)
LIBRARY_PATH 或 LD_LIBRARY_PATH(对于动态库),这样编译器和链接器会自动在这些路径中查找库文件。
在系统变量或者用户变量中建立变量名分别为LIBRARY_PATH 和LD_LIBRARY_PATH的变量,值就填库的路径。
这个非常关键,特别是动态库,如果不配置环境变量会一直找不到库
编译链接程序
使用命令行链接静态库并编译
gcc main.c -o 3-testCLib-static -L
C:/Users/Administrator/Desktop/nosee/code/c/2024-C-Code/3-文件 -ldatatest
这个命令指定将main.c 编译成3-testCLib-static.exe可执行文件
- -L 后面指定的是库的路径 如果是
-L.
的话就是在当前工程目录下寻找库 - -l 后面跟着的是库的名字 我的库的名字是
libdatatest.lib
但是这里需要输入datatest
配置构建任务task.json文件编译并链接
这个文件定义了编译和运行任务
,还包括库文件的搜索路径,以及 -l 参数来链接特定的库。
指定头文件路径也是必要的 否则会显示找不到头文件
{
"version": "2.0.0",
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: gcc.exe 生成活动文件",
"command": "C:/Program Files/mingw64/bin/gcc.exe",
"args": [
"-fexec-charset=GBK",//防止中文输出乱码
"-fdiagnostics-color=always",
"-g",
"${workspaceFolder}\\*.c",
"-o",
"${workspaceFolder}\\${workspaceRootFolderName}.exe",
"-I", // 添加额外的头文件路径
"C:/Program Files/mingw64/x86_64-w64-mingw32/myinclude",
"-LC:/Program Files/mingw64/x86_64-w64-mingw32/mylib",
"-ldatatest"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"group": "build",
"detail": "编译器: \"C:/Program Files/mingw64/bin/gcc.exe\""
}
]
}
这样就定义好了库文件的搜索路径和该链接哪个库,就可以直接用快捷键执行生成任务而不需要人为用命令行来链接了
如果有多个库文件需要链接的话 需要按顺序一个个写
gcc -o my_program my_program.c -L/usr/local/lib -lmath
-L/usr/lib -lxml2 -lsqlite3
配置编译器设置
在 VSCode 中,c_cpp_properties.json 文件用于配置 C/C++ 项目的 IntelliSense 功能,包括编译器路径、包含路径等。你可以通过 Ctrl+Shift+P 打开命令面板,然后输入 “C/C++: Edit Configurations (UI)” 来编辑这个文件。在这里,你可以设置 includePath
来指定编译器搜索头文件的路径,以及 compilerPath
来指定编译器的路径。如果不写包含头文件的路径的话,优先搜索标准库的头文件
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**",
"C:/Program Files/mingw64/x86_64-w64-mingw32/myinclude"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"windowsSdkVersion": "10.0.19041.0",
"cStandard": "c17",
"cppStandard": "c++17",
"compilerPath": "C:/Program Files/mingw64/bin/gcc.exe",
"intelliSenseMode": "gcc-x64"
}
],
"version": 4
}
对应文件
在c_cpp_properties.json
中对应路径的文件夹下放好对应的源文件和头文件,这样就可以转定义看到实现
了,也可以看到自己的头文件
在对应task.json
的库文件存放的路径下放好对应的静态库
这样以后只需要在task.json中修改需要链接的库就好了
。虽然还是要选择自己要链接的库,但是已经比以前方便很多了。
尝试自动链接动态库
配置编译器设置是一样的,也就是c_cpp_properties.json,因为这个配置对于目前的工程来说就是指定编译器和头文件路径
需要注意的点是动态库必须配置环境变量,而且配置后电脑重启才能生效,所以做完这步最好重启一下电脑,否则怎么都会找不到库的
配置环境变量(非常重要)
在用户环境变量下的Path变量里放入自己的库的路径
系统环境变量也是如此操作
LD_LIBRARY_PATH
最好也在系统环境变量下新建一个名字叫LD_LIBRARY_PATH的变量 内容也是动态库的路径
这三步做完记得重启,这样电脑就会在这个路径下去搜索动态库了
生成动态库文件
gcc -c myprint.c -o myprint.o -fpic
gcc myprint.o -o libmyprint.dll -shared -fpic
gcc -c main.c -o main.o -I C:/Program Files/mingw64/x86_64-w64-mingw32/myinclude
配置构建任务task.json
{
"version": "2.0.0",
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: gcc.exe 生成活动文件",
"command": "C:/Program Files/mingw64/bin/gcc.exe",
"args": [
"-fexec-charset=GBK",//防止中文输出乱码
"-fdiagnostics-color=always",
"-g",
"${workspaceFolder}\\*.c",
"-I", // 添加额外的头文件路径
"C:/Program Files/mingw64/x86_64-w64-mingw32/myinclude",
"-LC:/Program Files/mingw64/x86_64-w64-mingw32/mylib",
"-lmyprint",
"-o",
"${workspaceFolder}\\${workspaceRootFolderName}.exe",
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"group": "build",
"detail": "编译器: \"C:/Program Files/mingw64/bin/gcc.exe\""
}
]
}
效果
从上面的图片可以看出我的工程文件是不包含头文件和源文件的,并且动态库也不在当前目录下,但是确可以通过转定义看到函数的实现和头文件,并且还可以正常执行调试
。
待续
本篇文章并没有深入介绍动静态库的区别,而且其实可以使用Cmake对需要链接的库、以及编译选项进行配置,日后再讲吧