Подтвердить что ты не робот

Создание "классов" в C, в стеке против кучи?

Всякий раз, когда я вижу C "класс" (любая структура, которая предназначена для использования при доступе к функциям, которые принимают указатель на нее как первый аргумент), я вижу, что они реализованы следующим образом:

typedef struct
{
    int member_a;
    float member_b;
} CClass;

CClass* CClass_create();
void CClass_destroy(CClass *self);
void CClass_someFunction(CClass *self, ...);
...

И в этом случае CClass_create всегда malloc это память и возвращает указатель на это.

Всякий раз, когда я вижу, что new появляется на С++ без необходимости, обычно это сбивает программистов на С++ с ума, но эта практика кажется приемлемой в C. Что дает? Есть ли какая-то причина, почему так называемые "классы классов", выделенные кучей, настолько распространены?

4b9b3361

Ответ 1

Есть несколько причин для этого.

  • Использование "непрозрачных" указателей
  • Отсутствие деструкторов
  • Встроенные системы (проблема)
  • Контейнеры
  • Инерция
  • "Лень"

Кратко обсудите их.

Для непрозрачных указателей он позволяет вам сделать что-то вроде:

struct CClass_;
typedef struct CClass_ CClass;
// the rest as in your example

Таким образом, пользователь не видит определения struct CClass_, изолируя ее от изменений к нему и позволяя другим интересным материалам, например, реализовать класс по-разному для разных платформ.

Конечно, это запрещает использование переменных стека CClass. Но, OTOH, можно видеть, что это не запрещает выделение объектов CClass статически (из некоторого пула) - возвращается CClass_create или, возможно, другая функция типа CClass_create_static.

Отсутствие деструкторов - так как компилятор C не будет автоматически уничтожать ваши объекты стека CClass, вам нужно сделать это самостоятельно (вручную вызывая функцию деструктора). Таким образом, единственным преимуществом остается тот факт, что распределение стека, в общем, быстрее, чем распределение кучи. OTOH, вам не нужно использовать кучу - вы можете выделить из пула или арены или что-то в этом роде, и это может быть почти так же быстро, как распределение стека, без возможных проблем с распределением стека, обсуждаемых ниже.

Встроенные системы - Стек не является "бесконечным" ресурсом, вы знаете. Конечно, для большинства приложений на сегодняшних "обычных" ОС (POSIX, Windows...) это почти так. Но на встроенных системах стек может быть всего несколько КБ. Эти экстремальные, но даже "большие" встроенные системы имеют стек, который находится в МБ. Таким образом, он будет исчерпан, если используется слишком сильно. Когда это происходит, в основном нет никакой гарантии, что произойдет - AFAIK, как на C, так и на С++, что "Undefined поведение". OTOH, CClass_create() может возвращать указатель NULL, когда вы потеряли память, и вы можете справиться с этим.

Контейнеры - пользователи С++, такие как распределение стека, но если вы создадите стек std::vector в стеке, его содержимое будет выделено в кучу. Вы можете настроить это, конечно, но это поведение по умолчанию, и это облегчает жизнь, чтобы сказать, что "все члены контейнера выделены в виде кучи", а не пытаются выяснить, как обращаться, если они не являются.

Инерция - ну, OO пришел из SmallTalk. Там все динамично, поэтому "естественный" перевод на C - это "положить все на кучу". Итак, первые примеры были такими, и они вдохновляли других на долгие годы.

" Лень" - если вы знаете, что хотите только объекты стека, вам нужно что-то вроде:

CClass CClass_make();
void CClass_deinit(CClass *me);

Но если вы хотите разрешить как стек, так и кучу, вам нужно добавить:

CClass *CClass_create();
void CClass_destroy(CClass *me);

Это больше работы для разработчика, но также смущает пользователя. Можно сделать несколько разные интерфейсы, но это не меняет того факта, что вам нужны два набора функций.

Конечно, причина "контейнеров" также частично является причиной "лености".

Ответ 2

Предполагая, что, как и в вашем вопросе, CClass_create и CClass_destroy используйте malloc/free, то для меня следующее: неправильная практика:

void Myfunc()
{
  CClass* myinstance = CClass_create();
  ...

  CClass_destroy(myinstance);
}

потому что мы могли бы легко избавиться от malloc и свободно:

void Myfunc()
{
  CClass myinstance;        // no malloc needed here, myinstance is on the stack
  CClass_Initialize(&myinstance);
  ...

  CClass_Uninitialize(&myinstance);
                            // no free needed here because myinstance is on the stack
}

с

CClass* CClass_create()
{
   CClass *self= malloc(sizeof(CClass));
   CClass_Initialize(self);
   return self;
}

void CClass_destroy(CClass *self);
{
   CClass_Uninitialize(self);
   free(self);
}

void CClass_Initialize(CClass *self)
{
   // initialize stuff
   ...
}

void CClass_Uninitialize(CClass *self);
{
   // uninitialize stuff
   ...
}

В С++ мы также предпочли бы это сделать:

void Myfunc()
{
  CClass myinstance;
  ...

}

чем это:

void Myfunc()
{
  CClass* myinstance = new CCLass;
  ...

  delete myinstance;
}

