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

GCC使用入门

文章目录

  • GCC简介
  • 单个文件编译过程
    • 预处理(Preprocessing)
    • 编译(Compilation)
    • 汇编(Assembly)
    • 链接(Linking)
  • 多文件编译过程
    • 头文件搜索路径
      • 三种不推荐的方法
      • 两种推荐的方法
  • 库文件
    • 静态库文件
      • 创建和使用静态库
      • 链接顺序
    • 动态库文件
      • 创建和使用动态库
  • Warning编译选项
  • 调试信息(-g)
  • 编译优化
  • 总结
  • 参考文献

GCC简介

gcc(GNU Compiler Collection,GNU编译器套件)是一个功能强大、跨平台的编译器套件,用于编译C、C++、Objective-C、Fortran、Ada、Go和D等多种编程语言的源代码。它是GNU项目的一部分,遵循GPL(GNU General Public License)许可证,因此可以自由使用和分发。gcc主要有如下特点:

  • 1、跨平台:gcc可以在多种操作系统上运行,包括Linux、Windows(通过MinGW)等。
  • 2、优化:gcc提供了多种优化选项,可以根据需要选择不同级别的优化来生成更高效率的代码。
  • 3、可扩展性:gcc支持通过插件和扩展来增加新功能或支持新的编程语言。
  • 4、开源
  • 5、多语言支持:支持C、C++、Objective-C、Fortran、Ada、Go和D等语言的编译。
  • 6、丰富的选项:gcc提供了大量的编译选项,允许开发者精细地控制编译过程,包括预处理、编译、汇编和链接等各个阶段。
  • 7、调试支持:gcc生成的代码可以与多种调试器(如gdb)配合使用,帮助开发者定位和解决程序中的问题。
  • 8、文档和社区支持:gcc拥有详细的文档和广泛的社区支持。

本文主要是介绍gcc的入门使用,gcc的安装教程请参考其他文章。

检验gcc是否安装可以使用gcc --version 。如果内容是类似下面的格式,则说明已经安装了gcc。

zld@zld:~$ gcc --version
gcc (Ubuntu 13.2.0-4ubuntu3) 13.2.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

单个文件编译过程

对于经典的"Hello, World"程序:

zld@zld:~/Codes/tmp0926$ cat -n hello.c 1  #include <stdio.h>23  int main()4  {5          printf("Hello, World\n");6          return 0;7  }

使用gcc编译单个文件非常简单,只需要使用命令gcc <源文件>即可,比如用gcc编译上面的hello.c,则用命令gcc hello.c就能编译成功,gcc会生成一个a.out的可执行文件(当然也可以通过参数-o来指定生成的文件名)

zld@zld:~/Codes/tmp0926$ gcc hello.c 
zld@zld:~/Codes/tmp0926$ ll
total 28
drwxrwxr-x  2 zld zld  4096 Sep 27 11:16 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rwxrwxr-x  1 zld zld 15952 Sep 27 11:16 a.out*
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c
zld@zld:~/Codes/tmp0926$ ./a.out 
Hello, World
# 通过 -o 参数生成可执行文件
zld@zld:~/Codes/tmp0926$ gcc hello.c -o hello
zld@zld:~/Codes/tmp0926$ ll
total 44
drwxrwxr-x  2 zld zld  4096 Sep 27 11:22 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rwxrwxr-x  1 zld zld 15952 Sep 27 11:16 a.out*
-rwxrwxr-x  1 zld zld 15952 Sep 27 11:22 hello*
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c
zld@zld:~/Codes/tmp0926$ ./hello 
Hello, World

上面的编译看似很简单,但其实这个过程可以分为4个阶段,分别是预处理(preprocessing)编译(compilation)汇编(assembly)链接(linking)

默认情况下,gcc编译不会保存中间结果,我们可以通过选项-save-temps来保存中间结果。如下所示,当加了-save-temps选项后,除了生成最终的可执行文件a.out外,还有以下几个文件a-hello.ia-hello.sa-hello.o这其实就是上面几个阶段生成的中间结果。其中,.i文件是预处理之后产生的结果,.s是编译后产生的结果,.o是汇编产生的结果。

zld@zld:~/Codes/tmp0926$ gcc hello.c -save-temps
zld@zld:~/Codes/tmp0926$ ll
total 56
drwxrwxr-x  2 zld zld  4096 Sep 27 11:28 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld 19640 Sep 27 11:28 a-hello.i
-rw-rw-r--  1 zld zld  1496 Sep 27 11:28 a-hello.o
-rw-rw-r--  1 zld zld   659 Sep 27 11:28 a-hello.s
-rwxrwxr-x  1 zld zld 15952 Sep 27 11:28 a.out*
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c

因此,对于上述hello.c文件,用gcc编译的过程可以用下图说明:
在这里插入图片描述

下面将结合hello.c源程序详细讲述这4个阶段的处理过程。

预处理(Preprocessing)

gcc编译过程的第一阶段就是预处理,预处理过程主要处理那些源代码文件中的以"#“开始的预编译指令。比如”#include"、"#define"等,主要处理规则如下:

  • 将所有的"#define"删除,并且展开所有的宏定义。
  • 处理所有条件预编译指令,比如"#if"、“#ifdef”、“#elif”、“#else”、“#endif”。
  • 处理"#include"预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
  • 删除所有的注释"//“和”/* */"。
  • 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
  • 保留所有的#pragma编译器指令,因为编译器需要使用它们。

经过预处理之后,生成的.i文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i文件中。

预处理阶段是用预处理器cpp程序处理的,可以用以下命令处理预处理过程。(-E表示只进行预处理)。

cpp hello.c > hello.i
或者
gcc -E hello.c -o hello.i

对上述的hello.c程序进行预处理:

zld@zld:~/Codes/tmp0926$ ll
total 12
drwxrwxr-x  2 zld zld 4096 Sep 27 11:39 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld   73 Sep 27 11:14 hello.c
zld@zld:~/Codes/tmp0926$ gcc -E hello.c -o hello1.i
zld@zld:~/Codes/tmp0926$ cpp hello.c > hello2.i
zld@zld:~/Codes/tmp0926$ ll
total 52
drwxrwxr-x  2 zld zld  4096 Sep 27 11:44 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld 19640 Sep 27 11:43 hello1.i
-rw-rw-r--  1 zld zld 19640 Sep 27 11:44 hello2.i
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c

预处理之后的结果如下:

zld@zld:~/Codes/tmp0926$ cat hello1.i
...
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 967 "/usr/include/stdio.h" 3 4# 2 "hello.c" 2# 3 "hello.c"
int main()
{printf("Hello, World\n");return 0;
}

对于上述的例子可能还不好理解,我们再举一个例子。

对于下面的一段程序test1.c

zld@zld:~/Codes/tmp0926$ cat -n test1.c 1  #define TEST "hello,world"23  /* This is a comments */4  const char str[] = TEST;

我们的预处理结果如下:

zld@zld:~/Codes/tmp0926$ cpp test1.c > test1.i
zld@zld:~/Codes/tmp0926$ ll
total 60
drwxrwxr-x  2 zld zld  4096 Sep 27 12:11 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld 19640 Sep 27 11:43 hello1.i
-rw-rw-r--  1 zld zld 19640 Sep 27 11:44 hello2.i
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c
-rw-rw-r--  1 zld zld    78 Sep 27 12:09 test1.c
-rw-rw-r--  1 zld zld   165 Sep 27 12:11 test1.i
zld@zld:~/Codes/tmp0926$ cat test1.i 
# 0 "test1.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "test1.c"const char str[] = "hello,world";

从上面的结果可以看到,源程序test1.c的第1行的宏定义#define TEST "hello,world"以及第3行的注释/* This is a comments */已经被删除了,并且在源程序的第四行 const char str[] = TEST;已经将该宏定义展开了。

编译(Compilation)

编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件(简单来说,编译就是将预处理之后的源代码翻译成汇编代码)。这是整个构建过程的核心部分。对于编译过程,gcc使用的是ccl程序来完成的。

编译过程相当于如下命令(-S表示执行编译后停止,不进行汇编和链接):

gcc -S hello.i -o hello.s
或者
gcc -S hello.c -o hello.s

对于hello.c程序,执行结果如下:

zld@zld:~/Codes/tmp0926$ gcc -S hello.i -o hello.s
zld@zld:~/Codes/tmp0926$ ll
total 40
drwxrwxr-x  2 zld zld  4096 Sep 27 12:24 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c
-rw-rw-r--  1 zld zld 19640 Sep 27 11:43 hello.i
-rw-rw-r--  1 zld zld   659 Sep 27 12:24 hello.s
-rw-rw-r--  1 zld zld    78 Sep 27 12:09 test1.c

如果想看详细过程,还可以加上选项-v,它表示显示gcc执行时的详细过程。
在这里插入图片描述

从上面可以看到在编译过程实际是由ccl程序执行的,因此也可以直接用ccl程序执行:

