HeadFirstC笔记_8 静态库与动态库:热插拔代码

#include中的尖括号代表标准头文件
如果在 #include 语句中使用尖括号,编译器就会在标准头文件目 录中查找头文件,而不是当前目录。
为了用 本地头文件编译程序,需要把尖括号换成双引号("xxx.h"):
   
    
    
  1. #include <stdio.h> // 标准头文件
  2. #include "encrypt.h" // 本地头文件
  3. #include "checksum.h"

标准头文件目录在哪里?
通常类UNIX操作 系统(如Mac或Linux)中,编译器会在以下 目录查找头文件:
   
    
    
  1. /usr/local/include -->通常用来放第三方库的头文件。
  2. /usr/include --> 一般用来放操作系统的头文件。
如果你windows上用的是MinGW版的gcc,编译器会在 下面这个目录中查找:
     
      
      
  1. C:\MinGW\include

如何共享代码?
假设你希望在不同程序之间共享两类代码:.h头文件 和.o目标文件,他们分散在计算机的不同地方,该怎么做?

共享.h头文件的3种方法:
1.把头文件保存在标准目录中
只要头文件在标准目录中,就可以用尖括 号包含它们:
   
    
    
  1. #include <encrypt.h>
2.在include语句中使用完整路径名
如果你想把头文件放在其他地方,如/my_header_ files,可以 把目录名加到 include 语句中:
   
    
    
  1. #include "/my_header_files/encrypt.h"
3.你可以告诉编译器去哪里找头文件
可以使用 gcc 的 -I 选项, 告诉编译器去哪里找头文件:
   
    
    
  1. 命令:gcc -I/my_header_files test_code.c ... -o test_code
-I 选项告诉 gcc 编译器还可以去哪里找头文件。编译器会 先检查 -I 选项中的目录,然后像往常一样检查所有标准目 录。

用完整路径名共享.o目标文件
可以把.o目标文件放在一个类似共享目录的地方。当编译 程序时,只要在目标文件前加上完整路径就行了:
   
    
    
  1. gcc -I/my_header_files test_code.c
  2. /my_object_files/encrypt.o
  3. /my_object_files/checksum.o -o test_code
/my_object_files就好比一个中央仓库,专门用来保存目标文件。
共享一、两个目标文件还好, 但如果数量很多呢?
有没有什么办法可 以告诉编译器我想共享一大堆目标文件?

只要创建目标文件存档,就可以一次告诉编译器一批目标文件。
把一批目标文件打包在一起就成了存档文件。创建 安全代码的存档文件,就可以很方便地在多个项目 之间共享代码。

存档中包含多个.o文件
打开终端或命令提示符,进入某个库目录,比 如/usr/lib或C:\MinGW\lib,库代码就放在这些目录 下。
你可以在库目录中看到一大批.a存档,你可以 用 nm 命令查看存档中的内容,如:
    
     
     
  1. 命令:nm libl.a

nm 命令列出了存档中保存文件的名字。libl.a有两 个目标文件:libmain.o和libyywrap.o。

用ar命令创建存档
存档命令( ar )会在存档文件中保存一批目标文件:
    
     
     
  1. 命令:ar -rcs libhfsecurity.a encrypt.o checksum.o
  2. -rcs 中的r:表示如果.a文件存在就更新它;c表示创建存档时不显示反馈信息;s告诉ar要在.a文件开头建立索引。
  3. libhfsecurity.a :要创建的存档名称
  4. 后面两个.o文件是要保存进去的目标文件
注意:务必把存档命名为 libXXX.a的形式
这是命名存档的标准方式,存档是静态库(static
library),所以要以lib开头,若不按此方式,编译器就找不到!

将.a文件保存在指定库目录下
1.把.a文件保存在标准目录中,如/usr/local/lib。
2.把.a文件放在其他自定义的库目录中,如/my_lib。

最后编译其他程序
创建库存档是为了能在其他程序中使用它,当你把存 档安装到标准目录后,就可以用 -l 开关编译代码:(小写的L,表示lib)
   
    
    
  1. 命令:gcc test_code.c -lhfsecurity -o test_code
  2. 1.别忘了,在用-l选项包含库之前列出源文件
  3. 2.hfsecurity叫编译器去找一个叫libhfsecurity.a的存档
  4. 3.如果要使用多个存档,可以设置多个-l选项
