5. 库相关

5. 库相关

有些时候我们编写的源代码并不需要将他们编译生成可执行程序,而是生成一些静态库动态库提供给第三方使用,下面来讲解在cmake中生成这两类库文件的方法。

5.1 什么是库

本部分介绍创建与使用静态库、动态库,知道静态库与动态库的区别,知道使用的时候如何选择。这里不深入介绍静态库、动态库的底层格式,内存布局等,有兴趣的同学,推荐一本书《程序员的自我修养------链接、装载与库》。

5.1.1 什么是库

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常

本质上来说,库是一种可执行的二进制格式,可以被操作系统载入内存执行。库有两种:静态库(.a .lib )和动态库(.so .dll)。

这里的静态、动态是指链接类型的区别,分为静态链接方式和动态链接方式。编译的过程如下:

									 ____
源文件 --> 预编译 --> 编译 --> 汇编 --> |链接| --> 可执行文件
									 ----
						.so/.dll	   ^
						.a/.lib        |
						______________/

5.1.2 静态库

之所以成为静态库 ,是因为在链接阶段,会将汇编生成的目标文件.o/.obj与引用到的库一起链接打包到可执行文件(ELF格式或者PE格式,前者 Linux 系统使用,后者 Windows 系统使用)中。因此对应的链接方式称为静态链接。

静态库和汇编生成的目标文件一起链接,可以生出可执行文件,那么静态库必定和 .o/.obj 文件格式相似。所以一个静态库可以看作是一组目标文件(.o/.obj)的集合,静态库可以很大,且方便移植。

静态库的特点:

  • 静态库对函数库的链接是放在编译期完成的
  • 程序在运行时与函数库无瓜葛,方便移植
  • 占用更多的存储空间资源,因为所有相关的目标文件与牵涉到的函数库都被合成为一个可执行文件

静态库生成

在 Linux 下使用 ar 工具,在 Windows 下使用 lib.exe 工具,可以将目标文件压缩在一起,并且对其进行编号和索引,以便查找和检索。创建静态库步骤如下图所示:

  • Linux 下静态库命名规则:lib[your_library_name].alib 为前缀,中间为静态库名,扩展名为 .a(Windows 下后缀名为 .lib

    • 先将源代码编译为 .o 的目标文件。我使用先前的简单自制数学库作为例子(为了方便示例,我这里将头文件和源代码文件放在同一个目录下)

      bash 复制代码
      tree .
      .
      ├── add.cpp
      ├── div.cpp
      ├── head.h
      ├── mul.cpp
      └── sub.cpp
      
      0 directories, 5 files

      生成目标文件:

      bash 复制代码
      gcc -c -Wall *.cpp

      此时生成了各自的同名目标文件:

      bash 复制代码
      tree 
      .
      ├── add.cpp
      ├── add.o
      ├── div.cpp
      ├── div.o
      ├── head.h
      ├── mul.cpp
      ├── mul.o
      ├── sub.cpp
      └── sub.o
    • 然后,通过 ar 工具将目标文件打包成 .a 静态库文件

      bash 复制代码
      ar -crv libmymath.a *.o
      a - add.o
      a - div.o
      a - mul.o
      a - sub.o

      然后查看文件:其中就有 libmymath.a,也就是我想要的静态库

      bash 复制代码
      ls
      add.cpp  add.o  div.cpp  div.o  head.h  libmymath.a  mul.cpp  mul.o  sub.cpp  sub.o

注:Windows 上如果使用 g++,生成静态库的操作是一样的,只是文件后缀有些许不同

静态库使用

此时我的工程目录是这样的(在这里并没有使用 CMakeLists.txt):

bash 复制代码
tree .
.
├── CMakeLists.txt
├── include
│   └── head.h
├── main.cpp
└── src
    ├── add.cpp
    ├── add.o
    ├── div.cpp
    ├── div.o
    ├── head.h
    ├── libmymath.a
    ├── mul.cpp
    ├── mul.o
    ├── sub.cpp
    └── sub.o

Linux 下使用静态库,只需要在编译的时候,需要如下操作:

  • 指定静态库的搜索路径-L 选项)、
  • 指定静态库名
    • Linux 下不需要 lib 前缀、不需要 .a 后缀,需要-l 选项,如果是
    • Windows 下使用了 mingw 或者 msys2 环境,则 需要 lib 前缀、不需要 .lib 后缀、 需要 -l 选项。
    • -l 选项后面的参数可以写在一起,也可以留一个空格

