QT&C++的数据库资源抽象和封装:内存优化与存储引擎实现

Qt/C++数据库封装与存储引擎

在项目开发中多次使用数据库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接口封装。这种设计理念源自操作系统资源管理的经典范式,也是系统级软件开发的核心原则之一——通过抽象与封装来有效管理复杂资源

  1. 清晰的接口边界:用户只需操作句柄,不关心内部实现
  2. 统一的资源管理:所有数据库实例集中管理
  3. 良好的封装性:内部数据结构完全隐藏
  4. 线程安全的基础:为后续扩展提供可能
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值