zld@zld:~/Codes/tmp0926$ /usr/libexec/gcc/x86_64-linux-gnu/13/cc1 hello.i main
Analyzing compilation unit
Performing interprocedural optimizations<*free_lang_data> {heap 920k} <visibility> {heap 920k} <build_ssa_passes> {heap 920k} <opt_local_passes> {heap 1224k} <remove_symbols> {heap 1224k} <targetclone> {heap 1224k} <free-fnsummary> {heap 1224k}Streaming LTO<whole-program> {heap 1224k} <fnsummary> {heap 1224k} <inline> {heap 1224k} <modref> {heap 1224k} <free-fnsummary> {heap 1224k} <single-use> {heap 1224k} <comdats> {heap 1224k}Assembling functions:<simdclone> {heap 1224k} main
Time variable                                   usr           sys          wall           GGCphase setup                        :   0.00 (  0%)   0.00 (  0%)   0.01 ( 33%)  1819k ( 80%)phase parsing                      :   0.00 (  0%)   0.01 (100%)   0.01 ( 33%)   403k ( 18%)phase opt and generate             :   0.01 (100%)   0.00 (  0%)   0.01 ( 33%)    60k (  3%)callgraph optimization             :   0.01 (100%)   0.00 (  0%)   0.00 (  0%)     0  (  0%)callgraph ipa passes               :   0.01 (100%)   0.00 (  0%)   0.00 (  0%)  4880  (  0%)preprocessing                      :   0.00 (  0%)   0.01 (100%)   0.00 (  0%)    35k (  2%)parser (global)                    :   0.00 (  0%)   0.00 (  0%)   0.01 ( 33%)   351k ( 15%)final                              :   0.00 (  0%)   0.00 (  0%)   0.01 ( 33%)  2120  (  0%)TOTAL                              :   0.01          0.01          0.03         2283k

汇编(Assembly)

汇编器就是将编译生成的汇编代码(.s)转变成机器可以执行的指令,在Linux系统上一般表现为ELF目标文件(OBJ文件)(简单来说,就是将汇编代码翻译成机器码),用到的汇编器工具为as。

汇编过程用到的命令如下:

gcc -c hello.s -o hello.o
或者
as hello.s -o hello.o

对于hello.c程序,执行结果如下:

zld@zld:~/Codes/tmp0926$ gcc -c hello.s -o hello.o
zld@zld:~/Codes/tmp0926$ as hello.s -o hello2.o
zld@zld:~/Codes/tmp0926$ ll
total 52
drwxrwxr-x  2 zld zld  4096 Sep 27 12:51 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld  1376 Sep 27 12:51 hello2.o
-rw-rw-r--  1 zld zld   659 Sep 27 12:27 hello2.s
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c
-rw-rw-r--  1 zld zld 19640 Sep 27 11:43 hello.i
-rw-rw-r--  1 zld zld  1376 Sep 27 12:51 hello.o
-rw-rw-r--  1 zld zld   474 Sep 27 12:40 hello.s
-rw-rw-r--  1 zld zld    78 Sep 27 12:09 test1.c

详细执行过程:
在这里插入图片描述

链接(Linking)

链接就是将上步生成的OBJ文件和系统库的OBJ文件、库文件链接起来,最终生成了可以在特定平台运行的可执行文件,用到的工具为ld或collect2。

链接的命令如下:

zld@zld:~/Codes/tmp0926$ gcc hello.o -o hello
zld@zld:~/Codes/tmp0926$ ll
total 68
drwxrwxr-x  2 zld zld  4096 Sep 27 13:11 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rwxrwxr-x  1 zld zld 15880 Sep 27 13:11 hello*
-rw-rw-r--  1 zld zld  1376 Sep 27 12:51 hello2.o
-rw-rw-r--  1 zld zld   659 Sep 27 12:27 hello2.s
-rw-rw-r--  1 zld zld    73 Sep 27 11:14 hello.c
-rw-rw-r--  1 zld zld 19640 Sep 27 11:43 hello.i
-rw-rw-r--  1 zld zld  1376 Sep 27 12:52 hello.o
-rw-rw-r--  1 zld zld   474 Sep 27 12:40 hello.s
-rw-rw-r--  1 zld zld    78 Sep 27 12:09 test1.c

其详细执行过程:
在这里插入图片描述

多文件编译过程

上一章节是讲的单个源文件的编译过程。在一个实际的项目中,一般情况下是包含多个源文件的。本章将介绍多个文件的编译过程。

我们首先将上一章的hello.c程序改造一下:该目录下有三个文件分别是hello.chello.hmain.c,其中,hello.c中定义了一个函数hello(),其功能就是简单的打印字符串。hello.h头文件中就是声明了hello()函数。在main.c中则是定义了main()函数,在main函数中调用了hello.c文件中的hello()函数。

zld@zld:~/Codes/tmp0926$ tree
.
├── hello.c
├── hello.h
└── main.czld@zld:~/Codes/tmp0926$ cat hello.c 
#include <stdio.h>
#include "hello.h"void hello(const char *string)
{printf("%s\n", string);
}zld@zld:~/Codes/tmp0926$ cat hello.h 
void hello(const char *string);zld@zld:~/Codes/tmp0926$ cat main.c 
#include <stdio.h>
#include "hello.h"int main()
{hello("hello,world");return 0;
}

对于多个源文件的编译,有两种方式,第1种就是将多个源程序一起编译生成可执行程序;第2种方法就是分别编译生成多个目标文件,然后再将这些目标文件再链接到一起生成可执行程序。

第1种方法:将多个源程序一起编译生成可执行程序。

zld@zld:~/Codes/tmp0926$ gcc hello.c main.c -o hello
zld@zld:~/Codes/tmp0926$ ll
total 36
drwxrwxr-x  2 zld zld  4096 Sep 27 13:48 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rwxrwxr-x  1 zld zld 16016 Sep 27 13:48 hello*
-rw-rw-r--  1 zld zld    99 Sep 27 13:31 hello.c
-rw-rw-r--  1 zld zld    32 Sep 27 13:31 hello.h
-rw-rw-r--  1 zld zld    88 Sep 27 13:33 main.c

第2种方法:分别编译生成多个目标文件,然后再将这些目标文件再链接到一起生成可执行程序。

zld@zld:~/Codes/tmp0926$ gcc -c hello.c -o hello.o
zld@zld:~/Codes/tmp0926$ gcc -c main.c -o main.o
zld@zld:~/Codes/tmp0926$ ll
total 28
drwxrwxr-x  2 zld zld 4096 Sep 27 13:50 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld   99 Sep 27 13:31 hello.c
-rw-rw-r--  1 zld zld   32 Sep 27 13:31 hello.h
-rw-rw-r--  1 zld zld 1360 Sep 27 13:49 hello.o
-rw-rw-r--  1 zld zld   88 Sep 27 13:33 main.c
-rw-rw-r--  1 zld zld 1488 Sep 27 13:50 main.o
zld@zld:~/Codes/tmp0926$ gcc hello.o main.o -o hello
zld@zld:~/Codes/tmp0926$ ll
total 44
drwxrwxr-x  2 zld zld  4096 Sep 27 13:50 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rwxrwxr-x  1 zld zld 16016 Sep 27 13:50 hello*
-rw-rw-r--  1 zld zld    99 Sep 27 13:31 hello.c
-rw-rw-r--  1 zld zld    32 Sep 27 13:31 hello.h
-rw-rw-r--  1 zld zld  1360 Sep 27 13:49 hello.o
-rw-rw-r--  1 zld zld    88 Sep 27 13:33 main.c
-rw-rw-r--  1 zld zld  1488 Sep 27 13:50 main.o
zld@zld:~/Codes/tmp0926$ ./hello 
hello,world

这里有以下几个问题值得讨论一下:

  • 问题1、在上面的方法1或者方法2中,都没有用到头文件hello.h,为什么?

在编译过程中,不需要用到头文件,这是因为在预处理步骤中,预处理的程序会将"#include"中包含的文件插入该预处理指令的位置。

  • 问题2、在预处理处理"#include"指令时,程序是在哪个目录查找到头文件的?

    对于该问题,先卖个关子,在后面的例子中再讲。

  • 问题3、在第2种方法中,当将多个目标文件链接再一起时,对目标文件的顺序有没有要求?

    比如上面是gcc hello.o main.o -o hello,那能不能按照gcc main.o hello.o -o hello的顺序呢?

    我们实践一下:

    zld@zld:~/Codes/tmp0926$ gcc main.o hello.o  -o hello2
    zld@zld:~/Codes/tmp0926$ ll
    total 60
    drwxrwxr-x  2 zld zld  4096 Sep 27 14:06 ./
    drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
    -rwxrwxr-x  1 zld zld 16016 Sep 27 13:50 hello*
    -rwxrwxr-x  1 zld zld 16016 Sep 27 14:06 hello2*
    -rw-rw-r--  1 zld zld    99 Sep 27 13:31 hello.c
    -rw-rw-r--  1 zld zld    32 Sep 27 13:31 hello.h
    -rw-rw-r--  1 zld zld  1360 Sep 27 13:49 hello.o
    -rw-rw-r--  1 zld zld    88 Sep 27 13:33 main.c
    -rw-rw-r--  1 zld zld  1488 Sep 27 13:50 main.o
    

    实践证明,gcc对目标文件的链接顺序是没有要求。之所以讲这个,是因为有些老的链接器如果顺序不对,则有可能报“undefined references”错误,如果调整一下链接顺序,则该错误又没有了。但是现在的编译器和链接器一般是不需要考虑这个顺序的,GCC就不需要考虑。

    但是,我们后面会看到,如果链接过程中有静态库文件和目标文件,则有顺序要求。

