c语言的设计模式学习

前言:更好的设计模式,能让我们设计出更优质和更利于维护的代码,本文针对c语言的设计模式进行学习总结以及知识分享,不一定全正确,请带着辩证思维进行学习,欢迎评论和纠错;

我在游览论坛时发现有个大佬使用c语言运用了面向对象的思路,解析了c++的设计模式,深受启发,于是在学习过程中,记录下自己的学习笔记,希望能帮助大家,欢迎讨论和纠错;

ps:本文引用了大佬的代码作为基础,做了一定的剖析,然后自己写了一些不入流的demo,供大家理解

下面为参考链接:
C语言和设计模式(总结篇) 用了多年的C_c程序设计 模式-CSDN博客
此为个人笔记链接:
【有道云笔记】c语言的设计模式学习.md
有道云笔记

继承,封装,多态

继承

源码:
typedef struct _parent
{
    int data_parent;
 
}Parent;
 
typedef struct _Child
{
    struct _parent parent;
    int data_child;
 
}Child;

解析:
此处代码模拟了继承的过程;
在设计c语言继承性的时候,只需要做的就是把继承数据放在继承的结构的首位置;
无论是数据的访问,数据的强转,数据的访问都不会有问题;

封装

源码:
struct _Data;
 
typedef  void (*process)(struct _Data* pData);
 
typedef struct _Data
{
    int value;
    process pProcess;
    
}Data;

解析:
这段代码定义了一个名为"_Data"的结构体,以及两个"typedef"类型:'procss'和'data';

1. strcut _Data是一个前向声明,它告诉编译存在一个名为'_Data'的结构体类型,但是具体的定义在后面给出;
这种做法的目的在c语言中常用于处理相互引用的结构体;
2. 'typedef  void (*process)(struct _Data* pData);'定义一个名为'process'的函数指针类型;
这种类型的函数指针可以指向一个函数,该函数接受一个指向'_Data'结构体的指针作为参数,并且不返回任何值;
3. 结构体定义了'_Data'结构体的具体内容,并且通过'typedef'将'_Data'结构体的别名设置为Data;
结构体包含两个成员:
  + 'int value;':一个'value'的整数类型成员
  + 'process pProcess;':一个名为'pProcess'的函数指针,其类型为先前定义的'process',即这个指针可以指向一个接受Data结构体指针为参数的函数,并且这个函数不返回任何值;
最后:
通过这种设计,你可以在Data结构体中存储一个整数值,并且为这个结构体关联一个处理函数,这个处理函数可以操作这个结构体;
这在设计面向对象的C语言代码时是一种常见的做法,通过函数指针来模拟类方法;

下面是代码demo:
#include <stdio.h>

// 前向声明
struct _Data;

// 定义process函数指针类型
typedef  void (*process)(struct _Data* pData);

// 定义Data结构体
typedef struct _Data
{
    int value;
    process pProcess;
    
}Data;

// 处理函数实现
void doubleValue(struct _Data* pData) {
    pData->value *= 2;
}

int main() {
    // 初始化Data实例
    Data myData = {10, doubleValue};
    printf("原始值: %d\n", myData.value);
    
    // 调用处理函数
    myData.pProcess(&myData);
    printf("处理后的值: %d\n", myData.value);

    return 0;
}

结论:
类似c++的封装函数功能,只需要调用对应的方法就可以访问或者修改内部的成员;
封装性的意义在于:函数和数据是捆绑在一起的,数据和数据是捆绑在一起的;
这样,我们可以通过一个简单的结构体指针去访问所有的数据,遍历所得函数;

多态

typedef struct _Play
{
    void* pData;
    void (*start_play)(struct _Play* pPlay);
}Play;

这是一个对多态的模仿
下面是代码举例,以播放视频和音频为例子

基础结构体:
typedef struct _MediaPlayer {
    void (*play)(struct _MediaPlayer* player); // 函数指针,模仿虚函数实现多态
    void* data; // 可以指向任何额外的数据,例如特定类型播放器的数据
} MediaPlayer;

音频播放器:
typedef struct {
    // 音频播放器特有的数据
    int audioQuality;
} AudioPlayerData;

void playAudio(MediaPlayer* player) {
    AudioPlayerData* data = (AudioPlayerData*)player->data;
    printf("Playing audio with quality %d\n", data->audioQuality);
}

视频播放器:
typedef struct {
    // 视频播放器特有的数据
    int videoResolution;
} VideoPlayerData;

void playVideo(MediaPlayer* player) {
    VideoPlayerData* data = (VideoPlayerData*)player->data;
    printf("Playing video with resolution %d\n", data->videoResolution);
}

