• 作者:老汪软件技巧
  • 发表时间:2024-11-17 17:02
  • 浏览量:

1.先导:

String是最基本的key-value结构,其中key是唯一标识。value为具体的值。而value不仅仅是字符串,也可以是数字(整数or浮点数)。value最多可以容纳的数据长度是512M。

我们关注String类型的底层数据结构实现:

这里主要介绍SDS,我们先谈优点:

SDS获取字符串长度的时间复杂度是O(1)

Redis的SDS API是安全的,拼接字符串不会造成缓冲流溢出

2.SDS代码中使用到的API

这段代码使用了多个常见的 C 语言标准库 API 来进行内存管理、字符串操作和输出等。我们逐一分析这些 API 和它们的用途。

1. strlen

size_t init_len = strlen(init);

strlen 是一个标准的 C 库函数,用于计算字符串的长度,不包括字符串的结束符 '\0'。它的定义如下:

size_t strlen(const char *str);

其中 str 是一个指向 C 风格字符串的指针。strlen 返回字符串的长度(类型为 size_t)。

2. malloc

struct SDSHeader *sds = (struct SDSHeader *)malloc(sizeof(struct SDSHeader) + init_len + 1);

malloc 是 C 标准库中的内存分配函数。它从堆内存中分配指定大小的内存块,并返回该内存块的起始地址。如果分配失败,则返回 NULL。

3. strcpy

strcpy(sds->buf, init);

strcpy 用于将一个 C 风格字符串(包括结束符 '\0')复制到另一个字符串缓冲区。函数原型如下:

char *strcpy(char *dest, const char *src);

4. realloc

sds1 = (SDS)realloc(sds1, sizeof(struct SDSHeader) + new_len + 1);

realloc 用于重新分配内存,可以调整已经分配的内存块的大小。如果扩展了内存,它会尝试将内存块移动到新的位置并返回新地址。函数原型如下:

void *realloc(void *ptr, size_t size);

5. strcat

strcat(sds1->buf, s2);

strcat 是 C 标准库中用于连接两个字符串的函数,函数原型如下:

char *strcat(char *dest, const char *src);

6. printf

printf("SDS string: %s\n", sds->buf);
printf("Length: %u\n", sdslen(sds1));
printf("Available space: %u\n", sdsavail(sds1));

printf 是 C 标准库中用于格式化输出的函数,原型如下:

int printf(const char *format, ...);

7. free

free(sds);

free 用于释放通过 malloc 或 realloc 分配的内存。函数原型如下:

void free(void *ptr);

总结:

这些 C 标准库函数在处理动态内存管理和字符串操作时非常常见,是 C 语言开发中不可或缺的工具。

3.代码与分析

#include 
#include 
#include // SDS 的结构体定义
struct SDSHeader {
    unsigned int len;       // 字符串的长度,不包括空字符
    unsigned int alloc;     // 已分配的空间(包括字符串末尾的空字符)
    unsigned char flags;    // 标志位,通常用来标记 SDS 的类型(简单或二进制)
    char buf[];             // 存放字符串内容的缓冲区
};
​
typedef struct SDSHeader *SDS;// 创建一个新的 SDS 字符串
SDS createSDS(const char *init) {
    size_t init_len = strlen(init); // 初始化字符串的长度
    struct SDSHeader *sds = (struct SDSHeader *)malloc(sizeof(struct SDSHeader)+init_len+1);
    // if (!sds) return NULL;
​
    sds->len = init_len;
    sds->alloc = init_len + 1;  // 为结束符 '\0' 分配空间
    sds->flags = 0;  // 初始化标志位
    strcpy(sds->buf,init); // 将初始化字符串复制到缓冲区
​
    return sds;
}
​
// 获取 SDS 字符串的长度
unsigned int sdslen(SDS sds) {
    return sds->len;
}