并将 main.cpp 中的头文件包含修改正确了:

cpp 复制代码
#include "include/head.h"
...

使用我们刚才生成的静态库:

Linux 环境:

bash 复制代码
 g++ -Wall -g  main.cpp -L ./src -lmymath -o main

Windows 下使用 mingw/msys2 环境:

powershell 复制代码
# 生成静态库
ar -crv libmymath.lib *.o  
# 使用静态库
 g++ -Wall -g main.cpp -L ./src -llibmymath -o main.exe

注意:Windows 下 -l 不需要 .lib 后缀,但是需要前面所有的名称,意思就是不能省略静态库前面的 lib

然后在当前目录下就生成了一个可执行文件:main

bash 复制代码
ls
CMakeLists.txt  include  main  main.cpp  src
./main
9.32 19.4052 -3.04 0.508091

注意:

Windows 下不同工具生成的库文件是不通用的,比如使用 Visual Studio 生成的静态库,g++ 就无法使用,反之亦然。

关于 Windows 下使用 Visual Studio 引入或者生成第三方库,可以参考这篇博客

5.1.3 动态库

通过上面的介绍,我们知道静态库已经可以达到代码复用的目的了,但是为什么还需要动态库呢?在我看来,有三个重要原因:

  • 静态库过于浪费空间,因为它将所有的目标文件和汇编都压缩成了一个库文件,这个文件就可能很大,而且静态库在内存中也会拷贝多份,会导致空间浪费

  • 由于开源协议的原因,部分框架或者软件不允许免费使用静态库(如Qt),否则要收费;

  • 另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库 liba.lib 更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。

