C语言设计模式整理
继承/多态/封装
多态
允许父类指针指向子类对象。
1 | //C++中的继承与多态 |
对象-行为模式
参考文献原文
1 | /// c++ |
这个结构体 music_file
是一个典型的设计模式,称为“对象-行为模式”或“函数指针成员模式”。它通过在结构体中包含函数指针来实现类似面向对象编程中的方法。每个函数指针都对应于一个操作,可以对 music_file
对象执行特定的操作。
结构体成员解释
HANDLE hFile
: 用于存储文件句柄,通常是一个文件的标识符。void (*read_file)(struct _music_file* pMusicFile)
: 读取文件的函数指针。void (*play)(struct _music_file* pMusicFile)
: 播放音乐文件的函数指针。void (*stop)(struct _music_file* pMusicFile)
: 停止播放音乐文件的函数指针。void (*back)(struct _music_file* pMusicFile)
: 后退(例如,跳到上一曲)的函数指针。void (*front)(struct _music_file* pMusicFile)
: 前进(例如,跳到下一曲)的函数指针。void (*down)(struct _music_file* pMusicFile)
: 降低音量的函数指针。void (*up)(struct _music_file* pMusicFile)
: 提高音量的函数指针。使用示例
- 定义具体的函数实现:
首先,你需要定义每个操作的具体实现函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28void read_file_impl(struct _music_file* pMusicFile) {
// 实现读取文件的逻辑
printf("Reading file with handle: %p\n", pMusicFile->hFile);
}
void play_impl(struct _music_file* pMusicFile) {
// 实现播放音乐的逻辑
printf("Playing file with handle: %p\n", pMusicFile->hFile);
}
void stop_impl(struct _music_file* pMusicFile) {
// 实现停止播放的逻辑
printf("Stopping file with handle: %p\n", pMusicFile->hFile);
}
void back_impl(struct _music_file* pMusicFile) {
// 实现后退的逻辑
printf("Going back in file with handle: %p\n", pMusicFile->hFile);
}
void front_impl(struct _music_file* pMusicFile) {
// 实现前进的逻辑
printf("Going forward in file with handle: %p\n", pMusicFile->hFile);
}
void down_impl(struct _music_file* pMusicFile) {
// 实现降低音量的逻辑
printf("Lowering volume for file with handle: %p\n", pMusicFile->hFile);
}
void up_impl(struct _music_file* pMusicFile) {
// 实现提高音量的逻辑
printf("Raising volume for file with handle: %p\n", pMusicFile->hFile);
} - 创建和初始化
music_file
结构体:
接下来,你需要创建一个music_file
结构体实例,并将其函数指针成员初始化为具体的实现函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int main() {
music_file myMusicFile;
myMusicFile.hFile = (HANDLE)0x1234; // 假设这是文件的句柄
myMusicFile.read_file = read_file_impl;
myMusicFile.play = play_impl;
myMusicFile.stop = stop_impl;
myMusicFile.back = back_impl;
myMusicFile.front = front_impl;
myMusicFile.down = down_impl;
myMusicFile.up = up_impl;
// 使用结构体的方法
myMusicFile.read_file(&myMusicFile);
myMusicFile.play(&myMusicFile);
myMusicFile.stop(&myMusicFile);
myMusicFile.back(&myMusicFile);
myMusicFile.front(&myMusicFile);
myMusicFile.down(&myMusicFile);
myMusicFile.up(&myMusicFile);
return 0;
}解释
- 函数指针初始化:在
main
函数中,我们为music_file
结构体的每个函数指针成员赋值,指向具体的实现函数。 - 调用方法:通过
myMusicFile
结构体实例调用这些函数指针,就像调用对象的方法一样。
这种设计模式使得music_file
结构体的行为可以灵活地配置和扩展,类似于面向对象编程中的多态性。
单例模式(Singleton Pattern)
参考文献原文
1 | /// c++ |
1 | /// c |
详细解释
你提供的代码实现了一个典型的“单例模式”(Singleton Pattern)。单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。在你的代码中,get_data
函数确保 DATA
结构体只有一个实例,并且这个实例可以通过 get_data
函数全局访问。
- 结构体定义:
1
2
3
4typedef struct _DATA
{
void* pData;
} DATA;DATA
结构体包含一个void*
类型的指针pData
,可以用来存储任何类型的数据。
get_data
函数:1
2
3
4
5
6
7
8
9
10void* get_data()
{
static DATA* pData = NULL;
if (NULL != pData)
return pData;
pData = (DATA*)malloc(sizeof(DATA));
assert(NULL != pData);
return (void*)pData;
}静态变量
pData
:static DATA* pData = NULL;
:静态变量pData
在函数第一次被调用时初始化为NULL
,并且在函数的多次调用中保持其值。这意味着pData
只会被初始化一次。
检查
pData
是否已经初始化:if (NULL != pData)
:如果pData
已经被初始化(即不为NULL
),则直接返回pData
。
初始化
pData
:pData = (DATA*)malloc(sizeof(DATA));
:如果pData
为NULL
,则使用malloc
分配内存,并将其地址赋值给pData
。assert(NULL != pData);
:使用assert
宏确保malloc
分配内存成功。如果malloc
失败(返回NULL
),程序会终止并显示错误信息。
返回
pData
:return (void*)pData;
:将pData
转换为void*
类型并返回。这样可以确保get_data
函数的返回类型与声明一致。
举个使用上述代码的例子
以下是一个示例程序,展示了两次调用 get_data
函数的情况:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
typedef struct _DATA {
void* pData;
} DATA;
void* get_data() {
static DATA* pData = NULL;
if (NULL != pData)
return pData;
pData = (DATA*)malloc(sizeof(DATA));
assert(NULL != pData);
return (void*)pData;
}
void set_data(void* data, void* value) {
DATA* pData = (DATA*)data;
pData->pData = value;
}
void* get_data_value(void* data) {
DATA* pData = (DATA*)data;
return pData->pData;
}
int main() {
// 第一次调用 get_data
void* data1 = get_data();
printf("First call to get_data: %p\n", data1);
// 设置 pData 的值
int value = 42;
set_data(data1, &value);
// 获取并打印 pData 的值
int* retrieved_value1 = (int*)get_data_value(data1);
printf("The value stored in pData (first call) is: %d\n", *retrieved_value1);
// 第二次调用 get_data
void* data2 = get_data();
printf("Second call to get_data: %p\n", data2);
// 获取并打印 pData 的值
int* retrieved_value2 = (int*)get_data_value(data2);
printf("The value stored in pData (second call) is: %d\n", *retrieved_value2);
// 释放内存
free(data1);
return 0;
}
运行结果
当你编译并运行上述程序时,输出将会是:1
2
3
4First call to get_data: 0x7ffeeb20b9a0
The value stored in pData (first call) is: 42
Second call to get_data: 0x7ffeeb20b9a0
The value stored in pData (second call) is: 42
解释
- 第一次调用
get_data
:data1
被赋值为get_data
函数返回的pData
。- 打印
data1
的地址。 - 设置
pData
的值为42
。 - 获取并打印
pData
的值。
- 第二次调用
get_data
:data2
被赋值为get_data
函数返回的pData
。- 打印
data2
的地址。 - 获取并打印
pData
的值。
关键点
- 地址相同:
data1
和data2
的地址是相同的,因为get_data
函数在第二次调用时直接返回了已经创建的pData
。 - 值相同:
data1
和data2
指向同一个DATA
结构体,因此它们的pData
值是相同的。
释放内存
- 注意:由于
data1
和data2
指向同一个DATA
结构体,只需要释放一次内存。在示例中,我们在main
函数的最后释放了data1
的内存。释放1
free(data1);
data1
的内存后,data2
也失去了其指向
单例模式的特点
- 唯一实例:确保在整个程序中,
DATA
结构体只有一个实例。 - 全局访问点:提供一个全局访问点(
get_data
函数),使得任何地方都可以获取到这个唯一实例。 - 延迟初始化:实例的创建是在第一次调用
get_data
函数时进行的,而不是在程序启动时立即创建,这样可以节省资源。使用场景
- 资源管理:确保资源(如文件句柄、数据库连接等)在整个程序中只有一个实例,避免资源浪费和冲突。
- 配置管理:全局配置对象,确保配置在程序的任何地方都能访问。
- 日志记录:日志记录器对象,确保日志记录的统一性和一致性。
注意事项
- 线程安全:在多线程环境中,
get_data
函数的实现需要考虑线程安全。可以使用互斥锁(mutex)来确保多线程环境下pData
的初始化是线程安全的。 - 内存管理:确保在程序结束时释放分配的内存,避免内存泄漏。
示例:线程安全的单例模式
在这个线程安全的版本中,使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct _DATA
{
void* pData;
} DATA;
static DATA* pData = NULL;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* get_data()
{
if (NULL != pData)
return pData;
pthread_mutex_lock(&mutex);
if (NULL == pData)
{
pData = (DATA*)malloc(sizeof(DATA));
assert(NULL != pData);
}
pthread_mutex_unlock(&mutex);
return (void*)pData;
}pthread_mutex_lock
和pthread_mutex_unlock
来确保pData
的初始化是线程安全的。
原型模式(Prototype Pattern)
1 | /// c++ |
1 | /// c |
代码解释
你提供的代码实现了一个对象克隆的设计模式,通常被称为原型模式(Prototype Pattern)。原型模式是一种创建型设计模式,它允许你通过复制现有对象来创建新对象,而无需通过常规的构造函数来创建对象。
结构体定义
1 | typedef struct _DATA { |
struct _DATA* (*copy)(struct _DATA* pData);
:这是一个函数指针,指向一个复制函数。这个函数接受一个struct _DATA*
类型的参数,并返回一个struct _DATA*
类型的指针。具体实例
1
DATA data_A = {data_copy_A};
data_A
是一个DATA
类型的实例,其copy
成员被初始化为data_copy_A
函数。复制函数
1
2
3
4
5
6struct _DATA* data_copy_A(struct _DATA* pData) {
DATA* pResult = (DATA*)malloc(sizeof(DATA));
assert(NULL != pResult);
memmove(pResult, pData, sizeof(DATA));
return pResult;
}data_copy_A
函数负责创建一个新的DATA
实例,并将pData
的内容复制到新实例中。malloc
用于分配内存。assert
用于确保内存分配成功。memmove
用于将pData
的内容复制到新分配的内存中。- 返回新创建的
DATA
实例的指针。克隆函数
1
2
3struct _DATA* clone(struct _DATA* pData) {
return pData->copy(pData);
} clone
函数是一个通用的克隆函数,它调用pData
对象的copy
成员函数来创建一个新的对象。原型模式的特点
- 对象创建:
- 通过复制现有对象来创建新对象,而不是通过构造函数。
- 这样可以避免复杂的构造过程,特别是在对象的创建过程非常复杂或昂贵时。
- 对象复制:
- 使用对象的
copy
方法来创建新的对象实例。 - 这个方法通常是一个深复制(Deep Copy),确保新对象与原对象完全独立。
- 使用对象的
- 灵活性:
- 减少重复代码:通过复制现有对象,可以避免重复的初始化代码。
- 提高性能:在某些情况下,复制现有对象比通过构造函数创建新对象更高效。
- 灵活性:可以轻松地添加或修改对象的复制行为,而无需修改客户端代码。
缺点
- 深复制的复杂性:实现深复制可能比较复杂,特别是在对象包含复杂数据结构时。
- 内存管理:需要确保正确地管理内存,避免内存泄漏。
示例使用
以下是一个示例程序,展示了如何使用clone
函数来创建DATA
对象的副本:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
typedef struct _DATA {
struct _DATA* (*copy)(struct _DATA* pData);
int value;
} DATA;
struct _DATA* data_copy_A(struct _DATA* pData) {
DATA* pResult = (DATA*)malloc(sizeof(DATA));
assert(NULL != pResult);
memmove(pResult, pData, sizeof(DATA));
return pResult;
}
struct _DATA* clone(struct _DATA* pData) {
return pData->copy(pData);
}
int main() {
// 创建一个 DATA 实例
DATA data_A = {data_copy_A, 42};
// 克隆 data_A
DATA* data_B = clone(&data_A);
// 打印原始和克隆对象的值
printf("Original value: %d\n", data_A.value);
printf("Cloned value: %d\n", data_B->value);
// 修改克隆对象的值
data_B->value = 84;
printf("Modified cloned value: %d\n", data_B->value);
// 确保原始对象的值没有改变
printf("Original value after modification: %d\n", data_A.value);
// 释放内存
free(data_B);
return 0;
}
组合模式(Composite Pattern)
组合模式类似于二叉树。
1 | typedef struct _Object |
代码解释
你提供的代码实现了一个组合模式(Composite Pattern)。组合模式是一种结构型设计模式,它允许你将对象组合成树形结构来表示“部分-整体”的层次结构。组合模式使得客户端可以一致地处理单个对象和对象组合。
结构体定义
1 | typedef struct _Object { |
struct _Object** ppObject;
:这是一个指向对象数组的指针,用于存储子对象。int number;
:表示子对象的数量。void (*operate)(struct _Object* pObject);
:这是一个函数指针,指向一个操作函数。这个函数接受一个struct _Object*
类型的参数。父对象的操作函数
1
2
3
4
5
6
7
8void operate_of_parent(struct _Object* pObject) {
int index;
assert(NULL != pObject);
assert(NULL != pObject->ppObject && 0 != pObject->number);
for(index = 0; index < pObject->number; index++) {
pObject->ppObject[index]->operate(pObject->ppObject[index]);
}
}operate_of_parent
函数负责遍历所有子对象,并调用每个子对象的operate
函数。assert
用于确保pObject
和ppObject
不为NULL
,并且number
不为0
。子对象的操作函数
1
2
3
4void operate_of_child(struct _Object* pObject) {
assert(NULL != pObject);
printf("child node!\n");
}operate_of_child
函数是一个简单的操作函数,用于处理子对象。在这个示例中,它只是打印一条消息。处理函数
1
2
3
4void process(struct Object* pObject) {
assert(NULL != pObject);
pObject->operate(pObject);
}process
函数是一个通用的处理函数,它调用pObject
的operate
函数来执行操作。组合模式的特点
- 树形结构:
- 组合模式允许你将对象组合成树形结构,表示“部分-整体”的层次关系。
- 树的每个节点可以是叶子节点(没有子节点)或组合节点(有子节点)。
- 统一接口:
- 无论是叶子节点还是组合节点,它们都实现了相同的接口(在这个例子中是
operate
函数)。 - 客户端可以一致地处理单个对象和对象组合,而不需要关心具体的实现。
- 无论是叶子节点还是组合节点,它们都实现了相同的接口(在这个例子中是
- 灵活性:
- 简化客户端代码:客户端可以一致地处理单个对象和对象组合,而不需要关心具体的实现。
- 灵活性:可以轻松地添加、删除或修改树的结构,而不需要修改客户端代码。
- 扩展性:可以通过添加新的叶子节点或组合节点来扩展系统功能。
缺点
- 复杂性:实现组合模式可能会增加代码的复杂性,特别是在处理复杂的树形结构时。
- 性能开销:递归遍历树形结构可能会带来一定的性能开销。
示例使用
以下是一个示例程序,展示了如何使用组合模式来创建和处理对象树:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
typedef struct _Object {
struct _Object** ppObject;
int number;
void (*operate)(struct _Object* pObject);
} Object;
void operate_of_parent(struct _Object* pObject) {
int index;
assert(NULL != pObject);
assert(NULL != pObject->ppObject && 0 != pObject->number);
for(index = 0; index < pObject->number; index++) {
pObject->ppObject[index]->operate(pObject->ppObject[index]);
}
}
void operate_of_child(struct _Object* pObject) {
assert(NULL != pObject);
printf("child node!\n");
}
void process(struct Object* pObject) {
assert(NULL != pObject);
pObject->operate(pObject);
}
int main() {
// 创建两个子对象
Object child1 = {NULL, 0, operate_of_child};
Object child2 = {NULL, 0, operate_of_child};
// 创建一个父对象,并将其子对象添加到父对象中
Object parent = {NULL, 2, operate_of_parent};
parent.ppObject = (struct _Object**)malloc(parent.number * sizeof(struct _Object*));
parent.ppObject[0] = &child1;
parent.ppObject[1] = &child2;
// 处理父对象
process(&parent);
// 释放分配的内存
free(parent.ppObject);
return 0;
}
代码解释
- 创建子对象:
child1
和child2
是两个子对象,它们的ppObject
为NULL
,表示它们没有子对象。number
为0
,表示它们没有子对象。operate
指向operate_of_child
函数,表示它们的操作是打印一条消息。
- 创建父对象:
parent
是一个父对象,它的number
为2
,表示它有两个子对象。ppObject
是一个指向子对象数组的指针,需要动态分配内存。operate
指向operate_of_parent
函数,表示它的操作是遍历并调用每个子对象的operate
函数。
- 处理父对象:
- 调用
process(&parent)
函数来处理父对象。 process
函数会调用parent
的operate
函数,即operate_of_parent
。operate_of_parent
函数会遍历parent
的子对象,并调用每个子对象的operate
函数,即operate_of_child
。
- 调用
- 释放内存:
- 使用
free
释放动态分配的内存,以避免内存泄漏。
- 使用
运行结果
运行上述程序,输出将会是:1
2child node!
child node!
这表明父对象成功地调用了其所有子对象的 operate
函数,每个子对象都打印了一条消息。
总结
通过这个示例,我们可以看到组合模式如何帮助我们构建和处理树形结构。客户端可以一致地处理单个对象和对象组合,而不需要关心具体的实现。这使得代码更加灵活和可扩展。
模板方法模式(Template Method Pattern)
1 | typedef struct _Basic |
你提供的代码实现了一个模板方法模式(Template Method Pattern)。模板方法模式是一种行为设计模式,它定义了一个操作中的算法骨架,而将一些步骤延迟到子类中实现。模板方法使得子类可以在不改变算法结构的情况下重定义算法的某些特定步骤。
代码解释
结构体定义
1 | typedef struct _Basic { |
void* pData;
:这是一个指向数据的指针,可以用于存储任何类型的数据。void (*step1)(struct _Basic* pBasic);
:这是一个函数指针,指向一个名为step1
的操作函数。void (*step2)(struct _Basic* pBasic);
:这是一个函数指针,指向一个名为step2
的操作函数。void (*process)(struct _Basic* pBasic);
:这是一个函数指针,指向一个名为process
的操作函数。模板方法
1
2
3
4void process(struct _Basic* pBasic) {
pBasic->step1(pBasic);
pBasic->step2(pBasic);
}process
函数是一个模板方法,它定义了算法的骨架。在这个例子中,算法的步骤是先调用step1
,再调用step2
。pBasic->step1(pBasic);
:调用step1
函数。pBasic->step2(pBasic);
:调用step2
函数。模板方法模式的特点
- 算法骨架:
- 模板方法模式定义了一个操作的算法骨架,但将某些步骤的具体实现延迟到子类中。
- 模板方法通常是一个最终方法(在C语言中无法直接实现,但可以通过约定来实现),以防止子类改变算法结构。
- 子类实现:
- 子类可以重定义算法的某些步骤,而不需要改变算法的结构。
- 这使得子类可以在不改变整体算法的情况下,提供不同的实现。
- 灵活性:
- 代码复用:
- 模板方法模式通过定义算法的骨架,使得公共部分的代码可以复用。
- 子类只需要实现特定的步骤,而不需要重复实现公共部分。
- 扩展性:
- 子类可以轻松地扩展或修改算法的某些步骤,而不需要改变算法的整体结构。
- 这使得代码更加灵活和可扩展。
- 控制反转:
- 子类依赖:
- 子类必须实现模板方法中定义的某些步骤,这可能会增加子类的复杂性。
- 如果子类没有正确实现这些步骤,可能会导致算法出错。
- 代码复杂性:
- 模板方法模式可能会增加代码的复杂性,特别是在处理复杂的算法时。
- 需要仔细设计模板方法和子类之间的关系。
示例使用
以下是一个示例程序,展示了如何使用模板方法模式来实现一个简单的算法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
typedef struct _Basic {
void* pData;
void (*step1)(struct _Basic* pBasic);
void (*step2)(struct _Basic* pBasic);
void (*process)(struct _Basic* pBasic);
} Basic;
void step1(struct _Basic* pBasic) {
printf("Step 1\n");
}
void step2(struct _Basic* pBasic) {
printf("Step 2\n");
}
void process(struct _Basic* pBasic) {
pBasic->step1(pBasic);
pBasic->step2(pBasic);
}
int main() {
// 创建一个 Basic 结构体实例
Basic basic = {NULL, step1, step2, process};
// 调用 process 函数
basic.process(&basic);
return 0;
}
代码解释
- 定义
Basic
结构体:void* pData;
:用于存储数据的指针,可以指向任何类型的数据。void (*step1)(struct _Basic* pBasic);
:指向step1
函数的指针。void (*step2)(struct _Basic* pBasic);
:指向step2
函数的指针。void (*process)(struct _Basic* pBasic);
:指向process
函数的指针。
- 定义具体的步骤函数:
step1
函数:模拟打开文件的操作。step2
函数:模拟读取文件内容的操作。
- 定义模板方法:
process
函数:定义了算法的骨架,先调用step1
,再调用step2
。
- 主函数:
进一步扩展
假设我们想扩展这个程序,增加一个关闭文件的步骤。我们可以定义一个新的步骤函数 step3
,并修改 process
函数以包含这个新步骤。
新的步骤函数
1 | void step3(struct _Basic* pBasic) { |
修改模板方法
1 | void process(struct _Basic* pBasic) { |
修改 Basic
结构体
我们需要在 Basic
结构体中添加一个指向 step3
函数的指针,并在 main
函数中初始化这个指针。1
2
3
4
5
6
7typedef struct _Basic {
void* pData;
void (*step1)(struct _Basic* pBasic);
void (*step2)(struct _Basic* pBasic);
void (*step3)(struct _Basic* pBasic);
void (*process)(struct _Basic* pBasic);
} Basic;
主函数
在 main
函数中,我们需要初始化 Basic
结构体的 step3
成员,并调用 process
函数。
1 | int main() { |
完整代码
1 |
|
运行结果
运行上述程序,输出将会是:1
2
3Step 1: Open file
Step 2: Read file content
Step 3: Close file
这表明模板方法模式成功地按照定义的算法骨架执行了文件处理的步骤,包括打开文件、读取文件内容和关闭文件。
进一步扩展
模板方法模式的一个重要特性是它允许子类(或子结构体)在不改变算法结构的情况下,重新定义算法的某些特定步骤。在 C 语言中,我们可以通过创建不同的结构体实例来实现这一点。
假设我们有一个新的文件处理需求,需要在读取文件内容之前进行一些预处理。我们可以创建一个新的结构体实例,并重定义 step2
函数。
新的步骤函数
1 | void step2_with_preprocessing(struct _Basic* pBasic) { |
主函数
在 main
函数中,我们可以创建一个新的 Basic
结构体实例,并初始化 step2
成员为 step2_with_preprocessing
。1
2
3
4
5
6
7
8
9
10
11int main() {
// 创建一个 Basic 结构体实例
Basic basic = {NULL, step1, step2, step3, process};
// 调用 process 函数
basic.process(&basic);
// 创建一个新的 Basic 结构体实例,重定义 step2
Basic basic_with_preprocessing = {NULL, step1, step2_with_preprocessing, step3, process};
// 调用 process 函数
basic_with_preprocessing.process(&basic_with_preprocessing);
return 0;
}
完整代码
1 |
|
代码解释
- 定义
Basic
结构体:void* pData;
:用于存储数据的指针,可以指向任何类型的数据。void (*step1)(struct _Basic* pBasic);
:指向step1
函数的指针。void (*step2)(struct _Basic* pBasic);
:指向step2
函数的指针。void (*step3)(struct _Basic* pBasic);
:指向step3
函数的指针。void (*process)(struct _Basic* pBasic);
:指向process
函数的指针。
- 定义具体的步骤函数:
step1
函数:模拟打开文件的操作。step2
函数:模拟读取文件内容的操作。step3
函数:模拟关闭文件的操作。step2_with_preprocessing
函数:模拟在读取文件内容之前进行预处理的操作。
- 定义模板方法:
process
函数:定义了算法的骨架,先调用step1
,再调用step2
,最后调用step3
。
- 主函数:
- 创建一个
Basic
结构体实例basic
,并初始化pData
为NULL
,step1
、step2
和step3
分别指向具体的步骤函数,process
指向模板方法。 - 调用
basic.process(&basic)
,执行标准的文件处理过程。 - 创建一个新的
Basic
结构体实例basic_with_preprocessing
,并初始化pData
为NULL
,step1
和step3
分别指向具体的步骤函数,step2
指向step2_with_preprocessing
,process
指向模板方法。 - 调用
basic_with_preprocessing.process(&basic_with_preprocessing)
,执行带有预处理的文件处理过程。
- 创建一个
运行结果
运行上述程序,输出将会是:1
2
3
4
5
6
7
8
9Standard file processing:
Step 1: Open file
Step 2: Read file content
Step 3: Close file
File processing with preprocessing:
Step 1: Open file
Step 2: Preprocess file content
Step 2: Read file content
Step 3: Close file
这表明模板方法模式成功地按照定义的算法骨架执行了文件处理的步骤,并且可以通过重定义特定步骤来实现不同的处理逻辑。
总结
通过这个示例,我们可以看到模板方法模式在 C 语言中的应用。它允许我们在不改变算法结构的情况下,通过重定义特定步骤来实现不同的功能。
工厂模式(Factory Pattern)
1 | // 工厂 |
应用
完成 main
函数的示例:1
2
3
4
5
6
7
8
9
10
11
12int main()
{
Shoe* leatherShoe = manufacture_new_shoe(LEATHER_TYPE);
leatherShoe->print_shoe(leatherShoe);
free(leatherShoe);
Shoe* rubberShoe = manufacture_new_shoe(RUBBER_TYPE);
rubberShoe->print_shoe(rubberShoe);
free(rubberShoe);
return 0;
}
代码解释
- 创建皮鞋:
Shoe* leatherShoe = manufacture_new_shoe(LEATHER_TYPE);
:调用工厂方法manufacture_new_shoe
,传入LEATHER_TYPE
,创建一个皮鞋对象。leatherShoe->print_shoe(leatherShoe);
:调用皮鞋对象的print_shoe
方法,打印“这是皮鞋”的信息。free(leatherShoe);
:释放皮鞋对象占用的内存。
- 创建胶鞋:
Shoe* rubberShoe = manufacture_new_shoe(RUBBER_TYPE);
:调用工厂方法manufacture_new_shoe
,传入RUBBER_TYPE
,创建一个胶鞋对象。rubberShoe->print_shoe(rubberShoe);
:调用胶鞋对象的print_shoe
方法,打印“这是胶鞋”的信息。free(rubberShoe);
:释放胶鞋对象占用的内存。
运行结果
当你编译并运行上述代码时,输出将会是:1
2This is a leather shoe!
This is a rubber shoe!
总结
通过工厂模式,我们可以将对象的创建逻辑集中管理,使得代码更加灵活和可扩展。客户端代码只需要调用工厂方法,而不需要关心对象的具体创建过程,这提高了代码的可维护性和可扩展性。
抽象工厂模式(Abstract Factory Pattern)
前面我们写过的工厂模式实际上是对产品的抽象。对于不同的用户需求,我们可以给予不同的产品,而且这些产品的接口都是一致的。而抽象工厂呢?顾名思义,就是说我们的工厂是不一定的。怎么理解呢,举个例子。
假设有两个水果店都在卖水果,都卖苹果和葡萄。其中一个水果店买白苹果和白葡萄,另外一个水果店卖红苹果和红葡萄。所以说,对于水果店而言,尽管都在卖水果,但是两个店卖的品种不一样。
定义(抽象)水果
1 | typedef struct _Apple |
定义具体的水果方法
1 | void print_white_apple() |
定义抽象工厂
1 | typedef struct _FruitShop |
定义具体的水果店
1 | Apple* sell_white_apple() |
创建工厂
1 | FruitShop* create_fruit_shop(int color) |
完整代码
包括创建工厂、使用工厂创建具体的水果,并调用打印方法:
1 |
|
代码解释
- 定义抽象水果:定义了
Apple
和Grape
结构体,每个结构体包含一个函数指针,用于打印水果的信息。 - 定义具体的水果方法:定义了四个具体的水果方法,分别打印不同颜色的苹果和葡萄。
- 定义抽象工厂:定义了
FruitShop
结构体,包含两个函数指针,分别用于创建苹果和葡萄。 - 定义具体的水果店方法:定义了四个具体的水果店方法,分别创建不同颜色的苹果和葡萄。
- 创建工厂:定义了
create_fruit_shop
函数,根据传入的颜色参数创建一个具体的水果店。 - 主函数:
- 创建一个卖白色水果的水果店。
- 创建一个卖红色水果的水果店。
- 通过水果店的方法创建具体的苹果和葡萄。
- 打印水果信息。
- 释放分配的内存。
通过这种方式,你可以在不修改客户端代码的情况下,轻松地添加新的水果类型或颜色。
责任链模式
AI:
责任链模式是什么来着?我记得它是一种设计模式,允许将请求沿着一条链传递,每个处理对象都有机会处理这个请求。这样可以避免请求的发送者和接收者之间的耦合,让系统更灵活。
那责任链模式的结构是怎样的呢?通常有一个处理者接口,每个处理者都有一个下一个处理者的引用。处理者可能会处理请求,或者将请求传递给下一个处理者。这样形成一个链式结构。
示例1
责任链模式是很实用的一种实际方法。举个例子来说,我们平常在公司里面难免不了报销流程。但是,我们知道公司里面每一级的领导的报批额度是不一样的。比如说,科长的额度是1000元,部长是10000元,总经理是10万元。
那么这个时候,我们应该怎么设计呢?其实可以这么理解。比如说,有人来找领导报销费用了,那么领导可以自己先看看自己能不能报。如果费用可以顺利报下来当然最好,可是万一报不下来呢?那就只能请示领导的领导了。
1 |
|
示例2
1 |
|
迭代器模式
C语言迭代器模式示例
迭代器模式是一种行为设计模式,允许你以顺序一致的方式遍历聚合对象中的各个元素,而无需暴露其内部表示。在C语言中,由于没有内置的集合类,因此需要手动实现迭代器模式。
以下是一个简单的C语言迭代器模式示例:
1. 定义集合结构
首先,定义一个表示集合的结构体。这里我们使用一个简单的数组作为集合。
1 | typedef struct { |
2. 定义迭代器结构
然后,定义一个表示迭代器的结构体。迭代器需要知道当前的位置(索引)以及它正在遍历的集合。
1 | typedef struct { |
3. 初始化迭代器函数
创建一个函数来初始化迭代器。这个函数将接受一个集合和一个起始索引,并返回一个初始化好的迭代器。
1 | Iterator* createIterator(Collection* collection, int start_index) { |
4. 检查是否还有下一个元素
创建一个函数来检查迭代器是否还有下一个元素可以访问。
1 | int hasNext(Iterator* iterator) { |
5. 获取下一个元素
创建一个函数来获取当前元素,并将迭代器移动到下一个位置。
1 | int next(Iterator* iterator) { |
6. 释放迭代器
创建一个函数来释放迭代器占用的内存。
1 | void releaseIterator(Iterator* iterator) { |
7. 主函数
在主函数中,创建一个集合,初始化迭代器,并遍历集合中的元素。
1 | int main() { |
输出
运行这个程序,你会看到以下输出:
1 | 1 2 3 4 5 |
解释
- 集合结构 (
Collection
):表示存储元素的容器。在这里,我们使用一个整数数组来存储元素。 - 迭代器结构 (
Iterator
):包含指向集合的指针和当前索引。索引用于跟踪当前遍历的位置。 - 初始化函数 (
createIterator
):创建并初始化迭代器,设置起始索引。 - 检查函数 (
hasNext
):检查是否还有下一个元素可以访问。 - 获取函数 (
next
):返回当前元素,并将索引递增以指向下一个元素。 - 释放函数 (
releaseIterator
):释放迭代器占用的内存。
通过这个示例,你可以看到迭代器模式如何封装集合的遍历逻辑,使得客户端代码不需要知道集合的内部结构即可遍历其中的元素。
外观模式
说明
外观模式(Facade Pattern)为子系统中的一组接口提供一个统一的接口,使得子系统更容易使用
注:子系统为同级别
例子
1 |
|
这个例子展示了如何使用外观模式简化复杂系统的使用,客户端只需要调用computer_start()
而不需要关心CPU、内存和硬盘之间的复杂交互。
输出结果:
1 | HardDrive read data from LBA 0, size 1024 |
代理模式
代理模式(Proxy Pattern) : 通过代理来访问真实的对象
保护代理示例(控制访问权限)
1 |
|
虚拟代理示例(延迟加载)
1 |
|
代码说明
- 保护代理示例:
- 代理在转发请求前执行访问控制
- 记录请求日志
- 客户端只与代理交互,不知道真实对象
- 虚拟代理示例:
- 延迟创建和加载昂贵的真实对象
- 第一次访问时加载真实对象
- 后续访问直接使用已加载的对象
- 代理模式优点:
- 控制对真实对象的访问
- 作为真实对象的替身,可以在访问对象时添加额外功能
- 实现延迟加载,提高系统性能
输出结果(保护代理):
1 | Client: Executing the client code with a proxy: |
输出结果(虚拟代理):
1 | Client: First access to image (will load): |
享元模式
参考文献
[2] C语言实现C++的封装继承与多态