Чи погано писати об’єктно-орієнтований C? [зачинено]


14

Мені здається, що я завжди пишу код на C, який переважно орієнтований на об'єкт, тому скажіть, що у мене був вихідний файл або щось, що я створив би структуру, а потім передаю вказівник на цю структуру функціям (методам), що належать цій структурі:

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

void destroyFoo(struct foo* foo); // frees foo and its things

Це погана практика? Як навчитися писати на C "правильним способом".


10
Значна частина Linux (ядра) написана таким чином, насправді вона навіть імітує ще більше OO-подібних концепцій, таких як віртуальний метод відправки. Я вважаю це досить правильним.
Кіліан Фот

13
" [T] він визначив, що Реальний програміст може писати програми FORTRAN будь-якою мовою. " - Ед Пост, 1983
Росс Паттерсон

4
Чи є якась причина, чому ви не хочете переходити на C ++? Не потрібно використовувати його частини, які вам не подобаються.
svick

5
Це дійсно ставить питання "Що таке" об'єктно-орієнтована "?" Я б не назвав цей об'єкт орієнтованим. Я б сказав, що це процедурна процедура. (У вас немає ні спадку, ні поліморфізму, ні інкапсуляції / здатності приховувати стан, і, ймовірно, відсутні інші ознаки ОО, які не відриваються від верхньої частини моєї голови.) Добре це чи погана практика не залежить від цієї семантики , хоча.
jpmc26

3
@ jpmc26: Якщо ви мовний рецептист, вам слід послухати Алана Кей, він винайшов цей термін, він може сказати, що це означає, і він каже, що OOP - все про обмін повідомленнями . Якщо ви мовний дескриптивіст, ви б ознайомилися з використанням цього терміна у спільноті розробки програмного забезпечення. Кук зробив саме це, він проаналізував особливості мов, які або претендують на або вважаються ОО, і виявив, що у них є одне спільне: обмін повідомленнями .
Йорг W Міттаг

Відповіді:


24

Ні, це не погана практика, навіть рекомендується це робити, хоча можна навіть використовувати такі конвенції, як struct foo *foo_new();іvoid foo_free(struct foo *foo);

Звичайно, як йдеться в коментарі, робіть це лише там, де це доречно. Немає сенсу використовувати конструктор для int.

Префікс foo_- це умовна умова, за якою дотримується багато бібліотек, оскільки вона захищає від зіткнення з називанням інших бібліотек. Інші функції часто мають умову використовувати foo_<function>(struct foo *foo, <parameters>);. Це дозволяє вам struct fooбути непрозорим.

Погляньте на документацію libcurl для конвенції, особливо з "просторами імен", щоб виклик функції curl_multi_*з першого погляду виглядав неправильно, коли повертався перший параметр curl_easy_init().

Є ще більш загальні підходи, див. Об'єктно-орієнтоване програмування за допомогою ANSI-C


11
Завжди з застереженням "де це доречно". ООП - це не срібляста куля.
Дедуплікатор

Чи немає у C просторів імен, де ви могли б оголосити ці функції? Схожий на std::string, ти не міг foo::create? Я не використовую C. Можливо, це лише в C ++?
Кріс Сірефіс

@ChrisCirefice У C немає просторів імен, тому багато авторів бібліотеки використовують префікси для своїх функцій.
Residuum

2

Це непогано, це чудово. Об'єктно-орієнтоване програмування - це гарна річ (якщо ви не захопитеся, ви можете мати занадто багато хорошого). C не є найбільш підходящою мовою для OOP, але це не повинно перешкодити вам отримати найкраще.


4
Я не можу погодитись, але ваша думка справді повинна бути підтримана деякою деталізацією.
Дедуплікатор

1

