C дизайн API: Кто должен выделить? [закрытый]

52
задан Tordek 20 July 2010 в 17:23
поделиться

10 ответов

Мой любимый пример хорошо спроектированного C API - GTK+, который использует метод #2, который вы описываете.

Хотя еще одно преимущество вашего метода #1 заключается не только в том, что вы можете выделить объект в стеке, но и в том, что вы можете повторно использовать один и тот же экземпляр несколько раз. Если это не будет обычным вариантом использования, тогда простота #2, вероятно, является преимуществом.

Конечно, это только моё мнение :)

9
ответ дан 7 November 2019 в 09:31
поделиться

Другой недостаток №2 заключается в том, что вызывающий абонент не может управлять распределением данных. Это можно обойти, предоставив клиенту API для регистрации его собственных функций распределения / освобождения (как это делает SDL), но даже это может быть недостаточно детализированным.

Недостатком №1 является то, что он не работает, когда выходные буферы имеют нестандартный размер (например, строки). В лучшем случае вам нужно будет предоставить другую функцию, чтобы сначала получить длину буфера, чтобы вызывающий мог ее выделить. В худшем случае это просто невозможно сделать эффективно (т.е. вычисление длины на отдельном пути слишком дорого по сравнению с вычислением и копированием за один раз).

Преимущество №2 состоит в том, что он позволяет отображать тип данных строго как непрозрачный указатель (т. Е.объявляйте структуру, но не определяйте ее и последовательно используйте указатели). Затем вы можете изменить определение структуры по своему усмотрению в будущих версиях вашей библиотеки, в то время как клиенты остаются совместимыми на двоичном уровне. С # 1 вы должны сделать это, потребовав от клиента указать версию внутри структуры каким-либо образом (например, все эти поля cbSize в Win32 API), а затем вручную написать код, который может обрабатывать как старые и более новые версии структуры, чтобы сохранять бинарную совместимость по мере развития вашей библиотеки.

В общем, если ваши структуры представляют собой прозрачные данные, которые не изменятся с будущими незначительными версиями библиотеки, я бы выбрал №1. Если это более или менее сложный объект данных, и вам нужна полная инкапсуляция, чтобы обезопасить его для будущей разработки, выберите №2.

15
ответ дан 7 November 2019 в 09:31
поделиться

Почему бы не предоставить обе функции, чтобы получить лучшее из обоих миров?

Используйте функции _init и _terminate, чтобы использовать метод #1 (или любое другое название, которое вы считаете нужным).

Используйте дополнительные функции _create и _destroy для динамического распределения. Поскольку _init и _terminate уже существуют, это фактически сводится к следующему:

myStruct *myStruct_create ()
{
    myStruct *s = malloc(sizeof(*s));
    if (s) 
    {
        myStruct_init(s);
    }
    return (s);
}

void myStruct_destroy (myStruct *s)
{
    myStruct_terminate(s);
    free(s);
}

Если вы хотите, чтобы это было непрозрачно, то сделайте _init и _terminate статическими и не раскрывайте их в API, предоставляйте только _create и _destroy. Если вам нужны другие выделения, например, с заданным обратным вызовом, предоставьте для этого другой набор функций, например, _createcalled, _destroycalled.

Главное - следить за выделениями, но вы должны делать это в любом случае. Для деаллокации вы всегда должны использовать аналог используемого аллокатора.

12
ответ дан 7 November 2019 в 09:31
поделиться

Оба функционально эквивалентны. Но, на мой взгляд, метод №2 проще в использовании. Несколько причин для предпочтения 2 вместо 1:

  1. Это более интуитивно понятно. Почему я должен вызывать free для объекта после того, как я (очевидно) уничтожил его с помощью myStruct_Destroy .

  2. Скрывает подробную информацию о myStruct от пользователя. Ему не нужно беспокоиться о его размере и т. Д.

  3. В методе №2 myStruct_init не нужно беспокоиться о начальном состоянии объекта.

  4. Вам не нужно беспокоиться об утечке памяти из-за того, что пользователь забыл позвонить бесплатно .