​
// 获取 SDS 字符串分配的内存大小
unsigned int sdsavail(SDS sds) {
    return sds->alloc - sds->len - 1;
}
​
// 释放 SDS
void freeSDS(SDS sds) {
    if (sds) {
        free(sds);
    }
}
​
// 连接两个 SDS 字符串
SDS sdsConcat(SDS sds1, const char *s2) {
    size_t s2_len = strlen(s2);
    size_t new_len = sdslen(sds1) + s2_len;
    if (sdsavail(sds1) < s2_len) {  // 如果空间不足,需要扩展
       sds1 = (SDS)realloc(sds1,sizeof(struct SDSHeader)+new_len+1);
        if (!sds1) return NULL;  // 内存分配失败
        sds1->alloc = new_len + 1;
    }
​
    strcat(sds1->buf, s2);  // 连接字符串
    sds1->len = new_len;    // 更新 SDS 长度
​
    return sds1;
}
​
// 打印 SDS 字符串
void printSDS(SDS sds) {
    printf("SDS string: %s\n", sds->buf);
}
​
int main() {
    // 创建一个 SDS 字符串
    SDS sds1 = createSDS("Hello");
    printSDS(sds1);
    printf("Length: %u\n", sdslen(sds1));
    printf("Available space: %u\n", sdsavail(sds1));
​
    // 连接另一个字符串
    sds1 = sdsConcat(sds1, " World!");
    printSDS(sds1);
    printf("Length: %u\n", sdslen(sds1));
    printf("Available space: %u\n", sdsavail(sds1));
​
    // 释放内存
    freeSDS(sds1);
​
    return 0;
}

4.SDS好就好在没有\0

我相信你在阅读完上述的博文后,会产生一个疑问:SDS和传统C字符串都是用char[]来存数据,凭什么说SDS能存图片,音频,视频是相较于传统的优点呢?

答案在于对于\0的处理

1. 传统 C 字符串的限制

传统的 C 字符串(char 数组)有以下一些限制,使得它们在存储二进制数据时不那么方便:

2. SDS 的优点

SDS(简单动态字符串)是 Redis 中的一种数据结构,它改进了传统 C 字符串的一些缺点。SDS 设计之初就是为了更好地支持字符串操作(包括二进制数据)。它的主要优点包括:

这种设计使得 SDS 不仅可以有效地管理动态字符串的内存,还可以通过预留空闲空间来减少内存重分配的频率,提升性能。

快速扩展: 与传统 C 字符串不同,SDS 允许通过简单的重分配来扩展字符串长度,并且能够高效地管理内存。每次分配新的内存时,SDS 会考虑到扩展的需求,预留一定的空间,以减少频繁的内存重分配,避免性能瓶颈。

避免了传统 C 字符串的 "O(n)" 操作: 传统 C 字符串在进行拼接操作时,通常需要复制整个字符串来扩展空间(例如 strcat),这是一种 O(n) 的操作。而 SDS 在扩展时仅仅是更新长度信息和内存块大小,不需要每次操作时都复制数据,从而大大提高了操作效率。

3. 存储二进制数据的优势

SDS 的二进制安全性是它相对于传统 C 字符串的最大优势之一。传统 C 字符串无法处理包含 \0 的数据,因此不适合用来存储二进制文件内容(如图片、视频、音频等)。而 SDS 可以存储包括 \0 在内的任意字节,这使得它非常适合处理二进制数据。

例如,如果你需要存储一个图片文件的二进制数据,并且这个文件中可能包含很多 0x00 字节,传统 C 字符串将无法处理这个问题,数据可能被截断。使用 SDS,你可以安全地存储整个二进制数据,不会遇到截断或者丢失的风险。

4. 例子

假设我们需要存储一张图片的二进制数据。如果使用传统 C 字符串,你可能会遇到以下问题:

char image_data[1000];
FILE *file = fopen("image.jpg", "rb");
fread(image_data, 1, 1000, file); // 假设图片数据较大// 如果图片数据包含 '\0' 字符,传统字符串将会截断数据
printf("Image data: %s\n", image_data); // 可能只打印部分数据,甚至可能打印乱码

然而,如果使用 SDS,则可以避免这个问题:

SDS sds_image = sdsnewlen(NULL, 1000);
FILE *file = fopen("image.jpg", "rb");
fread(sds_image, 1, 1000, file); // SDS 可以处理包含 '\0' 的数据
​
printf("Image data: %s\n", sds_image); // 不会发生截断,二进制数据可以完整存储

总结

虽然传统的 C 字符串也使用 char 来存储数据,但由于 C 字符串必须以 \0 结尾,它们并不适合存储二进制数据。SDS 的设计克服了这一问题,支持存储任意二进制数据(包括包含 \0 的数据),并且具有更好的内存管理和性能优化。因此,SDS 是一种更加灵活和高效的数据结构,尤其适合在需要处理二进制数据的场景中使用。


上一条查看详情 +面向 OpenAI 接口编程笔记
下一条 查看详情 +没有了