Как думать, как программист C после того, как смещен с языка ООП? [закрыто]

38

Раньше я использовал только языки объектно-ориентированного программирования (C ++, Ruby, Python, PHP) и сейчас изучаю C. Мне трудно найти правильный способ сделать что-то на языке без понятия «Объект». Я понимаю, что можно использовать ООП-парадигмы в C, но я бы хотел изучить C-идиоматический способ.

При решении программной задачи первое, что я делаю, это представляю объект, который решит проблему. Какими шагами я могу заменить это, когда использую не-ООП парадигму императивного программирования?

Мас Баголь
источник
15
Мне еще предстоит найти язык, который бы соответствовал моему образу мышления, поэтому я должен «скомпилировать» свои мысли для любого языка, который я использую. Одна концепция, которую я нашел полезной, - это «кодовая единица», будь то метка, подпрограмма, функция, объект, модуль или инфраструктура: каждая из них должна быть инкапсулирована и предоставлять четко определенный интерфейс. Если вы используете нисходящий подход уровня объекта, в C вы можете начать с составления набора функций, которые ведут себя так, как будто проблема была решена. Часто хорошо разработанные C API выглядят как ООП, но qux = foo.bar(baz)становятся qux = Foo_bar(foo, baz).
Амон
Для того, чтобы эхо - Амон , сосредоточить внимание на следующем: граф-подобная структуре данных, указатели, алгоритмы, выполнение (поток управления) коды (функция), указатели на функции.
Rwong
1
LibTiff (исходный код на github) является примером того, как организовать большие программы на Си.
rwong
1
Как программист C # я бы пропустил делегаты (указатели на функции с одним связанным параметром) гораздо больше, чем объекты.
CodesInChaos
Лично я нашел большинство C простым и понятным, с заметным исключением препроцессора. Если бы мне пришлось заново изучать C, это было бы одной областью, на которой я бы сосредоточил свои усилия.
Бизиклоп

Ответы:

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

Вот и все.

Как ты написал класс? Это почти то, как вы пишете .C файл. Конечно, вы не получите таких вещей, как полиморфизм и наследование методов, но вы все равно можете смоделировать их с разными именами функций и составом .

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

Дальнейшее чтение
объектно-ориентации в ANSI C

Роберт Харви
источник
9
Вы также можете сделать typedefэто structи сделать что - то классное . и typedef-ed типы могут быть включены в другие structs, которые сами могут быть typedef-ed. то, что вы не получаете с C, это перегрузка операторов и поверхностно простое наследование классов и членов внутри, которые вы получаете в C ++. и вы не получите много странного и неестественного синтаксиса, который вы получаете с C ++. Я действительно люблю концепцию ООП, но я думаю, что C ++ - ужасная реализация ООП. я как C , так как это является меньшим языком и выходит из синтаксиса из языка , который лучше оставить функции.
Роберт Бристоу-Джонсон
22
Как человек, чей родной язык был / есть C, я рискну сказать это . a lot of things actually work better without the overhead of classes
haneefmubarak
1
Для расширения было разработано много вещей без ООП: операционные системы, серверы протоколов, загрузчики, браузеры и так далее. Компьютеры не думают с точки зрения объектов и не нуждаются в этом. В самом деле, они часто довольно медленно заставляют это.
edmz
Контрапункт: a lot of things actually work better with addition of class-based OOP. Источник: TypeScript, Dart, CoffeeScript и все остальные способы, которыми индустрия пытается уйти от функционального / прототипного языка ООП.
Ден
Чтобы расширить, многое было разработано с ООП: все остальное. Люди действительно думают с точки зрения объектов и программ, написанных для других людей, чтобы читать и понимать.
День
18

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

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

Обратите внимание, что объектно-ориентированное программирование - не единственная парадигма программирования . В некоторых случаях целесообразны и другие парадигмы ( функциональное программирование в стиле Ocaml или Haskell или даже Scheme или Commmon Lisp, логическое программирование в стиле Prolog и т. Д. И т. Д. ... Читайте также блог Дж. Питрата о декларативном искусственном интеллекте). См. Книгу Скотта: Прагматика языка программирования.