之所以把存档命名为libXXX.a,就是因为 -l 选 项后的名字必须与存档名的一部分匹配。如果你的存 档叫libawesome.a,那么就
可以用 -lawesome 开关编译程序。
如果想把存档放在非标准目录的其他地方,比如/my_lib,可以 用 -L 选项告诉编译器去哪个目录查找存档:
      
       
       
  1. 命令:gcc test_code.c -L/my_lib -lhfsecurity -o test_code

练习:
有人把自己的源 代码和encrypt、checksum的源代码放在了同一个目录下,他想在这 个目录中创建 libhfsecurity.a 存档,然后用它来编译程序,
你将帮助他补 全makefile。
encrypt.c文件:
       
        
        
  1. #include "encrypt.h"
  2. void encrypt(char *message) {
  3. while (*message) {
  4. *message = *message ^ 31;
  5. message++;
  6. }
  7. }
encrypt.h文件:
        
         
         
  1. void encrypt(char *message);
checksum.c文件:
         
          
          
  1. #include "checksum.h"
  2. int checksum(char *message) {
  3. int c = 0;
  4. while (*message) {
  5. c += c ^ (int) (*message);
  6. message++;
  7. }
  8. return c;
  9. }
checksum.h文件:
          
           
           
  1. int checksum(char *message);
bank_vault.c文件:
       
        
        
  1. #include <stdio.h>
  2. #include <checksum.h>
  3. #include <encrypt.h>
  4. // 如果这两个头文件被写进了静态库.a文件中,就可以使用尖括号,不过头文件要放在本目录中
  5. // 此时使用gcc编译,要告诉编译器头文件的路径,-I./headerpath,本目录就用-I.
  6. int main() {
  7. // char* str = "Hello World"; // 这里传字符指针的话就不行,因为你指向的常量区,后面加密想对此字符串修改是不行的。
  8. char s[] = "Speak friend and enter";
  9. encrypt(s);
  10. printf("Encrypted to '%s'\n", s);
  11. printf("Checksum is %i\n", checksum(s));
  12. encrypt(s);
  13. printf("Decrypted back to '%s'\n", s);
  14. printf("Checksum is %i\n", checksum(s));
  15. return 0;
  16. }
注意: bank_vault 程序用了尖括号的# include 语句: include   <encrypt.h>   #include   <checksum.h>
下面是makefile文件:
        
         
         
  1. encrypt.o: encrypt.c
  2. gcc -c encrypt.c -o encrypt.o // 注意命令的这行必须是tab键开头,不能用空格键
  3. checksum.o: checksum.c
  4. gcc -c checksum.c -o checksum.o
  5. libhfsecurity.a: encrypt.o checksum.o
  6. ar -rcs libhfsecurity.a encrypt.o checksum.o
  7. bank_vault: bank_vault.c libhfsecurity.a // 注意:windows下后两行要写到文件开头位置才会执行全部命令
  8. gcc bank_vault.c -I. -L. -lhfsecurity -o bank_vault
  9. // 需要-I.,因为头文件在“.”(当前)目录下。
编译运行:
 

问: ar命令的存档格式在所有系统中都是一样的吗?
答: 不是,虽然不同平台之间 存档格式区别不大,但存档中目标代 码的格式在不同操作系统中可谓天差 地别。

问: 创建库存档以后能不能查看里面的内容?
答: 可以,ar –t  libxxx.a 会列 出.a存档中的目标文件,如:

 
问: 存档会像可执行文件那样把目标文件链接在一起吗?
答: 不会,目标文件以独立文 件的形式保存在存档中。

问: 我可以把任何类型的文件放在存档中吗?
答: 不可以,ar命令会先检查 文件类型。

问: 我能从存档中提取某个目标文件吗?
答: 可以的,你可以使用ar –x libhfsecurity.a encrypt.o命令 把encrypt.o文件从libhfsecurity.a中提 取出来。

问: 为什么要叫“静态”链接?
答: 因为一旦链接以后就不能 修改。静态链接就好比在咖啡中加入 牛奶,混在一起就不能再分开。

静态链接的弊端:一旦链接,就不能改变。
问题是用这种方法构建的程序是静态的。一旦用这些独立的 目标代码创建了可执行文件,就没有办法修改这些原料,除 非重新构建整个程序。

解决之道:在运行时动态链接
可以把目标代码分别保存在单独的文件中,在程序运行 时才把它们动态链接到一起。

