Як мислити як програміст C після зміщення мови OOP? [зачинено]


38

Раніше я використовував лише об'єктно-орієнтовані мови програмування (C ++, Ruby, Python, PHP), і зараз навчаюсь C. Мені важко з'ясувати правильний спосіб робити речі мовою без поняття 'Об’єкт'. Я усвідомлюю, що можна використовувати парадигми OOP в C, але я хотів би дізнатися C-ідіоматичний спосіб.

При вирішенні проблеми програмування перше, що я роблю, - це уявити собі предмет, який вирішить проблему. Якими кроками я замінюю це під час використання парадигми імперативного програмування, що не є OOP?


15
Я ще не можу знайти мову, яка б відповідала моєму мисленню, тому мені доводиться «складати» свої думки на будь-якій мові, якою я користуюся. Однією з концепцій, які я вважаю корисною, є концепція "блоку коду", будь то мітка, підпрограма, функція, об'єкт, модуль чи рамка: кожна з них повинна бути інкапсульована і відкрити чітко визначений інтерфейс. Якщо ви використовуєте підхід на рівні об'єкта зверху вниз, в C ви можете почати з складання набору функцій, які ведуть себе так, як ніби проблема була вирішена. Часто добре розроблені C-API схожі на OOP, але це qux = foo.bar(baz)стає qux = Foo_bar(foo, baz).
амон

Для відлуння амона зосередьтесь на наступному: структура даних, схожа на графік, покажчики, алгоритми, виконання (керуючий потік) коду (функції), функціональні вказівники.
rwong

1
LibTiff (вихідний код на github) - приклад того, як організувати великі програми C.
rwong

1
Як програміст на C # я б сумував за делегатами (покажчики функцій з одним прив’язаним параметром) набагато більше, ніж я пропускав би об'єкти.
CodesInChaos

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

Відповіді:


53
  • Програма змінного струму - це сукупність функцій.
  • Функція - це сукупність висловлювань.
  • Ви можете інкапсулювати дані за допомогою struct.

Це воно.

Як ви писали клас? Це майже так, як ви пишете .C файл. Зрозуміло, ви не отримуєте такі речі, як поліморфізм методу та успадкування, але ви можете імітувати ті, хто має різні назви функцій та склад .

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

Подальше зчитування
об'єктної орієнтації в ANSI C


9
Ви також можете typedefце structзробити і зробити щось подібне до класу . і typedef-ed типи можуть бути включені в інші structs, які самі можуть бути typedef-ed. те, чого ви не отримуєте з C, - це перевантаження оператора і надмірно просте успадкування класів та членів, які потрапляють у C ++. і ви не отримуєте багато дивного і неприродного синтаксису, який отримуєте за допомогою C ++. Я дуже люблю концепцію OOP, але я думаю, що C ++ - це потворна реалізація OOP. я як C , так як це є меншим мовою і виходить з синтаксису з мови , який краще залишити функції.
Роберт Брістоу-Джонсон

22
Як хтось, першою мовою якої був / є С, я б ризикнув це сказати . a lot of things actually work better without the overhead of classes
haneefmubarak

1
Для розширення без OOP було розроблено багато речей: операційні системи, сервери протоколів, завантажувачі, браузери тощо. Комп'ютери не думають з точки зору предметів, а також не потрібно. Дійсно, їм часто досить повільно це змусити.
edmz

Контрапункт: a lot of things actually work better with addition of class-based OOP. Джерело: TypeScript, Dart, CoffeeScript та всі інші способи, які індустрія намагається відійти від функціональної / прототипової мови OOP.
День

Для розширення з OOP було розроблено багато речей : все інше. Люди природно думають з точки зору об'єктів, а програми написані для того, щоб інші люди читали та розуміли.
День

18

Прочитайте SICP та вивчіть схему та практичну ідею абстрактних типів даних . Тоді кодування в C легко (оскільки при SICP, трохи C, трохи PHP, Ruby тощо) ваше мислення буде досить розширеним, і ви зрозумієте, що об'єктно-орієнтоване програмування може бути не найкращим стилем у всі випадки, але лише для якихось програм). Будьте уважні до динамічного розподілу пам’яті C , що, мабуть, найважча частина. Стандарт мов програмування C99 або C11 та його стандартна бібліотека С насправді досить бідні (він не знає про TCP або каталоги!), І вам часто знадобляться деякі зовнішні бібліотеки або інтерфейси (наприклад,POSIX , libcurl для клієнтської бібліотеки HTTP, libonion для бібліотеки серверів HTTP, GMPlib для bignums, деяка бібліотека, як libunistring для UTF-8 тощо).

