在项目开发中多次使用数据库API后,我对其内部封装实现产生了浓厚兴趣。为此,我决定在QT平台上实践开发一个哈希数据库存储引擎。这个项目涉及诸多技术细节,将有效提升我的C++编程能力。
1.句柄管理与单例模式
句柄管理机制能有效隔离底层数据库对象,避免开发者直接修改数据库结构。由于项目通常涉及多个数据库且需要全局访问,因此必须设计一个具备唯一性的数据库管理类,采用单例模式实现这一需求。
内存布局:
┌─────────────────────────────────────┐
│ DatabaseManager (单例) │
│ ┌──────────────────────────────┐ │
│ │ QMap<database_t, database_it_t*> │ │
│ │ handle1 → db1对象地址 │ │
│ │ handle2 → db2对象地址 │ │
│ │ ... │ │
│ └──────────────────────────────┘ │
│ │
│ ┌──────────────────────────────┐ │
│ │ QMap<QString, database_t> │ │
│ │ "db1" → handle1 │ │
│ │ "db2" → handle2 │ │
│ │ ... │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘
用户手中的句柄:
┌─────────┐ ┌─────────┐
│ handle1 │ │ handle2 │
│ (void*) │ │ (void*) │
└────┬────┘ └────┬────┘
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ db1对象 │ │ db2对象 │
└─────────┘ └─────────┘
数据结构体设计
typedef void* database_t; // 句柄
//数据项
typedef struct {
void* data;
int32_t data_size;
}database_item_t;
//数据库结构
typedef struct {
QString name;
int32_t capacity;//容量
int32_t size;//已有数据个数
QHash<QString, database_item_t> items;
}database_it_t;
数据库管理器对象设计
class DatabaseManager{
private:
static DatabaseManager* m_instance;//单例
QMap<database_t, database_it_t*> m_handleToDb;//句柄到数据库
QMap<QString, database_t> m_nameToHandle;//名称到句柄
int32_t m_dbCounter;// 计数器,用于生成默认名称
DatabaseManager() : m_dbCounter(0) {}
public:
static DatabaseManager* instance();
static void destroy();
database_t createDatabase(int32_t size);//(公共版本,只有size参数)
database_t createDatabase(const QString& name, int32_t size); //(内部版本,有名称)
database_it_t* getDatabase(database_t handle);
database_t getDatabaseByName(const QString& name);
void destroyDatabase(database_t handle);
void clearDatabaseItems(database_it_t* db);
};
2.内存管理核心策略
栈对象 vs 堆对象的内存管理
创建数据库时,需要分别在堆和栈上构建结构体,具体实现如下:
//原来的写法
database_t DatabaseManager::createDatabase(const QString& name, int32_t size){
....
database_it_t db ; // 在栈上创建对象
memset(&db, '\0', sizeof(db));
db.name = name;
db.capacity = size;
db.size = 0;
database_t handle = static_cast<database_t>(db);// ❌ 传递栈地址
// 但不能转换为handle长期保存!因为函数返回后db会被销毁
...
}
// ✅ 正确:在堆上分配
database_it_t* db = new database_it_t(); // 在堆上创建
// new会自动调用构造函数,不需要memset
db->name = name; // 正确:使用箭头操作符
db->capacity = size;
database_t handle = static_cast<database_t>(db); // ✅ 堆地址可以长期保存
常见混淆点分析:
// 混淆1:结构体变量 vs 结构体指针
database_it_t db; // 变量,用点操作符 .
database_it_t* pDb; // 指针,用箭头操作符 ->
// 混淆2:取地址操作
database_t handle1 = static_cast<database_t>(db); // ❌ db不是指针
database_t handle2 = static_cast<database_t>(&db); // ✅ &db获取地址
// 混淆3:new的返回值
database_it_t* p1 = new database_it_t; // ✅ 分配内存,返回指针
database_it_t* p2 = new database_it_t(); // ✅ 加括号,值初始化
// database_it_t* p3 = new database_it_t[10]; // 分配数组
栈对象存储:值语义 vs 引用语义
// 情况1:数据库对象(错误示例)
database_it_t db; // 栈对象
// ... 初始化
database_t handle = static_cast<database_t>(&db); // ❌ 存储指针
// 问题:db在函数结束后销毁,handle变成悬空指针
// 情况2:数据项对象
database_item_t item; // 栈对象
item.data = malloc(data_size); // 堆上分配实际数据
item.data_size = data_size;
database->items.insert(key, item); // ✅ 复制整个item
// 关键:insert()复制了item的内容,不是指针!
两种构造方式
//C
typedef struct {
QString name;
int32_t capacity;
int32_t size;
QHash<QString, database_item_t> items;
}database_it_t;
// 使用
database_it_t db;
memset(&db, '\0', sizeof(db));
db.name = name;
db.size = 0;
//C++
// 为database_it_t添加构造函数
struct database_it_t {
QString name;
int32_t capacity;
int32_t size;
QHash<QString, database_item_t> items;
// 构造函数
database_it_t(const QString& n = "", int32_t cap = 0)
: name(n), capacity(cap), size(0) {
// items会自动初始化
}
};
// 使用
database_it_t* db = new database_it_t(name, size);
// 更简洁:database_it_t* db = new database_it_t{name, size, 0};
对象销毁机制设计
在开发实践中,开发者往往更关注对象创建而忽视对象销毁,这种疏忽可能导致严重的内存泄漏问题。
void DatabaseManager::destroy(){
if(m_instance){
//清理所有数据库
auto handles = m_instance->m_handleToDb.keys();
for(auto handle : handles){
m_instance->destroyDatabase(handle);
}
delete m_instance;
m_instance = nullptr;
}
}
void DatabaseManager::destroyDatabase(database_t handle){
auto it = m_handleToDb.find(handle);
if(it != m_handleToDb.end()){
database_it_t* db = it.value();
//清理item中的数据
clearDatabaseItems(db);
m_nameToHandle.remove(db->name);
delete db;
m_handleToDb.erase(it);
}
}
void DatabaseManager::clearDatabaseItems(database_it_t* db){
if(!db) return;
for(auto& item : db->items){
if(item.data){
free(item.data);
item.data = nullptr;
}
}
db->items.clear();
db->size = 0;
db->capacity = 0;
}
3. API设计与C/C++接口
错误处理
最初阶段,我习惯使用各种数字返回值配合qDebug()函数进行调试输出,但这种做法不够规范。更合理的方式是设计标准化的错误处理机制,这样能更准确地定位程序中的问题所在。
// 错误码定义
typedef enum{
DB_SUCCESS = 0,
DB_ERROR_INVALID_PARAM = -1,//参数初始化错误
DB_ERROR_DB_NOT_FOUND = -2,//数据库不存在
DB_ERROR_INVALID_KEY = -3,//非法键
DB_ERROR_KEY_EXISTS = -4,//键不存在
DB_ERROR_MEMORY_FULL = -5,//容量已满
DB_ERROR_KEY_NOT_FOUND = -6,//键未找到
DB_ERROR_UNKNOWN = -99 //未知错误
} db_error_t;
主要API接口
//创建数据库
database_t database_create(int32_t size);
//添加数据项
db_error_t database_add_item(database_t db, void* id, int32_t id_len, void* data, int32_t data_size);
//获取数据项
void* database_get_item(database_t db, void* id, int32_t id_len);
//删除数据项
int32_t database_remove_item(database_t db, void* id, int32_t id_len);
//清理数据库
void database_destory(database_t db);
参数设计陷阱:strlen vs sizeof
// 当前的API设计:
int32_t database_add_item(database_t db,
void* id, // ❓ 是字符串还是二进制?
int32_t id_len, // ❓ 用strlen还是sizeof?
void* data,
int32_t data_size);
// 问题:用户需要自己决定id_len的计算方式
// 这导致了两种常见错误:
// 1. 对字符串使用sizeof
// 2. 对二进制数据使用strlen
1.字符串的内存布局
const char* str = "hello";
// 内存布局:
// 地址: 0x1000: 'h' (0x68)
// 地址: 0x1001: 'e' (0x65)
// 地址: 0x1002: 'l' (0x6C)
// 地址: 0x1003: 'l' (0x6C)
// 地址: 0x1004: 'o' (0x6F)
// 地址: 0x1005: '\0' (0x00)
// strlen工作方式:从0x1000开始,遇到0x00停止,计数5
// sizeof(str):是指针大小(64位为8)
// sizeof("hello"):是数组大小,包含'\0',为6
2.整数的内存布局
int32_t num = 0x12345678; // 十进制:305419896
// 内存布局(小端):
// 地址: 0x2000: 0x78 // 最低字节
// 地址: 0x2001: 0x56
// 地址: 0x2002: 0x34
// 地址: 0x2003: 0x12 // 最高字节
// 注意:没有'\0'终止符!
// strlen((char*)&num)的危险:
// 从0x2000读取:0x78 → 字符 'x'
// 从0x2001读取:0x56 → 字符 'V'
// 从0x2002读取:0x34 → 字符 '4'
// 从0x2003读取:0x12 → 控制字符
// 然后继续读取0x2004... ❌ 越界访问!
// 可能:1)遇到0字节提前结束,返回错误的长度
// 2)访问非法内存,导致段错误
核心要点总结
strlen:用于C风格字符串,运行时计算,不包含’\0’sizeof:用于类型或变量,编译时确定,返回内存字节数- 字符串字面量:
sizeof("hello")包含'\0',strlen("hello")不包含 - 指针变量:
sizeof(ptr)返回指针大小,不是指向数据的大小
4. 存储引擎实现
上述实现方案体现了数据库管理的几个关键细节:首先采用句柄机制配合单例模式,构建了适配项目需求的API接口封装。这种设计理念源自操作系统资源管理的经典范式,也是系统级软件开发的核心原则之一——通过抽象与封装来有效管理复杂资源。
- 清晰的接口边界:用户只需操作句柄,不关心内部实现
- 统一的资源管理:所有数据库实例集中管理
- 良好的封装性:内部数据结构完全隐藏
- 线程安全的基础:为后续扩展提供可能
Qt/C++数据库封装与存储引擎


被折叠的 条评论
为什么被折叠?