На самом деле, программист на C или в Ocaml, как правило, не хочет кодировать в стиле объектно-ориентированного программирования. Нет причин заставлять себя думать об объектах, когда это бесполезно.

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

Загляните в исходный код некоторых существующих свободных программ на C (смотрите github & sourceforge, чтобы найти их). Вероятно, было бы полезно установить и использовать дистрибутив Linux: он сделан почти только из свободного программного обеспечения, у него есть отличные компиляторы бесплатного программного обеспечения C ( GCC , Clang / LLVM ) и инструменты разработки. Смотрите также Advanced Linux Programming, если вы хотите разрабатывать для Linux.

Не забудьте собрать все предупреждения и информацию об отладке, например, особенно на gcc -Wall -Wextra -gэтапах разработки и отладки, и научиться использовать некоторые инструменты, например, valgrind для поиска утечек памяти , gdbотладчик и т. Д. Постарайтесь хорошо понять, что не определено поведение и строго избегать его (помните, что программа может иметь некоторое UB и иногда, кажется, "работает").

Когда вам действительно нужны объектно-ориентированные конструкции (в частности, наследование ), вы можете использовать указатели на связанные структуры и функции. Вы можете иметь свой собственный механизм vtable , каждый «объект» должен начинаться с указателя на указатели на structсодержащую функцию. Вы используете возможность приведения типа указателя к другому типу указателя (и тот факт, что вы можете приводить struct super_stтипы, содержащие те же типы полей, что и те, которые начинают struct sub_stэмулировать наследование). Обратите внимание, что C достаточно для реализации довольно сложных объектных систем, в частности, следуя некоторым соглашениям , как демонстрирует GObject (из GTK / Gnome).

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

Поскольку C - это язык очень низкого уровня, важно определить и задокументировать ваши собственные соглашения (вдохновленные практикой в ​​других программах на C), в частности, об управлении памятью, и, возможно, также некоторые соглашения об именах. Полезно иметь представление об архитектуре набора команд . Не забывайте , что C компилятор может сделать много оптимизаций на свой код (если вы попросите его), так что не все равно слишком много о выполнении микро-оптимизаций вручную, отпуск , что ваш компилятор ( для оптимизации компиляции выпущен програмное обеспечение). Если вы заботитесь о тестировании производительности и производительности, вам следует включить оптимизацию (после отладки вашей программы).gcc -Wall -O2

Не забывайте, что иногда метапрограммирование полезно . Довольно часто большое программное обеспечение, написанное на C, содержит некоторые сценарии или специальные программы для генерации некоторого кода C, используемого где-либо еще (и вы также можете использовать некоторые грязные трюки препроцессора C , например, X-макросы ). Существует несколько полезных генераторов программ на Си (например, 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 ).

Помните о концепциях и методах сбора мусора ( подсчет ссылок часто является методом управления памятью в C, и это, ИМХО, плохая форма сборки мусора, которая плохо справляется с циклическими ссылками ; у вас могут быть слабые указатели, чтобы помочь в этом, но это может быть сложно). В некоторых случаях вы могли бы рассмотреть возможность использования консервативного сборщика мусора Бема .

Василий Старынкевич
источник
7
Честно говоря, независимо от этого вопроса, чтение SICP, несомненно, является хорошим советом, но для OP это, вероятно, приведет к следующему вопросу: «Как мыслить как программист на Си после предвзятого отношения к SICP».
Док Браун
1
Нет, потому что схема от SICP и PHP (или Ruby или Python) настолько отличается, что OP получит гораздо более широкое мышление; и SICP довольно хорошо объясняет, что такое абстрактный тип данных на практике, и это очень полезно понять, в частности, для кодирования на C.
Василий Старынкевич
1
SICP - странное предложение. Схема сильно отличается от С.
Брайан Гордон
Но SICP учит многим хорошим привычкам, и знание Scheme помогает при кодировании на C (для концепций замыканий, абстрактных типов данных и т. Д.)
Василий Старынкевич
5

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