动态库的特征

  • 动态库把对一些库函数的链接载入推迟到程序运行的时期;
  • 可以实现进程之间的资源共享 。(因此动态库也称为共享库,.so 全称为 shared object

Window 与 Linux 执行文件格式不同,在创建动态库的时候有一些差异。

  • 在Windows系统下的执行文件格式是 PE格式 ,动态库需要一个 DllMain 函数做出初始化的入口,通常在导出函数的声明时需要有 _declspec(dllexport) 关键字。

  • Linux 下 gcc/g++ 编译的执行文件默认是 ELF格式 ,**不需要初始化入口,亦不需要函数做特别的声明,**编写比较方便。

与创建静态库不同的是,不需要打包工具(ar、lib.exe),直接使用编译器即可创建动态库。

动态库生成

动态链接库的名字形式为 lib[your_library_name].so ,前缀是 lib ,后缀名为 .so

  • 针对于实际库文件,每个共享库都有个特殊的名字 soname 。在程序启动后,程序通过这个名字来告诉动态加载器该载入哪个共享库;
  • 在文件系统中,soname 仅是一个链接到实际动态库的链接 。对于动态库而言,每个库实际上都有另一个名字给编译器来用。它是一个指向实际库镜像文件的链接文件(lib+soname+.so)。

还是以先前的工程代码为例,删除所有生成的文件,恢复到最初的状态:

bash 复制代码
tree .
.
├── CMakeLists.txt
├── include
│   └── head.h
├── main.cpp
└── src
    ├── add.cpp
    ├── div.cpp
    ├── head.h
    ├── mul.cpp
    └── sub.cpp

2 directories, 8 files
  • 进入文件夹,生成目标文件,添加编译器选项 -fPIC

    bash 复制代码
    g++ -c -Wall -fPIC *.cpp

    -fPIC :创建与地址无关的编译程序(Position Independent Code),是为了能够在多个程序之间共享。

    查看生成后的文件:

    bash 复制代码
    tree .
    .
    ├── add.cpp
    ├── add.o
    ├── div.cpp
    ├── div.o
    ├── head.h
    ├── mul.cpp
    ├── mul.o
    ├── sub.cpp
    └── sub.o
    
    0 directories, 9 files
  • 然后,生成动态库,要添加编译器选项 -shared

    bash 复制代码
    g++ -Wall -shared -o libMyDynamicMath.so *.o

    查看生的文件:已经生成了我们想要的动态库文件 libMyDynamicMath.so

    bash 复制代码
    add.cpp  div.cpp  head.h               mul.cpp  sub.cpp
    add.o    div.o    libMyDynamicMath.so  mul.o    sub.o

    上述步骤可以合二为一:

    bash 复制代码
    g++ -Wall -fPIC -shared -o libMyDynamicMath.so *.cpp
    
    # Windows 的 MinGW/msys2 环境下:
    g++ -Wall -fPIC -shared -o libMyDynamicMath.dll *.cpp

动态库使用

使用方式和静态库类似,如我们要引入动态库,编译一个可执行程序:

bash 复制代码
Linux 环境下:
g++ -Wall -g main.cpp -L ./src -lMyDynamicMath -o main

# Windows 的 MinGW/msys2 环境下:
g++ -Wall -g main.cpp -L ./src -llibMyDynamicMath -o main.exe

这时候我们运行:

bash 复制代码
./main
./main: error while loading shared libraries: libMyDynamicMath.so: cannot open shared object file: No such file or directory

找不到!感觉要炸了!

找不到动态库

根据编译器的错误提示,可以看出,是找不到 .so 的文件,我们需要进行如下设置:

  • 运行时指定库路径

    bash 复制代码
    LD_LIBRARY_PATH=/home/yuzu/cmake_proj/proj4/src ./main
  • 将动态链接库添加到标准库目录 ,如:usr/lib 或者 usr/local/lib,可能需要超级用户权限

  • 设置 LD_LIBRARY_PATH 添加库所在目录到 LD_LIBRARY_PATH 环境变量中。你可以使用以下命令:

    bash 复制代码
    export LD_LIBRARY_PATH=/path/to/your/library:$LD_LIBRARY_PATH
  • 如果安装在其他目录,需要将其添加到 /etc/ld.so.cache 文件中,步骤如下:

    • 编辑 /etc/ld.so.conf 文件,加入库文件所在目录的路径
    • 运行 ldconfig ,该命令会重建 /etc/ld.so.cache 文件

我这里选取第一种方式,结果如下:

而在 Windows 环境下,动态链接库一般和可执行程序放在同一个目录下,Windows 下的 .dll 查找顺序为:

  1. exe 所在目录
  2. Windows 系统目录
  3. 当前目录
  4. PATH 指定的目录

5.2 CMake 与库

上面 5.1 部分,我们知晓了在没有其他构建工具的时候,我们是怎么构建静态库和动态库的,可是 5.1 中的操作如果是在小工程中还好,如果工程文件很多,项目很复杂,那么将是灾难,这时候就需要构建工具出来帮忙了,而 CMake 就可以做的很好。

5.2.1 使用 CMake 制作库文件

我将 main.cpp 单独移动到工程主目录下,现在目录结构如下所示:

bash 复制代码
tree .
.
├── CMakeLists.txt
├── include
│   └── head.h
├── main.cpp
└── src
    ├── add.cpp
    ├── div.cpp
    ├── head.h
    ├── mul.cpp
    └── sub.cpp

2 directories, 8 files

制作静态库

在 CMake 中,制作静态库采用如下命令:

cmake 复制代码
add_library(库名称 STATIC 源文件1 [源文件2] ...)

在 Linux 中,静态库名字分为三部分:lib+库名字+.a,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。

在 Windows 中虽然库名和 Linux 格式不同,但也只需指定出名字即可。

cmake 复制代码
# 指定cmake最低版本
cmake_minimum_required(VERSION 3.17)

# 项目名称
project(CALC)

# 指定C++标准
set(CMAKE_CXX_STANDARD 20)

# 指定可执行文件目录
set(HOME ${CMAKE_CURRENT_SOURCE_DIR})  
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)

# 指定头文件目录
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)

# 搜索指定目录下的源文件,保存文件列表到到变量 MAIN_SRC
file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

# 生成静态库
add_library(calc STATIC ${MAIN_SRC})

在程序家目录下,使用 cmake 命令执行:

bash 复制代码
mkdir build && cd build
cmake ..
make

命令行提示:

bash 复制代码
[ 20%] Building CXX object CMakeFiles/calc.dir/src/add.cpp.o
[ 40%] Building CXX object CMakeFiles/calc.dir/src/div.cpp.o
[ 60%] Building CXX object CMakeFiles/calc.dir/src/mul.cpp.o
[ 80%] Building CXX object CMakeFiles/calc.dir/src/sub.cpp.o
[100%] Linking CXX static library libcalc.a
[100%] Built target calc

此时查看 build 目录,就生成了我们的静态库 libcalc.a

制作动态库

在cmake中,如果要制作动态库,需要使用的命令如下:

cmake 复制代码
add_library(库名称 SHARED 源文件1 [源文件2] ...) 

在Linux中,动态库名字分为三部分:lib+库名字+.so,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。

在Windows中虽然库名和Linux格式不同,但也只需指定出名字即可。