Не погано. Він підтримує використання RAII, який запобігає виникненню багатьох помилок (витоки пам'яті, використання неініціалізованих змінних, використання після безкоштовного тощо, що може спричинити проблеми з безпекою).

Отже, якщо ви хочете скласти свій код тільки з GCC або Clang (а не з компілятором MS), ви можете використовувати cleanupатрибут, який належним чином знищить ваші об'єкти. Якщо ви декларуєте свій об'єкт таким чином:

my_str __attribute__((cleanup(my_str_destructor))) ptr;

Потім my_str_destructor(ptr)буде запущено, коли ptr вийде за межі області. Просто майте на увазі, що його не можна використовувати з аргументами функції .

Крім того, не забудьте використовувати my_str_у своєму методі імена, тому Cщо не має просторів імен, і це легко зіткнутися з іншою назвою функції.


2
Afaik, RAII - це використання неявного виклику деструкторів для об'єктів в C ++ для забезпечення очищення, уникаючи необхідності додавати явні виклики випуску ресурсів. Отже, якщо я не дуже помиляюся, RAII і C взаємно виключають.
cmaster - відновити моніку

@cmaster Якщо ви використовуєте #defineваші імена типів, __attribute__((cleanup(my_str_destructor)))ви отримаєте їх як неявні в цілому #defineобсязі (вони будуть додані до всіх ваших змінних оголошень).
Marqin

Це працює, якщо a) ви використовуєте gcc, b) якщо ви використовуєте тип лише у функціональних локальних змінних, і c) якщо ви використовуєте тип лише у відкритій версії (жодних вказівників на #defineтип 'd чи масиви на нього). Коротше кажучи: це не стандартний C, і ви платите з великою гнучкістю у використанні.
cmaster - відновити моніку

Як було сказано у моїй відповіді, це також працює в кланге.
Marqin

Ах, я цього не помічав. Це дійсно робить вимогу a) значно менш суворою, оскільки це робить __attribute__((cleanup()))майже квазістандартним. Однак b) і c) все ще стоять ...
cmaster - відновіть моніку

-2

Переваг такого коду може бути багато, але, на жаль, Стандарт С не був написаний для його полегшення. Компілятори історично пропонували ефективні гарантії поведінки, ніж те, що вимагав Стандарт, що давав можливість писати такий код набагато чіткіше, ніж це можливо в Стандарті С, але останнім часом компілятори почали скасовувати такі гарантії в ім'я оптимізації.

Найголовніше, що багато компіляторів С історично гарантували (за задумом, якщо не документацією), що якщо два типи структури містять однакову початкову послідовність, то вказівник на будь-який тип може використовуватися для доступу до членів цієї загальної послідовності, навіть якщо типи не пов'язані між собою, і далі, що для встановлення загальної початкової послідовності всі вказівники на структури є рівнозначними. Код, який використовує таку поведінку, може бути набагато більш чистим і безпечним для типів, ніж код, який цього не робить, але, на жаль, хоча Стандарт вимагає, щоб структури, що мають спільну початкову послідовність, повинні бути викладені так само, він забороняє коду фактично використовувати вказівник одного типу для доступу до початкової послідовності іншого.

Отже, якщо ви хочете написати об'єктно-орієнтований код на C, вам доведеться вирішити (і слід прийняти це рішення на початку) або перестрибнути через безліч обручів, щоб дотримуватися правил типу вказівника C, і бути готовим мати сучасні компілятори генерують безглуздий код, якщо один прослизає вгору, навіть якщо старші компілятори створили б код, який працює за призначенням, або ж документують вимогу, що код може бути використаний лише для компіляторів, налаштованих для підтримки поведінки вказівника старого стилю (наприклад, використання "-но-строгий-згладжування") Деякі люди вважають "-но-суворий-згладжуючий" злом, але я б припустив, що корисніше думати про "-фно-строгий-згладжування" як мову, яка пропонує більшу смислову потужність для деяких цілей, ніж "стандартний" C,але за рахунок оптимізацій, які можуть бути важливими для деяких інших цілей.

Наприклад, на традиційних компіляторах історичні компілятори інтерпретують такий код:

struct pair { int i1,i2; };
struct trio { int i1,i2,i3; };

void hey(struct pair *p, struct trio *t)
{
  p->i1++;
  t->i1^=1;
  p->i1--;
  t->i1^=1;
}