Чтобы избежать ненужного new/delete.

Ответ 3

В C, когда какой-то компонент предоставляет функцию "create", разработчик компонента также контролирует процесс инициализации компонента. Поэтому он не только эмулирует С++ 'operator new, но также и конструктор класса.

Предоставление этого контроля над инициализацией означает гораздо большую проверку ошибок на входах, поэтому сохранение контроля упрощает обеспечение последовательного и предсказуемого поведения.

Я также делаю исключение для malloc всегда, используемого для выделения памяти. Это часто бывает так, но не всегда. Например, в некоторых встроенных системах вы обнаружите, что malloc/free не используется вообще. Функции X_create могут выделяться другими способами, например. из массива, размер которого фиксирован во время компиляции.

Ответ 4

Это порождает множество ответов, потому что это несколько основано на мнениях. Тем не менее я хочу объяснить, почему я лично предпочитаю, чтобы мои "объекты C" были выделены в кучу. Причина заключается в том, чтобы мои поля были скрыты (говорят: частные) от потребления кода. Это называется непрозрачным указателем. На практике это означает, что ваш заголовочный файл не определяет используемый struct, он только объявляет его. В качестве прямого следствия потребительский код не может знать размер struct, и поэтому распределение стека становится невозможным.

Преимущество: потребительский код никогда не может зависеть от определения struct, это означает, что вы не можете каким-то образом сделать содержимое struct непоследовательным извне и избегайте ненужной перекомпиляции кода потребления при изменении struct.

Первая проблема решается в С++, объявляя поля private. Но определение вашего class по-прежнему импортируется во все единицы компиляции, которые его используют, что делает необходимым их перекомпилировать даже тогда, когда меняются только члены private. Решение, часто используемое в - это шаблон pimpl: имеют все частные члены во втором struct (или: class), который определен только в файле реализации. Конечно, для этого требуется, чтобы ваш pimpl был выделен в куче.

Добавление к этому: современные языки ООП (например, или ) имеют средства для выделения объектов (и, как правило, решить, будет ли он стек или куча внутри) без кода вызова, зная об их определении.

Ответ 5

В общем, тот факт, что вы видите *, не означает, что он был malloc 'd. Например, вы могли бы получить указатель на глобальную переменную static; в вашем случае, действительно, CClass_destroy() не принимает никакого параметра, который предполагает, что он уже знает некоторую информацию об уничтоженном объекте.

Кроме того, указатели, независимо от того, являются ли они или нет malloc 'd, - это единственный способ, позволяющий вам изменить объект.

Я не вижу особых причин для использования кучи вместо стека: вы не получаете меньше используемой памяти. Однако для инициализации таких "классов" необходимо инициализировать/уничтожать функции, потому что основная структура данных может фактически содержать динамические данные, поэтому использование указателей.

Ответ 6

Я бы изменил "конструктор" на void CClass_create(CClass*);

Он не вернет экземпляр/ссылку структуры, но будет вызван на один.

В зависимости от того, выделяется ли он на "стек" или динамически, он полностью зависит от требований к сценарию использования. Однако вы его выделяете, вы просто вызываете CClass_create() передачу выделенной структуры в качестве параметра.

{
    CClass stk;
    CClass_create(&stk);

    CClass *dyn = malloc(sizeof(CClass));
    CClass_create(dyn);

    CClass_destroy(&stk); // the local object lifetime ends here, dyn lives on
}

// and later, assuming you kept track of dyn
CClass_destroy(dyn); // destructed
free(dyn); // deleted

Просто будьте осторожны, чтобы не возвращать ссылку на локальную (выделенную в стеке), потому что этот UB.

Однако вы его выделите, вам нужно будет вызвать void CClass_destroy(CClass*); в нужном месте (конец этого времени жизни объекта, который есть), и если динамически выделено, также освободите эту память.

Различать распределение/освобождение и построение/уничтожение, те, которые не являются одинаковыми (даже если в С++ они могут автоматически соединяться вместе).

Ответ 7

C не хватает определенных вещей, которые программисты на C++ принимают как должное, а именно:

  • общедоступные и частные спецификаторы
  • конструкторы и деструкторы

Большое преимущество этого подхода состоит в том, что вы можете скрыть структуру в своем файле C и заставить правильную конструкцию и разрушение выполнять свои функции создания и уничтожения.

Если вы обнаружите структуру в вашем .h файле, это означает, что пользователи могут получить доступ к элементам, которые напрямую нарушают инкапсуляцию. Также не форсирование создания не позволяет создать неправильную конструкцию вашего объекта.

Ответ 8

Поскольку функция может возвращать только выделенную стекю структуру, если она не содержит указателей на другие выделенные структуры. Если он содержит только простые объекты (int, bool, floats, chars и массивы из них, но без указателя), вы можете выделить его в стеке. Но вы должны знать, что если вы его вернете, оно будет скопировано. Если вы хотите разрешить указатели на другие структуры или хотите избежать копирования, используйте кучу.

Но если вы можете создать структуру в блоке верхнего уровня и использовать ее только в вызываемых функциях и никогда не возвращать ее, тогда соответствующий стек будет

