如何在C中连接字节数组



我当前的concat函数:

char* concat(char* a, int a_size,
char* b, int b_size) {
char* c = malloc(a_size + b_size);
memcpy(c, a,            a_size);
memcpy(c + a_size, b,   b_size);
free(a);
free(b);
return c;
}

但这需要额外的内存。是否可以使用realloc附加两个字节数组而不产生额外的内存空间?

类似:

void append(char* a, int a_size, char* b, int b_size)
...
char* a = malloc(2);
char* b = malloc(2);
void append(a, 2, b, 2);
//The size of a will be 4.

Jean-François Fabre回答了上述问题,我想指出的是,使用以下结构可以更好地管理此类字节数组:

typedef struct {
size_t         max;  /* Number of chars allocated for */
size_t         len;  /* Number of chars in use */
unsigned char *data;
} bytearray;
#define  BYTEARRAY_INIT  { 0, 0, NULL }
void bytearray_init(bytearray *barray)
{
barray->max  = 0;
barray->len  = 0;
barray->data = NULL;
}
void bytearray_free(bytearray *barray)
{
free(barray->data);
barray->max  = 0;
barray->len  = 0;
barray->data = NULL;
}

要声明一个空字节数组,可以使用bytearray myba = BYTEARRAY_INIT;bytearray myba; bytearray_init(&myba);。两者是等价的。

当您不再需要该阵列时,请调用bytearray_free(&myba);。请注意,free(NULL)是安全的,不会执行任何操作,因此释放已初始化但未使用的bytearray是完全安全的。

附加到bytearray:

int bytearray_append(bytearray *barray, const void *from, const size_t size)
{
if (barray->len + size > barray->max) {
const size_t  len = barray->len + size;
size_t        max;
void         *data;
/* Example policy: */
if (len < 8)
max = 8; /* At least 8 chars, */
else
if (len < 4194304)
max = (3*len) / 2;  /* grow by 50% up to 4,194,304 bytes, */
else
max = (len | 2097151) + 2097153 - 24; /* then pad to next multiple of 2,097,152 sans 24 bytes. */
data = realloc(barray->data, max);
if (!data) {
/* Not enough memory available. Old data is still valid. */
return -1;
}
barray->max  = max;
barray->data = data;
}
/* Copy appended data; we know there is room now. */
memmove(barray->data + barray->len, from, size);
barray->len += size;
return 0;
}

由于该函数至少在理论上无法重新分配内存,因此如果成功,它将返回0,如果不能重新分配足够的内存,则返回非零。

不需要malloc()调用,因为realloc(NULL, size)malloc(size)完全等效。

"增长政策"是一个很有争议的问题。你只需要制作max = barray->len + size,就可以完成了。但是,动态内存管理功能相对较慢,所以在实践中,我们不想为每一个小的添加都调用realloc()

上面的策略试图做得更好,但不要太激进:它总是分配至少8个字符,即使需要更少的字符。最多4194304个字符,可额外分配50%。在此之上,它将分配大小四舍五入到2097152的下一个倍数,并减去24。这背后的推理很复杂,但它更多的是为了说明和理解,而不是其他任何东西;这绝对不是"这是最好的,这也是你应该做的">。此策略确保每个字节数组最多分配4194304=222个未使用的字符。然而,2097152=221是AMD64(x86-64)上一个巨大页面的大小,并且是基本上所有架构上本机页面大小的两倍的幂。它也足够大,可以在基本上所有这样做的体系结构上从所谓的sbrk()分配切换到内存映射。这意味着,如此巨大的分配为每个分配使用了堆的一个单独部分,而未使用的部分通常只是虚拟内存,在访问之前不一定有任何RAM支持。因此,在大多数体系结构上,此策略往往适用于极短字节数组和超长字节数组。

当然,如果知道(或测量!)典型工作负载中字节阵列的典型大小,则可以为此优化增长策略,并获得更好的结果。

最后,它使用memmove()而不是memcpy(),以防有人希望重复同一字节数组的一部分:memcpy()只有在源和目标区域不重叠的情况下才有效;CCD_ 17即使在这种情况下也能工作。


当使用更高级的数据结构(如哈希表)时,上述结构的变体通常很有用。(也就是说,这在有很多空字节数组的情况下要好得多。)

数据不是指向数据的指针,而是结构本身的一部分,作为C99灵活的阵列成员:

typedef struct {
size_t         max;
size_t         len;
unsigned char  data[];
} bytearray;

您不能声明字节数组本身(即bytearray myba;将不起作用);您总是声明一个指针到这样的字节数组:bytearray *myba = NULL;。指针为NULL的处理方式与空字节数组相同。