Программа обычно разделяется на файлы (модули), каждый файл обычно имеет группу связанных функций. В начале каждого файла вы объявляете (вне любой функции) переменные, которые будут использоваться всеми функциями в этом файле. Если вы используете «статический» квалификатор, эти переменные будут видны только внутри этого файла (но не из других файлов). Если вы не используете «статический» спецификатор для переменных, определенных вне функций, они будут доступны и из других файлов, и эти другие файлы должны объявить переменную как «внешнюю» (но не определять ее), так что компилятор будет искать их в других файлах.

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

мандрил
источник
3

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

В 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 ++ и различных других формальных ОО-языках внутри объекта метод объекта принимает первый аргумент, который является указателем на объект, во многом как версия C bar()выше. В качестве примера того, как это выходит на поверхность в C ++, рассмотрим, как std::bindможно использовать, чтобы приспособить методы объекта к сигнатурам функций:

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

Как уже отмечали другие, реальное различие заключается в том, что формальные ОО-языки могут реализовывать полиморфизм, управление доступом и другие отличные функции. Но сущность объектно-ориентированного программирования, создание и управление дискретными, сложными структурами данных, уже является фундаментальной практикой в ​​C.

лютик золотистый
источник
2

Одна из главных причин, по которой людям предлагается изучать C, это то, что это один из самых низких языков программирования высокого уровня. Языки ООП позволяют легче думать о моделях данных и шаблонном коде и передаче сообщений, но в конце концов микропроцессор выполняет код шаг за шагом, перепрыгивая через блоки кода (функции в C) и перемещаясь ссылки на переменные (указатели на C) вокруг, чтобы разные части программы могли обмениваться данными. Думайте о C как о языке ассемблера на английском языке - предоставляя пошаговые инструкции для микропроцессора вашего компьютера - и вы не ошибетесь. В качестве бонуса, большинство интерфейсов операционной системы работают как вызовы функций C, а не как парадигмы ООП,

Gaurav
источник
2
IMHO C - это язык низкого уровня, но намного выше, чем ассемблер или машинный код, поскольку компилятор C может выполнять много низкоуровневых оптимизаций.
Василий Старынкевич
Компиляторы Си также, во имя «оптимизации», переходят к модели абстрактной машины, которая может отрицать законы времени и причинности при заданном входном сигнале, что может привести к неопределенному поведению, даже если естественное поведение кода на машине, где Это будет соответствовать требованиям в противном случае. Например, функция uint16_t blah(uint16_t x) {return x*x;}будет работать одинаково на машинах с unsigned int16 битами или с 33 или более битами. Однако некоторые компиляторы для машин с unsigned intдлиной от 17 до 32 бит могут рассматривать вызов этого метода ...
суперкат
... как предоставление разрешения для компилятора сделать вывод о том, что никакая цепочка событий, которая привела бы к тому, что методу было присвоено значение, превышающее 46340, могла произойти. Даже если умножение 65533u * 65533u на любой платформе приведет к значению, которое при приведении к uint16_tполучит 9, стандарт не предписывает такое поведение при умножении значений типа uint16_tна 17–32-битных платформах.
суперкат
-1

Я также являюсь уроженцем ОО (в целом C ++), которому иногда приходится выживать в мире C. Для меня принципиально большим препятствием является обработка ошибок и управление ресурсами.

В C ++ у нас есть throw, чтобы передать ошибку от того места, где оно происходит, до самого верхнего уровня, где мы можем с ним справиться, и у нас есть деструкторы для автоматического освобождения нашей памяти и других ресурсов.

Вы можете заметить, что многие C API включают функцию init, которая предоставляет вам typedef'd void *, который на самом деле является указателем на структуру. Затем вы передаете это в качестве первого аргумента для каждого вызова API. По сути, это становится вашим указателем «это» из C ++. Он используется для всех внутренних структур данных, которые скрыты (очень концепция ОО). Вы также можете использовать его для управления памятью, например, иметь функцию myapiMalloc, которая распределяет память по памяти и записывает malloc в вашу версию C указателя this, чтобы вы могли быть уверены, что она освободится, когда ваш API вернется. Также, как я недавно обнаружил, вы можете использовать его для хранения кодов ошибок и использовать setjmp и longjmp, чтобы дать вам поведение, очень похожее на throw catch. Объединение обеих концепций дает вам большую функциональность программы на C ++.

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

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

Фил Розенберг
источник