结合:
int main() {
    AudioPlayerData audioData = {320};
    MediaPlayer audioPlayer = {playAudio, &audioData};
    
    VideoPlayerData videoData = {1080};
    MediaPlayer videoPlayer = {playVideo, &videoData};
    
    // 使用同一个函数调用,实现不同的行为
    audioPlayer.play(&audioPlayer); // 播放音频
    videoPlayer.play(&videoPlayer); // 播放视频

    return 0;
}

结论:
多态是面向对象编程中的一个核心概念,它允许对象通过指向其基类的指针或引用来访问派生类的成员函数;
在此处,我们利用结构体的内部成员进行了巧妙的模仿,我们不需要清楚结构体内部成员是什么,只需要调用它内部,后面的事情不需要我们考虑;
不同的接口做不同的工作;

善用上面三个操作,会让代码可读性和可维护性提升,在使用过程注意指针的运用;

单件模式

单件模式(Singleton Pattern)是一种设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来获取该实例;
它经常被用于管理共享资源,比如配置文件,数据库连接等;
单件模式通过隐藏构造函数和提供一个静态方法来保证只能创建一个实例;

单件模式的关键点:

  1. 私有的构造函数:确保外部不能直接通过new关键字来创建类的实例;
  2. 静态私有成员变量:持有类的唯一实例;
  3. 公有的静态方法:提供全局访问点,外部通过这个方法获取实例;这个方法需要判断实例是否已经创建,如果没有,则创建实例;如果已经创建,直接返回该实例。

demo

typedef struct _DATA
{
    void* pData;
}DATA;

void* get_data()
{
    static DATA* pData = NULL;
    
    // 如果pData已经被初始化,则直接返回这个唯一的实例
    if(NULL != pData)
        return pData;
 
    // 首次调用时,为pData分配内存,并确保分配成功
    pData = (DATA*)malloc(sizeof(DATA));
    assert(NULL != pData); //assert的作用是现计算表达式 expression如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用abort来终止程序运行
    pData->pData = NULL; // 初始化指针

    // 返回指向唯一实例的指针
    return (void*)pData;
}

具体运用:
typedef struct _DATA
{
    void* pData;
} DATA;

static DATA* get_instance()
{
    static DATA* instance = NULL;
    if (instance == NULL) {
        instance = (DATA*)malloc(sizeof(DATA));
        assert(instance != NULL);
        instance->pData = NULL;
    }
    return instance;
}

void* get_data()
{
    DATA* instance = get_instance();
    return instance->pData;
}

void set_data(void* data)
{
    DATA* instance = get_instance();
    instance->pData = data;
}

void free_instance()
{
    DATA* instance = get_instance();
    if (instance != NULL) {
        free(instance);
        instance = NULL;
    }
}

int main()
{
    int* pGlobalValue = malloc(sizeof(int)); // 为全局值分配内存
    *pGlobalValue = 10; // 初始化全局值

    set_data(pGlobalValue); // 设置单例存储的数据

    // 获取并打印全局值
    int* pFetchedValue = (int*)get_data();
    printf("Global value: %d\n", *pFetchedValue);

    // 修改全局值
    pFetchedValue = 30;

    // 再次获取并打印全局值,验证它被成功修改
    pFetchedValue = (int*)get_data();
    printf("Global value after modification: %d\n", *pFetchedValue);

    // 释放全局值占用的内存和单例对象
    free(pGlobalValue);
    free_instance();

    return 0;
}

总结:
从内存安全的角度来看:单例模式通过控制实例的创建和生命周期管理可以减少内存泄漏和野指针的风险,相对于直接管理堆空间来说,可以认为是更“安全”的;

如果是存储局部的数据,需要注意在存储前的释放,因为局部数据生命周期结束后,其地址上的内柔也被释放,而单件模式的存储是通过指针进行操作
typedef struct _DATA
{
    void* pData;
    size_t size; // 存储数据大小
} DATA;

static DATA* get_instance()
{
    static DATA* instance = NULL;
    if (instance == NULL) {
        instance = (DATA*)malloc(sizeof(DATA));
        instance->pData = NULL;
        instance->size = 0;
    }
    return instance;
}

void set_data(void* data, size_t size)
{
    DATA* instance = get_instance();
    if (instance->pData != NULL) {
        free(instance->pData); // 如果之前有数据,先释放
    }
    instance->pData = malloc(size); // 分配内存以存储新数据
    memcpy(instance->pData, data, size); // 复制数据
    instance->size = size;
}

void* get_data()
{
    DATA* instance = get_instance();
    return instance->pData;
}

void free_instance()
{
    DATA* instance = get_instance();
    if (instance != NULL) {
        if (instance->pData != NULL) {
            free(instance->pData);
        }
        free(instance);
        instance = NULL;
    }
}