Ваші "об'єкти" часто знаходяться в C деяких споріднених struct, і ви визначаєте набір функцій, що діють на них. Для коротких або дуже простих функцій розгляньте їх визначення відповідною struct, як static inlineу деяких файлах заголовка, foo.hщоб бути #include-d в іншому місці.

Зауважте, що об'єктно-орієнтоване програмування - не єдина парадигма програмування . В деяких випадках варті й інші парадигми ( функціональне програмування à la Ocaml, Haskell або навіть Scheme або Commmon Lisp, логічне програмування à la Prolog, і т.д. тощо ... Читайте також блог Дж. Пітрата про декларативний штучний інтелект). Дивіться книгу Скотта: Прагматика мови програмування

Власне, програміст на C або в Ocaml зазвичай не хоче кодувати в об'єктно-орієнтованому стилі програмування. Немає причин змушувати себе думати про предмети, коли це не корисно.

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

Загляньте в вихідний код деякого наявного вільного програмного забезпечення на C (див. Github & sourceforge, щоб знайти його). Можливо, встановлення та використання дистрибутива Linux було б корисним: він створений майже лише з вільного програмного забезпечення, у ньому є чудові безкоштовні компілятори програмного забезпечення C ( GCC , Clang / LLVM ) та засоби розробки. Дивіться також Розширене програмування Linux, якщо ви хочете розвиватися для Linux.

Не забудьте скласти всі попередження та інформацію про налагодження, наприклад - gcc -Wall -Wextra -gособливо під час етапів розробки та налагодження - та навчитися користуватися деякими інструментами, наприклад, valgrind для полювання на витоки пам'яті , gdbналагоджувач тощо. Слідкуйте за тим, щоб добре зрозуміти, що не визначено поведінки і рішуче уникайте цього (пам’ятайте, що програма може мати певний UB і іноді, здається, «працює»).

Коли вам справді потрібні об'єктно-орієнтовані конструкції (зокрема спадкування ), ви можете використовувати вказівники на споріднені структури та функції. Ви можете мати власну vtable машину , кожен "об'єкт" починається з вказівника на покажчики, що structмістять функції. Ви використовуєте можливість передачі типу вказівника іншому типу вказівника (і того, що ви можете передавати з struct super_stмістять ті ж типи полів, що і ті, що починають struct sub_stз емуляції спадкування). Зауважте, що C достатньо для реалізації досить складних об'єктних систем, зокрема, дотримуючись деяких конвенцій, - як демонструє GObject (від GTK / Gnome).

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

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

Не забувайте, що інколи корисне метапрограмування . Досить часто велике програмне забезпечення, написане на C, містить деякі сценарії або спеціальні програми для створення деякого коду С, який використовується в іншому місці (і ви також можете грати деякі брудні хитрощі C-препроцесора , наприклад, X-макроси ). Існує кілька корисних генераторів програм C (наприклад, yacc або gnu bison для генерації парсерів, gperf для створення ідеальних хеш-функцій тощо). У деяких системах (зокрема Linux і POSIX) ви навіть можете генерувати якийсь код C під час виконання generated-001.cфайлу, компілювати його до спільного об'єкта, виконуючи якусь команду (наприклад gcc -O -Wall -shared -fPIC generated-001.c -o generated-001.so) під час виконання, динамічно завантажувати спільний об'єкт за допомогою dlopen& отримати покажчик функції від імені за допомогою dlsym . Я роблю такі хитрощі в MELT (мова, схожа на Lisp, що може бути корисною для вас, оскільки вона дозволяє налаштувати компілятор GCC ).

Будьте в курсі концепцій та методів збору сміття ( підрахунок посилань часто є технікою управління пам’яттю в С, і це IMHO погана форма збору сміття, яка не справляється з круговими посиланнями ; у вас можуть бути слабкі вказівки, щоб допомогти у цьому, але це може бути хитро). В деяких випадках ви можете подумати про використання консервативного сміттєзбірника Бома .


