众所周知,C++语言的类机制为我们提供了类的构造和析构功能,可以很方便地帮我们完成对应的初始化和资源释放的业务逻辑,但是C语言本身却没有提供类似的功能。然而编译器却为我们提供了扩展能力来支持C语言在程序启动时进行构造和析构操作,通过声明指定的函数为构造函数,那么该函数就可以先于main函数执行,而析构函数会在main结束后或者exit被调用后执行。
对于GCC和CLANG编译器,可以将要构造的函数添加__attribute__((constructor))进行修饰,被修饰后的函数将成为构造函数,如:
__attribute__((constructor)) void consturctor_func()
{
;
}
或者
void __attribute__((constructor)) consturctor_func()
{
;
}
以上两种写法都可以。
对于GCC和CLANG编译器,可以将要构造的函数添加__attribute__((destructor))进行修饰,被修饰后的函数将成为析构造函数,如:
__attribute__((destructor)) void desturctor_func()
{
printf("destructor\n");
}
void __attribute__((destructor)) desturctor_func()
{
printf("destructor\n");
}
虽然GCC和CLANG本身没有限制析构和构造函数可以有入参,但是我们确实也没法往这个函数里面传递参数调用它的调用者也不关心其返回值,所以这类一般都是声明为无参数的,也没有返回值。
完整的代码如下:
#include <stdio.h>
int main()
{
printf("main\n");
}
__attribute__((constructor)) void consturctor_func()
{
printf("constructor\n");
}
__attribute__((destructor)) void desturctor_func()
{
printf("destructor\n");
}
输出:
constructor
main
destructor
另外,GCC和CLANG还提供了优先级的概念,即可以设置某个构造函数优先于另外的构造函数优先执行。譬如:
__attribute__((constructor(1))) void consturctor1_func()
{
;
}
__attribute__((constructor(2))) void consturctor2_func()
{
;
}
析构函数也是类似。
#include <stdio.h>
int main()
{
printf("main\n");
}
void __attribute__((constructor(1))) consturctor1_func()
{
printf("constructor1\n");
}
void __attribute__((constructor(2))) consturctor2_func()
{
printf("constructor2\n");
}
__attribute__((destructor(1))) void desturctor1_func()
{
printf("destructor1\n");
}
__attribute__((destructor(2))) void desturctor2_func()
{
printf("destructor2\n");
}
输出:
constructor1
constructor2
main
destructor2
destructor1
emsp;从以上代码的输出可以看到,优先级数值比较小的构造函数优先执行,而优先级数值比较大的析构函数优先执行,执行顺序对于构造和析构函数来说正好是反的。
在MS VC++编译器中,却没有对应的功能,不过我在stackoverflow上看到有人贴了下面的解决方案,如:
```c
// Initializer/finalizer sample for MSVC and GCC/Clang.
// 2010-2016 Joe Lowe. Released into the public domain.
#include <stdio.h>
#include <stdlib.h>
#ifdef __cplusplus
#define INITIALIZER(f) \
static void f(void); \
struct f##_t_ { f##_t_(void) { f(); } }; static f##_t_ f##_; \
static void f(void)
#elif defined(_MSC_VER)
#pragma section(".CRT$XCU",read)
#define INITIALIZER2_(f,p) \
static void f(void); \
__declspec(allocate(".CRT$XCU")) void (*f##_)(void) = f; \
__pragma(comment(linker,"/include:" p #f "_")) \
static void f(void)
#ifdef _WIN64
#define INITIALIZER(f) INITIALIZER2_(f,"")
#else
#define INITIALIZER(f) INITIALIZER2_(f,"_")
#endif
#else
#define INITIALIZER(f) \
static void f(void) __attribute__((constructor)); \
static void f(void)
#endif
static void finalize(void)
{
printf( "finalize\n");
}
INITIALIZER( initialize)
{
printf( "initialize\n");
atexit( finalize);
}
int main( int argc, char** argv)
{
printf( "main\n");
return 0;
}
其中构造函数是用宏INITIALIZER来定义的,其中析构函数是通过atexit来指定程序推出的时候调用哪个函数,而且atexit是C的标准函数,可以多次调用atexit来设置多个析构函数。
下面再分析一下INITIALIZER的宏定义:
如果是c++,则走第一个分支,它是通过声明一个带构造函数的结构体,然后定义一个该结构体的静态变量来实现函数的调用。如果是c,则走第二个分支:
-
#pragma section(".CRT$XCU",read)
: 这个指令定义了一个名为 “.CRT$XCU” 的段(section),并指定了其属性为可读(read)。 -
#define INITIALIZER2_(f,p) ...
: 这个宏定义了一个初始化函数的模板,它将函数f
放置在指定的段中,并通过链接器的/include
选项将其包含在输出文件中。 -
static void f(void);
: 这里声明了一个静态函数f
,用于执行特定的初始化操作。 -
__declspec(allocate(".CRT$XCU")) void (*f##_)(void) = f;
: 这行代码将函数指针f##_
分配到 “.CRT$XCU” 段,并将其初始化为函数f
。这样做可以确保在程序启动时该函数被调用。 -
__pragma(comment(linker,"/include:" p #f "_"))
: 这个指令告诉链接器在输出文件中包含函数f
,以确保在程序启动时调用该函数。 -
#ifdef _WIN64
: 这里检查是否为 64 位 Windows 平台。 -
#define INITIALIZER(f) INITIALIZER2_(f,"")
: 这个宏根据平台选择具体的初始化函数宏。在 64 位 Windows 平台上,调用INITIALIZER2_(f, "")
;在其他情况下,调用INITIALIZER2_(f, "_")
。
最终,通过使用这些宏,您可以定义一个函数,并在程序启动时自动执行该函数的特定操作,而无需显式调用它。这种技术通常用于执行一些全局初始化工作,例如初始化全局变量、注册回调函数等。
实现上来说还是相当的曲折。