程序员和懒惰齐头并进,这就是为什么C吓到很多人。我在这里向您展示如何击败手动记忆管理,而不是相反,并希望您一路上学到的一些很酷的东西!
为什么C?
c是每个人都知道的那种语言,许多人声称知道,很少有人知道。大多数人将其“学习”为大学的一两个课程,并记住它是指指针和手动记忆管理的那种语言。
我不是在这里告诉你为什么手动内存管理实际上是一件好事。我不是在这里告诉你为什么C很棒,您的语言很糟糕,而您却不将C用于所有内容。 C社区中太多的人已经这样做了,而且大多数人甚至都不知道语法之外的语言。
c是一种硬语言,如果我说不是,我会撒谎。但是,就像任何语言一样,您可以学习巧妙的方法,使您的生活在编写C代码时更轻松。如果您可以善于懒惰(随时随地打击懒惰的语言),您将在其他语言中做得更好。
所以让我们这样做!让我们摆脱大多数手动内存管理的开销,并使用易于实现且易于使用的工具更快地使我们的代码。
竞技场分配器
要使C齿轮转动,请回想一下,我们用malloc()
分配内存,并用C标准库提供的free()
免费/Deallocate MOMERATE。您指定要用于malloc()
的字节数量,并将结果分配给指针变量。然后,您将该变量传递给free()
,并释放内存。
在其他语言中,这通常是为您完成的,但是在C中,每次需要在编写代码时不知道的空间时,都需要使用这些工具。一开始听起来很容易,但是随着项目的越来越大,这些事情很难跟踪。
因此,很容易迷失而乱七八糟,导致segfault
和内存泄漏很难追踪。竞技场分配器通过一次分配大量内存来解决此问题,然后根据需要在整个程序中根据需要分发该内存。最后,大部分记忆是free()
的一次。我们有效地将多个malloc
和free()
变成了几个。
这不仅使我们的写作C变得更加容易,还可以使我们的代码更快。 malloc
和free
很慢,尤其是 free
。而且,作为C程序员,我们知道快速和缓慢是坏的,对吗?对吗?
计划我们的代码样式
在编写代码方面,最好提出一种单一的样式并坚持下去。一致性是关键,可维护性是宝藏。
对于这个项目,我想创建一些容易使用的东西,但仍然强大。这意味着我们需要完成两件事:
-
我们希望我们的C代码编译尽可能多的编译器并在尽可能多的平台上运行。
-
我们希望我们的工具易于集成到某人可能需要的项目中,而不管项目的复杂性或用户的专业知识如何。
要完成1
,我们将使用ANSI-C,这是最古老的C标准,它仍在许多地方使用。 C标准是向后兼容的,因此我们的代码将使用ANSI-C或更新的任何代码库中使用。
要完成2
,我们将使我们的项目单头制成,这意味着所有代码和功能都包含在一个标头文件中,并且可以通过包括它来使用。我们将使用预处理器来允许一些配置。
现在为代码
基础
让我们开始在我们的竞技场分配器上开始,并创建一个名为arena.h
的标头文件。让我们从设置标头警卫开始。
#ifndef ARENA_H
#define ARENA_H
/* CODE HERE */
#endif /* !ARENA_H */
现在,我们可以通过创建我们的Arena
数据结构和一些函数的远期声明来奠定一些基础。
#include <stddef.h> /* For NULL and size_t */
#include <stdlib.h> /* For malloc() and free() */
typedef struct
{
char *region; /* The memory chunk */
size_t index; /* How we will divide up memory */
size_t size; /* The size of our memory chunk */
} Arena;
/* Creating the arena, all malloc()'s happen here */
Arena* arena_create(size_t size);
/* Allocating memory from the arena */
void* arena_alloc(Arena *arena, size_t size);
/* Reset the arena to "free" without calling free() */
void arena_clear(Arena* arena);
/* Destroy the arena. Actually calls free() */
void arena_destroy(Arena *arena);
无视函数;稍后,我们将深入深入。现在,让我们专注于竞技场struct
。它有一个char *region
,这是我们将大量内存块的指针。我使用char
指针,因为C标准可以保证char
是一个大小的字节。
它还具有index
和size
。 index
将跟踪我们到目前为止分配了多少region
,size
将成为region
的能力。两者都是size_t
,最适合表示内存中的内存和位置。
arena_create()
现在让我们编写用于创建竞技场的功能。
Arena* arena_create(size_t size)
{
Arena *arena = malloc(sizeof(Arena));
if(arena == NULL)
{
return NULL;
}
arena->region = malloc(size);
if(arena->region == NULL)
{
return NULL;
}
arena->index = 0;
arena->size = size;
return arena;
}
我们的功能将为Arena
(将返回给呼叫者)及其区域分配内存。如果两者之后结束了NULL
,则意味着malloc()
失败了,因此我们最好返回NULL
自己以进一步模仿malloc()
的行为。
呼叫者将为region
提供大小的字节。然后,index
将被预设到0
,并将size
设置为相应。
arena_alloc()
现在我们将实施分配器的分配部分。
void* arena_alloc(Arena *arena, size_t size)
{
if(arena == NULL)
{
return NULL;
}
if(arena->region == NULL)
{
return NULL;
}
if(arena->size - arena->index < size)
{
return NULL;
}
arena->index += size;
return arena->region + (arena->index - size);
}
就像以前一样,我们检查以确保我们的arena
及其region
不是NULL
。然后,我们检查竞技场是否有足够的空间进行分配。如果我们不这样做,它将失败,我们再次返回NULL
。如果这样做,我们会更改index
以准备下一个分配,然后返回region
指针,但随着原始index
的偏移。
注意我们如何返回void
指针。这再次复制了malloc()
的行为,该行为允许返回的指针解释为任何其他类型的指针。
arena_clear()
清除内存是最简单的部分。
void arena_clear(Arena* arena)
{
if(arena == NULL)
{
return;
}
arena->index = 0;
}
我们对传递的arena
进行检查,但是由于我们不使用region
,因此我们不需要检查它。我们将index
设置回0
,这将使我们覆盖竞技场中已经存在的任何东西。这使我们无需任何free()
即可轻松重复内存。
arena_destroy()
摧毁竞技场是第二部分。我们在arena
和region
上执行NULL
检查,如果可以的话,它们。这将释放我们在内存中分配的所有内容,实际上只有两个呼叫,有效地分配了我们想要的许多分配。
void arena_destroy(Arena *arena)
{
if(arena == NULL)
{
return;
}
if(arena->region != NULL)
{
free(arena->region);
}
free(arena);
}
与预处理器一起想
我们的竞技场分配已经完成了,现在我们只需要根据我们早些时候为自己设定的准则将其纳入工作状态。
想到的第一件事是stdlib.h
依赖性。我们仅将其用于malloc()
和free()
,许多C代码库都有其自己的版本。我们不想强迫他们使用stdlib.h
实现,尤其是因为这些实现可能并不总是存在。
让我们用一些宏来包裹malloc
和free
,并使程序员能够用自己的宏来替换它们。
#if !defined(ARENA_MALLOC) || !defined(ARENA_FREE)
#include <stdlib.h>
#define ARENA_MALLOC malloc
#define ARENA_FREE free
#endif /* !defined ARENA_MALLOC, ARENA_FREE */
然后,我们需要删除顶部添加的第一个#include <stdlib.h>
,并在我们的arena.h
文件中将所有malloc
替换为ARENA_MALLOC
和所有free
。
现在默认情况下,我们的实现将使用stdlib
的malloc
和free
,但是有人可以通过在包含之前定义宏来指定自己的malloc
和free
,例如:
:
#define ARENA_MALLOC my_custom_malloc
#define ARENA_FREE my_custom_free
#include "arena.h"
对于急于使用我们的图书馆的人来说,这可能有些不清楚,因此,如果未指定替换,以及用户可以定义以抑制上述警告的宏,我们可以添加警告。
#if !defined(ARENA_MALLOC) || !defined(ARENA_FREE)
#ifndef ARENA_SUPPRESS_MALLOC_WARN
#warning \
"Using <stdlib.h> malloc and free, because a replacement for one or both \
was not specified before including 'arena.h'."
#endif /* !ARENA_SUPPRESS_MALLOC_WARN */
#include <stdlib.h>
#define ARENA_MALLOC malloc
#define ARENA_FREE free
#endif /* !defined ARENA_MALLOC, ARENA_FREE */
现在某人没有他们自己的自定义分配器和Deallocator可以通过以下方式使用我们的竞技场分配器:
#define ARENA_SUPPRESS_MALLOC_WARN
#include "arena.h"
完成触摸和最终产品
我们的单位分配器几乎处于工作状态。对于仅在一个文件中使用竞技场的单文件项目或项目,这将非常有效。如果我们想在项目中的多个文件中使用竞技场分配器,我们将遇到重复函数的链接器错误。
我们可以通过更多的预处理器技巧来解决此问题。我们将介绍一个最终的宏,该宏将围绕我们的ARENA_ALLOC
和ARENA_FREE
宏,以及我们所有功能定义称为ARENA_IMPLEMENTATION
。我们的最终代码应该看起来像:
#ifndef ARENA_H
#define ARENA_H
#include <stddef.h>
typedef struct
{
char *region;
size_t index;
size_t size;
} Arena;
Arena* arena_create(size_t size);
void* arena_alloc(Arena *arena, size_t size);
void* arena_alloc_aligned(Arena *arena, size_t size, unsigned int alignment);
void arena_clear(Arena* arena);
void arena_destroy(Arena *arena);
#ifdef ARENA_IMPLEMENTATION
#if !defined(ARENA_MALLOC) || !defined(ARENA_FREE)
#ifndef ARENA_SUPPRESS_MALLOC_WARN
#warning \
"Using <stdlib.h> malloc and free, because a replacement for one or both \
was not specified before including 'arena.h'."
#endif /* !ARENA_SUPPRESS_MALLOC_WARN */
#include <stdlib.h>
#define ARENA_MALLOC malloc
#define ARENA_FREE free
#endif /* !defined ARENA_MALLOC, ARENA_FREE */
Arena* arena_create(size_t size)
{
Arena *arena = ARENA_MALLOC(sizeof(Arena));
if(arena == NULL)
{
return NULL;
}
arena->region = ARENA_MALLOC(size);
if(arena->region == NULL)
{
return NULL;
}
arena->index = 0;
arena->size = size;
return arena;
}
void* arena_alloc(Arena *arena, size_t size)
{
if(arena == NULL)
{
return NULL;
}
if(arena->region == NULL)
{
return NULL;
}
if(arena->size - arena->index < size)
{
return NULL;
}
arena->index += size;
return arena->region + (arena->index - size);
}
void arena_clear(Arena* arena)
{
if(arena == NULL)
{
return;
}
arena->index = 0;
}
void arena_destroy(Arena *arena)
{
if(arena == NULL)
{
return;
}
if(arena->region != NULL)
{
ARENA_FREE(arena->region);
}
ARENA_FREE(arena);
}
#endif /* ARENA_IMPLEMENTATION */
#endif /* ARENA_H */
现在,一个翻译单元中的一个文件至少需要在包含之前定义以下宏:
#define ARENA_IMPLEMENTATION
#define ARENA_SUPPRESS_MALLOC_WARN
#include "arena.h"
以及在项目中使用我们的竞技场分配器的任何其他文件只需要正常包含它。
#include "arena.h"
结论
恭喜!您已经写了C!
的竞技场分配器这是一段漫长的旅程。如果有很多您不了解的话,请不要感到难过,在术语中迷路真的很容易。我鼓励您进一步研究竞技场分配者和C编程语言。您可以在搜索或chatgpt提示中使用本文中了解的术语和短语。
可以找到该项目的代码(具有附加功能和文档)here。我希望您受到启发,继续学习如何在C中懒惰。至少,我希望您学到了一些东西。
谢谢您的阅读。