动态库——加强版目标文件
动态库和你屡屡创建的.o目标文件很像,但又不完全一
样。动态库和存档也很像,也可以从多个.o目标文件创
建。
不同的是,这些目标文件在动态库中链接成了一段
目标代码。

首先,创建目标文件
在把hfcal.c代码转换为动态库之前需要把它先编译 为.o目标文件,像这样:
       
        
        
  1. 命令:gcc -I./includes -fPIC -c hfcal.c -o hfcal.o
这次在创建hfcal.o时多加了一个标 志: -fPIC 。它告诉 gcc 你想创建位置无关代码。有 的操作系统和处理器要用位置无关代码创建库,
样它们才能在运行时决定把代码加载到存储器的哪 个位置。 事实上在大多数操作系统中都不需要加这个选择。 试试吧,不加也没有关系。

什么是位置无关代码?
位置无关代码就是无论计算机把它加载到存储器的哪个位置都可以运行的 代码。想象你有一个动态库,它要使用加载点500个字节以外的某个全局 变量的值,那么如果操作系统把库加载到其他地方就会出错。只要让编译 器创建位置无关的代码,就可以避免这种问题。
其实包括Windows在内的一些操作系统在加载动态库时会使用一种叫存储器映 射的技术,也就是说所有代码其实都是位置无关的。若你在Windows上用 刚刚那条命令编译代码,gcc可能会给出一条警告,告诉你不需要-fPIC 选项。你既可以奉命删除它,也可以当作没看见。

不同平台上动态库的叫法
在Windows中,动态库通常叫动
态链接库,后缀名是 .dll ;
在Linux和Unix上,它们叫共享目
标文件,后缀名 .so ;
在Mac上,它们就叫动态库,后缀
名 .dylib 。

创建动态库:
尽管后缀名不同,但创建它们的方法相同:

 

-shared 选项告诉 gcc 你想把.o目标文件转化为动态库。
注意:编译 器创建动态库时会把库的名字保存在文件中,假设你在Linux 中创建了一个叫libhfcal.so的库,那么libhfcal.so文件就会记 住它的库名叫fcal。也就是说,一旦你用某个名字编译了库, 就不能再修改文件名了, 若想重命名库,就必须用新的名字重新编译一次,这一点很重要!

使用动态库:
一旦创建了动态库,你就可以像静态库那样使用它。可以像
这样建立 elliptical 程序:
        
         
         
  1. gcc -I/include -c elliptical.c -o elliptical.o
  2. gcc elliptical.o -L/libs -lhfcal -o elliptical
尽管你使用的命令和静态存档一模一样,但两者编译的方式 不同。因为库是动态的,所以编译器不会在可执行文件中包 含库代码,而是插入一段用来查找库的“占位符”代码,并 在运行时链接库。

不同平台下如何运行动态库
Mac
你可以直接运行程序。当你在Mac中编译程序时,文件的完整路 径/libs/libhfcal.dylib保存在可执行文件中,程序启动时知道去哪里 找它。

 Linux
在Linux和大部分Unix中,编译器只会记录libhfcal.so库的文件名, 而不会包含路径名。也就是说如果不把hfcal库保存到标准目录(如 /usr/lib),程序就找不到它。为了解决这个问题,Linux会检查保 存在 LD_LIBRARY_PATH 变量中的附加目录。只要把库目录添加 到 LD_LIBRARY_PATH 中,并export 它, elliptical 就能找到 libhfcal.so。(export是一条Linux命令,用来将自定义变量设为环境变量 )。

 Windows
那用Cygwin和MinGW版 gcc 编译的代码呢?两种编译器都会创建 Windows下的DLL库与可执行文件。同Linux一样,Windows可执行文 件也只保存hfcal库的名字,不保存目录名。 不过Windows没有用 LD_LIBRARY_PATH 变量去找hfcal库。Windows 程序会先在当前目录下查找,如果没找到就去查找保存在 PATH 变量中 的目录。
Cygwin
如果用Cygwin编译了程序,可以在Bash shell中这样运行它:

 MinGW
如果用MinGW编译了程序,可以在命令提示符中这样运行它:
注意运行程序前,先配置PATH变量:   PATH=%PATH%;E:\eclipse_c_workspace\chapter8\src\lib

