Разница между выделением памяти для члена структуры (указатель или массив) в C - PullRequest
1 голос
/ 15 апреля 2020

Есть ли эффективная разница между этими двумя стилями выделения памяти?

1.

typedef struct {
  uint8_t *buffer;
} Container;

Container* init() {
  Container* container = calloc(sizeof(Container), 1);
  container->buffer = calloc(4, 1);
  return container;
}

2.

typedef struct {
  uint8_t buffer[4];
} Container;

Container* init() {
  Container* container = calloc(sizeof(Container), 1);
  return container;
}

Насколько я понимаю, вся структура Container будет выделена кучей, а buffer будет указывать на то же самое. Это правильно?

Ответы [ 2 ]

7 голосов
/ 15 апреля 2020

Разница есть.

Я попытаюсь проиллюстрировать пример.

Как отмечали другие:

  1. Первый пример сложнее управлять, но вы можете изменить размер буфера в любое время.
  2. Вторым примером легче управлять (вам не нужно заботиться об освобождении буфера отдельно), но вы можете иметь только фиксированный размер буфера.

Как указано в комментариях: в случае, если буфер является последним элементом в структуре (как в приведенном примере), можно выделить для буфера любую длину.

Например,

int extra_bytes_needed = ...;
Container* container = calloc(sizeof(Container) + extra_bytes_needed, 1);

С левой стороны изображения - ваш первый случай.

С правой стороны изображения - ваш второй случай.

Left is first case, Right is second case.

3 голосов
/ 15 апреля 2020

Владислав очень хорошо проиллюстрировал разницу; Но что это значит? Различная организация памяти имеет пару последствий:

  1. struct container во втором примере можно использовать как есть; это не требует никакой инициализации. Например,
typedef struct {
  uint8_t buffer[4];
} Container;

Container c; 
strcpy(c.buffer, "Yes");

в порядке, но, скорее всего, с первой версией будет взломано sh, поскольку указатель c.buffer будет неинициализирован и будет содержать недопустимый адрес.

Производительность структуры со встроенным буфером, вероятно, лучше, потому что на init() сохраняется одно выделение. Локальная память также может быть проблемой: при динамическом распределении c буферная память, возможно, находится далеко от структуры памяти, поэтому она не находится в кэше.

Еще один момент. Здесь вы имитируете C ++, а init() играет роль фабрики с конструктором.

К сожалению, пока доступно определение struct Container, любой пользователь может создать неинициализированный Container и использовать его, что приведет к катастрофическим последствиям. (В C ++ мы объявили бы конструктор частным, но мы не можем сделать это в C.)

Единственный способ предотвратить создание пользователем struct Container - это скрыть его реализацию . Это напоминает идиому C ++ Pimpl: У пользователя нет заголовка, который фактически определяет Container, но есть только заголовок, определяющий операций над ним, которые принимают и возвращают указатели на Container (как ваш init()). Container остается неполным типом для пользователя.

Вот пример. Эта версия контейнера имеет следующие функции:

  • Он не обеспечивает прямой доступ к данным, а вместо этого раздает копии данных. Является ли это приемлемыми накладными расходами, зависит от варианта использования. Я просто хотел подчеркнуть, что нам нужно нулевое знание Контейнера. Он полностью скрыт. За исключением реинжиниринга типа, вообще невозможно манипулировать содержимым, кроме как через его официальный интерфейс. (Это может быть недостатком.)

  • Фактический буфер (и, следовательно, размер) теперь динамический c. Единственное ограничение на размер пользовательских данных устанавливается системой.

  • Контейнер выделяет память для пользователя, когда пользователь получает копию данных в контейнере, аналогично POSIX scanf "символ назначения-выделения" 'm'.

  • Контейнер поддерживает отдельные размеры для объема выделенной памяти и того, сколько фактически занято пользовательскими данными. Это позволяет избежать ненужных перераспределений.

Пользователь видит контейнер как этот заголовок с набором сигнатур функций:

#ifndef CONTAINER_INTERFACE_H
#define CONTAINER_INTERFACE_H

