什么是延迟创建?
延迟创建,也常被称为延迟初始化(Lazy Initialization),是一种重要的性能优化和资源管理设计模式,其核心思想是:将一个对象、资源或数据的创建和初始化过程,推迟到第一次真正需要它的时候才执行。

这就像一个“按需加载”的机制,在程序启动时,我们不会立即创建所有可能用到的对象,而是先创建一个“占位符”或“空壳”,当程序运行到某个特定点,第一次尝试访问这个对象时,我们才检查它是否已经被创建,如果没有,就立即进行创建和初始化;如果已经创建,就直接使用。
为什么使用延迟创建?(优点)
- 提升启动速度:这是最直接的好处,程序启动时,只执行必要的初始化,跳过耗时的资源创建(如打开大文件、建立网络连接、分配巨大内存等),让程序能更快地响应用户。
- 节省内存和系统资源:并非所有程序运行路径都会用到所有资源,如果某个对象在本次执行中从未被访问,那么延迟创建技术就完全避免了为它分配内存和资源,从而节省了宝贵的系统资源。
- 提高代码的健壮性和容错性:对于一些可能失败的操作(如连接数据库、加载配置文件),可以在真正需要时再尝试,如果程序启动后从未使用该功能,那么即使初始化失败,也不会影响程序的其他部分。
缺点
- 首次访问延迟:第一次访问需要该对象的代码时,可能会因为需要执行创建和初始化而出现短暂的“卡顿”或延迟,影响用户体验。
- 代码复杂度增加:需要额外的机制来检查对象是否已被初始化,并确保线程安全,这会使代码变得比直接创建更复杂。
- 隐藏的依赖关系:对象的创建被推迟,可能导致在运行时才暴露出依赖项缺失或配置错误的问题,而不是在编译或启动时。
在 C 语言中实现延迟创建的几种常见方法
C 语言没有内置的“延迟创建”关键字,我们需要手动实现这个模式,以下是几种主流的实现方式。
使用全局指针和标志位(最经典、最简单)
这是最基础、最容易理解的方法,我们使用一个全局指针来指向对象,并用一个全局标志位来记录它是否已经被创建。
实现步骤:

- 定义一个全局指针,初始值为
NULL。 - 定义一个全局的
int或bool标志位,初始值为0或false。 - 创建一个“获取”函数,每次调用时都检查标志位。
- 如果标志位为
false,则执行创建逻辑,将对象地址赋给指针,并将标志位置为true。 - 如果标志位为
true,则直接返回指针指向的对象。
- 如果标志位为
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 1. 定义一个结构体作为我们要延迟创建的对象
typedef struct {
int id;
char data[128];
} MyObject;
// 2. 定义全局指针和标志位
MyObject* g_lazy_object = NULL;
int g_is_initialized = 0;
// 3. 获取对象的函数(延迟创建的核心)
MyObject* get_lazy_object() {
if (!g_is_initialized) { // 检查是否已初始化
printf("第一次调用:正在创建 MyObject...\n");
g_lazy_object = (MyObject*)malloc(sizeof(MyObject));
if (g_lazy_object == NULL) {
perror("内存分配失败");
exit(EXIT_FAILURE);
}
g_lazy_object->id = 101;
strcpy(g_lazy_object->data, "This is a lazily created object.");
g_is_initialized = 1; // 标记为已初始化
} else {
printf("后续调用:直接返回已创建的对象,\n");
}
return g_lazy_object;
}
// 4. 使用示例
void do_something_with_object() {
MyObject* obj = get_lazy_object();
printf("操作对象: ID=%d, Data=%s\n", obj->id, obj->data);
}
int main() {
printf("程序启动,MyObject 尚未被创建,\n");
// 第一次调用,会触发创建
do_something_with_object();
printf("--------------------\n");
// 第二次调用,直接返回已创建的对象
do_something_with_object();
printf("--------------------\n");
// 第三次调用,同样直接返回
do_something_with_object();
// 记得在程序结束时释放资源!
if (g_lazy_object) {
free(g_lazy_object);
g_lazy_object = NULL;
g_is_initialized = 0;
}
return 0;
}
输出:
程序启动,MyObject 尚未被创建。
第一次调用:正在创建 MyObject...
操作对象: ID=101, Data=This is a lazily created object.
--------------------
后续调用:直接返回已创建的对象。
操作对象: ID=101, Data=This is a lazily created object.
--------------------
后续调用:直接返回已创建的对象。
操作对象: ID=101, Data=This is a lazily created object.
使用函数内的静态局部变量(线程安全,C99以后)
这是一种更优雅、更现代的方法,利用了 C99 标准引入的静态局部变量的特性。
实现步骤:

- 创建一个函数,而不是一个全局变量。
- 在函数内部,将对象定义为
static局部变量。 - 在函数内部,检查这个静态变量是否为
NULL。- 如果是,就创建它。
- 如果不是,就直接返回它。
关键点:
- 静态局部变量:它的生命周期是整个程序运行期间,而不是函数的调用栈帧,这意味着它只会被初始化一次。
- 线程安全:在 C11 标准中,
static局部变量的初始化是原子操作,因此这种方法在符合 C11 标准的编译器上是线程安全的,在旧标准下,如果存在多线程竞争,仍需加锁。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int id;
char data[128];
} MyObject;
// 获取对象的函数,对象在此函数内部延迟创建
MyObject* get_lazy_object_v2() {
// static 关键字确保了 this_object 只被初始化一次
static MyObject* this_object = NULL;
if (this_object == NULL) {
printf("第一次调用:正在创建 MyObject...\n");
this_object = (MyObject*)malloc(sizeof(MyObject));
if (this_object == NULL) {
perror("内存分配失败");
exit(EXIT_FAILURE);
}
this_object->id = 202;
strcpy(this_object->data, "This is a lazily created object (v2).");
} else {
printf("后续调用:直接返回已创建的对象,\n");
}
return this_object;
}
// 使用示例
void do_another_thing() {
MyObject* obj = get_lazy_object_v2();
printf("操作对象: ID=%d, Data=%s\n", obj->id, obj->data);
}
int main() {
printf("程序启动,使用静态局部变量方法,\n");
do_another_thing();
printf("--------------------\n");
do_another_thing();
printf("--------------------\n");
do_another_thing();
// 释放内存
MyObject* obj_to_free = get_lazy_object_v2();
if (obj_to_free) {
free(obj_to_free);
// 注意:静态指针无法在函数内部设为NULL,
// 但其生命周期随程序结束而结束,通常无需手动置NULL。
}
return 0;
}
使用“虚拟代理”模式(更高级)
这种方法不直接管理对象本身,而是通过一个“代理”来间接访问,代理在第一次请求时,会完成对象的创建,然后将自己替换为真正的对象,这在 C 语言中实现起来比较复杂,通常用于更面向对象的设计,但我们可以模拟其核心思想。
核心思想:
- 创建一个代理结构体,它包含一个指向真实对象的指针。
- 所有对外的操作都通过代理函数进行。
- 代理函数内部检查真实对象是否存在,不存在则创建。
这种方法的好处是可以在代理层添加额外的逻辑,如访问控制、缓存等。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 真实对象
typedef struct {
int id;
char data[128];
} RealObject;
// 代理对象
typedef struct {
RealObject* real_obj;
} ProxyObject;
// 创建真实对象的函数
RealObject* create_real_object() {
printf("代理正在创建真实对象...\n");
RealObject* obj = (RealObject*)malloc(sizeof(RealObject));
obj->id = 303;
strcpy(obj->data, "Created by a proxy.");
return obj;
}
// 获取代理对象的函数(延迟创建发生在这里)
ProxyObject* get_proxy() {
static ProxyObject proxy = { .real_obj = NULL };
if (proxy.real_obj == NULL) {
proxy.real_obj = create_real_object();
} else {
printf("代理返回已存在的真实对象,\n");
}
return &proxy;
}
// 通过代理操作对象
void operate_via_proxy() {
ProxyObject* p = get_proxy();
// 注意:这里直接暴露了内部结构,更好的做法是提供代理的API函数
printf("通过代理操作: ID=%d, Data=%s\n", p->real_obj->id, p->real_obj->data);
}
int main() {
operate_via_proxy();
printf("--------------------\n");
operate_via_proxy();
// 释放资源
ProxyObject* p = get_proxy();
if (p->real_obj) {
free(p->real_obj);
p->real_obj = NULL;
}
return 0;
}
多线程环境下的注意事项
在多线程程序中,延迟创建的“检查-创建”操作不是原子的,可能会导致竞态条件。
- 线程 A 检查到对象为
NULL。 - 线程 B 也检查到对象为
NULL。 - 线程 A 开始创建对象。
- 线程 B 也开始创建对象。 结果可能是内存泄漏(对象被创建两次)或程序崩溃。
解决方案:使用互斥锁
#include <pthread.h>
// ... (前面的全局变量定义)
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
MyObject* get_lazy_object_thread_safe() {
pthread_mutex_lock(&g_mutex); // 加锁
if (!g_is_initialized) {
printf("线程安全模式:正在创建 MyObject...\n");
g_lazy_object = (MyObject*)malloc(sizeof(MyObject));
// ... 初始化 ...
g_is_initialized = 1;
}
pthread_mutex_unlock(&g_mutex); // 解锁
return g_lazy_object;
}
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 全局指针+标志位 | 简单直观,易于理解 | 全局变量污染,非线程安全 | 单线程程序,学习示例 |
| 静态局部变量 | 代码封装性好,无全局污染,C11下线程安全 | 首次访问有微小延迟,静态变量生命周期长 | 推荐用于大多数单线程或C11+多线程场景 |
| 虚拟代理模式 | 结构清晰,可扩展性强(可添加缓存、日志等) | 实现相对复杂,代码量多 | 需要复杂访问控制或额外功能的场景 |
对于 C 语言开发者来说,使用函数内的静态局部变量是实现延迟创建最推荐、最简洁的方式,尤其是在遵循现代 C 标准(C99 及以后)的情况下,如果必须处理多线程环境,则需要结合互斥锁来保证线程安全。