通常来说,编译所消耗的时间要比链接所消耗的时间要长,因此,如果把所有的程序都写到一个文件中,那只要有修改,则需要重新编译和链接,对于一个大型的程序来说,这是非常耗时的。但如果分成了多个文件,那我们可以只对修改的文件进行编译,然后再将编译后生成的目标文件和之前没有修改的文件的目标文件再一起链接即可,这样就能减少整个构建的时间。

头文件搜索路径

上面的例子是将3个文件(main.c、hello.c、hello.h)放到同一个目录中的。但有过实际项目的人都知道,一般不会将所有文件放到同一个目录中的,而是比如头文件放到注入include的目录中,库文件放到诸如lib的目录中,然后源文件也会基于各自的功能分别创建各自的目录的。

我们稍微改一下上面3个文件的目录。目录结构如下。将hello.c文件放到了hello目录中,将hello.h放到include目录中,而main.c和hello文件夹以及include文件夹在同一层中。

zld@zld:~/Codes/tmp0926$ tree
.
├── hello
│   └── hello.c
├── include
│   └── hello.h
└── main.c3 directories, 3 files

此时,我们按照上面的方法1再编译一下(这里注意一下,我是在当前main.c文件所处的目录编译的,所以hello.c的相对位置就是hello/hello.c):

zld@zld:~/Codes/tmp0926$ gcc main.c hello/hello.c -o hello1
main.c:2:10: fatal error: hello.h: No such file or directory2 | #include "hello.h"|          ^~~~~~~~~
compilation terminated.
hello/hello.c:2:10: fatal error: hello.h: No such file or directory2 | #include "hello.h"|          ^~~~~~~~~
compilation terminated.

从上面可知,编译报错了,错误原因是找不到hello.h头文件。为什么会找不到头文件呢?我们用-v选项来看下详细信息:

zld@zld:~/Codes/tmp0926$ gcc main.c hello/hello.c -v -o hello1
...
#include "..." search starts here:
#include <...> search starts here:/usr/lib/gcc/x86_64-linux-gnu/13/include/usr/local/include/usr/include/x86_64-linux-gnu/usr/include
End of search list.
Compiler executable checksum: edbc28f9c9bb85637ee0b8e5b79ac141
main.c:2:10: fatal error: hello.h: No such file or directory2 | #include "hello.h"|          ^~~~~~~~~
compilation terminated.

我只显示了关键的一些信息,从上面可以看到当搜索#include “…” 时是在当前目录下查找#include "..." search starts here:。在搜索#include <...>是在以下路径查找(下面的这些路径称为系统路径):

#include <...> search starts here:/usr/lib/gcc/x86_64-linux-gnu/13/include/usr/local/include/usr/include/x86_64-linux-gnu/usr/include

很显然,"hello.h"既不在当前路径下,也不在系统路径上,所以找不到而报错。

既然知道报错的原因了,那就知道怎么解决问题了。

三种不推荐的方法

  • 方法1:在#include中写出头文件hello.h的绝对路径。如下所示:

    zld@zld:~/Codes/tmp0926/hello$ cat hello.c 
    #include <stdio.h>
    // #include "hello.h"
    #include "/home/zld/Codes/tmp0926/include/hello.h"zld@zld:~/Codes/tmp0926$ cat main.c 
    #include <stdio.h>
    // #include "hello.h"
    #include "/home/zld/Codes/tmp0926/include/hello.h"
    

    修改完成后,再次编译,这次就编译成功了,而且运行正确:

    zld@zld:~/Codes/tmp0926$ gcc main.c hello/hello.c -o hello2
    zld@zld:~/Codes/tmp0926$ ll
    total 36
    drwxrwxr-x  4 zld zld  4096 Sep 27 16:49 ./
    drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
    drwxrwxr-x  2 zld zld  4096 Sep 27 16:47 hello/
    -rwxrwxr-x  1 zld zld 16016 Sep 27 16:49 hello2*
    drwxrwxr-x  2 zld zld  4096 Sep 27 14:31 include/
    -rw-rw-r--  1 zld zld   142 Sep 27 16:46 main.c
    zld@zld:~/Codes/tmp0926$ ./hello2 
    hello,world
    

    这种方式非常不推荐

    • 1、可移植性差
      • 绝对路径是特定于一个具体的文件系统布局的。如果代码被移植到其他系统或环境,文件系统的结构可能不同,导致编译器无法找到头文件。
      • 这使得代码在不同开发者之间、不同机器之间或不同项目之间的共享和复用变得困难。
    • 2、维护困难。
      • 如果头文件的位置发生变化,则所有包含绝对路径的源代码文件都需要更新。
      • 这增加了维护负担,尤其是在大型项目中,头文件的位置可能会频繁变动。
  • 方法2:在#include中写出头文件hello.h的相对路径。如下所示:

    zld@zld:~/Codes/tmp0926/hello$ cat hello.c 
    #include <stdio.h>
    // #include "hello.h"
    #include "../include/hello.h"zld@zld:~/Codes/tmp0926$ cat main.c 
    #include <stdio.h>
    // #include "hello.h"
    #include "include/hello.h"
    

    运行结果:

    zld@zld:~/Codes/tmp0926$ gcc main.c hello/hello.c -o hello3
    zld@zld:~/Codes/tmp0926$ ll
    total 52
    drwxrwxr-x  4 zld zld  4096 Sep 27 17:37 ./
    drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
    drwxrwxr-x  2 zld zld  4096 Sep 27 17:36 hello/
    -rwxrwxr-x  1 zld zld 16016 Sep 27 16:49 hello2*
    -rwxrwxr-x  1 zld zld 16016 Sep 27 17:37 hello3*
    drwxrwxr-x  2 zld zld  4096 Sep 27 14:31 include/
    -rw-rw-r--  1 zld zld   118 Sep 27 17:35 main.c
    zld@zld:~/Codes/tmp0926$ ./hello3 
    hello,world
    

    这种方法比方法1要好一些,如果各个文件的相对位置不变,则也不用花费许多功夫维护(实际上,有些开源代码上有这么写)。但是如果相对位置一变,就需要修改对应的include。因此,这种方法本人也是不推荐的。

  • 方法3:将头文件放到上面所说的系统路径中,然后在写#include语句时,就可以不想相对或绝对路径了:

    zld@zld:~/Codes/tmp0926/include$ cp hello.h /usr/local/include/
    cp: cannot create regular file '/usr/local/include/hello.h': Permission denied
    zld@zld:~/Codes/tmp0926/include$ su
    Password: 
    root@zld:/home/zld/Codes/tmp0926/include# cp hello.h /usr/local/include/root@zld:/home/zld/Codes/tmp0926/hello# cat hello.c 
    #include <stdio.h>
    #include "hello.h"root@zld:/home/zld/Codes/tmp0926# cat main.c 
    #include <stdio.h>
    #include "hello.h"
    

    运行结果:

    root@zld:/home/zld/Codes/tmp0926# gcc main.c hello/hello.c -o hello4
    root@zld:/home/zld/Codes/tmp0926# ./hello4
    hello,world
    

    但是这也有一些问题。比如,从上面可以知道,我将头文件拷贝到系统路径时,必须要root权限。此外,如果将所有的头文件都拷贝到系统路径,那这个系统路径下将包含各种各样的头文件,这样这个目录下将变得非常的杂乱不堪。因此,也是不推荐这种方法。

两种推荐的方法

上面的3种方法,我们都不太满意,那有什么更好的办法呢?我们想一下,如果我们告诉编译器去哪个路径去找头文件,那是不是事情就解决了?

有两种方法可以解决这个事情:

  • 1、设置C头文件的搜索路径的环境变量。

    export C_INCLUDE_PATH=/home/zld/zld/codes/0926:$C_INCLUDE_PATH
    

    将上面的例子,用该方法编译一下,编译过程如下:

    # 设置C语言头文件的环境变量
    root@zld:/home/zld/Codes/tmp0926# export C_INCLUDE_PATH=$C_INCLUDE_PATH:/home/zld/Codes/tmp0926/include
    root@zld:/home/zld/Codes/tmp0926/include# env | grep C_INCLUDE
    C_INCLUDE_PATH=:/home/zld/Codes/tmp0926/include#重新编译与运行(注意:我已经将方法3中拷贝到系统路径下的hello.h文件已经删除了,防止影响)
    root@zld:/home/zld/Codes/tmp0926# gcc main.c hello/hello.c -o hello5
    root@zld:/home/zld/Codes/tmp0926# ./hello5 
    hello,world
    
  • 2、在gcc命令中加上-Idir选项,其中dir是头文件的路径,I是字母i的大小,并且I与dir之间没有空格。