最终的 CMakeLists.txt 文件:

cmake 复制代码
# 指定cmake最低版本
cmake_minimum_required(VERSION 3.17)

# 项目名称
project(CALC)

# 指定C++标准
set(CMAKE_CXX_STANDARD 20)

# 指定可执行文件目录
set(HOME ${CMAKE_CURRENT_SOURCE_DIR})  
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)

# 指定头文件目录
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)

# 搜索指定目录下的源文件,保存文件列表到到变量 MAIN_SRC
file(GLOB MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

# 生成动态库
add_library(calc SHARED ${MAIN_SRC})

和上面一样运行 cmake 命令,就能在 build 目录下得到想要的 .so 文件

bash 复制代码
CMakeCache.txt  Makefile             compile_commands.json  libcalc.so
CMakeFiles      cmake_install.cmake  libcalc.a

这里的 libcalc.so 是有可执行权限的,而静态库 .a 没有可执行权限

如何使用生成的库

需要发布两部分数据:

  • 需要 include 目录中的头文件
  • 需要生成的动态库 / 静态库:功能类似于 .cpp 的源代码,不过是二进制格式的

指定输出路径

指定动态库生成目录:[[depricated]]

对于生成的库文件来说和可执行程序一样都可以指定输出路径。由于在 Linux 下生成的动态库默认是有执行权限的,所以可以按照生成可执行程序的方式去指定它生成的目录:

cmake 复制代码
cmake_minimum_required(VERSION 3.17)
project(CALC_LIB)
set(CMAKE_CXX_STANDARD 17)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

# 指定动态库生成目录
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)
add_library(calc SHARED ${SRC_LIST})

这种方式来说,其实就是通过set命令给 EXECUTABLE_OUTPUT_PATH 宏设置了一个路径,这个路径就是可执行文件生成的路径。

都适用的库生成方式

即既可以指定静态库目录,也可以指定动态库生成目录,(只要我们想)可以生成多个:

cmake 复制代码
cmake_minimum_required(VERSION 3.17)
project(CALC_LIB)
set(CMAKE_CXX_STANDARD 11)

set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)

# 指定 静态库/动态库 生成目录
set(LIBRARY_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)

include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

add_library(calcso SHARED ${SRC_LIST})
add_library(calcst STATIC ${SRC_LIST})

build 目录下执行 cmake 和 make 命令,然后查看 lib 目录文件:

bash 复制代码
tree
.
├── libcalcso.so
└── libcalcst.a

0 directories, 2 files

我这里生成了两个文件,一个静态库文件 libcalcst.a 和一个动态库文件 libcalcso.so

5.2.2 使用已有库文件

目录结构:

bash 复制代码
tree
.
├── CMakeLists.txt
├── bin
├── include
│   └── head.h
├── lib
│   ├── libcalcso.so
│   └── libcalcst.a
├── main.cpp
└── src
    ├── add.cpp
    ├── div.cpp
    ├── mul.cpp
    └── sub.cpp

4 directories, 9 files

可以看到,此时 lib 目录下有两个库文件:libcalcso.solibcalcst.a

链接静态库

在 CMake 中,链接静态库命令如下:

cmake 复制代码
link_libraries(<static_lib> [<static_lib> ...])
  • 参数1:指定要链接的静态库名称
    • 可以是全名:libxxx.a
    • 也可以是掐头去尾后的名称:xxx
  • 参数2-N:要链接的其他静态库的名称

注意

如果不是系统提供的静态库,是自己写的或者第三方的库,则可能出现静态库找不到的情况,此时要指定静态库的路径

cmake 复制代码
link_directories(${CMAKE_CURRENT_SOURCE_DIR}/lib)
  • 如果有多个路径,那么路径和路径之间使用空格进行分隔;

  • link_directories 既可以指定静态库路径,又可指定动态库路径

修改后的 CMakeLists.txt 文件:

cmake 复制代码
cmake_minimum_required(VERSION 3.17)
project(CALC_LIB)
set(CMAKE_CXX_STANDARD 11)

set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)
# 包含头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 搜索指定目录下源文件
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

# 指定链接静态库的目录
link_directories(${PROJECT_SOURCE_DIR}/lib)
# 链接静态库:指定需要的静态库名称
link_libraries(calcst)

add_executable(main ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)

在 build 目录下运行 cmake ..make ,然后就生成了我们想要的可执行文件 main:

bash 复制代码
./
├── CMakeLists.txt
├── bin/
│   └── main*
......

链接动态库