як виконання наступних кроків для того, щоб: збільшити перший член *p, доповнити найнижчий біт першого члена *t, потім декрементувати перший член *pі доповнити найнижчий біт першого члена *t. Сучасні компілятори переставлять послідовність операцій таким чином, який код, який буде більш ефективним, якщо pі tідентифікувати різні об'єкти, але змінить поведінку, якщо вони цього не роблять.

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

Дещо менш надуманий приклад виникне, якби хотілося написати функцію, щоб зробити щось на зразок підміна двох покажчиків на довільні типи. У переважній більшості компіляторів "C" 1990-х років, що може бути здійснено за допомогою:

void swap_pointers(void **p1, void **p2)
{
  void *temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}

Однак у стандарті C потрібно використовувати:

#include "string.h"
#include "stdlib.h"
void swap_pointers2(void **p1, void **p2)
{
  void **temp = malloc(sizeof (void*));
  memcpy(temp, p1, sizeof (void*));
  memcpy(p1, p2, sizeof (void*));
  memcpy(p2, temp, sizeof (void*));
  free(temp);
}

Якщо *p2він зберігається у виділеному сховищі, а тимчасовий покажчик не зберігається у виділеному сховищі, ефективним типом *p2стане тип тимчасового вказівника та код, який намагається використовувати *p2як будь-який тип, який не відповідає тимчасовому вказівнику тип буде викликати не визначене поведінку. Слід бути впевненим вкрай малоймовірно, що компілятор помітить таке, але оскільки сучасна філософія компілятора вимагає від програмістів уникати не визначеної поведінки будь-якою ціною, я не можу придумати жодного іншого безпечного способу написання вищевказаного коду без використання виділеного сховища .


Покровитель: Хочете коментувати? Основний аспект об'єктно-орієнтованого програмування - це можливість мати декілька типів спільних спільних аспектів, і вказівник на будь-який подібний тип може бути використаний для доступу до цих загальних аспектів. Приклад ОП цього не робить, але він ледве дряпає поверхню "об'єктно-орієнтованої". Історичні компілятори C дозволяють писати поліморфний код набагато чистішим чином, ніж це можливо в сьогоднішньому стандарті C. Проектування об'єктно-орієнтованого коду на C вимагає того, щоб визначити, на яку саме мову орієнтований. З яким аспектом люди не згодні?
supercat

Гм ... розум, ви показуєте, як гарантії, що надаються стандартом, не дозволяють вам чітко отримувати доступ до членів загальної початкової підпорядкованості? Тому що я думаю, що саме на цей час ваша зухвальство щодо злого сміливості оптимізуватись у межах контрактної поведінки? (Це я здогадуюсь про те, що двоє прихильників вважають заперечними.)
Дедуплікатор

OOP не обов'язково вимагає успадкування, тому сумісність між двома структурами не є особливою проблемою на практиці. Я можу отримати справжній OO, поставивши в структуру покажчики функцій та викликавши ці функції певним чином. Звичайно, цей foo_function(foo*, ...)псевдо-OO в C - це лише певний стиль API, який, схоже, виглядає як класи, але його слід більш правильно називати модульним програмуванням з абстрактними типами даних.
амон

@ Дедуплікатор: Дивіться вказаний приклад. Поле "i1" є членом загальної початкової послідовності обох структур, але сучасні компілятори вийдуть з ладу, якщо код спробує використовувати "структурну пару *" для доступу до початкового члена "struct trio".
supercat

Який сучасний компілятор C не відповідає цьому прикладу? Будь-які цікаві варіанти потрібні?
Дедупликатор

-3

Наступним кроком є ​​приховання декларації структури. Ви поміщаєте це у файл .h:

typedef struct foo_s foo_t;

foo_t * foo_new(...);
void foo_destroy(foo_t *foo);
some_type foo_whatever(foo_t *foo, ...);
...

А потім у файлі .c:

struct foo_s {
    ...
};

6
Це може бути наступним кроком, залежно від мети. Це так чи ні, хоча це навіть не віддалено намагається відповісти на питання.
Дедуплікатор
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.