# -I加上绝对路径
root@zld:/home/zld/Codes/tmp0926# gcc main.c hello/hello.c -I/home/zld/Codes/tmp0926/include -o hello6
root@zld:/home/zld/Codes/tmp0926# ./hello6
hello,world# -I加上相对路径:点'.'表示当前路径
root@zld:/home/zld/Codes/tmp0926# gcc main.c hello/hello.c -I. -o hello7

上述两个方法比之前的三种方法,要方便得多。因此推荐这两种方法。在这两种方法中,在命令中加上-I选项更加常用。因为如果换到其他的环境中,那还得重新设置一下环境变量。但是命令行中加上-I选项一般可以在makefile文件中都已经写好了,因此更加方便。

对于添加环境变量的方法,对于C++来说,是用CPLUS_INCLUDE_PATH,对于静态库文件来说是用LIBRARY_PATH,对于动态库文件来说是LD_LIBRARY_PATH。至于什么是静/动态库文件,本文后面会讲到。

这里做一下总结:

# C头文件搜索路径环境变量设置
C_INCLUDE_PATH=$C_INCLUDE_PATH:/xxx/yyy/zzz
export C_INCLUDE_PATH# C++头文件搜索路径环境变量设置
CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/xxx/yyy/zzz
export CPLUS_INCLUDE_PATH# 动态库搜索路径环境变量设置
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/xxx/yyy/zzz/lib
export LD_LIBRARY_PATH# 静态库搜索路径环境变量设置
LIBRARY_PATH=$LIBRARY_PATH:/xxx/yyy/zzz/lib
export LIBRARY_PATH

同样,对于gcc命令中,可以加-L来添加库文件路径。

# 在gcc命令中添加 -Idir 选项来添加头文件搜索路径
# 在gcc命令中添加 -Ldir 选项来添加库文件搜索路径

库文件

请大家思考一个问题,在hello.c程序中,我们调用了printf()函数,但是我们实际上并不知道这个函数是怎么实现的,我们只是#include <stdio.h>

zld@zld:~/Codes/tmp0926$ cat -n hello.c 1  #include <stdio.h>23  int main()4  {5          printf("Hello, World\n");6          return 0;7  }

假设还有另外一个场景:假设你是一个公司的老板,你们公司开发了一个非常牛B的模块,这时候有客户希望单独将该模块提供给他们使用。作为公司的老板,你自然是不希望将该模块的源代码提供给客户。

上面的两个场景都涉及到库(Library)文件。把库文件和头文件给到对方就可以达到提供功能又不暴露源码的目的了。

库文件可以分成静态库文件和动态库文件。在Linux下,静态库文件是以.a后缀结束的文件。动态库文件是以.so后缀结束的文件。

静态库文件

什么是静态库文件呢?我举一个例子。假设当前你有4个文件:add.csub.cmulti.cdiv.c。从名字也可以看出来,它们分别实现了加法、减法、乘法、除法的功能。并且将这4个源文件都生成了对应的目标文件:add.osub.omulti.odiv.o。我们前面说过,如果其他程序要引用这4个文件,那么可以在gcc命令生成可执行程序时,加上这4个目标文件即可。加上4个文件还好,但是如果有非常多的文件呢?这样一个个添加似乎不太合理,效率也不太高。另外一方面,加减乘除这四个文件,其实可以统称为对数字的运算嘛。能不能将这4个文件像压缩包一样压缩成一个文件呢?可以,静态库文件就是这样干的。

通俗的说,静态库(Static Library)文件就是一个打包了多个目标文件(.o文件)的归档(archive)文件。打个比方就是,目标文件相当于图书馆里面的书。而静态库文件就是图书馆(我想这就是为啥库文件英文是Library了)。

静态库文件有以下特点:

  • 1.编译时链接:静态库在编译时被链接到最终的可执行文件中,这意味着库中的代码和数据会被直接复制到可执行文件中。
  • 2.独立性:由于静态库在编译时就被整合到可执行文件中,因此生成的可执行文件是独立的,不依赖于任何外部的库文件。这使得静态库编译的程序具有更好的可移植性和部署简便性。
  • 3.重复代码整合:如果多个程序使用相同的静态库,每个程序都会在自己的可执行文件中包含一份库代码的副本。这增加了可执行文件的大小,但同时确保了每个程序都有完整的代码副本,无需担心其他程序对库代码的修改或删除。
  • 4.性能考虑:静态库在编译时就被整合到可执行文件中,因此没有运行时加载库的开销。这可能在某些性能敏感的应用中是一个优势。
  • 5.更新和维护挑战:如果静态库中的代码需要更新或者修复,必须重新编译链接所有依赖于该库的程序。这可能会是一个繁琐的过程,特别是在大型项目或涉及多个依赖库的情况下。

创建和使用静态库

在Linux中,可以用ar命令来创建静态库:

ar cr libNAME.a file1.o file2.o ... filen.o

其中,其中cr是选项,c表示create,r表示replace。ar其实就是archiver的缩小。

通过该命令,最终创建出一个静态库文件libNAME.a

我们还可以使用以下命令查看一个静态库文件中包含的目标文件列表:

ar t libNAME.a

举例

假设有如下4个文件,func1.c中定义了一个函数func1(),它的功能是返回两个整型数相加的值。func2.c中定义了func2()函数,该函数的功能是打印给定的数字。在main.c中分别调用了func1函数和func2函数。在mylib.h中定义了两个函数的声明。我们试图将func1.cfunc2.c的程序生成一个静态库文件,然后共main.c使用。

zld@zld:~/Codes/tmp0926$ ll
total 24
drwxrwxr-x  2 zld zld 4096 Sep 27 22:07 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld   63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld   97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld  101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld   44 Sep 27 22:05 mylib.hzld@zld:~/Codes/tmp0926$ cat func1.c 
#include "mylib.h"int func1(int x, int y)
{return (x+y);
}
zld@zld:~/Codes/tmp0926$ cat func2.c 
#include <stdio.h>
#include "mylib.h"void func2(int x)
{printf("The result is %d\n", x);
}
zld@zld:~/Codes/tmp0926$ cat main.c 
#include <stdio.h>
#include "mylib.h"int main()
{int i;i = func1(1,2);func2(i);return 0;
}
zld@zld:~/Codes/tmp0926$ cat mylib.h 
int func1(int x, int y);
void func2(int x);

首先,将func1.cfunc2.c文件分别生成两个目标文件(.o):

# -Wall是告警选项,本文后面会讲到,如果你还不了解,可以去掉这个选项
zld@zld:~/Codes/tmp0926$ gcc -Wall func1.c -c
zld@zld:~/Codes/tmp0926$ gcc -Wall func2.c -c
zld@zld:~/Codes/tmp0926$ ll
total 32
drwxrwxr-x  2 zld zld 4096 Sep 27 22:14 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld   63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld 1224 Sep 27 22:13 func1.o
-rw-rw-r--  1 zld zld   97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld 1512 Sep 27 22:14 func2.o
-rw-rw-r--  1 zld zld  101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld   44 Sep 27 22:05 mylib.h

然后,将func1.ofunc2.o两个目标文件生成一个hello的静态库文件:

# 生成静态库文件
zld@zld:~/Codes/tmp0926$ ar cr libhello.a func1.o func2.o
zld@zld:~/Codes/tmp0926$ ll
total 36
drwxrwxr-x  2 zld zld 4096 Sep 27 22:16 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld   63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld 1224 Sep 27 22:13 func1.o
-rw-rw-r--  1 zld zld   97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld 1512 Sep 27 22:14 func2.o
-rw-rw-r--  1 zld zld 2948 Sep 27 22:16 libhello.a
-rw-rw-r--  1 zld zld  101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld   44 Sep 27 22:05 mylib.h# 查看静态库文件libhello.a包含的目标文件
zld@zld:~/Codes/tmp0926$ ar t libhello.a 
func1.o
func2.o

这样,我们就生成了静态库文件libhello.a

最后,生成最终的可执行文件并运行:

zld@zld:~/Codes/tmp0926$ gcc -Wall main.c libhello.a -o hello
zld@zld:~/Codes/tmp0926$ ./hello 
The result is 3# 也可以先生成main.o文件,然后再和静态库文件链接生成最终可执行文件
zld@zld:~/Codes/tmp0926$ gcc main.o libhello.a -o hello2
zld@zld:~/Codes/tmp0926$ ./hello2
The result is 3

上述命令我们是通过libNAME的方式来链接的,我们还可以通过-lNAME的方式来链接,因此,也可以用以下的命令实现:

zld@zld:~/Codes/tmp0926$ gcc -Wall main.o -lhello -o hello6
/usr/bin/ld: cannot find -lhello: No such file or directory
collect2: error: ld returned 1 exit status

我们发现,命令行竟然执行失败了,说找不到-lhello,也就是说,找不到libhello.a。明明我们当前目录下有libhello.a,那为啥找不到呢?这是因为通过-lhello的方式是在系统目录下去找,而系统目录下没有该静态库文件,因此找不到该hello库文件。那怎么办呢?前面我们说过有三种方法:

  • 1、Command-line options ‘-I’ and ‘-L’, from left to right.在命令行中加上-I或者-L选项,指明头文件或者库文件的目录。其中-I是指明头文件的目录,-L是指明库文件的目录。

  • 2、设置环境变量。Directories specified by environment variables, such as C_INCLUDE_PATH and

    # C头文件搜索路径环境变量设置
    C_INCLUDE_PATH=$C_INCLUDE_PATH:/xxx/yyy/zzz
    export C_INCLUDE_PATH# C++头文件搜索路径环境变量设置
    CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/xxx/yyy/zzz
    export CPLUS_INCLUDE_PATH# 动态库搜索路径环境变量设置
    LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/xxx/yyy/zzz/lib
    export LD_LIBRARY_PATH# 静态库搜索路径环境变量设置
    LIBRARY_PATH=$LIBRARY_PATH:/xxx/yyy/zzz/lib
    export LIBRARY_PATH
    
  • 3、将头文件或者库文件放到系统目录中。

前面我们在头文件的搜索过程说过不推荐使用方法3,并且在那里也有示例,因此这里我们就不再对方法3进行举例了。我们只对方法1和方法2进行举例。

我们使用第1种方法:

zld@zld:~/Codes/tmp0926$ gcc main.o -L. -lhello -o h6
zld@zld:~/Codes/tmp0926$ ll
total 120
drwxrwxr-x  2 zld zld  4096 Sep 27 23:09 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld    63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld  1224 Sep 27 22:13 func1.o
-rw-rw-r--  1 zld zld    97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld  1512 Sep 27 22:14 func2.o
-rwxrwxr-x  1 zld zld 16080 Sep 27 23:09 h6*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:20 hello2*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:26 hello3*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:27 hello4*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:27 hello5*
-rw-rw-r--  1 zld zld  2948 Sep 27 22:16 libhello.a
-rw-rw-r--  1 zld zld   101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld  1432 Sep 27 22:20 main.o
-rw-rw-r--  1 zld zld    44 Sep 27 22:05 mylib.h

在上面的命令gcc main.o -L. -lhello -o h6中,-L.表示指定搜索库库文件目录为当前目录(所以用的是点号.),然后指定外部库为-lhello

从上面的执行结果可以看到,成功生成了可执行文件h6。

-L也可以用绝对路径:

zld@zld:~/Codes/tmp0926$ gcc main.o -L/home/zld//Codes/tmp0926 -lhello -o h7
zld@zld:~/Codes/tmp0926$ ll
total 136
drwxrwxr-x  2 zld zld  4096 Sep 27 23:11 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld    63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld  1224 Sep 27 22:13 func1.o
-rw-rw-r--  1 zld zld    97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld  1512 Sep 27 22:14 func2.o
-rwxrwxr-x  1 zld zld 16080 Sep 27 23:09 h6*
-rwxrwxr-x  1 zld zld 16080 Sep 27 23:11 h7*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:20 hello2*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:26 hello3*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:27 hello4*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:27 hello5*
-rw-rw-r--  1 zld zld  2948 Sep 27 22:16 libhello.a
-rw-rw-r--  1 zld zld   101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld  1432 Sep 27 22:20 main.o
-rw-rw-r--  1 zld zld    44 Sep 27 22:05 mylib.h

下面使用第2种方法:

# 首先创建环境变量
zld@zld:~/Codes/tmp0926$  env | grep LIB
zld@zld:~/Codes/tmp0926$ pwd
/home/zld/Codes/tmp0926
zld@zld:~/Codes/tmp0926$ export LIBRARY_PATH=/home/zld/Codes/tmp0926:$LIBRARY_PATH
zld@zld:~/Codes/tmp0926$ env | grep LIB
LIBRARY_PATH=/home/zld/Codes/tmp0926:# 执行命令
zld@zld:~/Codes/tmp0926$ gcc main.o -lhello -o h8
zld@zld:~/Codes/tmp0926$ ll
total 152
drwxrwxr-x  2 zld zld  4096 Sep 27 23:14 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld    63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld  1224 Sep 27 22:13 func1.o
-rw-rw-r--  1 zld zld    97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld  1512 Sep 27 22:14 func2.o
-rwxrwxr-x  1 zld zld 16080 Sep 27 23:09 h6*
-rwxrwxr-x  1 zld zld 16080 Sep 27 23:11 h7*
-rwxrwxr-x  1 zld zld 16080 Sep 27 23:14 h8*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:20 hello2*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:26 hello3*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:27 hello4*
-rwxrwxr-x  1 zld zld 16080 Sep 27 22:27 hello5*
-rw-rw-r--  1 zld zld  2948 Sep 27 22:16 libhello.a
-rw-rw-r--  1 zld zld   101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld  1432 Sep 27 22:20 main.o
-rw-rw-r--  1 zld zld    44 Sep 27 22:05 mylib.h

链接顺序

我们前面说过,将几个目标文件进行链接时,各个目标文件的位置在哪是没有关系的,比如下面的几种链接顺序都是可以生成可执行程序的:

zld@zld:~/Codes/tmp0926$ gcc main.o func1.o func2.o -o hello3
zld@zld:~/Codes/tmp0926$ ./hello3 
The result is 3
zld@zld:~/Codes/tmp0926$ gcc func1.o func2.o main.o -o hello4
zld@zld:~/Codes/tmp0926$ ./hello4
The result is 3
zld@zld:~/Codes/tmp0926$ gcc func2.o func1.o main.o -o hello5
zld@zld:~/Codes/tmp0926$ ./hello5
The result is 3

那我们之前的命令:

gcc -Wall main.c libhello.a -o hello6
或者
gcc main.o libhello.a -o hello7

可以将main.c(或者main.o)与libhello.a的顺序对调吗?也就是说,下面的命令会执行正确吗?

gcc -Wall libhello.a main.c -o hello
gcc libhello.a main.o -o hello7

我们运行一下,发现两条命令都报错了:

zld@zld:~/Codes/tmp0926$ gcc -Wall libhello.a main.c -o hello
/usr/bin/ld: /tmp/ccxCA1TA.o: in function `main':
main.c:(.text+0x17): undefined reference to `func1'
/usr/bin/ld: main.c:(.text+0x24): undefined reference to `func2'
collect2: error: ld returned 1 exit statuszld@zld:~/Codes/tmp0926$ gcc libhello.a main.o -o hello7
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x17): undefined reference to `func1'
/usr/bin/ld: main.c:(.text+0x24): undefined reference to `func2'
collect2: error: ld returned 1 exit status

链接器的主要任务之一就是解析符号引用,即将目标文件和库文件中的未解析符号(如函数和变量)与其他文件或库中的已定义符号进行匹配。链接器按照命令中指定的顺序依次处理每个目标文件和库文件。如果被依赖的目标文件(或库)在引用它的目标文件(或库)之前被处理,链接器将无法找到并解析这些符号引用,因为它们还未被加入到符号集合中。这有可能导致undefined reference错误。

在上面的命令gcc libhello.a main.o -o hello7中,gcc是按照从左到右的顺序依次处理的,这样被依赖的库libhello.a先被处理,而引用它的目标文件main.o后被处理,因此就无法解析func1func2函数了。

动态库文件

和静态库对应的还有动态库。既然已经有了静态库,那为什么还要动态库呢?这是由于静态库的特点导致的。我们前面说过,如果多个程序使用相同的静态库,每个程序都会在自己的可执行文件中包含一份库代码的副本。这样会导致内存空间的浪费。并且如果静态库中的代码需要更新或者修复,必须重新编译链接所有依赖于该库的程序,这样会导致难以维护和更新。

正是由于静态库的这些缺点,引入了动态库。

动态库文件,也称为动态库链接(Dynamic Link Library,简称DLL)在Windows系统上,或在Linux和Unix系统上称为共享对象库(Shared Object Library,简称so),是一种包含可被多个程序同时使用的代码和数据的库文件。

使用动态库可以节省内存空间,因为多个程序可以共享同一个库文件中的代码,而不需要在每个程序的可执行文件中都包含一份副本。此外,它还有助于软件更新,因为只需要更新一个库文件,而不必重新编译所有依赖该库的应用程序。

动态库的主要特点包括:

  • 1.共享性:多个应用程序可以同时使用同一个动态库中的函数和资源,这有助于减少系统的整体内存占用。
  • 2.可更新性:如果动态库中的错误被修复或者功能得到增强,只需要替换掉旧版本的库文件,无需重新编译或重新安装依赖此库的所有应用程序。
  • 3.模块化:动态库支持将大型应用程序分解为更小、更易于管理的部分,这有利于团队合作开发和维护。

下面总结一下动态库文件和静态库文件的区别:

  • 1、链接时机:动态库文件在程序运行时被加载到内存中,而静态库文件在程序编译链接时被整合到可执行文件中。
  • 2、文件大小与磁盘空间:动态库文件不会增加可执行文件的大小,多个程序可以共享一个动态库文件,节省磁盘空间。静态库文件会增加可执行文件的大小,因为静态库的代码被完全复制到了可执行文件中,如果多个程序使用同一个静态库,会造成存储资源的浪费。
  • 3、运行时依赖:动态库文件编译的程序在运行时需要外部库文件的支持,如果动态库缺失或版本不匹配,程序可能无法正常运行。静态库文件编译的程序在运行时不需要外部库文件的支持,因为它们已经包含了所有必要的代码和数据。
  • 4、更新与维护:动态库文件可以独立于程序进行更新,当需要更新时,只需要替换掉旧的动态库文件,无需重新编译链接依赖于它的所有程序。静态库文件更新时需要重新编译链接所有依赖该库的程序,以确保所有程序都使用更新后的代码。
  • 5、性能与内存使用:动态库文件在程序运行时被加载,可能会增加一些加载时间,但多个程序可以共享同一个动态库文件,节省内存使用。静态库文件在程序运行时已经加载到内存中,无需额外的加载时间,但可能会导致内存使用量的增加,尤其是当多个程序使用同一个静态库时。

在实际中,动态库用得更多。

创建和使用动态库

在Linux上,动态链接库的名字形式为 libxxx.so,前缀是lib,后缀名为".so"。

可以通过gcc编译器使用-fPIC-shared选项来创建共享对象文件(.so文件)。其中-fPIC 创建与地址无关的编译程序(pic,position independent code),是为了能够在多个应用程序间共享。

gcc -fpic -shared 源文件名... -o 动态库文件或者先使用 gcc -c 指令将指定源文件编译为目标文件,再由目标文件生成动态链接库
gcc -c -fPIC 源文件名... -o 目标文件
gcc -shared 目标文件... -o 动态库文件

我们对之前的4个文件,将func1.cfunc2.c合并创建一个动态库文件。

zld@zld:~/Codes/tmp0926$ ll
total 24
drwxrwxr-x  2 zld zld 4096 Sep 27 23:59 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld   63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld   97 Sep 27 22:06 func2.c
-rw-rw-r--  1 zld zld  101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld   44 Sep 27 22:05 mylib.hzld@zld:~/Codes/tmp0926$ gcc -fpic -shared func1.c func2.c -o libhello.so
zld@zld:~/Codes/tmp0926$ ll
total 40
drwxrwxr-x  2 zld zld  4096 Sep 28 00:04 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld    63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld    97 Sep 27 22:06 func2.c
-rwxrwxr-x  1 zld zld 15600 Sep 28 00:04 libhello.so*
-rw-rw-r--  1 zld zld   101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld    44 Sep 27 22:05 mylib.h

然后,像静态库链接一样,和main.c一起生成可执行文件:

zld@zld:~/Codes/tmp0926$ gcc main.c libhello.so -o h1
zld@zld:~/Codes/tmp0926$ ll
total 56
drwxrwxr-x  2 zld zld  4096 Sep 28 00:10 ./
drwxrwxr-x 11 zld zld  4096 Sep 26 18:54 ../
-rw-rw-r--  1 zld zld    63 Sep 27 22:04 func1.c
-rw-rw-r--  1 zld zld    97 Sep 27 22:06 func2.c
-rwxrwxr-x  1 zld zld 15968 Sep 28 00:10 h1*
-rwxrwxr-x  1 zld zld 15600 Sep 28 00:04 libhello.so*
-rw-rw-r--  1 zld zld   101 Sep 27 22:07 main.c
-rw-rw-r--  1 zld zld    44 Sep 27 22:05 mylib.h
zld@zld:~/Codes/tmp0926$ ./h1 
./h1: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory

发现执行可执行程序h1时失败,说无法找到动态库文件libhello.so

运行由动态库生成的可执行文件时,必须确保程序在运行时可以找到这个动态库。和静态库文件一样,可以通过添加动态库的环境变量来解决,或者在执行上述gcc命令时添加动态库路径。

# 通过添加动态库环境变量解决
zld@zld:~/Codes/tmp0926$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/zld/Codes/tmp0926
zld@zld:~/Codes/tmp0926$ export LD_LIBRARY_PATH
zld@zld:~/Codes/tmp0926$ ./h1 
The result is 3# 在执行gcc命令时添加动态库链接
zld@zld:~/Codes/tmp0926$ gcc main.c -lhello -L. -o h2
zld@zld:~/Codes/tmp0926$ ./h2 
The result is 3zld@zld:~/Codes/tmp0926$ gcc main.c libhello.so -L. -o h3
zld@zld:~/Codes/tmp0926$ ./h3
The result is 3

同静态库文件一样,动态库文件在链接时也有顺序要求,如果将上述命令换一个顺序,则也会报错:

zld@zld:~/Codes/tmp0926$ gcc libhello.so main.c -L. -o h4
/usr/bin/ld: /tmp/ccLWWSlT.o: in function `main':
main.c:(.text+0x17): undefined reference to `func1'
/usr/bin/ld: main.c:(.text+0x24): undefined reference to `func2'
collect2: error: ld returned 1 exit status

Warning编译选项

我们在前面的一些例子中,增加了-Wall选项,这个选项就是一个编译告警选项。事实上,在实际的项目中,一般总是会增加告警选项的。

比如下面的一段程序中,在打印printf("MAX + MIN = %f\n", MAX + MIN);中,我们将格式化输出写成%f了(即浮点型),但是实际结果是一个整型。

[zld@localhost 0926]$ cat test.c 
#include <math.h>
#include <stdio.h>
#define MAX 3
#define MIN 1
int main()
{printf("MAX + MIN =  %f\n", MAX + MIN);return 0;
}

如果不加告警选项,则不会有告警信息,并且运行结果不符合预期:

[zld@localhost 0926]$ gcc test.c -o test
[zld@localhost 0926]$ ./test 
MAX + MIN =  0.000000

因此,无论如何,都建议加上告警选项,提前识别出告警风险。

# 加上告警选项上,就有告警信息提示了
[zld@localhost 0926]$ gcc -Wall test.c -o test2
test.c: In function ‘main’:
test.c:7: warning: format ‘%f’ expects type ‘double’, but argument 2 has type ‘int’# 根据提示,修改程序。然后告警消失
[zld@localhost 0926]$ cat test.c 
#include <math.h>
#include <stdio.h>
#define MAX 3
#define MIN 1
int main()
{printf("MAX + MIN =  %d\n", MAX + MIN);     #这里改成%dreturn 0;
}
[zld@localhost 0926]$ gcc -Wall test.c -o test3
[zld@localhost 0926]$ ./test3 
MAX + MIN =  4      # 运行结果正确

一些常用的告警选项:

  • 1、-w(小写)禁止所有告警信息

  • 2、以**-W**(大写)开头开启特定的告警。比如

    -Wreturn-type(返回值告警),
    -Wsign-compare(有符号和无符号对比告警)
    -Wall (除extra外的所有告警)
    -Wextra (all外的其他告警)
  • 3、以“-Wno-”开头关闭特定的警告;

例如:
-Wno-return-type (取消返回值告警)
-Wno-sign-compare(取消有符号和无符号对比告警)
  • 4、将告警转变成错误。
    • -Werror :所有告警当错误报
    • -Werror= 将指定的警告转换为错误。
    • 反过来:-Wno-error取消编译选项-Werror

GCC定义了非常多的告警信息,这里就不一一列出来了,可以直接参考gcc文档。

调试信息(-g)

一般来说,之前的gcc命令生成的可执行程序都不包含调试信息,如果程序崩溃了,那么则获取崩溃的文件名以及行号。

gcc提供了-g 调试选项,这样生成的可执行程序,倘若出现问题,便可以使用 gdb 找出问题具体出现的位置,便于问题的解决。

例如,对于下面一段有问题的程序:

[zld@localhost 0926]$ cat -n null.c 1  int a(int *p);23  int main()4  {5          int *p = 0;6          return a(p);7  }89  int a(int *p)10  {11          int y = *p;12          return y;13  }

在第5行,因为p赋值为0,也就是NULL。因此在调用函数a时,在第11行对指针进行解引用时,会出错。但是这是运行时错误,因此在编译时,编译器不会报错:

[zld@localhost 0926]$ gcc -Wall -g null.c -o null
[zld@localhost 0926]$ 

但是在运行时,就会报错:

[zld@localhost 0926]$ ./null 
Segmentation fault (core dumped)

但是看当前目录,并没有产生core文件,这是因为很多操作系统默认是不产生core文件的。可以通过ulimit -c查看。如果是0,则说明不会产生core文件。