Однако, если ваша реализация API поставляется как отдельная разделяемая библиотека, метод №2 является обязательным. Чтобы изолировать ваш модуль от любого несоответствия в реализациях malloc / new и free / delete в версиях компилятора, вы должны сохранить выделение памяти и выделение себе. Обратите внимание, это больше верно для C ++, чем для C.

4
ответ дан 7 November 2019 в 09:31
поделиться

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

Другой вариант, о котором вы не упомянули, который большую часть времени будет излишним, - это передать указатель на функцию, который api использует в качестве распределителя. Это не позволяет вам использовать стек, но позволяет сделать что-то вроде замены использования malloc пулом памяти, который по-прежнему контролирует api, когда он хочет выделить.

Что касается того, какой метод является правильным дизайном api, в стандартной библиотеке C это делается в обоих направлениях. strdup () и stdio используют второй метод, в то время как sprintf и strcat используют первый метод.Лично я предпочитаю второй метод (или третий), если 1) я не знаю, что мне никогда не понадобится перераспределение и 2) я ожидаю, что время жизни моих объектов будет коротким, и поэтому использование стека очень удобно

edit: На самом деле есть еще один вариант, и это плохой вариант с заметным прецедентом. Вы можете сделать это так же, как strtok () со статикой. Не очень хорошо, упомянуто только для полноты картины.

3
ответ дан 7 November 2019 в 09:31
поделиться

Оба способа подходят, я предпочитаю использовать первый, поскольку большая часть C, которую я делаю, предназначена для встроенных систем, а вся память - это либо крошечные переменные в стеке, либо статически выделенные. Таким образом, не может быть нехватки памяти, либо у вас ее достаточно с самого начала, либо с самого начала вы облажались. Полезно знать, когда у вас 2К RAM :-) Итак, все мои библиотеки похожи на №1, где предполагается, что память выделяется.

Но это крайний случай разработки C.

Сказав это, я бы, вероятно, по-прежнему выбрал №1. Возможно использование init и finalize / dispose (а не уничтожение) для имен.

2
ответ дан 7 November 2019 в 09:31
поделиться

Это могло бы дать некоторый элемент отражения:

case # 1 имитирует схему распределения памяти C ++ с более или менее теми же преимуществами:

  • простое размещение временных файлов в стеке ( или в статических массивах, или в таком, чтобы написать собственный распределитель структур, заменяющий malloc).
  • легко освободить память, если что-то пойдет не так в init

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

Смешанный API между случаем №1 и случаем №2 также распространен: есть поле, используемое для передачи указателя на некоторую уже инициализированную структуру, если оно равно нулю, оно выделяется (и указатель всегда возвращается). С таким API за бесплатные обычно отвечает вызывающая сторона, даже если инициализация выполнила выделение.

В большинстве случаев я бы выбрал случай №1.

2
ответ дан 7 November 2019 в 09:31
поделиться

Оба варианта приемлемы - как вы заметили, между ними есть компромиссы.

Есть множество реальных примеров того и другого - как Дин Хардинг говорит, GTK + использует второй метод; OpenSSL - это пример, который использует первый.

1
ответ дан 7 November 2019 в 09:31
поделиться

Я бы выбрал (1) с одним простым расширением, а именно, чтобы ваша функция _init всегда возвращала указатель на объект. Тогда инициализация указателя может выглядеть так:

myStruct *s = myStruct_init(malloc(sizeof(myStruct)));

Как видите, в правой части будет только ссылка на тип, а не на переменную. Простой макрос дает вам (2), по крайней мере частично

#define NEW(T) (T ## _init(malloc(sizeof(T))))

и инициализация указателя читается как

myStruct *s = NEW(myStruct);
1
ответ дан 7 November 2019 в 09:31
поделиться

Метод №2 каждый раз.

Почему? потому что с методом номер 1 вы должны передать детали реализации вызывающему. Вызывающий должен знать по крайней мере , насколько велика структура. Вы не можете изменить внутреннюю реализацию объекта, не перекомпилировав код, который его использует.

15
ответ дан 7 November 2019 в 09:31
поделиться
Другие вопросы по тегам:

Похожие вопросы: