数据存储器
分配存储空间
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