首页 > 解决方案 > 如何在 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.

标签: cmemory-managementmemcpy

解决方案


虽然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您已初始化但未使用的 a 是完全安全的。

附加到 a 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 个字符,即使需要的字符更少。最多 4,194,304 个字符,它会额外分配 50%。在此之上,它将分配大小四舍五入为 2,097,152 的下一个倍数并减去 24。这背后的原因很复杂,但更多的是为了说明和理解;这绝对不是“这是最好的,这也是你应该做的”。该策略确保每个字节数组最多分配 4,194,304 = 2 22 个未使用的字符。然而,2,097,152 = 2 21是 AMD64 (x86-64) 上巨大页面的大小,并且是基本上所有架构上原生页面大小的 2 的幂倍数。它也足够大,可以在基本上所有这样做的架构上从所谓的 sbrk() 分配切换到内存映射。这意味着如此巨大的分配使用堆的单独部分,而未使用的部分通常只是虚拟内存,不一定由任何 RAM 支持,直到被访问。因此,在大多数体系结构上,该策略对于非常短的字节数组和非常长的字节数组都非常有效。

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

最后,它使用memmove()代替memcpy(),以防万一有人希望重复同一字节数组的一部分:memcpy()仅在源区域和目标区域不重叠时才有效;memmove()即使在这种情况下也有效。


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

数据不是指向数据的指针,而是结构本身的一部分,作为 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?这个想法是因为调整/重新分配字节数组可能会改变指针,我们返回新的。如果分配失败,我们返回NULLerrno == ENOMEM就像malloc()/ realloc()do 一样。但是,由于所需的新长度为零,因此通过释放旧字节数组(如果有)来节省内存,并返回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不为零,则生成的字节数组正是所需的大小;否则应用增长策略。


推荐阅读