7
Чесно кажучи, невимовно з цього питання читання SICP - це, без сумніву, хороша порада, але для ОП це, ймовірно, призведе до наступного питання "Як мислити як програміст C після упередженості з SICP".
Док Браун

1
Ні, тому що Схеми від SICP & PHP (або Ruby або Python) настільки різні, що ОП отримає набагато ширше мислення; і SICP досить добре пояснює, що таке абстрактний тип даних на практиці, і це дуже корисно для розуміння, зокрема для кодування в C.
Basile Starynkevitch

1
SICP - дивна пропозиція. Схема сильно відрізняється від C.
Брайан Гордон

Але SICP викладає багато корисних звичок, і знання схеми допомагає при кодуванні на C (для понять про закриття, абстрактні типи даних тощо)
Basile Starynkevitch

5

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

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

Отже, коротше, ви спочатку думаєте про процедури (функції), а потім переконуєтесь, що всі функції мають доступ до необхідної їм інформації.


3

API API часто - можливо навіть зазвичай - мають по суті об'єктно-орієнтований інтерфейс, якщо дивитися на них правильним чином.

В C ++:

class foo {
    public:
        foo (int x);
        void bar (int param);
    private:
        int x;
};

// Example use:
foo f(42);
f.bar(23);

В:

typedef struct {
    int x;
} foo;

void bar (foo*, int param);

// Example use:
foo f = { .x = 42 };
bar(&f, 23);

Як вам може бути відомо, в C ++ та різних інших формальних мовах OO під кришкою об'єктний метод приймає перший аргумент, який є вказівником на об’єкт, подібно до версії С bar()вище. Для прикладу, коли це виходить на поверхню в C ++, розглянемо, як std::bindможна використовувати для пристосування об'єктних методів функціонування підписів:

new function<void(int)> (
    bind(&foo::bar, this, placeholders::_1)
//                  ^^^^ object pointer as first arg
);

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


2

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


2
IMHO C - мова низького рівня, але набагато вища, ніж асемблер або машинний код, оскільки компілятор C може зробити багато низьких рівнів оптимізації.
Базиле Старинкевич

C-компілятори також від імені "оптимізації" рухаються до абстрактної моделі машини, яка може заперечувати закони часу та причинності при введенні, що спричинить не визначене поведінку, навіть якщо природна поведінка коду на машині, де він знаходиться Виконання буде відповідати інакше задовольнити вимоги Наприклад, функція uint16_t blah(uint16_t x) {return x*x;}працюватиме однаково на машинах, у яких unsigned int16 біт, або де вона становить 33 біт і більше. Деякі компілятори для машин із unsigned int17 до 32 бітами, однак, можуть розглянути виклик цього методу ...
supercat

... як надання дозволу компілятору зробити висновок про те, що жоден ланцюжок подій, що спричинив би присвоєння методу значення, що перевищує 46340, не може відбутися. Навіть незважаючи на те, що множення 65533u * 65533u на будь-якій платформі дало б значення, яке при передачі uint16_tдасть 9, Стандарт не передбачає такої поведінки при множенні значень типу uint16_tна 17 - 32-бітних платформах.
supercat

-1

Я також є вихователем OO (C ++ взагалі), якому іноді доводиться виживати в світі C. Для мене головне найбільше перешкода - це подолання помилок та управління ресурсами.

У C ++ ми повинні кинути помилку, звідки це відбувається аж до верхнього рівня, де ми можемо з цим боротися, і у нас є деструктори, які автоматично звільняють нашу пам’ять та інші ресурси.

Ви можете помітити, що багато API API містять функцію init, яка забезпечує вам typedef'd void *, який є дійсно вказівником на структуру. Потім ви передаєте це як перший аргумент для кожного дзвінка API. По суті, це стає вашим "цим" покажчиком від C ++. Він звикає до всіх прихованих внутрішніх структур даних (дуже OO концепція). Ви також можете використовувати його для управління пам’яттю, наприклад, мати функцію, яку називають myapiMalloc, яка викриває вашу пам’ять і записує malloc у вашій C версії цього покажчика, щоб ви могли переконатися, що він звільнений, коли ваш API повернеться. Крім того, як я нещодавно виявив, ви можете використовувати його для зберігання кодів помилок, а також використовувати setjmp та longjmp, щоб надати вам поведінку, дуже схожу на викид. Поєднання обох концепцій дає багато функціональних можливостей програми C ++.

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

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

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