/* An abstract container. It can hold arbitrary amounts of data
   by means of danamic allocation. An out-of-memory condition will make
   it exit with an exit code of 1.
*/
#include <stddef.h>  // size_t

/** Forward declaration, actual definition unknown */
struct Container;
typedef struct Container Container; // convenience

/** Create and initialize a Container of size 0.
*/
Container *ac_init();

/** Delete a Container and free its buffer */
void ac_dispose(Container *container);

/** Obtain the data in the given container. Note that we don't
    expose the internal pointer to the user.
    @param userBuf is a pointer a pointer  
    which will be set to an allocated memory area of sufficient 
    size. The user must free() it. If the container does not hold data,
    *userBuf is not changed.
    @return the number of bytes actually copied, which is also the
    size of the allocated buffer.
*/
size_t ac_get(Container *container, unsigned char **userBuf);

/** Fill the container buffer with user data.
    @return the number of bytes actually copied
*/
void ac_put(Container *container, const unsigned char *userData, size_t userDataSz);

/* ... (Many) more functions for more complicated structs */

#endif //ndef CONTAINER_INTERFACE_H

Простой пример использования:

#include <stdio.h>
#include <stdlib.h> // exit, malloc etc.
#include <string.h>
#include "container-interface.h"

/// Obtain a copy of the container data and print it.
void printContainerData(Container *c)
{
    unsigned char *dataFromContainer; // will be set by ac_get
    size_t contDataSz = ac_get(c, &dataFromContainer);
    if(contDataSz == 0)
    {
        printf("[empty]\n");
    }
    else
    {
        dataFromContainer[contDataSz-1] = 0; // terminate string just in case.
        printf("String from container:               ->%s<-\n", (const char *)dataFromContainer);
        free(dataFromContainer);
    }
}

int main()
{
    char *userInput; // will be set by scanf
    Container *c = ac_init();


    while(1) // exit by EOF (Ctrl-Z or Ctrl-D)
    {
        printf("Please enter a line (empty for exit) ->");
        // EOF etc. will make scanf return something other than 1.
        // Use the fancy "m" POSIX extension in the format string
        // which allocates memory for us, obviating maximum line length
        // considerations.
        if(scanf("%m[^\n]", &userInput) != 1) { break; }
        getchar(); // read away remaining newline
        ac_put(c, (unsigned char *)userInput, strlen(userInput)+1);
        printContainerData(c);
        free(userInput);
    }
    ac_dispose(c); // kinda unnecessary in a hosted environment, but good habit.
}

Последний (скрытый, как правило, в библиотеке, которая связана только с) реализация контейнера и его члена "Функции выглядят так:

#include <stdlib.h> // exit, malloc etc.
#include <string.h>    // memcpy
#include "container-interface.h" // to make sure the function signatures match

/** The actual definition of Container. The user never sees this. */
struct Container
{
    unsigned char *buf;
    size_t dataSz;
    size_t allocSz;
};

/** Create and initialize a struct Container */
struct Container *ac_init()
{
    struct Container *newCont = malloc(sizeof(struct Container));
    if(!newCont) { exit(1); } // out of mem
    newCont->dataSz = 0;
    newCont->allocSz = 0;
    newCont->buf = NULL;
    return newCont;
}

void ac_dispose(struct Container *container)
{
    free(container->buf);
    free(container);
}

size_t ac_get(struct Container *container, unsigned char **userBuf)
{
    if(container->dataSz > 0)
    {
        *userBuf = malloc(container->dataSz);
        if(!*userBuf) { exit(1); } // out of mem
        memcpy(*userBuf, container->buf, container->dataSz);
    }
    return container->dataSz;
}

void ac_put(struct Container *container, const unsigned char *userData, size_t userDataSz)
{
    if(userDataSz != 0)
    {
        if(container->allocSz < userDataSz)
        {
            free(container->buf);
            container->buf = malloc(userDataSz);
            if(!container->buf) { exit(1); } // out of mem
            container->allocSz = userDataSz;
        }
        memcpy(container->buf, userData, userDataSz);
    }
    container->dataSz = userDataSz;
}

/* ... (Many) more functions for more complicated structs */


...