int main()
{
    int localValue = 10; // 局部变量
    set_data(&localValue, sizeof(localValue)); // 存储局部变量的值

    int* pFetchedValue = (int*)get_data();
    printf("Stored value: %d\n", *pFetchedValue);

    // 修改局部变量并再次存储
    localValue = 20;
    set_data(&localValue, sizeof(localValue)); // 更新存储的值

    // 再次获取并打印存储的值
    pFetchedValue = (int*)get_data();
    printf("Updated stored value: %d\n", *pFetchedValue);

    free_instance(); // 清理资源

    return 0;
}

2024/3/9补充:
和同事进行讨论学习,发现文中引用代码的设计有一个bug,那就是线程安全没有考虑进去:
原型代码:
#include <string.h>
#include <assert.h>
 
class object
{
public:
    static class object* pObject;
 
    static object* create_new_object()
    {
        if(NULL != pObject)
   return pObject;
 
  pObject = new object();
  assert(NULL != pObject);
  return pObject;
    }
 
private:
    object() {}
    ~object() {}
};
 
class object* object::pObject = NULL;
我用以下代码进行测试,发现获取实例的时候,得到了两个不同的地址,无疑是创建了两个对象;
#include <iostream>
#include <thread>
#include <vector>
#include <assert.h>

class object {
public:
    static object* pObject;

    static object* create_new_object() {
        if (NULL == pObject) {
            pObject = new object();
            assert(NULL != pObject);
        }
        return pObject;
    }

private:
    object() {}
    ~object() {}
};

object* object::pObject = NULL;

void attempt_to_create_singleton() {
    object* singleton = object::create_new_object();
    std::cout << "Singleton address: " << singleton << std::endl;
}

int main() 
{
    const int NUM_THREADS = 10;
    std::vector<std::thread> threads;

    for (int i = 0; i < NUM_THREADS; ++i) {
        threads.push_back(std::thread(attempt_to_create_singleton));
    }

    for (auto& th : threads) {
        th.join();
    }

    return 0;
}


优化如下:
object* object::pObject = NULL; 替换成 object* object::pObject = object.create_new_object();
这样就能在线程使用前对对象进行了一次初始化,不需要线程锁的操作,只生产一个对象


这个部分需要结合引用文章,文中c++的用法配合阅读;
我对文中出现的线程问题进行了修复;

那c语言的呢?其实也很简单,模仿c++写法
DATA* instance = get_instance();//优先初始化

原型模式

原型模式是一种创建型设计模式,使得对象能够复制自身,以便创建一个新的对象;
这个模式在需要根据现有对象创建新对象,但又不想依赖于它们的具体类时非常有用;

代码解析

typedef struct _DATA
{
    struct _DATA* (*copy) (struct _DATA* pData);
} DATA;
首先,定义了一个名为'_DATA'的结构体,它包含一个指向函数的指针'copy';
这个函数接受一个指向'_DATA'结构体的指针作为参数,并返回一个新的'_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;
}
实现了'data_copy_A'函数,这个函数创建了'pData'的一个深拷贝;
它首先分配新的内存空间给拷贝,然后使用'memmove'函数将原始对象的内容复制到新对象中;

DATA data_A = {data_copy_A};
一个简单的结构体初始化,没什么好解释的

struct _DATA* clone(struct _DATA* pData)
{
    return pData->copy(pData);
}

提供一个'clone'函数,接受一个'_DATA'的指针,并使用这个进行复制对象;

以下是我的一些见解和优化:
typedef struct _DATA {
    struct _DATA* (*copy) (const struct _DATA* pData);
} DATA;
这个函数的传参是个指针,利用const保护,防止修改指针的内柔;

struct _DATA* data_copy_A(const struct _DATA* pData) {
    if (pData == NULL) {
        return NULL; // 处理空指针情况
    }
    
    DATA* pResult = (DATA*)malloc(sizeof(DATA));
    if (pResult == NULL) {
        // 处理内存分配失败情况
        return NULL;
    }
    // 使用memmove之前应检查pResult是否非NULL
    memmove(pResult, pData, sizeof(DATA));
    pResult->copy = data_copy_A; // 确保复制的对象也有copy方法
    return pResult;
}
优化'assert',在生产资源的情况下,这个函数不应该出现这里,因此把终断代码改为手动检查并处理错误是很有必要的;
在这里对复制的对象也进行了方法的给予,确保它自身也有复制能力;

struct _DATA* clone(const struct _DATA* pData) {
    if (pData == NULL || pData->copy == NULL) {
        return NULL; // 保证pData非空且具有有效的copy方法
    }
    return pData->copy(pData);
}
这里见仁见智,我增加了对空指针的检查,是为了避免空指针解引用的可能性,提高函数的健壮性;

补充的讲解:
在大佬文章的评论区下,有人对memcpy和memmove有争议,在对两个函数进行简要分析后,我更推崇此处使用memmove;
两个函数额功能都是:从源内存地址复制n个字节到目标内存地址;
但是在重叠地址上,memmove会采取适当的措施确保所有字节都被正确复制,避免在复制过程中覆盖还未读取的数据;
在效率上,memcpy更优,在安全上,memmove更优;