在程序编写过程中,除了在项目中引入静态库,很多时候也会使用一些标准的或者第三方提供的动态库。动态库是可以被进程之间共享的。

在 CMake 中链接动态库的命令 target_link_libraries :

cmake 复制代码
target_link_libraries(
    <target> 
    <PRIVATE|PUBLIC|INTERFACE> <item>... 
    [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)

target:指定要加载动态库的文件的名字

  • 该文件可能是一个源文件
  • 该文件可能是一个动态库文件
  • 该文件可能是一个可执行文件

PRIVATE|PUBLIC|INTERFACE:动态库的访问权限,默认为 PUBLIC

  • 如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,一般无需指定,使用默认的 PUBLIC 即可

  • 动态库的链接具有传递性,如果动态库 A 链接了动态库 B、C,动态库 D 链接了动态库 A,此时动态库 D 相当于也链接了动态库 B、C,并可以使用动态库 B、C 中定义的方法。

    cmake 复制代码
    target_link_libraries(A B C)
    target_link_libraries(D A)
  • PUBLIC :在 public 后面的库会被 link 到前面的 target 中,并且里面的符号也会被导出,提供给第三方使用;

  • PRIVATE:在 private 后面的库仅被 link 到前面的 target 中,并且终结掉,第三方不能感知我们具体调了什么库;

  • INTERFACE:在 interface 后面引入的库不会被链接到前面的 target 中,只会导出符号。

链接系统动态库

动态库的链接和静态库是完全不同的:

  • 静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了;
  • 动态库在生成可执行程序 的链接阶段不会被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存;

因此,在 CMake 中指定要链接的动态库的时候,应该将命令写到生成了可执行文件之后

cmake 复制代码
cmake_minimum_required(VERSION 3.17)
project(CALC_TEST)
set(CMAKE_CXX_STANDARD 11)

set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)

include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)

add_executable(main ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)

target_link_libraries(main pthread)

target_link_libraries(main pthread) 中:

  • main : 对应的是最终生成的可执行程序的名字
  • pthread :这是可执行程序要加载的动态库,这个库是系统提供的线程库,全名为libpthread.so ,在指定的时候一般会掐头(lib)去尾(.so)。

注意:

file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) 中递归包含了 src 下(包含子文件夹)所有的 .cpp 文件

链接第三方动态库

先前我们自己生成了一个动态库 libcalcso.so

此时的工程目录:

bash 复制代码
.
├── CMakeLists.txt
├── bin
│   └── main
├── include
│   └── head.h
├── lib
│   └── libcalcso.so
├── main.cpp
└── src
    ├── add.cpp
    ├── div.cpp
    ├── mul.cpp
    └── sub.cpp

我们在测试的 main.cpp 中使用了自己制作的动态库 libcalcso.so ,如果还使用了系统提供的 libpthread.so ,则此时 CMakeLists.txt 应该这样写:

cmake 复制代码
cmake_minimum_required(VERSION 3.17)
project(CALC_TEST)
set(CMAKE_CXX_STANDARD 11)

set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)

include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)

# 指定链接的第三方动态库的目录,如果只用系统的动态库,可以不写这个
link_directories(${PROJECT_SOURCE_DIR}/lib)

add_executable(main ${SRC_LIST})

target_link_libraries(main calcso pthread)

第 15 行的 calcso pthread 都是可执行程序 main 要链接的动态库的名字

在 build 目录下照常 cmake ..make,就能得到生成的可执行文件:

bash 复制代码
make
Consolidate compiler generated dependencies of target main
[ 50%] Linking CXX executable ../bin/main
[100%] Built target main

tree ../bin
../bin
└── main

0 directories, 1 file

运行该文件:

bash 复制代码
cd ../bin
./main
9.32 19.4052 -3.04 0.508091

如果运行 main 程序提示如下信息,说明加载动态库失败,原因为不知道这个动态库放到了什么位置:

bash 复制代码
./main: error while loading shared libraries: libcalcso.so: cannot open shared object file: No such file or directory

在 CMake 中可以在生成可执行程序之前,通过命令指定出要链接的动态库的位置,指定静态库位置使用的也是这个命令:

cmake 复制代码
link_directories(path)

注意:这里的 path 是包含了动态库所在目录的路径,不是具体某一个动态库的绝对路径。

此外,Windows 系统的 MinGW 环境可用上面的CMakeLists.txt 文件,但是要成功运行该文件,可以将 .dll 文件和 .exe 文件放在同一个目录下