特别是,要查看这样的数组有多少data项,可以使用访问器函数(也在与数据结构相同的头文件中定义),而不是myba.len:

static inline size_t  bytearray_len(bytearray *const barray)
{
return (barray) ? barray->len : 0;
}
static inline size_t  bytearray_max(bytearray *const barray)
{
return (barray) ? barray->max : 0;
}

(expression) ? (if-true) : (if-false)是一个三元算子。在这种情况下,第一个函数与完全等效

static inline size_t  bytearray_len(bytearray *const barray)
{
if (barray)
return barray->len;
else
return 0;
}

如果您想知道bytearray *const barray,请记住指针声明是从右到左读取的,*是"指向的指针"。所以,这只是意味着barray是常量,一个指向字节数组的指针。也就是说,我们可以更改它指向的数据,但不会更改指针本身。编译器通常可以自己检测到这些东西,但这可能会有所帮助;然而,要点是提醒我们人类程序员,指针本身是不可更改的。(这种变化只能在功能本身中看到。)

由于这样的数组通常需要调整大小,因此调整大小通常被放入一个单独的辅助函数中:

bytearray *bytearray_resize(bytearray *const barray, const size_t len)
{
bytearray *temp;
if (!len) {
free(barray);
errno = 0;
return NULL;
}
if (!barray) {
temp = malloc(sizeof (bytearray) + len * sizeof barray->data[0]);
if (!temp) {
errno = ENOMEM;
return NULL;
}
temp->max = len;
temp->len = 0;
return temp;
}
if (barray->len > len)
barray->len = len;
if (barray->max == len)
return barray;
temp = realloc(barray, sizeof (bytearray) + len * sizeof barray->data[0]);
if (!temp) {
free(barray);
errno = ENOMEM;
return NULL;
}
temp->max = len;
return temp;
}

errno = 0在里面做什么?这个想法是因为调整字节数组的大小/重新分配可能会改变指针,所以我们返回新的指针。如果分配失败,我们会像malloc()/realloc()一样,用errno == ENOMEM返回NULL。但是,由于所需的新长度为零,这会通过释放旧字节数组(如果有的话)来节省内存,并返回NULL。但由于这不是错误,我们将errno设置为零,这样调用方就更容易检查是否发生了错误。(如果函数返回NULL,请检查errno。如果errno为非零,则表示发生错误;您可以使用strerror(errno)获取描述性错误消息。)

您可能还注意到了sizeof barray->data[0],即使barray为NULL时也会使用它。这没关系,因为sizeof不是一个函数,而是一个运算符:它根本不访问右侧,只计算右侧所指对象的大小。(当右侧大小是类型时,只需要使用括号。)这种形式很好,因为它可以让程序员在不更改任何其他代码的情况下更改data成员的类型。

要将数据附加到这样一个字节数组中,我们可能希望能够指定是否预计会对同一数组进行进一步的附加,或者这是否可能是最后的附加,这样只需要确切的所需内存量。为了简单起见,我在这里只实现精确大小的版本。注意,此函数返回一个指向(修改的)字节数组的指针:

bytearray *bytearray_append(bytearray *barray,
const void *from, const size_t size,
int exact)
{
size_t  len = bytearray_len(barray) + size;
if (exact) {
barray = bytearray_resize(barray, len);
if (!barray)
return NULL; /* errno already set by bytearray_resize(). */
} else
if (bytearray_max(barray) < len) {            
if (!exact) {
/* Apply growth policy */
if (len < 8)
len = 8;
else
if (len < 4194304)
len = (3 * len) / 2;
else
len = (len | 2097151) + 2097153 - 24;
}
barray = bytearray_resize(barray, len);
if (!barray)
return NULL; /* errno already set by the bytearray_resize() call */
}
if (size) {
memmove(barray->data + barray->len, from, size);
barray->len += size;
}
return barray;
}

这一次,我们声明了bytearray *barray,因为我们更改了barray在函数中指向的位置。如果第四个参数final为非零,则得到的字节数组正是所需的大小;否则将采用增长政策。

是的,因为如果新的大小更大,realloc将保留缓冲区的开始:

char* concat(char* a, size_t a_size,
char* b, size_t b_size) {
char* c = realloc(a, a_size + b_size);
memcpy(c + a_size, b,  b_size);  // dest is after "a" data, source is b with b_size
free(b);
return c;
}

c可能与a不同(如果系统无法将原始内存块的大小调整为新大小),但如果是这样,a指向的位置将被释放(您不能释放它),并且原始数据将被"移动"。

我的建议是警告函数的用户,必须使用malloc分配输入缓冲区,否则会严重崩溃。

最新更新