在允许的情况下,我认为牺牲忽略不可计的效率换取安全性的性价比会更加高;

2024-03-20更新

组合模型

概念

组合模式是允许你将对象组合成树形结构来表示“部分-整体”的层次结构;通过这种方式,客户端可以统一地对待单个对象和对象的组合;
组合模式主要用于希望客户端忽略组合对象与单个对象的差异时;在这种模式中,使用了一个抽象类/接口来表示节点对象和叶节点对象;这样,客户端可以通过一个统一的接口操作个别对象和组合对象;

组合模式的三个成员:

  1. 抽象组件:一个抽象的接口,定义了叶子和容器的共有方法,如添加(add),删除(remove),获取(get)子部件等方法;
  2. 叶子:再组合中标识叶节点,叶节点没有子节点;
  3. 容器:一个容器可以包含多个叶子或容器,即可以包含多个子部件;容器实现抽象组件中定义的操作,比如对子部件的管理操作;
组合模式使用场景一般是希望使用者以相同的方式对待个别对象和组合对象,是处理树形结构的有效方法;

代码demo

#include <stdio.h>
#include <stdlib.h>

// 菜单组件接口
typedef struct MenuItem {
    void (*print)(struct MenuItem* self);
    char* name;
} MenuItem;

// 菜单项,作为叶子节点
typedef struct {
    MenuItem base; // 继承自MenuItem
    // 可以添加更多菜单项特有的属性
} MenuLeaf;

// 菜单容器,可以包含多个菜单项或者子菜单
typedef struct {
    MenuItem base; // 继承自MenuItem
    MenuItem** children; // 子菜单项数组
    int childCount; // 子菜单项的数量
} MenuComposite;

// 打印菜单项的名称
void printMenuLeaf(MenuItem* menuItem) {
    printf("Menu Item: %s\n", menuItem->name);
}

// 打印整个菜单的结构,包括它的所有子菜单项
void printMenuComposite(MenuItem* menuItem) {
    MenuComposite* menu = (MenuComposite*)menuItem;
    printf("Menu: %s\n", menu->base.name);
    for (int i = 0; i < menu->childCount; ++i) {
        menu->children[i]->print(menu->children[i]);
    }
}

// 创建一个菜单项
MenuLeaf* createMenuLeaf(char* name) {
    MenuLeaf* item = (MenuLeaf*)malloc(sizeof(MenuLeaf));
    item->base.print = printMenuLeaf;
    item->base.name = name;
    return item;
}

// 创建一个菜单容器
MenuComposite* createMenuComposite(char* name) {
    MenuComposite* menu = (MenuComposite*)malloc(sizeof(MenuComposite));
    menu->base.print = printMenuComposite;
    menu->base.name = name;
    menu->children = NULL;
    menu->childCount = 0;
    return menu;
}

// 向菜单容器中添加菜单项
void addMenuItem(MenuComposite* menu, MenuItem* item) {
    menu->childCount++;
    menu->children = realloc(menu->children, menu->childCount * sizeof(MenuItem*));
    menu->children[menu->childCount - 1] = item;
}

int main() {
    // 创建菜单项
    MenuLeaf* item1 = createMenuLeaf("Item 1");
    MenuLeaf* item2 = createMenuLeaf("Item 2");

    // 创建菜单容器并添加菜单项
    MenuComposite* menu = createMenuComposite("Main Menu");
    addMenuItem(menu, (MenuItem*)item1);
    addMenuItem(menu, (MenuItem*)item2);

    // 展示菜单结构
    menu->base.print((MenuItem*)menu);

    // 释放内存
    free(item1);
    free(item2);
    free(menu->children);
    free(menu);

    return 0;
}

/*
realloc 函数用于重新分配内存块的大小,它既可以增加也可以减少已分配内存的大小
demo:
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 初始化一个大小为 5 的动态数组
    int* arr = (int*)malloc(5 * sizeof(int));
    if (arr == NULL) {
        perror("Initial malloc failed");
        return EXIT_FAILURE;
    }

    // 使用数组...
    for (int i = 0; i < 5; ++i) {
        arr[i] = i;
    }

    // 现在增加数组的大小到 10
    int* temp = realloc(arr, 10 * sizeof(int));
    if (temp == NULL) {
        perror("realloc failed");
        free(arr); // 释放原始内存
        return EXIT_FAILURE;
    }
    arr = temp; // 更新 arr 指针

    // 使用扩展的数组...
    for (int i = 5; i < 10; ++i) {
        arr[i] = i;
    }

    // 最后,释放内存
    free(arr);
    return EXIT_SUCCESS;
}
*/

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值