C/C++相关的文件、__declspec(dllexport)、__declspec(dllimport)

最近想整理一下C/C++相关的文件以及文件内部的作用,虽然常常面对C/C++等编译器,面对各类文件也习以为常了,但一直没有深究过。

.c文件具体实现代码
.h文件是编译时必须的(当然.h应该不是必须的,在.c文件里手动声明所有用到的函数也可以),声明着函数接口
.lib(静态链接库)是链接时需要的:
.dll(动态链接库)是运行时需要的,含有函数的可执行代码
.obj文件就是cpp经过预编译,然后编译后的文件,obj文件与cpp文件名字一一对应

.lib文件:

静态库中的函数和数据被编译进一个二进制文件(通常扩展名为.LIB)
lib库有两种:
(1)静态链接库(Static Libary)
(2)动态连接库(DLL)的导入库(Import Libary)

  1. lib包含函数代码本身,在编译时直接将代码加入程序当中,称为静态链接库static link library。(这种方式不是很灵活,因为lib被编译到.exe中,写出的程序体积大,但是只需要发布exe即可,不需要dll文件。也就是说 .lib 是包含有实现代码的,也就是聚合了各个.obj的文件)
  2. lib包含了函数所在的dll文件和文件中函数位置的信息(入口),代码由运行时加载在进程空间中的dll提供,称为动态链接库dynamic link library。(这种方式更灵活,写的程序体积小,但是需要.exe和dll同时发布)。链接完成了,LIB也没用了。
    动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数,程序运行的时候再从DLL中寻找相应函数代码。当然该 DLL 无需放入到 EXE 文件中,EXE 文件执行时可以“动态”地引用和卸载这个 DLL 文件(从而.dll与 .exe 是独立的两个文件)
    (也就是说,DLL也是分静态调用和动态调用,动态调用,可能像C#一样,通过反射动态加载)

https://blog.csdn.net/ljianhui/article/details/9005935

.dll 文件(动态链接库):

DLL不是可执行文件,DLL是一个包含可由多个程序同时使用的代码和数据的库,动态链接就是把调用的函数所在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,

如果有dll文件,那么lib一般是一些索引信息,记录了dll中函数的入口和位置,dll中是函数的具体内容;动态链接的情况下,有两个文件:一个是LIB文件,一个是DLL文件。LIB包含被DLL导出的函数名称和位置,DLL包含实际的函数和数据,应用程序使用LIB文件链接到DLL文件。在应用程序的可执行文件中,存放的不是被调用的函数代码,而是DLL中相应函数代码的地址;
如果只有lib文件,那么这个lib文件是静态编译出来的,索引和实现都在其中。使用静态编译的lib文件,在运行程序时不需要再挂动态库,当然缺点是导致应用程序比较大,而且失去了动态库的灵活性,发布新版本时要发布新的应用程序才行。

.c文件和.h文件:

从C编译器角度看,.h和.c皆是浮云,.h中一般放的是同名.c文件(实际上名字可以随意取,只不过一般是一样,方便理解)中定义的变量、数组、函数的声明,需要让.c外部使用的声明。#include “xx.h” 这个宏的意思就是把当前这一行删掉,把 xx.h 中的内容原封不动的插入在当前行的位置。由于想写这些函数声明的地方非常多(每一个调用 xx.c 中函数的地方,都要在使用前声明一下子),所以用 #include “xx.h” 这个宏就简化了许多行代码——让预处理器自己替换好了。也就是说,xx.h 其实只是让需要写 xx.c 中函数声明的地方调用(可以少写几行字)
同样存在一定的问题:我平时只想调用 xx.c 中的某个函数,却 include了 xx.h 文件,岂不是宏替换后出现了很多无用的声明?没错,确实引入了很多垃圾 ,但是它却省了你不少笔墨,并且整个版面也看起来清爽的多。鱼与熊掌不可得兼,就是这个道理。反正多些声明(.h一般只用来放声明,而放不定义,参见拙著 “过马路,左右看”)也无害处,又不会影响编译,何乐而不为呢?

https://www.cnblogs.com/Dageking/p/4054863.html

关于动态链接和静态链接:

静态链接和动态链接两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时,下面来详细介绍这两种链接方式。

  1. 为什么要进行静态链接:
    在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个*.c文件会形成一个*.o文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接
  2. 静态链接使用到静态链接库,动态链接使用到动态链接库,两者区别:
    在vs中新建生成静态库的工程,编译生成成功后,只产生一个.lib文件。
    在vs中新建生成动态库的工程,编译成功后,产生一个.lib文件和一个.dll文件
    静态库中的lib:该LIB包含函数代码本身(即包括函数的索引,包括具体实现代码),在编译时直接将代码加入程序当中。
    动态库中的lib:该LIB包含了函数所在的DLL文件和文件中函数位置的信息(索引),函数实现代码由运行时加载在进程空间中的DLL提供。
    并且,lib是编译时用到的,dll是运行时用到的。如果要完成源代码的编译,只需要lib;如果要使动态链接的程序运行起来,只需要dll。

https://blog.csdn.net/kang___xi/article/details/80210717

整个编译过程:预处理(Preprocessing)->编译(Compilation)->汇编(Assemble)->链接(Linking)->.exe

预处理:
①将所有的“#define”删除,并且展开所有的宏定义
②处理所有的条件编译指令,如:“#if”、“#ifdef”、“#elif”、“#else”、“endif”等。
③处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。
(注意:这个过程可能是递归进行的,也就是说被包含的文件可能还包含其他文件,并且不可以重复声明,比如某个文件里引入了#include<ABCD.h>,如果其他文件也#include<ABCD.h>,那么就相当于展开声明了多次,就会出错,而为了避免重复声明采用:
在头文件中,要防止重复声明,要么写#pragma once,要么:

#ifndef XXX_H //如果没有定义XXX.h
#define XXX_H
//头文件中的内容
#endif
条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等。 
这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。  

预处理之后的程序还是文本,可以用文本编辑器打开。诞生 .i 文件

编译
这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。 诞生 .s 文件
此步骤结束后生成的文件内容类似于:

    .file   "test.c"
    .section    .rodata
.LC0:
    .string "a=%d, b=%d, a+b=%d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    $32, %esp
    movl    $2, 20(%esp)
    movl    $3, 24(%esp)
    movl    24(%esp), %eax
    movl    %eax, 4(%esp)
    movl    20(%esp), %eax
    movl    %eax, (%esp)
    call    add 
    movl    %eax, 28(%esp)
    movl    28(%esp), %eax
    movl    %eax, 12(%esp)
    call    printf
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret 
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
    .section    .note.GNU-stack,"",@progbits

汇编
汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。
这一步会为每一个源文件产生一个目标文件诞生 .o 目标文件

链接
链接过程将多个目标文件以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。
链接分为两种静态链接动态链接

  1. 静态链接,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。
    这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
  2. 动态链接
    在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。C/C++编译过程对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。
    自此诞生可执行文件

https://www.cnblogs.com/ericling/articles/11736681.html

一些其他问题
  1. __declspec(dllexport)和__declspec(dllimport)
    这是修饰语句,用来说明函数是导出函数或导入函数
    __declspec(dllexport)的作用,它就是为了省掉在DEF文件中手工定义导出哪些函数的一个方法。当然,如果你的DLL里全是C++的类的话,你无法在DEF里指定导出的函数,只能用__declspec(dllexport)导出类。但是,MSDN文档里面,对于__declspec(dllimport)的说明让人感觉有点奇怪,先来看看MSDN里面是怎么说的:
    不使用 __declspec(dllimport) 也能正确编译代码,但使用 __declspec(dllimport) 使编译器可以生成更好的代码。
    编译器之所以能够生成更好的代码,是因为它可以确定函数是否存在于 DLL 中,这使得编译器可以生成跳过间接寻址级别的代码,而这些代码通常会出现在跨 DLL 边界的函数调用中。
    但是,若想使用DLL 中使用的变量,则必须使用 __declspec(dllimport) 声明才行。
那我就来试验一下,假定,你在DLL里只导出一个简单的类
注意,假定你已经在项目属性中定义了SIMPLEDLL_EXPORT
///SimpleDLLClass.h
#ifdef SIMPLEDLL_EXPORT
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT
#endif

class DLL_EXPORT SimpleDLLClass
{
public:
SimpleDLLClass();
virtual ~SimpleDLLClass();
virtual getValue() { return m_nValue;};
private:
int m_nValue;
};

///SimpleDLLClass.cpp
#include "SimpleDLLClass.h"
SimpleDLLClass::SimpleDLLClass()
{
m_nValue=0;
}
SimpleDLLClass::~SimpleDLLClass()
{
}
然后你在使用这个DLL中的类,在你的cpp中,没有定义SIMPLEDLL_EXPORT
所以此时对于cpp来说,.h中就相当于class SimpleDLLClass{};
这正好对应MSDN上说的__declspec(dllimport)定义与否都可以正常使用。

修改上述代码,将m_nValue改为static变量:

///SimpleDLLClass.h
#ifdef SIMPLEDLL_EXPORT
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT 
#endif

class DLL_EXPORT SimpleDLLClass
{
public:
SimpleDLLClass();
virtual ~SimpleDLLClass();
virtual getValue() { return m_nValue;};
private:
DLL_EXPORT static int m_nValue;
};

///SimpleDLLClass.cpp
#include "SimpleDLLClass.h"
SimpleDLLClass::SimpleDLLClass()
{
static m_nValue=0;
}
SimpleDLLClass::~SimpleDLLClass()
{
}
int SimpleDLLClass::m_nValue=0;
改完之后,再去LINK一下,这时会发现,结果是LINK告诉你找不到这个m_nValue。
正好对应MSDN上说的若想使用DLL 中使用的静态变量,则必须使用 __declspec(dllimport) 声明才行。

所以需要将代码改为:

///SimpleDLLClass.h
#ifdef SIMPLEDLL_EXPORT
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT __declspec(dllimport)
#endif
原来dllimport是为了更好的处理类中的静态成员变量的
如果没有静态成员变量,那么这个__declspec(dllimport)无所谓。

总结一下:对于.h文件,定义了SIMPLEDLL_EXPORT,其内部的DLL_EXPORT__declspec(dllexport),对于cpp没有定义SIMPLEDLL_EXPORT,所以在没有修改时,没有DLL_EXPORT没有定义,而修改成static变量后,DLL_EXPORT__declspec(dllimport)

_declspec(dllexport)与_declspec(dllimport)都是DLL内的关键字,即导出与导入。他们是将DLL内部的类与函数以及数据导出与导入时使用的。主要区别在于,dllexport是在这些类、函数以 及数据的申明的时候使用。用过表明这些东西可以被外部函数使用,即(dllexport)是把DLL中的相关代码(类,函数,数据)暴露出来为其他应用程 序使用。使用了(dllexport)关键字,相当于声明了紧接在(dllexport)关键字后面的相关内容是可以为其他程序使用的。而 dllimport关键字是在外部程序需要使用DLL内相关内容时使用的关键字。当一个外部程序要使用DLL内部代码(类,函数,全局变量)时,只需要在程序内部使用(dllimport)关键字声明需要使用的代码就可以了,即(dllimport)关键字是在外部程序需要使用DLL内部相关内容的时候才 使用。(dllimport)作用是把DLL中的相关代码插入到应用程序中。

  1. 导出函数:dll里的函数或类或数据成员(类和数据成员的应用下面再说)不同于一般的文件,导出函数就是说这个函数可供应用程序调用,
    而没有这个修饰语句的函数就是非导出函数,它只能在dll的内部被调用,而不能被应用程序调用。类似于public和private修饰的那样。
__declspec(dllexport) int add(int a, int b)
{
return a+b;
}
__declspec(dllexport) int sub(int a, int b)
{
return a-b;
}

可以使用工具查看dll文件内封装起来的函数,查看结果如下:
在这里插入图片描述
上图中ordinal为函数的序号,hint表示提示码,rva是表示函数在dll中的地址,name表示函数名。
我们声明的导出函数为add和sub,但发现name中的函数名并非是add和sub
原因:
这是因为在C++中为了支持函数重载,在编译器进行编译链接时,会将函数名和形参一起进行考虑,往往会去篡改函数的名字,按照自己定义的法则去改变函数的名字。这也导致了一个问题:不同的C++的编译器定义的改名的法则并不相同,函数名各不相同。从而使得在调用dll内函数时,会有找不到函数入口的问题

https://blog.csdn.net/ljianhui/article/details/9005935
https://www.cnblogs.com/Dageking/p/4054863.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值