我的C++规范 - 数据存储器

数据存储器

分配存储空间

main.cpp
复制代码
#include <iostream>
#include <cstring>
#include <vector>
#include <vector>
#include "mclog.h"

// 存放字符类型的结构体,用于判断和格式化字符串
struct enum_type
{
    static const char digit = 'i';
    static const char chars = 's';
    static const char in = '[';
    static const char out = ']';
    static const char ready = ':';
};

// 用于保存二进制数据,可以是任何类型的类型
struct data_save
{
    int size = 0;
    void *data = nullptr;
};

// 创建一个结构体用于保存数据类型
// 存在 next 变量的同类型结构,说明 type_input 是链表结构,单向链表
// type 保存的类型
// data 具体的数据
// next 连续存储的下一个位置,指向同类型
struct type_input
{
    char type = '0';
    data_save data;
    type_input *next = nullptr;
};

// 处理数据输入格式并保存数据的类
class data_storage
{
public:
    // 初始化总数量为可以存储一个值
    data_storage()
    {
        resize_arr(1);
    }

    // 释放所以手动分配的空间,打印是否存在内存泄露
    ~data_storage()
    {
        delete_arr();
        MCLOG("未释放内存次数 " $(_size_allo));
    }

    // 解析字符串获取到 type_input 的结构列表,存储到 arr 数组中
    void parse_input(const std::string &str)
    {
        type_input *input = push_input_list(str);
        show_input(input);
        push_arr(input);
    }

    // show_input 的重载函数
    void show_input()
    {
        for (int i = 0; i < _size_arr; i++)
        {
            show_input((type_input *)_arr[i]);
        }
    }

    // show_input 的重载函数
    void show_input(int index)
    {
        if (index < _size_arr)
        {
            show_input((type_input *)_arr[index]);
        }
    }

private:
    // 从 type_input 链表获取一次存入的多个数据,输出成一行显示
    void show_input(type_input *input)
    {
        std::cout << "储存内容: ";
        type_input *go = input;
        
        // 从链表第一个表头位置开始,逐步遍历所有内容
        // 如果为空则表示链表结束了,不执行并退出
        while (go != nullptr)
        {
            // 根据具体类型,格式化成数字或文本
            if (go->type == enum_type::digit)
            {
                int value = *(int *)go->data.data;
                std::cout << "[数字: " << value << "]  ";
            }
            else if (go->type == enum_type::chars)
            {
                std::string str((char *)go->data.data, go->data.size);
                std::cout << "[文本: " << str << "]  ";
            }

            // 获取指向下一个 type_input 数据的指针赋值给自己,遍历行为
            go = go->next;
        }
        std::cout << std::endl;
    }

    // 将链表头放入 arr 数组
    void push_arr(type_input *input)
    {
        _arr[_size_arr] = input;
        _size_arr++;

        // arr 数组空间不足时自动扩容,类似 vector 行为
        if (_size_arr == _cap_arr)
        {
            resize_arr(_cap_arr * 2);
        }
    }

    // 从字符串中解析出 type_input 链表结构,可能存在有多个 type_input 数据
    // 函数会返回 type_input 链表的表头,可以根据表头遍历所有的数据
    type_input *push_input_list(const std::string &str)
    {
        // 定义 head 链表头和 next 跟进节点指针
        type_input *head = nullptr;
        type_input *next = nullptr;

        // 格式化判断准备
        std::string data;
        bool in = false;
        bool ready = false;
        char type = '0';
        for (int i = 0; i < str.size(); i++)
        {
            // 单次遍历所有传入的字符串
            char go = str[i];
            if (go == enum_type::in)
            {
                // 进入分割点,每一段数据都需要满足这个分隔点的格式才可以存储
                in = true;
            }
            else if (in && go == enum_type::out)
            {
                // 退出分割点,已经完成第一段的数据截取,开始从截取的数据中分析内容
                // 分配一个 type_input 的数据结构用于存储截取内容
                type_input *input = (type_input *)new_mem(sizeof(type_input));
                if (head == nullptr)
                {
                    head = input;
                }
                if (next)
                {
                    next->next = input;
                }

                // 链表操作,用于指向下一个 type_input 数据,下一次进入时就可以自己从尾部追加
                // head 表头指针是不变的,next 下一个跟随指针总是会移动到链表的尾部
                next = input;
                input->next = nullptr;
                input->type = type;

                if (type == enum_type::chars)
                {
                    // 文本格式,分配内容并复制到 type_input 的空间
                    int len = data.size();
                    char *p = (char *)new_mem(len);
                    std::memcpy(p, data.c_str(), data.size());
                    input->data.data = p;
                    input->data.size = len;
                }
                else if (type == enum_type::digit)
                {
                    // 数字格式,分配内容并复制到 type_input 的空间
                    int num = std::stoi(data);
                    int len = sizeof(int);
                    int *p = (int *)new_mem(len);
                    *p = num;
                    input->data.data = p;
                    input->data.size = len;
                }

                // 第一次截取完成,缓存状态归零,准备下一次截取
                in = false;
                ready = false;
                data.clear();
            }
            else if (in && go == enum_type::ready)
            {
                // 准备截取数据
                ready = true;
            }
            else if (in && ready == false)
            {
                // 获取数字或文本类型
                type = go;
            }
            else if (in && ready)
            {
                // 正在截取数据,缓存数据中
                data.push_back(go);
            }
        }

        return head;
    }

    // 释放 type_input 链表结构,从头开始逐个释放分配的数据
    void delete_input_list(type_input *input)
    {
        type_input *go = input;
        while (go != nullptr)
        {
            type_input *tm = go;
            go = go->next;
            delete_mem(tm->data.data);
            delete_mem(tm);
        }
    }

    // 给 arr 数组分配空间,会分配一块新空间,将旧空间赋值到新空间之后,释放旧空间
    void resize_arr(size_t size)
    {
        void **p = (void **)new_mem(size * sizeof(void *));
        for (int i = 0; i < _size_arr; i++)
        {
            p[i] = _arr[i];
        }
        delete_mem(_arr);

        _arr = p;
        _cap_arr = size;
        MCLOG("重新分配空间" $(_cap_arr) $(_size_arr))
    }

    // 释放所有的 arr 数组内容,包括存储在数组上的 type_input 链表结构数据
    void delete_arr()
    {
        for (int i = 0; i < _size_arr; i++)
        {
            delete_input_list((type_input *)_arr[i]);
        }

        delete_mem(_arr);
        _size_arr = 0;
        _cap_arr = 0;
        _arr = nullptr;
    }

    // 统一的释放函数,记录次数
    void delete_mem(void *p)
    {
        if (p != nullptr)
        {
            _size_allo--;
            delete[] (char *)p;
        }
    }

    // 统一的分配函数,记录次数
    void *new_mem(size_t len)
    {
        void *p = new char[len];
        _size_allo++;
        return p;
    }

private:
    // 分配记录,用于查看是否内存泄露
    int _size_allo = 0;

    // arr 数组当前储存的个数
    int _size_arr = 0;

    // arr 数组当前分配的总空间
    int _cap_arr = 0;

    // _arr 是数组,可以存储 void* 指针类型的数据,用于存储 type_input 数据列表的表头
    // 拥有表头之后,可以遍历所有的内容,可以掌握所有的 type_input 数据
    void **_arr = nullptr;
};

int main(int argc, char **argv)
{
    MCLOG("测试环节");
    {
        data_storage storage;
        storage.parse_input("[i:123][s:qwe][i:456][i:789][s:hello][s:world]");
        storage.parse_input("[i:100]");
        storage.parse_input("[i:200][i:300]");
        storage.parse_input("[s:hello]");
        storage.show_input();
    }

    MCLOG("\n用户操作");
    {
        bool run = true;
        int id = 0;
        std::string buff;
        data_storage storage;
        while (run)
        {
            std::string str = "\n欢迎来到数据存储机\n"
                              "1.存储\n"
                              "2.查看\n";
            std::cout << str << std::endl;
            std::cin >> buff;

            if (buff == "quit")
            {
                run = false;
            }
            else if (buff == "1")
            {
                std::cout << "存储格式 [i:XXX][s:XXX]" << std::endl;
                std::cin >> buff;
                storage.parse_input(buff);
                std::cout << "存储成功,请记住你的ID,下次查看时使用,ID: " << id << std::endl;
            }
            else if (buff == "2")
            {
                std::cout << "请输出要查看的ID" << std::endl;
                std::cin >> buff;
                int index = std::stoi(buff);
                storage.show_input(index);
            }
            id++;
        }
        MCLOG("退出程序");
    }
    return 0;
}
打印结果
复制代码
测试环节 [/home/red/open/github/mcpp/example/17/main.cpp:242]
重新分配空间[_cap_arr: 1] [_size_arr: 0]  [/home/red/open/github/mcpp/example/17/main.cpp:199]
储存内容: [数字: 123]  [文本: qwe]  [数字: 456]  [数字: 789]  [文本: hello]  [文本: world]
重新分配空间[_cap_arr: 2] [_size_arr: 1]  [/home/red/open/github/mcpp/example/17/main.cpp:199]
储存内容: [数字: 100]
重新分配空间[_cap_arr: 4] [_size_arr: 2]  [/home/red/open/github/mcpp/example/17/main.cpp:199]
储存内容: [数字: 200]  [数字: 300]
储存内容: [文本: hello]
重新分配空间[_cap_arr: 8] [_size_arr: 4]  [/home/red/open/github/mcpp/example/17/main.cpp:199]
储存内容: [数字: 123]  [文本: qwe]  [数字: 456]  [数字: 789]  [文本: hello]  [文本: world]
储存内容: [数字: 100]
储存内容: [数字: 200]  [数字: 300]
储存内容: [文本: hello]
未释放内存次数 [_size_allo: 0]  [/home/red/open/github/mcpp/example/17/main.cpp:39]

用户操作 [/home/red/open/github/mcpp/example/17/main.cpp:252]
重新分配空间[_cap_arr: 1] [_size_arr: 0]  [/home/red/open/github/mcpp/example/17/main.cpp:199]

欢迎来到数据存储机
1.存储
2.查看

1
存储格式 [i:XXX][s:XXX]
[i:100][i:53][i:999]
储存内容: [数字: 100]  [数字: 53]  [数字: 999]
重新分配空间[_cap_arr: 2] [_size_arr: 1]  [/home/red/open/github/mcpp/example/17/main.cpp:199]
存储成功,请记住你的ID,下次查看时使用,ID: 0

欢迎来到数据存储机
1.存储
2.查看

1
存储格式 [i:XXX][s:XXX]
[s:money][i:98][s:GameTime][i:51]
储存内容: [文本: money]  [数字: 98]  [文本: GameTime]  [数字: 51]
重新分配空间[_cap_arr: 4] [_size_arr: 2]  [/home/red/open/github/mcpp/example/17/main.cpp:199]
存储成功,请记住你的ID,下次查看时使用,ID: 1

欢迎来到数据存储机
1.存储
2.查看

2
请输出要查看的ID
1
储存内容: [文本: money]  [数字: 98]  [文本: GameTime]  [数字: 51]

欢迎来到数据存储机
1.存储
2.查看

quit
退出程序 [/home/red/open/github/mcpp/example/17/main.cpp:286]
未释放内存次数 [_size_allo: 0]  [/home/red/open/github/mcpp/example/17/main.cpp:39]

既然你已经通过上一篇文章掌握了指针的用法,那就趁热打铁的进一步了解指针吧

这一篇文章我给出的例子是,使用堆区数据来存储用户输入的数据,可以存储整数和文本两种格式,它将教会你如果去运用指针依旧分配空间

不过我要先声明的是,这一篇文章的代码中充满了不好的习惯,这些代码是为了演示使用指针和堆区来存储数据而强行使用的,在真正的编码中,实现一样的功能我们可不需要怎么复杂的操作,而且这一篇文章充斥着各种垃圾代码,新手请不要模仿

你可以先看一看 main.cpp 文件中的例子,然后我会一步步解析这份代码

二级指针

data_storage 类中的 void **_arr 是二级指针,一种指向指针的指针,普通指针内储存的是具体数据的地址,二级指针存储的是普通指针的地址,也就是说二级指针指针的还是指针

实际上这种嵌套结构是一种链表,你还可以声明三级四级指针,因为本指针这种多级指针只是指针低一下指针的指针而已

不要被这种定义给绕晕了,void **_arr 二级指针完全可以看成是一个数组,类似 int buf[10] 的数据,只是变成了 void* buf[10] ,如果你知道指针的大小为 8 byte 的话 void* buf[10] 就是 8 * 10 byte

定义一个二级指针之后,你就可以像普通数组一样使用它

结构体链表

你可能会疑惑,为什么 type_input 中存在一个 type_input *next 的变量,这个就是链表的实现方式,是一种可以自己指向自己的数据结构

如果一个结构体中,存在一个指针指向了一下一样的结构体,那你就可以跟着这一个指针一直寻找下一个结构体的数据,这是永无止境,除非 next = nullptr 这是它停止的标志,声明已经没有下一个了

数据结构

如果你了解了二级指针和结构体链表,那你就可以得出这个 data_storage 类到底在干嘛,他用 arr 数据存在 type_input 链表的表头,这样就可以顺序的存储所有分配出来的数据,而 type_input 链表的长度是可变的,这意味着它可以随意存储用户输入的任意长度的数据,将那些数据转为链表存起来

这个结构看来起来像极了 hash map 哈希表,就是提面容器中提到的 unordered_map 容器

看到这张图,我相信你已经知道数据的二进制结构到底是如何排布的了,可能你只需要在注意一些细节就能明白 data_storage 类的所有实现过程

分配空间

我是用 new_mem delete_mem 两个函数限制了 new delete 的使用,通过函数我可以清晰的知道分配和释放操作是否是一致,如果发生了内存泄露我可以进行排查

这里使用的是 new[] delete[] 表明他们释放的是一整快连续的空间,是以数组形式分配的空间,释放也是一样,不要搞混了

如果你分配的空间是需要以数组的形式连续分配,需要使用 new[] 分配 delete[] 释放

如果你只需要单块的独立内存,请使用 new 分配 delete 释放

空指针

你会发现不管是 arr 数据,或者是分配的 new_mem 分配的数据,都是采用了 void* 这种空指针类型,这种类型表明了存在一块数据内存,但是这个数据的类型是不确定的,空指针数据是不能使用的,而且空指针本身的类型不明确,一旦转换错误程序就直接崩溃了,这是非常不安全的行为

不要使用空指针是 编程规范 的重要一步

空指针是古老代码的糟粕,它是用于缓解多类型数据接口的手段,但是C++有模板可以用,所以不必使用空指针这种不确定的类型

链表的指向

你是否会疑惑 push_input_list 函数是如何将多个 type_input 结构体数据连接成一条线的,里面有两个关键的指针 head 和 next

你要知道链表的特点是不关心有几个数的,它只关心第一个和最后一个,中间具体有几个都是无所谓的,因为链表的遍历方式是通过 next 指针,它只会找到下一个,并告诉你是否存在下一个

那么回头看 head next 他们总是会指向第一个和最后一个,head 指针的第一个是不表的,next 指向的最后一个总是在出现新的 type_input 结构体后改变,这时候只需要将前一个和当前 type_input 结构体进行连接,然后 next 指针就可以推进到当前最新位置,保持在尾部了

数组扩容

在一个需要动态变化的数据中,你不能分配 int buf[10] 这种只有固定大小的空间,否则无法存储超出的部分

你需要从堆区中分配一块内存,然后等这个内存用完之后,在重新分配一快更大的内存,这个大小通常是旧内存的两倍,然后你要把旧内存复制到新内存中,并删除掉旧内存

这个逻辑几乎就是 vector 容器的工作原理,只不过但你不使用容器时就会变的很麻烦,在 resize_arr 函数中简单实现了这个功能

数组扩容几乎是必须的,因为你很难保证数据是不变的,一旦发生改变导致数量更大,你就必须要进行扩容

你要知道的是,动态扩容要比一开始就创建一个超级大的空间要好的多,否则内存就被这种行为消耗殆尽了

更好的方法

你在这一分代码中看到了一大堆的指针,还有 new delete 等需要从堆区分配的数据,但你要记住这只是为了然你清楚如果操作指针的一个例子而已

如果是以正常的形式实现相同的功能,你不需要 new 也不知道指针,只需要 一个 vector 容器即可,你只需要定义一个 std::vector<std::vector<std::string>> 的容器就可以等效的存储所有的数据,包括文本,整数或者其他的数据

请保持简单的方法来实现一些功能吧,因为C++是十分复杂的,很多技巧学到了也可能从来都用不上,但是也要能看懂才行,否则你会看不懂很多C++的代码

项目路径

复制代码
https://github.com/HellowAmy/mcpp.git
相关推荐
2301_817497331 小时前
C++中的适配器模式实战
开发语言·c++·算法
csbysj20201 小时前
W3C XML 活动
开发语言
Max_uuc1 小时前
【C++ 硬核】消灭 void*:用 std::variant 实现嵌入式“类型安全”的多态 (Type-Safe Union)
开发语言·c++
枫叶丹42 小时前
【Qt开发】Qt系统(十)-> Qt HTTP Client
c语言·开发语言·网络·c++·qt·http
Allen_LVyingbo2 小时前
医疗大模型预训练:从硬件选型到合规落地实战(2025总结版)
开发语言·git·python·github·知识图谱·健康医疗
范纹杉想快点毕业2 小时前
自学嵌入式系统架构设计:有限状态机入门完全指南,C语言,嵌入式,单片机,微控制器,CPU,微机原理,计算机组成原理
c语言·开发语言·单片机·算法·microsoft
leiming62 小时前
c语言更进一步
c语言·开发语言
王老师青少年编程2 小时前
2025信奥赛C++提高组csp-s复赛真题及题解:道路修复
c++·真题·csp·信奥赛·csp-s·提高组·复赛
九皇叔叔2 小时前
【07】SpringBoot3 MybatisPlus 删除(Mapper)
java·开发语言·mybatis·mybatis plus