Ответ 9

Если фиксировано максимальное количество объектов какого-либо типа, которые должны будут существовать одновременно, система должна будет что-то делать с каждым "живым" экземпляром, а рассматриваемые предметы не потребляют слишком много денег, наилучшим подходом обычно является не распределение кучи, а распределение стека, а скорее статически распределенный массив, а также методы "создать" и "уничтожить". Использование массива позволит избежать необходимости поддерживать связанный список объектов и позволит обрабатывать случай, когда объект не может быть немедленно уничтожен, потому что он "занят" (например, "занят" ). если данные поступают на канал через прерывание или DMA, когда пользовательский код решает, что он больше не интересуется каналом и распоряжается им, код пользователя может установить флаг "dispose when done" и вернуться, не беспокоясь о наличии ожидающего прерывания или DMA перезаписывают хранилище, которое больше не выделяется для него].

Использование пула фиксированного размера объектов фиксированного размера делает выделение и де-распределение намного более предсказуемыми, чем хранение хранилища из кучи смешанного размера. Этот подход невелик в тех случаях, когда спрос является переменным, и объекты занимают много места (индивидуально или коллективно), но когда спрос в основном согласован (например, приложение требует 12 объектов все время, а иногда требуется до 3 более), он может работать намного лучше, чем альтернативные подходы. Единственным недостатком является то, что любая установка должна быть выполнена в том месте, где объявлен статический буфер, или должна выполняться исполняемым кодом в клиентах. Невозможно использовать синтаксис переменной-инициализации на клиентском сайте.

Кстати, при использовании этого подхода нет необходимости в том, чтобы клиентский код получал указатели на что угодно. Вместо этого можно определить ресурсы, используя любой размер целого. Кроме того, если количество ресурсов никогда не будет превышать количество бит в int, может оказаться полезным, чтобы некоторые переменные состояния использовали один бит на ресурс. Например, можно было бы иметь переменные timer_notifications (записанные только через обработчик прерываний) и timer_acks (написанные только по основному коду) и указать, что бит N из (timer_notifications ^ timer_acks) будет установлен, когда таймер N хочет обслуживать. Используя такой подход, код должен читать только две переменные, чтобы определить, нуждается ли какой-либо таймер в обслуживании, вместо того, чтобы читать одну переменную для каждого таймера.

Ответ 10

Является ли ваш вопрос "почему в C нормально распределять память динамически и на С++ это не?"

С++ имеет много конструкций, которые делают новый избыточный. копировать, перемещать и нормальные конструкторы, деструкторы, стандартную библиотеку, распределители.

Но в C вы не можете обойти это.

Ответ 11

На самом деле это обратная реакция на С++, что делает "новый" слишком простым.

Теоретически использование этой модели построения класса в C идентично использованию "нового" в С++, поэтому не должно быть разницы. Однако то, как люди склонны думать о языках, различно, поэтому способ, которым люди реагируют на код, отличается.

В C очень часто думают о точных операциях, которые компьютер должен будет сделать для достижения ваших целей. Это не универсально, но это очень распространенное мышление. Предполагается, что вы потратили время на анализ затрат/выгод для malloc/free.

В С++ стало намного проще писать строки кода, которые многое для вас, без вашего понимания. Для кого-то довольно часто писать строку кода и даже не осознавать, что это вызвало 100 или 200 новых/удалений! Это вызвало обратную реакцию, когда разработчик С++ фанатично зацикливается на новостях и удаляет их из опасения, что их случайно называет повсюду.

Это, конечно, обобщения. Ни в коем случае не все сообщества C и С++ подходят для этих форм. Однако, если вы получаете флэш, используя новые вместо того, чтобы помещать вещи в кучу, это может быть основной причиной.

Ответ 12

Это довольно странно, что вы так часто видите. Вы, должно быть, выглядели как "ленивый" код.

В C метод, который вы описываете, обычно зарезервирован для "непрозрачных" типов библиотек, то есть типов структуры, определения которых намеренно становятся невидимыми для кода клиента. Поскольку клиент не может объявлять такие объекты, идиома должна действительно динамически распределяться в "скрытом" библиотечном коде.

Если скрытие определения структуры не требуется, типичная C-идиома обычно выглядит следующим образом

typedef struct CClass
{
    int member_a;
    float member_b;
} CClass;

CClass* CClass_init(CClass* cclass);
void CClass_release(CClass* cclass);

Функция CClass_init инициализирует объект *cclass и возвращает тот же указатель, что и результат. То есть бремя выделения памяти для объекта помещается на вызывающего, и вызывающий может выделить его любым способом, который он считает нужным

CClass cclass;
CClass_init(&cclass);
...
CClass_release(&cclass);

Классическим примером этой идиомы будет pthread_mutex_t с pthread_mutex_init и pthread_mutex_destroy.

В том же время, используя прежнюю технику непрозрачных типов (как в исходном коде), как правило, сомнительная практика. Это совершенно сомнительно, как бесплатное использование динамической памяти в С++. Он работает, но опять же, используя динамическую память, когда она не требуется, она нахмурилась в C, как и на С++.