有点复杂,这就是为什么绝大部分使用动态库的程序 要把动态库保存在标准目录下。在Linux和Mac中,动态库通常保存 在/usr/lib或/usr/local/lib中;而在Windows中,程序员通常把.DLL和 可执行文件保存在同一个目录下。

下面是上面用到的代码:
hfcal.h----->放在/header目录下
           
            
            
  1. void display_calories(float weight, float distance, float coeff);
hfcal.c----->放在本目录下
            
             
             
  1. #include <stdio.h>
  2. #include <hfcal.h>
  3. void display_calories(float weight, float distance, float coeff) {
  4. printf("Weight: %3.2f lbs\n", weight);
  5. printf("Distance: %3.2f miles\n", distance);
  6. printf("Calories burned: %4.2f cal\n", coeff * weight * distance);
  7. }
elliptical.c------>放在本目录下,生成的动态库libhfcal.dll放在/lib目下
             
              
              
  1. // gcc elliptical.c -I./header -L./lib -lhfcal -o elliptical // 这里如果是从源代码elliptical.c开始生成可执行文件,那么这里 -I./header就不能省
  2. // gcc elliptical.o -L./lib -lhfcal -o elliptical // 如果是从目标文件 elliptical.o开始的话,那头文件路径就可以省掉,因为前面生成.o目标文件时已经包含了该路径
  3. // 这个hfcal可以是静态库hfcal.a ,也可以是动态库hfcal.dll,如果两者都有,他会先用静态库
  4. // gcc -shared hfcal.o -o ./lib/libhfcal.dll // 将.o文件编译成动态库.dll,放在./lib/目录下,libhfcal.dll前面的lib可要可不要,后面用的时候都可以用hfcal来找到
  5. // 如果使用动态库编译的exe文件和.dll在同一目录下,可以直接打开
  6. // 如果.dll在另一个目录./lib中的话,就需要将该目录设置到环境变量中
  7. // cmd中临时设置.dll所在目录lib的环境变量指令----》 PATH=%PATH%;E:\eclipse_c_workspace\chapter8\src\lib
  8. // 如何用eclipse自动生成.dll和.a文件
  9. // 在新建project里选择SharedLibrary或StaticLibrary,然后把源文件丢进去编译即可生成
  10. #include <stdio.h>
  11. #include <hfcal.h>
  12. int main() {
  13. display_calories(115.2, 11.3, 0.79);
  14. return 0;
  15. }

替换动态库
hfcal_UK.c----->放在本目录下
            
             
             
  1. // 英国板跑步机显示代码
  2. #include <stdio.h>
  3. #include <hfcal.h>
  4. void display_calories(float weight, float distance, float coeff) {
  5. printf("Weight: %3.2f kg\n", weight / 2.2046);
  6. printf("Distance: %3.2f km\n", distance * 1.609344);
  7. printf("Calories burned: %4.2f cal\n", coeff * weight * distance);
  8. }
将此代码重新编译成动态库libhfcal.dll,替换之前的,然后直接运行elliptical.exe,无需重新编译它,即可显示新的内容。

正确运行了
treadmill 程序不需要重新编译就能从新的库中动态获取代码。 有了动态库,就能在运行时替换代码。不用重新编译程序,你就 能修改它。如果你有很多程序,它们共享一段相同的代码,通过 建立动态库,就可以同时更新所有程序。

问: 我想改变动态库的名字,于是重命名了文件名,但编译器找不到它,为什么?
答: 编译器在编译动态库时会 在文件中保存库名。如果你重命名了 文件,文件中的名字还是没变。如果 想修改动态库的名字就必须重新编译 它。

问: 为什么Linux不直接在可执行文件中保存库路径名?那样不就可以不用设置LD_LIBRARY_PATH了吗?
答: 这是一种设计上的选择, 如果不保存路径名,程序就可以使用 不同版本的库。当你要开发新的库 时,这种设计的好处就特别明显。

问: 如果不同的程序使用相同的动态库,动态库会加载一次还是多次?这些程序会共享它吗?
答: 这取决于操作系统。有的 操作系统会为每个进程加载一个动态 库,有的则会共享动态库以节省存储 器。

问: 动态库是配置程序的最好方式吗?
答: 通常情况下,配置文件可 能比动态库更简单。但如果想连接一 些外部设备,通常会用动态库作为驱 动。

猜你喜欢

转载自blog.csdn.net/woshiwangbiao/article/details/53705301