[zld@localhost 0926]$ ulimit -c
0

可以通过ulimit -c unlimited来修改可以产生core文件:

[zld@localhost 0926]$ ulimit -c unlimited
[zld@localhost 0926]$ ulimit -c
unlimited

再次运行,就产生了core文件了:

[zld@localhost 0926]$ ./null 
Segmentation fault (core dumped)
[zld@localhost 0926]$ ll
-rw-------. 1 zld zld 188416 Sep 26 03:47 core.6484

然后就可以用gdb工具来定位了:

[zld@localhost 0926]$ gdb null core.6484 
zld@zld:~/Codes/tmp0926$ gdb null core 
GNU gdb (Ubuntu 14.0.50.20230907-0ubuntu1) 14.0.50.20230907-git
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:<http://www.gnu.org/software/gdb/documentation/>.For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from null...
[New LWP 2738]This GDB supports auto-downloading debuginfo from the following URLs:<https://debuginfod.ubuntu.com>
Enable debuginfod for this session? (y or [n]) n
Debuginfod has been disabled.
To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `./null'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000625194ea415b in a (p=0x0) at null.c:13
13                      int y = *p;
(gdb) backtrace 
#0  0x0000625194ea415b in a (p=0x0) at null.c:13
#1  0x0000625194ea4149 in main () at null.c:7

由于本文是介绍gcc的使用,因此关于gdb如何使用,本文不做过多描述。大家可以在网上搜索如何使用gdb。

编译优化

gcc 提供了为了满足用户不同程度的的优化需要,提供了近百种优化选项,用来对{编译时间,目标文件长度,执行效率}这个三维模型进行不同的取舍和平衡。优化的方法不一而足,总体上将有以下几类:1)精简操作指令;2)尽量满足cpu的流水操作;3)通过对程序行为地猜测,重新调整代码的执行顺序;4)充分使用寄存器;5)对简单的调用进行展开等等。想全部了解这些编译选项,并在其中挑选适合的选项进行优化,无疑像个噩梦般的过程。

幸好gcc提供了从O0O3这几种不同的优化级别供大家选择。

在编译时,如果没有指定上面的任何优化参数,则默认为 -O0,即没有优化。

参数 -O1-O2-O3 中,随着数字变大,代码的优化程度也越高,不过这在某种意义上来说,也是以牺牲程序的可调试性为代价的,因此优化和调试是一对矛盾体

举例:

下面一段代码:

zld@zld:~/Codes/tmp0926$ cat test.c 
#include <stdio.h>double powern(double d, unsigned n)
{double x = 1.0;unsigned j;for (j=1;j<=n;j++){x *= d;}return x;
}int main()
{double sum = 0.0;unsigned i;// 循环20亿次for (i=1;i <=2000000000;i++){sum += powern(i, i%5);}printf("sum = %g\n", sum);return 0;
}

用不同的优化级别看下运行的时间,从结果上看,随着优化级别的调高,相应的运行时间也逐渐减少。

zld@zld:~/Codes/tmp0926$ gcc -Wall -O0 test.c -o O0
zld@zld:~/Codes/tmp0926$ time ./O0 
sum = 1.28e+45real    0m12.983s
user    0m12.963s
sys     0m0.000szld@zld:~/Codes/tmp0926$ gcc -Wall -O1 test.c -o O1
zld@zld:~/Codes/tmp0926$ time ./O1 
sum = 1.28e+45real    0m3.522s
user    0m3.517s
sys     0m0.000szld@zld:~/Codes/tmp0926$ gcc -Wall -O2 test.c -o O2
zld@zld:~/Codes/tmp0926$ time ./O2 
sum = 1.28e+45real    0m3.245s
user    0m3.233s
sys     0m0.004s
zld@zld:~/Codes/tmp0926$ zld@zld:~/Codes/tmp0926$ gcc -Wall -O3 test.c -o O3
zld@zld:~/Codes/tmp0926$ time ./O3 
sum = 1.28e+45real    0m3.124s
user    0m3.118s
sys     0m0.000s# 加上循环展开优化(-funroll-loop),又进一步优化了
zld@zld:~/Codes/tmp0926$ gcc -Wall -O4 -funroll-loops test.c -o O4
zld@zld:~/Codes/tmp0926$ time ./O4 
sum = 1.28e+45real    0m2.755s
user    0m2.748s
sys     0m0.000s

我们上面说过,编译优化和调试信息(-g)通常来说是一对矛盾体。我个人的建议是要优先保证调试信息,毕竟如果没有调试信息,当程序崩溃时,我们都无法定位,这在实际工作中是一件非常麻烦的事,得加班加点了。。。。。

总结

这个章节是对前面章节用到的一些gcc命令以及选项做一个总结。

参数选项含义
-E仅执行预处理,不进行编译、汇编和链接(生成后缀为 .i 的预编译文件)
-S执行编译后停止,不进行汇编和链接(生成后缀为 .s 的预编译文件)
-c编译程序,但不链接成为可执行文件(生成后缀为 .o 的文件)
-o对输出文件命名
-O/-O1/-O2/-O3优化代码,减少代码体积,提高代码效率,但是相应的会增加编译的时间
-lNAME(这里是小写的L)指定程序要链接的库,NAME为库文件名称。可以是静态库文件,也可以是动态库文件
-Ldir指定-l(小写-L)所使用到的库文件所在路径。dir为具体的路径
-Idir(这里是大写的i)增加 include 头文件路径。dir为具体的路径
-DNAME预定义宏,NAME为相应的宏名称
-shared生成共享文件,然后可以与其它文件链接生成可执行文件
-fpic生成适用于共享库的与地址无关的代码(PIC)
-w不输出任何警告信息
-Wall开启编译器的批量告警选项
-Werror将所有的警告当成错误进行处理,在所有产生警告的地方停止编译
-g生成调试信息,方便gdb调试
-save-temps保存编译中间结果
-v显示gcc执行时的详细过程
–version显示gcc的版本信息

参考文献

1、GCC, the GNU Compiler Collection - GNU Project

2、https://blog.csdn.net/bandaoyu/article/details/115419255

3、https://blog.csdn.net/qq_31108501/article/details/51842166
4、https://www.runoob.com/w3cnote/gcc-parameter-detail.html
5、文心一言

相关文章:

GCC使用入门

文章目录 GCC简介单个文件编译过程预处理(Preprocessing)编译(Compilation)汇编(Assembly)链接(Linking) 多文件编译过程头文件搜索路径三种不推荐的方法两种推荐的方法 库文件静态库文件创建和使用静态库链接顺序 动态库文件创建和使用动态库 Warning编译选项调试信息(-g)编译…...

CSS3 字体

CSS3 字体 CSS3字体是网页设计和开发中的一个重要方面&#xff0c;它允许设计师使用各种字体来增强网页的视觉效果和用户体验。在本文中&#xff0c;我们将探讨CSS3字体的基本概念、特性、使用方法以及最佳实践。 1. CSS3字体基本概念 CSS3字体是指使用CSS3样式表来控制网页…...

LeetCode题练习与总结:为运算表达式设计优先级--241

一、题目描述 给你一个由数字和运算符组成的字符串 expression &#xff0c;按不同优先级组合数字和运算符&#xff0c;计算并返回所有可能组合的结果。你可以 按任意顺序 返回答案。 生成的测试用例满足其对应输出值符合 32 位整数范围&#xff0c;不同结果的数量不超过 10^…...

金融科技革命:API接口开放平台,畅通金融服务之路

金融科技是近年来蓬勃发展的领域&#xff0c;它利用先进的技术手段来改善和创新金融服务。在金融科技的革命中&#xff0c;API接口开放平台扮演着重要的角色&#xff0c;它通过提供统一的接口服务&#xff0c;让金融机构和其他行业能够更方便地进行数据交换和合作。本文将以挖数…...

Java8后新特性介绍

1.接口私有方法&#xff08;Java9&#xff09; 在Java9之前&#xff0c;interface接口只能定义abstract抽象方法和default默认方法。如果有多个默认方法使用了相同的处理逻辑&#xff0c;那只能写重复代码&#xff0c;或者再单独建个类进行调用。Java9解决了此类问题&#xff…...

Arthas monitor(方法执行监控)

文章目录 二、命令列表2.3 monitor/watch/trace/stack/tt 相关2.3.1 monitor&#xff08;方法执行监控&#xff09;举例1&#xff1a;监控demo.MathGame类&#xff0c;并且每5S更新一次状态。 二、命令列表 2.3 monitor/watch/trace/stack/tt 相关 使用场景&#xff1a; monit…...

语言的副作用

副作用产生于表达式中有至少一处计算&#xff0c;且其中全部或部分计算会影响表达式其他项&#xff0c;这可能产生副作用。编译器的优化很可能凸显副作用。 赋值 副作用并非都是有害的&#xff0c;比如基本的赋值 a b, 对a而言是产生副作用&#xff0c;但完成了赋值要求。 序…...

centos磁盘逻辑卷LVM创建

centos磁盘逻辑卷LVM创建 一、磁盘逻辑卷LVM说明二、centos磁盘使用情况三、LVM安装指南1.LVM工具安装1. yum list lvm2. yum search lvm3. yum search pvcreate4. yum list lvm25. yum install lvm2 2.创建物理卷2.1磁盘情况查看2.2创建物理卷&#xff08;PV&#xff09; 3.创…...

BUUCTF蜘蛛侠呀

解压后发现是流量包&#xff0c;好多icmp包 发现icmp包尾部有$$STRAT打头16进制的字符串&#xff0c;好多重复得。我们只需要提取尾部这些字符串是当icmp的type0时上图标识为褐色的字符串&#xff0c;还需要把16进制的字符串转为对应的字符串&#xff08;bytes 类型&#xff09…...

大数据新视界 --大数据大厂之基于 MapReduce 的大数据并行计算实践

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…...

win自带录屏怎么用?让视频制作更简单!

win自带录屏怎么用&#xff1f;Windows系统内置的录屏功能&#xff0c;以其便捷高效著称&#xff0c;轻松满足多样化需求。无论是快速捕捉会议要点、制作教学视频&#xff0c;还是直播精彩游戏瞬间&#xff0c;都能一键启动&#xff0c;无缝录制。无需额外安装软件&#xff0c;…...

修改Kali Linux的镜像网站

由于官方的镜像可能会出现连接不上的问题导致无法安装我们所需要的包&#xff0c;所以需要切换镜像站为国内的&#xff0c;以下是一些国内常用的Kali Linux镜像网站&#xff0c;它们提供了与Kali Linux官方网站相同的软件包和资源&#xff0c;但访问速度更快&#xff1a; 清华…...

Docker精讲:基本安装,简单命令及核心概念

docker服务部署 docker是一个容器管理工具&#xff0c;其内部容器才是具体服务&#xff0c;所以我们在安装docker时不需要有太多定制内容&#xff0c;只需要通过yum安装即可 1. 更新系统包 #更新现有依赖包&#xff0c;防止现有依赖包版本过低影响docker安装 yum update2. 安…...

利用git将项目上传到github

采用git而不是在pycharm中共享的原因&#xff1a;可能会出现上图报错 目录 1、创建github仓库2、在 git bash 中初始化Git仓库&#xff0c;添加文件&#xff0c;上传代码 1、创建github仓库 2、在 git bash 中初始化Git仓库&#xff0c;添加文件&#xff0c;上传代码...

828华为云征文 | 华为云X实例CPU性能测试详解与优化策略

目录 引言 1. 测试环境搭建 1.1 测试实例的选择 1.2 CPU性能测试工具介绍 1.3 安装和配置Sysbench 2. CPU性能测试方法 2.1 测试场景设定 2.2 Sysbench单线程CPU性能测试 2.3 Sysbench多线程CPU性能测试&#xff08;4线程&#xff09; 2.4 高强度多线程CPU性能测试&a…...

ass字幕文件怎么导入视频mp4?ass字幕怎么编辑?视频加字幕超简单!

ass字幕文件怎么导入视频mp4&#xff1f;ass字幕怎么编辑&#xff1f;在视频制作和观看过程中&#xff0c;添加字幕是一项常见的需求&#xff0c;特别是对于外语视频或需要辅助阅读的场景。ASS&#xff08;Advanced SubStation Alpha&#xff09;字幕文件是一种常用的字幕格式&…...

camunda + oracle 启动报错 解决方法

启动报错如下&#xff1a; java.sql.SQLException: sql injection violation, comment not allow : select * from ( select a.*, ROWNUM rnum from (select RES.ID_,RES.REV_,RES.DUEDATE_,RES.PROCESS_INSTANCE_ID_,RES.EXCLUSIVE_from ACT_RU_JOB RESwhere (RES.RETRIES_ &g…...

变幅液压系统比例阀放大器

变幅液压系统是用于控制起重机或类似设备臂架角度变化的关键系统&#xff0c;它通过调节液压缸的伸缩来实现臂架的升降和变幅。以下是一些关于变幅液压系统的基本原理、组成和应用领域的信息&#xff1a; 基本原理&#xff1a;变幅液压系统通常由液压泵、液压缸、液压马达、控制…...

在 Ubuntu 安装 Python3.7(没有弯路)

注&#xff1a;当前Ubuntu版本为18.04 下载Python源码包 wget https://www.python.org/ftp/python/3.7.12/Python-3.7.12.tgz安装前准备 安装依赖组件 apt-get updateapt-get install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libs…...

Linux 简易shell编写

shell shell是壳&#xff0c;外壳的意思&#xff0c;一般我们使用linux系统有用图形化界面的也有使用命令行界面的&#xff0c;这两个都是一种shell&#xff0c;以命令行为例&#xff1a; 如图这个就是我这里的命令行格式&#xff0c;在$符后面写的就是执行的指令&#xff0c;…...

POLYGON Nature - Low Poly 3D Art by Synty 树木植物

一个低多边形资源包,包含可以添加到现有多边形风格游戏中的树木、植物、地形、岩石、道具和特效 FX 资源。 为 POLYGON 系列提供混合样式树这一新增功能。弥合 POLYGON 与更传统的层级资源之间的差距。还提供了一组经典的 POLYGON 风格的树木和植被以满足你的需求。 该包还附带…...

了解什么是瞪羚企业

瞪羚企业”是指以科技创新或商业模式创新为支撑&#xff0c;进入高成长期的中小企业。识别范围主要是符合国家和省战略性新兴产业发展方向的产业领域&#xff0c;涵盖新兴产业、新一代信息技术&#xff08;包括大数据、物联网和云计算、高端软件、互联网&#xff09;、生物健康…...

寻找两个正序数的中位数(C)

最近面试&#xff0c;发现要手撕算法加上机试&#xff0c;被完败&#xff0c;索性给自己立一个目标&#xff0c;一周训练2次。 第一题。 给定两个大小分别为 m 和 n 的正序&#xff08;从小到大&#xff09;数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。 …...

YOLOv10涨点改进:IoU优化 | Unified-loU,用于高品质目标检测的统一loU ,2024年8月最新IoU

💡💡💡现有IoU问题点:IoU (Intersection over Union)作为模型训练的关键,极大地显示了当前预测框与Ground Truth框之间的差异。后续研究者不断在IoU中加入更多的考虑因素,如中心距离、纵横比等。然而,仅仅提炼几何差异是有上限的;而且新的对价指数与借据本身存在潜在…...

Spring Boot 实现动态配置导出,同时支持公式和动态下拉框渲染和性能优化案例示范

在业务系统中&#xff0c;数据导出是一个非常常见且重要的功能&#xff0c;本文将详细介绍如何在 Spring Boot 中实现这一功能&#xff0c;并结合 MySQL 数据库、MyBatis 作为数据访问层&#xff0c;EasyExcel 作为导出工具&#xff0c;展示如何在电商交易系统中搭建灵活、可扩…...

一网打尽 运维必封的50个高危端口清单,零基础入门到精通,收藏这一篇就够了

文件传输相关端口&#xff1a; • TCP 20、21&#xff1a;FTP 服务&#xff08;文件传输协议&#xff09;端口&#xff0c;FTP 传输数据时未加密&#xff0c;容易受到攻击&#xff0c;如匿名上传下载、爆破、嗅探、远程执行等攻击&#xff0c;可能导致敏感文件泄露。 • TCP …...

方法 WebDriverWait

定义&#xff1a; WebDriverWait是Selenium WebDriver提供的一个工具类&#xff0c;它允许你设置等待条件&#xff0c;直到这个条件成立&#xff0c;才继续执行代码。这对于处理网页上的异步加载元素特别有用&#xff0c;比如等待某个元素变得可见、可点击等。 from se…...

LOESS(Locally Estimated Scatterplot Smoothing)

文章目录 LOESS 原理详解&#xff1a;LOESS 的优点&#xff1a;LOESS 的缺点&#xff1a;Python 实现代码&#xff1a;代码说明&#xff1a; LOESS&#xff08;Locally Estimated Scatterplot Smoothing&#xff09;&#xff0c;即局部加权回归&#xff0c;是一种非参数回归方法…...

每天学习一个技术栈 ——【Django Channels】篇(1)

在当今快速发展的技术领域&#xff0c;掌握多种技术栈已经成为开发者提升竞争力的关键。随着实时应用需求的不断增加&#xff0c;如何高效地处理并发请求和实时通信变得尤为重要。在众多解决方案中&#xff0c;Django Channels作为Django框架的强大扩展&#xff0c;能够轻松实现…...

js设计模式-工厂模式 单例模式 观察者模式 发布订阅模式 原型模式 代理模式 迭代器模式

1 工厂模式 // 工厂模式: 调用函数返回对象function factory(name, age){return {name: name,age: age} }const person1 factory(Tom, 18); // 类似的库使用工厂函数的有: jQuery, React.createElement,axios.create,vue.createApp等 2 单例模式 // 单例模式&#xff1a;单…...