基于Redis 设计与实现实现的redis,根据书上的API描述,自己实现C++版本的redis。
SDS是redis内部自己实现的数据结构,相较于c的char *,扩宽了内存大小,减少了对内存的申请,更加迅速。
上代码
#ifndef SDS_H
#define SDS_H
#include <cstddef>
#include <cstring>
#define SDS_MAX_PREALLOC (1024*1024) // 预分配的最大空间
class SDS {
private:
size_t m_len=0; // 字符串的长度
size_t m_free=0; // 字符串剩余的可用空间
void realloc(size_t newlen){
// 表示现在我需要newlen长度的空间了,你能不能行
if(newlen<m_free+m_len){
return ; //可以
}
//不可以,重新分配空间
char *newstr;
if(newlen>SDS_MAX_PREALLOC){
newstr=new char[newlen+SDS_MAX_PREALLOC+1];
m_free=newlen+SDS_MAX_PREALLOC - m_len;
}else{
newstr=new char[(newlen<<1) + 1];
m_free=newlen*2 - m_len;
}
memcpy(newstr,m_str,m_len);
delete[] m_str;
m_str=newstr;
};
void concatenate(const char* cString, size_t len){
realloc(m_len+len);
memcpy(m_str+m_len,cString,len);
m_len+=len;
m_str[m_len]='\0';
m_free=m_free-len;
}
void resize(size_t newlen){
if(newlen<m_len){
newlen=m_len;
}
newlen++;
char *newstr=new char[newlen+1];
memcpy(newstr,m_str,newlen);
newstr[newlen]='\0';
delete[] m_str;
m_str=newstr;
m_free=newlen-m_len;
}
public:
char* m_str=new char[0]; // 保存字符串内容的指针
SDS(const char *cString):SDS(cString,strlen(cString)){
}
SDS(const char* cString , size_t len){
realloc(len);
//复制
m_len=len;
m_free-=m_len;
memcpy(m_str,cString,m_len);
m_str[m_len]='\0';
};
~SDS(){
delete[] m_str;
}
// 创建一个包含给定 C 字符串的 SDS
static SDS create(const char* cString){
return SDS(cString,strlen(cString));
};
// 创建一个不包含任何内容的空 SDS
static SDS createEmpty(){
return SDS("",0);
};
// 返回 SDS 的已使用空间字节数
size_t length() const{
return m_len;
};
// 返回 SDS 的未使用空间字节数
size_t availableSpace() const{
return m_free;
};
const char* data() const{
return m_str;
};
// 创建一个给定 SDS 的副本(copy)
SDS duplicate() const{
return SDS(m_str,m_len);
};
// 清空 SDS 保存的字符串内容
void clear(){
m_free+=m_len;
m_len=0;
};
// 将给定 C 字符串拼接到 SDS 字符串的末尾
void concatenate(const char* cString){
concatenate(cString,strlen(cString));
};
// 将给定 SDS 字符串拼接到另一个 SDS 字符串的末尾
void concatenate(const SDS& sds){
concatenate(sds.m_str,sds.m_len);
};
// 将给定的 C 字符串复制到 SDS 里面, 覆盖 SDS 原有的字符串
void copy(const char* cString){
clear();
concatenate(cString);
};
// 用空字符将 SDS 扩展至给定长度
void growZero(size_t length){
resize(length);
};
// 保留 SDS 给定区间内的数据, 不在区间内的数据会被覆盖或清除
void range(int start, int end){
if(start<0 || start>end){
return;
}
int newlen=end-start+1;
m_free=m_free + m_len - newlen;
m_len=newlen;
memcpy(m_str,m_str+start,newlen);
m_str[newlen]='\0';
};
// 从 SDS 左右两端分别移除所有在 C 字符串中出现过的字符
void trim(const char* cString){
size_t left=0,right=0;
while(right<m_len){
bool flag=false;
for(const char* p=cString;*p!='\0';p++){
if(m_str[right]==*p){
flag=true;
break;
}
}
if(!flag){
m_str[left]=m_str[right];
left++;
}
right++;
}
m_free+=m_len-left;
m_len=left;
m_str[m_len]='\0';
};
// 对比两个 SDS 字符串是否相同
bool compare(const SDS& sds) const{
if(m_len!=sds.m_len){
return false;
}else
return memcmp(m_str,sds.m_str,m_len)==0;
};
};
#endif /* SDS_H */
数据结构
size_t m_len 记录当前字符本身的长度
size_t m_free 记录当前字符空余的长度
cahr *m_str 记录当前所有字符,为了便于测试,我就把他放到public里了
内存分配策略
增加长度
如果新加的长度够放到缓冲区里----就放到缓冲区,改变m_len m_free的值
如果不够,计算新的字符长度,m_len + strlen(char * str) ;
当新的长度小于1MB,分配一个new_len*2+1长度的数组
当新的长度大于1MB,分配一个new_len+1MB+1byte长度的数组
减少长度
留这里,不改变。
这样就很好了
- 避免了频繁的申请内存
- 0不再是结束标志位,m_len才是,可以储存任意的字符。
- 可以调用memcpy memcmp来对字符进行赋值和比较
方法函数的设计
realloc
传入一个新的参数,表示新的字符串大小,用于执行内存分配的策略
void realloc(size_t newlen){
// 表示现在我需要newlen长度的空间了,你能不能行
if(newlen<m_free+m_len){
return ; //可以
}
//不可以,重新分配空间
char *newstr;
if(newlen>SDS_MAX_PREALLOC){
newstr=new char[newlen+SDS_MAX_PREALLOC+1];
m_free=newlen+SDS_MAX_PREALLOC - m_len;
}else{
newstr=new char[(newlen<<1) + 1];
m_free=newlen*2 - m_len;
}
memcpy(newstr,m_str,m_len);
delete[] m_str;
m_str=newstr;
};
concatenate
拼接字符串,将cString的长度为len,拼接到当前字符的末尾
void concatenate(const char* cString, size_t len){
realloc(m_len+len);
memcpy(m_str+m_len,cString,len);
m_len+=len;
m_str[m_len]='\0';
m_free=m_free-len;
}
resize
改变当前字符缓冲区的大小,这里加了一个判断,看是不是小于当前已经存储的字符大小
void resize(size_t newlen){
if(newlen<m_len){
newlen=m_len;
}
newlen++;
char *newstr=new char[newlen+1];
memcpy(newstr,m_str,newlen);
newstr[newlen]='\0';
delete[] m_str;
m_str=newstr;
m_free=newlen-m_len;
}
测试代码
使用gtest
#include <gtest/gtest.h>
#include "SDS.h"
TEST(SDSTest, CompareStrings) {
// Happy path
{
SDS sds1("hello");
SDS sds2("hello");
EXPECT_TRUE(sds1.compare(sds2));
}
// Edge case: empty strings
{
SDS sds1("");
SDS sds2("");
EXPECT_TRUE(sds1.compare(sds2));
}
// Edge case: strings of different lengths
{
SDS sds1("hello");
SDS sds2("world");
EXPECT_FALSE(sds1.compare(sds2));
}
// Edge case: strings with different characters
{
SDS sds1("hello");
SDS sds2("hallo");
EXPECT_FALSE(sds1.compare(sds2));
}
}
TEST(SDSTest, CreateTest) {
SDS s1 = SDS::create("Hello");
ASSERT_EQ(s1.length(), 5);
ASSERT_EQ(s1.availableSpace(), 5);
}
// Test createEmpty
TEST(SDSTest, CreateEmptyTest) {
SDS s2 = SDS::createEmpty();
ASSERT_EQ(s2.length(), 0);
ASSERT_EQ(s2.availableSpace(), 0);
}
// Test concatenate
TEST(SDSTest, ConcatenateTest) {
SDS s1 = SDS::create("Hello");
s1.concatenate(" World");
ASSERT_EQ(s1.length(), 11);
ASSERT_STREQ(s1.duplicate().m_str, "Hello World");
}
// Test copy
TEST(SDSTest, CopyTest) {
SDS s2 = SDS::createEmpty();
s2.copy("Test");
ASSERT_EQ(s2.length(), 4);
ASSERT_STREQ(s2.duplicate().m_str, "Test");
}
// Test growZero
TEST(SDSTest, GrowZeroTest) {
SDS s2 = SDS::createEmpty();
s2.growZero(10);
ASSERT_EQ(s2.length() + s2.availableSpace(), 11);
ASSERT_EQ(s2.length(), 0); // Double check the expected behavior
}
// Test compare
TEST(SDSTest, CompareTest) {
SDS s1 = SDS::create("Hello");
ASSERT_TRUE(s1.compare(s1.duplicate()));
SDS s2 = SDS::createEmpty();
ASSERT_FALSE(s1.compare(s2));
}
// Test trim
TEST(SDSTest, TrimTest) {
SDS s1 = SDS::create("Hello World");
s1.trim("erld");
ASSERT_EQ(s1.length(), 5);
ASSERT_STREQ(s1.m_str, "Ho Wo");
}
// Test range
TEST(SDSTest, RangeTest) {
SDS s2 = SDS::create("Test");
s2.range(1, 3);
ASSERT_EQ(s2.length(), 3);
ASSERT_STREQ(s2.m_str, "est");
}
内存泄露检验
valgrind --leak-check=full -s ./tinyredis_test_sds_test
但是我不知道为啥会invalid read