Cc стиль программирования


Стиль программирования на C++

Я старый (но не слишком старый) Java-программист, который решил изучить С++. Но я видел, что большая часть стиля программирования на C++ — это. ну, просто чертовски уродливый!

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

Итак, наконец, есть ли у меня причина продолжать эту резню в ООП и все, что хорошо и праведно в программировании, или я могу просто игнорировать эти старомодные соглашения на С++ и использовать мой хороший стиль программирования Java

Кстати, я изучаю С++, потому что хочу заниматься программированием игры.

На веб-сайте С++ я нашел реализацию Windows:

Этот класс находится в файле заголовка и конструкторе:

находится в исходном файле .cpp.

Что я могу сделать только:

И теперь конструктор находится внутри своего класса.

В дополнение к тому, что говорили здесь другие, есть еще более важные проблемы:

1) Большие единицы перевода приводят к более длительному времени компиляции и большему размеры объектных файлов.

2) Круговые зависимости! И это большой. И это может быть почти всегда фиксируются путем разделения заголовков и источника:

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

Но если вы разделите их на правильные .h и .cpp файлы и используете форвардные объявления, он удовлетворит компилятор:

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

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

Доверяйте, что у С++ есть причина для этой идиомы, потому что, если вы этого не сделаете, у вас будет много головных болей для вас. Я знаю:/

Когда код строит, препроцессор С++ создает блок перевода. Он начинается с файла .cpp, анализирует #includes, захватывает текст из заголовков и генерирует один большой большой текстовый файл со всеми заголовками и кодом .cpp. Затем эта единица перевода скомпилируется в код, который будет запускаться на платформе, на которую вы нацеливаете. Каждая единица перевода заканчивается как один объектный файл.

Итак, файлы .h будут включены в несколько единиц перевода, а файл .cpp просто включается в один.

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

Итак, минимизируя материал в файлах .h, резко улучшается время компиляции. Вы можете отредактировать файл .cpp для изменения функции, и только одна единица перевода должна быть перестроена.

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

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

.h и .cpp часто разделяют объявления от определений, и здесь есть какой-то порядок и концептуальная инкапсуляция. заголовки имеют «что», файлы реализации имеют «как».

Я обнаружил, что все в одном файле в java не дезорганизовано. Итак, каждый по-своему.

Соглашения и идиомы С++ имеют хорошо продуманные рассуждения, как и любой другой язык для идиом, которые принимает его сообщество.

Я думаю, что лучше всего java (как глагол), когда вы пишете Java, и на С++, когда вы С++! Вы поймете, почему с опытом работы на этом языке. То же, что и переход на любой новый язык.

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

Во всяком случае, я просто хотел опубликовать в другой части вопроса: есть ли С++ OOP masacre?

С++ — многостраничный язык. Он позволяет procedural, объектно-ориентированный и generic. И это добродетель, а не недостаток. Вы по-прежнему можете использовать чистый ООП, если хотите, но ваш код, несомненно, выиграет от обучения и знания, когда использовать другие парадигмы.

В качестве примера мне не нравятся классы , где все функции-члены являются статическими, а данных нет. Нет причин создавать класс утилиты больше, чем просто группировать набор бесплатных функций под общим именем. Вы должны сделать это на Java, поскольку он настаивает на чистом синтаксисе OO, который, как и commaed Tom, не совпадает с реальным OO. В С++ определение пространства имен и набора бесплатных функций предлагает аналогичное решение без необходимости блокировать создание объектов, объявляя частный конструктор, а также позволяя пользователям создавать объекты, не содержащие смысла, из пустого класса.

Для меня опыт (я когда-то был программистом только на Java) научил меня, что существуют различные инструменты, которые лучше подходят для разных задач. Я еще не нашел золотой молоток и преимущество С++ заключается в том, что вы можете выбрать другой молот для каждой отдельной задачи, даже в том же компиляционном блоке.

Коррекция: litb сообщает мне в комментарии, что WNDCLASS — это структура в API win32. Так что критика класса, не использующего списки инициализации, является простой бессмыслицей, и поэтому я удаляю ее.

Стиль программирования – залог успеха программиста

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

Стиль программирования – это набор правил и принципов написания кода для удобного чтения и восприятия кода программы.

Стиль программирования – залог успеха программиста, почему? Спросите Вы, ответ очень прост, потому что Вам будет намного легче разрабатывать или дорабатывать какой-нибудь крупный проект, или возвращаться к доработке даже самых простых проектов. Так как читать и разбираться в коде, тоже нужно уметь, например, Вы пришли в организацию работать программистам, вести какую-нибудь программу, а весь код не понятно, как и кем написан, и чтобы в нем хоть как-то разобраться, понадобится очень много времени, если вообще Вы в нем сможете разобраться. Или Вы сами написали некую программу и забыли про нее, а через полгода она Вам понадобилась, но с новыми функциями, и Вы смотрите на свой собственный код и не понимаете что там и как. Поэтому общий стиль программирования сводится к удобному восприятию кода программы, т.е. читабельности кода. Например, увидев код, Вы сможете сразу сказать — «ага, здесь такая-то переменная и она мне нужно для этого, а вот здесь вызов функции и она делает вот это».

Вообще существует много правил, которые можно отнести к общепризнанному стилю программирования, независимо от самого языка программирования, и некоторые из них я сегодня затрону.

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

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

Теперь перейдем непосредственно к самим правилам.

Правила хорошего стиля программирования

Комментирование

Из названия этого правила сразу понятно, что свой код нужно комментировать. Но, сразу скажу Вам, запомните следующее:

  • слишком много комментариев это – плохо;
  • непонятные комментарии это – плохо;
  • не объясняющие суть кода комментарии – это плохо.

Код необходимо комментировать там, где у программиста, читающего код, могут возникнуть вопросы, что для чего нужно и почему именно так. Другими словами, например программист, читающий код говорит «а почему здесь именно так написано, вот так намного будет лучше» и вдруг видит комментарий, что это сделано именно так потому что…. И, конечно же, сразу понимает. В разных языках программирования разный синтаксис, который обозначает комментарии, но он достаточно прост везде, например:

В Visual Basic:

Комментарии обозначаются апострофом, после которого и будет идти сам текст комментария.

В PHP:

В php комментарии уже обозначаются двумя слешами (//), это если Вы хотите закомментировать одну строку, а если Вы хотите закомментировать целый кусок кода, то можно использовать /* в начале кода, который Вы хотите закомментировать и */ в конце.

Отступы и пробелы

Это означает, что для удобного и понятного чтения многие конструкции в языках программирования принято выделять с помощью, например, табуляции или пробела. Например, в конструкциях if then else.

В Visual Basic:

Здесь по блокам видно, что и где, просто это пример маленький, а если у Вас, например, 1000 строк кода и все в столбик написано, я думаю, Вы не сразу поймете, где что. Поэтому следуйте этому правилу, и у Вас сразу будет более читабельный код.

Перенос кода на новую строку

Продолжая тему читабельности кода, перейдем к переносам. Если у Вас строка кода не умещается на экране монитора, то обязательно переносите код на новую строку. Например, в том же самом Visual Basic это делается с помощью нижнего подчеркивания:

Название переменных

В некоторых организациях принято каким-то специальным образом называть переменные с помощью префиксов и так далее, чтобы было понятно, что это за переменная. Вы в свою очередь можете придумать для себя свои префиксы или просто называть переменные, которые несут в себе смысловую нагрузку, не просто называть «aaa», «bbb», «ccc» или вообще просто одной буквой. По названию переменной должно быть понятно для чего она нужна, например, если в переменной будет храниться имя пользователя, ее можно назвать name и сразу все понятно. Также многие программисты используют разный регистр в название переменных, для наглядного выделения их, например, UserName, но запомните, что регистр нужно учитывать, когда эти переменные Вы будете использовать.

Написание функций

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

Теперь давайте посмотрим, как выглядит кусок кода без использования общего стиля программирования и тот же самый код, но уже с использованием всех правил и принципов. Примеры ниже написаны на языке PHP.

Плохой стиль программирования:

Хороший стиль программирования:

Ну и где более понятно написан код? Я думаю, Вы уже поняли, что стиль программирования – это залог успеха! Соблюдайте все эти правила и принципы, и Вы будете относиться к числу программистов, которые имеют хороший стиль программирования.

Стиль программирования на C++

Я старый (но не слишком старый) Java-программист, который решил изучить C++. Но я видел, что большая часть стиля программирования C++. ну, просто чертовски некрасиво!

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

Итак, наконец, есть ли какая-либо причина для меня продолжать эту резню и все, что хорошо и справедливо в программировании, или я могу просто игнорировать эти старомодные соглашения c++ и использовать мой хороший стиль программирования Java?

кстати, я изучаю C++, потому что хочу заниматься программированием игр.

на веб-сайте c++ я нашел реализацию Windows:

этот класс находится в файле заголовка и конструкторе:

находится по адресу a .СРР источник файл.

что я мог бы просто сделать это:

и сейчас конструктор находится внутри своего класса.

16 ответов

в дополнение к тому, что другие сказали здесь, есть еще более важные проблемы:

1) Большие единицы перевода приводят к более длительному времени компиляции и большему размеры файлов объектов.

2) циклических зависимостей! А этот-самый большой. И он может почти всегда исправляется путем разделения заголовков И источника:

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

но если вы разделите их на правильные .h и .cpp-файлы и использование прямых объявлений удовлетворят компилятор:

теперь код, который на самом деле должен знать спецификацию второго класса, может просто включать файл заголовка, который не должен знать спецификацию первого класса.


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

поверьте, что у C++ есть причина для этой идиомы, потому что если вы этого не сделаете, вы сделаете много головных болей для себя. Я знаю :/

при построении кода препроцессор C++ создает блок перевода. Начинается .cpp-файл, анализирует #включает захват текста из заголовков и генерирует один большой текстовый файл со всеми заголовками и .код СРР. Затем эта единица перевода компилируется в код, который будет работать на платформе, на которую вы нацелены. Каждая единица перевода заканчивается как один объектный файл.

Так, .H файлы включаются в несколько единиц перевода и a .cpp файл просто получает входит в один.

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

Так.. минимизация вещи .H файлы значительно улучшают время компиляции. Вы можете отредактировать .cpp-файл для изменения функции, и только одна единица перевода должна быть перестроенный.

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

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

.h и .cpp-файлы часто отделяют объявления от определений, и здесь есть какой-то порядок и концептуальная инкапсуляция. заголовки имеют «что», файлы реализации имеют «как».

Я нахожу, что все в одном файле на java дезорганизовано. Итак, каждому свое.

C++ соглашения и идиомы все хорошо продуманные рассуждения, как и любой другой язык делает для идиом, которые принимает его сообщество.

Я думаю, что лучше java (как глагол), когда вы пишете Java, и на C++, когда вы C++! Вы поймете, почему с опытом языка. То же самое, что переход на любой новый язык.

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

в любом случае я просто хотел опубликовать другую часть вопроса: Является ли C++ ООП masacre?

C++ — это мультипарадигменный язык. Это позволяет процедурные, объект ориентированный и generic Программирование. И это добродетель, а не дефект. Вы все еще можете использовать pure OOP, если хотите, но ваш код, безусловно, выиграет от обучения и знания, когда использовать другие парадигмы.

в качестве примера, мне не нравится служебные классы, где все функции-члены статичны, и нет данных. Нет причин создавать служебный класс больше, чем просто группировать набор свободных функций под общим именем. Ты должен это сделать. в Java, как он настаивает на чисто синтаксис ОО, который, как commeted по Тома, это не то же самое, что реальный ОО. В C++ определение пространства имен и набора свободных функций предлагает аналогичное решение без необходимости блокировки создания объектов путем объявления частного конструктора или разрешения пользователям создавать экземпляры объектов без смысла из пустого класса.

для меня опыт (я когда-то был программистом только на Java) научил меня, что есть разные инструменты, которые лучше подходят разные проблемы. Мне еще предстоит найти золотой молоток и преимущество C++ заключается в том, что вы можете выбрать другой молоток для каждой другой задачи, даже в том же блоке компиляции.

коррекция: litb сообщает мне в комментарии, что WNDCLASS является структурой в win32 API. Таким образом, критика класса, не использующего списки инициализации, является простой бессмыслицей, и поэтому я удаляю ее.

найти стиль, который работает для вас, как и все остальные. Никто не заставляет вас использовать один из «уродливых» стилей, если только ваш работодатель не применяет документ с рекомендациями. ;-)

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

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

тем не менее, вы будете очень рады узнать, что многие разработчики C++ согласны с вами на этот счет. Итак, продолжайте и поместите всю свою реализацию в *.H-файлы. Есть школа стиля, которая согласна с вы.

вызов функций из ниоткуда, вместо использования методов внутри классов; все это только кажется. неправильно!

Итак, наконец, есть ли какая-либо причина для меня продолжать эту резню в ООП

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

C++ — это многопарадигмальный язык, а не только язык OO. Шаблоны-это форма общего программирования, которая может быть применена к процедурным, ООП и метапрограммным парадигмам c++. Со следующим стандартом C++ вы также увидите некоторые функциональные парадигмы.

весь этот материал о размещении определения класса в файле заголовка и методов в другом исходном файле;

Это имеет свои истоки с языка программирования Си, еще в 70-х. C++ был разработан для обратной совместимости с C.

язык программирования D-это попытка исправить многие недостатки C++ (и, как я сказал ранее, их много), но поддерживать хорошие функции C++, включая все различные парадигмы, поддерживаемые c++: процедурные, ООП, метапрограммирование и функциональные.

Если вы хотите вырваться из ООП мышления, попробуйте D! затем, когда вы получите ощущение того, как чтобы смешать и сопоставить различные парадигмы, вы можете (если хотите) вернуться к C++ и научиться справляться с его синтаксисом и другими недугами.

P. S. Я использовать C++ ежедневно и я болею за него-это мой любимый язык программирования. Но, как знает любой опытный программист на C++, У C++ есть свои недостатки. Но как только вы освоите различные парадигмы и узнаете, как работать с болезнями C++, у вас будет невероятно мощный инструмент (для этого все язык) на вашем удаление.

удачи в ваших приключениях!

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

Так что это вопрос удобства.

Я нахожу разделение декларации-реализации действительно полезным. Когда у меня есть классы монстров с

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

  • что может сделать класс (его интерфейс, заданный его объявлением)
  • как класс это делает (его реализация)

невозможность сделать это на C# или Java довольно расстраивает, особенно когда у меня нет Intellisense под рукой.

Если вы пишете DLL C++ и вы помещаете любой код в заголовки, вы в конечном итоге с трудом отлаживаете беспорядок. DLL будет содержать код из .cpp файлы, но пользователи DLL будет иметь некоторые из кода, встроенных в себя.

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

отдельное объявление и определение является наименьшим из различий между C++ и Java. Основным отличием современного C++ является важность «семантики значений».

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

Swap в частности является очень важно помнить, потому что он не поддерживается непосредственно языком, но он действительно должен быть в любом ценностном типе.

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

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

как стереотипный программист, который программирует в одной конкретной парадигме, вы решили, что некоторые вещи, которые не знакомы с вами, просто уродливы.

C++ — это другой язык, это многопарадигм, и у него есть много переносов из C. Как заявили другие, это просто так.

заголовочные файлы для компилятора, чтобы проверить базовый синтаксис, не зная реализации. Если вы тяжелый Java-программист, вы должны быть хорошо знакомы с вся программа к концепции интерфейса. Подумайте о файле заголовка, где находится интерфейс.

Если вы мне не верите, посмотрите на некоторые моды для игр. Вполне возможно найти файл заголовка CryEngine2 где-нибудь, потому что модам Crysis нужно будет поговорить с ним, но ему не нужно понимать, как он работает. Файлы заголовков будут определять, как в конечном итоге вызывающая функция должна настроить стек для вызова другой функции.

Не condensending, но я думаю, что вам действительно нужно программировать в чем-то совершенно другом. Исследуйте другие парадигмы, найдите некоторые языки и начните писать игрушечные приложения. Сначала все будет выглядеть уродливо, но в конце концов вы увидите преимущества.

то же самое я сказал и о функциональном программировании. Теперь я жалуюсь на то, как PHP вырезал функциональное программирование с массивами.

Это совсем не некрасиво. Я бы сказал, что да. МММ разные. Вывод вашего старого стиля на новую платформу, это уродливо (у меня было много дискуссий об этом с мистером скитом).

помните Java был определен через несколько лет после C++, поэтому разумно, что они исправили некоторые конструкции (таким же образом C# учится на Java).

Итак, я бы предложил сохранить этот стиль C++.

подумайте об этом. Заголовочные файлы похожи на объявления интерфейса и cpp файлы являются реализацией.

Я думаю, что c++ не имеет ключевого слова «interface», и таким образом вы отделяете интерфейс класса от его реализации. Аналогично этому в Java:

Я думаю, что многие программисты режут зубы продуктами MicroSoft (и их примером кода) и/или программированием для Windows API и ранних соглашений о кодировании Microsoft, которые использовались в фрагменте кода в вашем вопросе (т. е. Венгерская нотация, капитализация определенных типов и т. д..). Я ненавижу смотреть на исходный код от MicroSoft, который выглядит так, как будто он был запущен через поперечный измельчитель бумаги и склеен вместе. Но уродливое соглашение о кодировании не является функцией или отражение языка C++, который я нахожу не более красивым или уродливым, чем большинство других языков.

на самом деле, я нахожу синтаксис C++ с минимальными ключевыми словами, фигурными скобками и богатым набором символьных операторов, которые не отвлекают и не вытесняют важные вещи: мои переменные, мой тип defs, мои методы; что, конечно же, позволяет мне сделать самый красивый код из всех : -)

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

Илон Маск рекомендует:  Что такое код msql_fieldname

стиль Вы программу до вас. Просто убедитесь, что вы понимаете, что «уродливый» стиль используют другие.

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

кроме того, C++ и Java это два разных языка, и использовать их нужно по-разному. Есть причины для заголовочных файлов (подумайте об этом как об объявлении интерфейса), например.

Стиль программирования

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

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

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

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

Хорошее правило – включать комментарии в процессе написания программы. Однако хорошие комментарии написать не просто, так как они должны быть также хорошо продуманы и проработаны, как и кодировка программы. Комментарии нужны как на стадиях проектирования и отладки программы, так и позже. Существует три типа комментариев: вводные, оглавления и пояснительные.

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

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

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

Комментарии должны быть правильными с самого начала и изменяться в соответствии с изменениями программы. Очевидно, что неправильные комментарии – это хуже, чем их отсутствие, поскольку они вводят в заблуждение программиста.

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

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

Выбор имен переменныхвыполняется так, чтобы наилучшим образом отобразить те величины, которые они представляют. Если ограничения на размер имени отсутствуют, то целесообразно использовать настолько длинные имена, насколько это нужно. Правильный выбор имен переменных – это залог удобочитаемости программ, так как он требует незначительных умственных усилий от программиста и столь же небольшого расхода машинного времени. Рекомендуется в качестве имен переменных использовать термины предметной области. В следующем фрагменте при определении площади прямоугольника используются несуразные идентификаторы, не имеющие ничего общего с предметной областью:

Не нашли то, что искали? Воспользуйтесь поиском:

Лучшие изречения: Только сон приблежает студента к концу лекции. А чужой храп его отдаляет. 8807 — | 7523 — или читать все.

Лучшие приемы программирования на C

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

Стили и нормы программирования

  • Необходимо использовать стиль программирования, который делает код читабельным и понятным. Несмотря на то, что некоторые разработчики имеют свой собственный стиль программирования или используют стиль программирования, принятый в их компании, хорошим тоном считается следовать стилю программирования Кернигана и Ритчи (Kernighan и Ritchie), используемому подавляющим большинством программистов на C. Однако, чересчур увлекшись, легко прийти к чему-нибудь такому:

Dishonorable mention, Чемпионат по самому непонятному коду на C (Obfuscated C Code Contest), 1984 г. Автор кода неизвестен.

  • Всегда в коде можно увидеть главную функцию, называемую main() . По стандарту ANSI эта функция определяется как int main(void) (если не нужно обрабатывать аргументы командной строки) или как int main( int argc, char **argv ) . Не-ANSI компиляторы могут пропускать объявление void или составлять список имен переменных и следовать их объявлениям.
  • Отступы

    Необходимо использовать вертикальные и горизонтальные отступы. Количество и расположение отступов и пробелов должно отражать структуру кода.

    Длинная строка условного оператора должна быть разбита на несколько строк. Например:

    будет лучше выглядеть как:

    Точно так же сложные циклы for должные быть разделены на несколько строк:

    Другие сложные выражения, такие как использующие оператор ?: тоже лучше разделить на несколько строк:

    Комментарии

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

    Такой стиль комментирования является избыточным:

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

    Правила наименования

    Имена с ведущими или завершающими знаками подчеркивания предназначены только для системы целей и не должны использоваться для каких-либо пользовательских имен переменных. Правила определяют следующие требования:

    1. Константы #define должны записываться ЗАГЛАВНЫМИ символами.
    2. Константы enum должны начинаться с заглавного символа или записываться полностью ЗАГЛАВНЫМИ символами.
    3. Слова function , typedef и имена переменных, так же как и struct , union и enum должны быть в нижнем регистре.

    Для понятности необходимо избегать имен, различающихся только регистром, например, foo и Foo . Точно так же лучше избегать одновременного использования имен foobar и foo_bar . Необходимо избегать любых имен, которые похожи друг на друга. На многих клавиатурах и во многих шрифтах l , 1 и I выглядят очень похоже. Переменная с именем l , в частности, плоха потому, что похожа на константу 1 .

    Имена переменных

    При выборе имени переменной не так важна длина имени, как понятность. Длинные имена могут назначаться глобальным переменным, которые редко используются, но индексы массивов, появляющиеся в каждой строке цикла, не должны быть значительно сложнее, чем i . Использование index или elementnumber не только усложняет набор, но и может сделать менее понятными детали вычислений. С длинными именами иногда сложнее понять, что именно происходит в коде. Легко сравнить:

    Имена функций

    Имена должны отражать то, что делают функции и что они возвращают. Функции используются в выражениях, часто в условных операторах if , поэтому они должны читаться соответственно. Например:

    непонятно, так как не говорит о том, возвращает ли функция TRUE при прохождении проверки или наоборот; использование вместо этого:

    делает все понятным.

    Объявление переменных

    Все объявления внешних переменных должны предваряться ключевым словом extern .

    Обозначение указателя, * , должно сопровождать имя переменной, а не ее тип:

    Второй пример объявления переменных не является неправильным, но могут возникнуть сомнения из-за того, что t и u не объявлены как указатели.

    Заголовочные файлы

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

    Следует избегать имен заголовочных файлов, совпадающих с именами стандартных библиотек. Строка #include «math.h» включает заголовочный файл стандартной библиотеки math, если файл с таким именем не будет найден в текущем каталоге. Если такое поведение — именно то, что нужно, то лучше оставить соответствующий комментарий.

    Наконец, использование абсолютных путей для заголовочных файлов — не самая лучшая идея. Опция компилятора C include-path ( -I на большинстве систем) — это предпочтительный метод обработки внешних библиотек и заголовочных файлов; он позволяет изменить структуру каталогов без необходимости изменения исходных кодов.

    scanf

    Не следует использовать scanf в серьезных приложениях. Обработка ошибок в этой функции неадекватна. Рассмотрим такой пример:

    Запустим тест:

    Enter an integer and a float: 182 52.38

    I read 182 and 52.380001

    Теперь другой тест:

    Enter an integer and a float: 6713247896 4.4

    I read -1876686696 and 4.400000

    ++ и —

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

    Нельзя позволять себе видеть то, чего на самом деле нет

    Рассмотрим следующий пример:

    На первый взгляд такой оператор while выглядит корректным кодом на C. Однако использование оператора присваивания вместо оператора сравнения приводит к появлению синтаксически некорректного кода. Так как приоритет оператора = является наименьшим, то данное выражение будет интерпретировано следующим образом (скобки добавлены для наглядности):

    Левая часть оператора присваивания:

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

    Явно выраженные намерения

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

    Иногда можно разрабатывать код, который предупреждает возможные ошибки. Например, можно ставить константы в левую часть оператора сравнения, т. е. вместо:

    можно написать так:

    В этом случае компилятор выдаст предупреждение:

    Такой стиль программирования позволяет компилятору находить потенциальные проблемы; пример кода выше неправилен, так как пытается присвоить значение для \t .

    Ошибки из-за специфики реализации языка программирования

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

    Выражение будет интерпретироваться по правилу максимального оператора. Если комментарии могут быть вложенными, то интерпретация будет следующей:

    Паре символов /* соответствует пара символов */ , поэтому выражение равно 1 . Если комментарии не вкладываются, то на некоторых системах /* в комментариях будет проигнорировано. На некоторых компиляторах будет также выдано предупреждение для вложенной последовательности /* . В любом случае выражение будет интерпретировано следующим образом:

    Сбрасывание буфера на диск

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

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

    Это выражение должно быть выполнено перед записью в stdout . В идеале этот код должен помещаться в начале функции main .

    getchar() — макрос или функция?

    Следующая программа выводит свои входные данные:

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

    Программу можно переписать следующим образом:

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

    Так как вызов функции обычно занимает довольно много времени, getchar часто реализуют в виде макроса. Этот макрос определен в stdio.h , поэтому когда #include удаляется, компилятор не знает, что такое getchar . На некоторых системах полагается, что getchar — это функция, возвращающая int .

    В действительности многие реализации компиляторов языка C имеют свои стандартные функции getchar , частично в целях защиты от таких оплошностей. Таким образом, ситуация, когда включение #include пропущено, влечет использование компилятором собственной версии функции getchar . Дополнительные вызовы этой функции делают программу медленнее. То же самое верно и для putchar .


    Пустой указатель

    Пустой указатель не указывает ни на какой объект. Неправильно использовать пустой указатель для любых целей, кроме присваивания и сравнения.

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

    Переход по пустому указателю может вызвать странные эффекты.

    Что означает a+++++b ?

    Единственный правильный способ интерпретации этого выражения такой:

    Однако правило длинного оператора предписывает разбить выражение следующим образом:

    Это синтаксически неверно, такой код эквивалентен строке:

    Но результат a++ не является lvalue и, следовательно, не может быть операндом для ++ . Таким образом, правила для разрешения логических двусмысленностей не могут в этом примере привести к синтаксически верной конструкции. На практике, конечно, лучший способ избежать таких конструкций — это полная уверенность в том, что код интерпретируется однозначно. Конечно, добавление пробелов помогает компилятору понять цель оператора, но предпочтительнее (в перспективе сопровождения кода) разбить конструкцию на две строки:

    Осторожное обращение с функциями

    Функции обеспечивают наиболее общее структурирование кода на C. Они должны использоваться для решения проблемы «сверху вниз» — для разбиения задачи на ряд более мелких подзадач до тех пор, пока подзадача не будет легко решаться. Это помогает реализовать модульность и упростить документирование программы. Кроме того, программы, составленные из большого числа маленьких функций, значительно легче для отладки.

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

    Если заголовочные файлы не объявляют тип возвращаемого значения библиотечных функций, необходимо сделать это самостоятельно. Окружив объявления конструкцией #ifdef/# , можно упростить портирование своего кода на другую платформу.

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

    «Висячий» else

    Нужно опасаться проблемы «висячего» else , если нет полной уверенности в правильности конструкции:

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

    Границы массива

    Необходимо проверять границы всех массивов, включая строки, так как сегодняшнее fubar ‘ может стать завтра floccinaucinihilipilification . В надежном программном обеспечении не используется gets() .

    Тот факт, что в C элементы нумеруются с нуля, делает более вероятными ошибки подсчета. Однако требуются некоторые усилия на изучение того, как уберечься от них.

    Пустой оператор

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

    Проверка выражений на истинность

    Не нужно оставлять по умолчанию проверку на ненулевое значение, т. е.:

    даже если FAIL имеет значение 0 , которое C рассматривает как ложь (конечно, здесь нужно соблюдать баланс с такими конструкциями как, например, показанная в разделе «Имена функций»). Явное значение поможет избежать ошибок, если вдруг кто-то решит, что при неудачном завершении должно возвращаться значение -1 вместо 0 .

    Частые затруднения вызывает функция проверки равенства строк strcmp , так как нет единого значения, означающего, что строки неравны. Предпочтительный вариант — определение в этом случае макроса STREQ :

    Использовать этот макрос можно в операторах следующего вида:

    Таким образом, функция получает желаемое поведение (не требуется переписывать или переопределять стандартные библиотечные функции типа strcmp() ).

    Не следует сравнивать логические выражения с 1 ( TRUE , YES и другими); вместо этого нужно проверять на равенство 0 ( FALSE , NO и так далее). Большинство функций гарантируют возвращение 0 в случае неудачного завершения, и возвращение лишь ненулевого значения в случае удачного завершения. Таким образом,

    лучше переписать так:

    Вложенные операторы

    А сейчас — время для разговора о вложенном операторе присваивания. В некоторых конструкциях нет лучшего способа присваивания, хотя он и влечет увеличение кода в операторе и ухудшение читабельности:

    Использование вложенного оператора присваивания для улучшения производительности возможно. Однако необходимо искать компромисс между увеличением скорости и усложнением сопровождения кода, которое возникает при использовании вложенных присваиваний в неподходящем месте. Например:

    не должно заменяться на:

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

    Оператор goto

    goto необходимо использовать крайне умеренно. Один из случаев, когда этот оператор полезен — это необходимость прервать многоуровневый оператор switch , for или while , хотя такая необходимость может свидетельствовать о том, что внутреннюю конструкцию лучше вынести в отдельный цикл.

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

    «Проваливание» через switch

    Когда блок кода имеет несколько меток, каждую из них нужно размещать на отдельной строке. Этот элемент стиля программирования согласуется с правилом установки вертикальных отступов и делает перекомпоновку (если она понадобится) сравнений case простой задачей. Использование предоставляемой языком С возможности «проваливания» в операторе switch должно обязательно комментироваться в целях упрощения последующего сопровождения кода. Каждый, кто испытал на себе неприятности от ошибок при использовании этой возможности, знает, насколько это важно!

    Хотя последний оператор break и не является необходимым, его использование предотвращает ошибку в случае, когда потребуется добавить еще один case . В случае, если используется вариант default , он должен быть последним и не требует оператора break .

    Константы

    Символические константы делают код более простым для чтения. Числовых констант, как правило, следует избегать; лучше использовать #define для задания понятного имени. Сосредоточение всех определений в одном месте (лучше всего — в заголовочном файле) также упрощает администрирование изменений в больших проектах, так как позволяет вносить изменения только в директивах #define . Можно рассматривать использование типа данных «перечисление» в качестве улучшенного способа объявления переменных, которые могут принимать только дискретные значения. Использование перечислений также позволяет компилятору выводить предупреждения при ошибках использования типа перечисления. И, наконец, явно перечисленные цифровые константы требуют меньше объяснений о своем происхождении при комментировании.

    Константы необходимо объявлять соответственно их использованию, т. е. необходимо указывать 540.0 для числа с плавающей точкой вместо 540 с прямым объявлением типа float . Есть случаи, в которых константы 0 и 1 могут возникать явно вместо своих объявлений строковыми константами. Например, если цикл for индексирует массив, то код:

    — нет. Во втором примере front_gate — это указатель; когда значение является указателем, то оно должно сравниваться с NULL , а не с 0 . Даже простые значения типа 1 или 0 часто лучше воспринимаются в качестве TRUE и FALSE (или YES и NO ).

    Не нужно использовать переменные с плавающей точкой там, где нужны дискретные значения. Это связано с не совсем корректным представлением чисел с плавающей точкой (можно вспомнить второй пример из раздела scanf выше). Сравнивать числа с плавающей точкой лучше используя или >= ; явное сравнение ( == или != ) может не обнаружить «достаточного» равенства.

    Символьные константы должны быть объявлены как символы, а не как числа. Нетекстовые символы являются более трудными для портирования. Если нетекстовые символы необходимы, в частности, при использовании в строках, они должны быть записаны в виде управляющих последовательностей из трех восьмеричных цифр, а не одной (например, ‘\007’ ). Даже в этом случае такое использование символов является платформозависимым и должно восприниматься таковым.

    Условная компиляция

    Условная компиляция полезна в случаях, когда требуется реализовать машинозависимый код, при отладке и для установок значений во время компиляции. Различные варианты управления могут легко привести к непредвиденным ситуациям. При использовании #ifdef для машинозависимого кода необходимо быть уверенным, что если тип машины не определен, то возвращается сообщение об ошибке, а не используется конфигурация по умолчанию. Директива #error предназначена как раз для этих целей. При использовании #ifdef для оптимизации лучше применять по умолчанию неоптимизированный код, чем некомпилируемый или некорректный. Необходимо тестировать неоптимизированный код.

    Разное

    • Утилиты для компиляции, такие как make , значительно упрощают задачу переноса приложения из одного окружения в другое. В процессе разработки make перекомпилирует только те модули, которые были изменены со времени последней компиляции.

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

    Также необходимо изучить документацию компилятора и выяснить, какие опции сделают его более «разборчивым». Работа компилятора заключается в том, чтобы быть точным, поэтому необходимо дать ему возможность выдать отчет о потенциальных проблемах, используя соответствующие опции компиляции.

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

    Никогда не нужно делать предположений о размере заданного типа данных, особенно указателей.

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

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

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

    Общеизвестно, что сопровождение приложения отнимает значительную часть времени программиста. Частично это происходит из-за использования платформозависимых и нестандартных особенностей, но в большей степени — из-за плохого стиля программирования. В этой статье дается несколько советов, которые помогают сберечь время, требуемое для сопровождения кода. Следование этим советам сделает сопровождение приложений командой разработчиков более простым.

    C++ — Functional-Style Programming in C++

    By David Cravey | August 2012


    C++ is a multiparadigm, systems-level language that provides high-level abstractions with very low (often zero) runtime cost. The paradigms commonly associated with C++ include procedural, object-oriented and generic programming. Because C++ provides excellent tools for high-level programming, even functional-style programming is quite reasonable.

    By functional-style programming, I don’t mean the programming is strictly functional, just that it’s easy to use many of the functional building blocks in C++. This article will focus on one of the most important functional programming constructs: working with values as opposed to identities. I’ll discuss the strong support C++ has always had for working with values, then show how the new C++ 11 standard expands this support with lambdas. Finally, I’ll introduce a method of working with immutable data structures that maintains the speed C++ is known for while providing the protection that functional languages have long enjoyed.

    Values vs. Identities

    Let me first explain what I mean by working with values rather than identities. Simple values such as 1, 2 and 3 are easy to identify. I could also say that 1, 2 and 3 are constant integer values. This would be redundant, however, because all values are actually constants and the values themselves never change (that is, 1 is always 1 and 1 will never be 2). On the other hand, the value associated with an identity may change (x might equal 1 now, but it could equal 2 later).

    Unfortunately, it’s easy to confuse values and value types. Value types are passed around by value rather than by reference. Though I want to focus here on the values and not the mechanism involved in using or copying them, it’s useful to see how value types go part way in preserving the concept of values versus identities.

    The code in Figure 1 demonstrates a simple use of a value type.

    Figure 1 Using a Value Type

    With only a small change, the variable y can become a reference type—which drastically changes the relationship between x and y, as shown in Figure 2.

    Figure 2 Using a Reference Type

    As Figure 3 shows, C++ also provides the const modifier, which prevents the programmer from making changes to a variable and thus further preserves the concept of a value. (As with most things in C++, however, there’s at least one way to defeat that protection. For more information, look up const_cast, which is intended for working with older code that isn’t “const correct.”)

    Figure 3 The const Modifier

    Note in Figure 3 that though y is passed by reference, the value of y is protected at compile time by a const modifier. This gives C++ programmers an efficient method of passing large objects while working with their values as opposed to their identities.

    With the const modifier, C++ has immutable data types that resemble those found in most functional programming languages. However, dealing with these immutable data types is difficult. Furthermore, making deep (full) copies of large objects for every small change isn’t efficient. Nonetheless, it should be clear that standard C++ has always had a concept of working with values (even if it’s not a very pure concept).

    Note that the support for value types extends to user-defined types through copy constructors and assignment operators. C++ copy constructors and assignment operators allow user-defined types to make a deep copy of the object. Keep in mind that while C++ copy constructors can be implemented to make a shallow copy, you’ll have to make sure the value semantics are preserved.

    C++ 11 Support for Functional-Style Programming

    C++ 11 brings a number of new tools for functional-style programming. Perhaps most important, C++ now has support for lambdas (also known as closures or anonymous functions). Lambdas allow you to compose your code in ways that wouldn’t have been practical before. This functionality was previously available through functors, which are powerful but less practical to use. (Actually, C++ lambdas write anonymous functors behind the scenes.) Figure 4 shows how lambdas have improved our code with a simple example that uses the C++ standard library (STL).

    Figure 4 Using Lambdas

    In this case, the for_each function applies a lambda to each element of a vector. It’s important to note that C++ lambdas have been designed to be used inline when possible; thus lambdas can run as fast as handcrafted code.

    While C++ is just one of the many imperative languages that now have lambdas, what makes C++ lambdas special is that (similar to functional programming languages) they can preserve the concept of working with values as opposed to identities. While functional programming languages accomplish this by making variables immutable, C++ does it by providing control over the capture. Consider the code in Figure 5.

    Figure 5 Capturing by Reference

    In this code, everything is captured by reference, which is the standard behavior for lambdas in other languages. Yet capturing by reference complicates things unless the variables being captured are immutable. If you’re new to working with lambdas, you probably expect the following output from the code:

    However, that’s not the output you get—and the program might even crash. This is because the variable ctr is captured by reference, so all of the lambdas use the final value of ctr (that is, 3, the value that made the for loop come to an end) and then access the array beyond its bounds.

    It’s also worth noting that to keep the ctr variable alive to be used by the lambda outside of the for loop, the ctr variable’s declaration has to be lifted out of the for loop. While some languages eliminate the need to lift value type variables to an appropriate scope, that doesn’t really solve the problem, which is that the lambda needs to use the value of ctr as opposed to the identity of the variable ctr. (There are workarounds for other languages that involve making an explicit copy to a temporary variable. However, this makes it a bit unclear as to what’s going on, and it’s error-prone because the original variable is also captured and thus is still available for use.)

    As Figure 6 shows, C++ provides a simple syntax to allow easy control of the lambda’s capture, which preserves the concept of working with values.

    Figure 6 C++ Syntax for Controlling Lambda Capture

    Don’t capture anything

    (exactly what I wanted in the first lambda example)

    Capture everything by reference

    (traditional lambda behavior, though not consistent with functional programming’s emphasis on values)

    Capture everything by value

    (while this preserves the concept of values, it limits the usefulness of the lambdas; also, it can be expensive to copy large objects)

    [&ctr] Capture only ctr, and capture ctr by reference [ctr] Capture only ctr, and capture ctr by value [&,ctr] Capture ctr by value and everything else by reference [=,&v] Capture v by reference and everything else by value [&, ctr1, ctr2] Capture ctr1 and ctr2 by value and everything else by reference

    It’s clear from Figure 6 that the programmer has complete control over how the lambda captures variables and values. However, while this preserves the concept of working with values, it does nothing to make working with complex data structures as values efficient.

    Immutable Data Types

    What’s missing are the efficient immutable data structures that some functional programming languages have. These languages facilitate immutable data structures that are efficient even when very large because they share common data. Creating data structures in C++ that share data is trivial—you just dynamically allocate data and each data structure has pointers to the data. Unfortunately, it’s more difficult to manage the lifetime of shared variables (for this reason, garbage collectors have become popular). Luckily, C++ 11 provides an elegant solution for working with shared variables through the std::shared_ptr template class, as shown in Figure 7.

    Figure 7 Sharing Variables

    The code in Figure 7 illustrates a simple use of std::shared_ptr and its helper function std::make_shared. Using std::shared_ptr makes it easy to share data among data structures without fear of leaking memory (as long as circular references are avoided). Note that std::shared_ptr provides the basic thread-safety guarantees, and runs fast because it uses a lock-free design. However, keep in mind that the basic thread-safety guarantee that std::shared_ptr provides doesn’t automatically extend to the object to which it’s pointing. Still, std::shared_ptr guarantees it will not reduce the thread-safety guarantee of the object it points to. Immutable objects inherently provide a strong thread-safety guarantee because once they’re created they never change. (Actually, they never change in an observable manner, which includes, among other things, an appropriate thread-safety guarantee.) Therefore, when you use a std::shared_ptr with an immutable object, the combination maintains the immutable object’s strong thread-safety guarantee.

    I can now easily create a simple immutable class that potentially shares data, as shown in Figure 8.

    Figure 8 An Immutable Class for Sharing Data

    The code in Figure 8 is a bit long, but most of it is boilerplate code for the constructors and assignment operators. The last two functions are the key to making the object immutable. Note that the SetS and SetD methods return a new object, which leaves the original object unchanged. (While including the SetS and SetD methods as members is convenient, it’s a bit of a lie, because they don’t actually change the original object. For a cleaner solution, see the ImmutableVector in Figures 9 and 10.) Figure 11 shows the Immutable class in action.

    Figure 9 Using the Smart ImmutableVector Template Class

    Figure 10 Methods for Operating on the ImmutableVector

    Figure 11 The Immutable Class in Action

    Note that object b shares the same string as object a (both strings are at the same address). Adding additional fields with associated getters and setters is trivial. Though this code is good, it’s a little more difficult to scale to containers when you’re being efficient. For example, a naïve ImmutableVector might maintain a list of shared pointers representing each element of the array. When the naïve Immutable-Vector is changed, the entire array of shared pointers would need to be duplicated, incurring additional cost as each shared_ptr element of the array would need its reference count to be incremented.

    There is a technique, though, that allows the data structure to share most of its data and minimize the duplication. This technique uses a tree of some form to require duplication of only the nodes that are directly affected by a change. Figure 12shows a comparison of a naïve ImmutableVector and a smart ImmutableVector.

    Figure 12 Comparing Naïve and Smart ImmutableVectors

    This tree technique scales nicely: as the number of elements grows, the percent of the tree that needs to be duplicated is minimized. Moreover, by adjusting the branching factor (so each node has more than two children), you can achieve a balance in memory overhead and node reuse.

    I developed a smart ImmutableVector template class that can be downloaded from archive.msdn.microsoft.com/mag201208CPP. Figure 9 shows how you can use my ImmutableVector class. (As previously noted, to make the immutable nature of the ImmutableVector clearer to the users, ImmutableVector uses static member functions for all actions that generate new versions.)

    For read-only actions, the vector can be used much like a regular vector. (Note that for this example I haven’t implemented iterators, but doing so should be fairly trivial.) For write actions, the AppendValue and TruncateValue static methods return a new ImmutableVector, thus preserving the original object. Unfortu-nately, this isn’t reasonable for the array subscript operator, so I made the array subscript operator read-only (that is, it returns a const reference) and provided a SubstituteValueAtIndex static method. It would be nice, however, to be able to make a large number of modifications using the array subscript operator in a single block of code. To facilitate this, ImmutableVector provides a GenerateFrom static method, which takes a lambda (or any other functor). The lambda in turn takes a reference to MutableVector as a parameter, which allows the lambda to work on a temporary MutableVector that can be changed freely like a normal std::vector. The example in Figure 10 shows the various methods for operating on the ImmutableVector.

    The beauty of the GenerateFrom static method is that the code within it can be written in an imperative way, while resulting in an immutable object that can be safely shared. Note that the Generate-From static method prevents unauthorized access to the underlying ImmutableVector by disabling the MutableVector it passed into the lambda as soon as the lambda exited. Please note as well that while ImmutableVector provides a strong thread-safety guarantee, its helper class MutableVector does not (and is intended to be only used locally within the lambda, not passed around to other threads). My implementation also optimizes for multiple changes within the Change method such that there’s minimal restructuring occurring on the temporary tree, which gives a nice performance boost.

    Wrapping Up

    This article gives you just a taste of how you can use functional-style programming in your C++ code. Moreover, C++ 11 features such as lambdas bring a touch of functional-style programming regardless of the paradigm used.

    David Cravey is a Visual C++ MVP who enjoys programming in C++ maybe a bit too much. You’ll find him presenting at local C++ user groups and universities. During the day he enjoys working at NCR, through TEKsystems in Fort Worth, Texas.

    Thanks to the following technical experts for reviewing this article: Giovanni Dicanio, Stephan T. Lavavej, Angel Hernández Matos, Alf P. Steinbach and David Wilkinson

    Стиль программирования C ++

    Не уверен, что мой вопрос соответствует всем правилам для вопроса StackOverflow, но я думаю, что он будет полезен для будущих пользователей.

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

    Итак, вот что мы выбрали:

    Начните с заглавной буквы и используйте заглавную букву для каждого нового слова (без подчеркивания, без пробелов).

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

    В заголовочном файле есть только прототипы функций / методов, реализация в cpp файл, избегайте использования using namespace для реализации файла.

    Начните с заглавной буквы и используйте заглавную букву для каждого нового слова (без подчеркивания, без пробелов). нет С как префикс класса или S как префикс структуры. Мы решили — это просто больше печатать.

    В некоторых случаях трудно понять, нужен ли нам класс или структура. Если структура просто хранит некоторые сгруппированные данные — это struct , Если структура хранит данные и имеет методы — это class , Исключительными методами являются операторы конструктора, деструктора и сравнения.

    Начните с заглавной буквы и используйте заглавную букву для каждого нового слова (без подчеркивания).

    Используйте наши собственные предопределенные типы, у нас есть:

    Начните со строчной буквы и используйте заглавную букву для каждого нового слова (без подчеркивания).

    Начинается с префикса m_ тогда слово начинается со строчной буквы и имеет заглавную букву для каждого нового слова (без подчеркивания).

    Используйте все заглавные буквы с подчеркиванием.

    Если значение будет проверено с помощью #ifdef или некоторые другие, то это должно быть определение препроцессора. В противном случае это const ,

    Начните с заглавной буквы и используйте заглавную букву для каждого нового слова (без подчеркивания).

    Скобки всегда должны идти в новую строку, кроме инициализации.

    else принадлежит новой строке, см. предыдущий пример.

    Даже одна строка кода после if или же else должны быть приложены

    Нет места вокруг стрелки и точки.

    1. использование #pragma once вместо определения охранников. ( #pragma once не является стандартным, поэтому в некоторых компиляторах определение охранников обязательно)
    2. Один заголовок только для одного класса.
    3. Заголовочные файлы только для определений. Инструкции по исполнению должны быть в cpp файл, даже если это геттер или сеттер. Это потому, что изменения в заголовке приводят к длительной компиляции.

    Используйте ссылку вместо указателя, если это возможно. Если возможно передать параметр в качестве ссылки (для объектов), предпочитайте передавать как const ссылка, если значение не будет изменено.

    Если функция может потерпеть неудачу, она должна вернуть bool значение true для успеха и false за неудачу. Для занятия методом GetLastError() является обязательным. Для функции, которая может дать сбой, код ошибки должен быть возвращен через дополнительный параметр, например, bool Function(int param, int* errorCode = NULL) Также мы решили не использовать исключения в нашем коде.

    В заголовочном файле первые открытые методы (конструкторы и деструкторы вверху), защищенные методы, защищенные переменные, приватные методы, приватные переменные. Нет открытых переменных, используйте геттеры и сеттеры.

    1. Выравнивание используйте 4 пробела или табуляции.
    2. Обтекание длинной строки, длиной не более 120 символов.

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


    Написать & а также * только после типа переменной.

    Это все, что мы решили использовать, вопрос в том, не упустили ли мы что-нибудь? Кроме того, есть что-нибудь глобально неприемлемо?

    Не стесняйтесь комментировать и спрашивать, если что-то не понятно, почему мы выбрали некоторые.

    Надеюсь, что это будет полезно для последних читателей.

    Хороший стиль программирования

    Конкретный вопрос заключается в следующем:
    стоит ли использовать this когда в каком-либо методе класса используешь переменную-поле класса?

    Лично я долгое время пользовался данной практикой и считал её довольно удобной, однако мой преподаватель сказал, что этого делать не нужно. В литературе я никаких сведений по данному вопросу не нашёл

    15.07.2011, 16:33

    Хороший тон программирования. Не догма — но полезно.
    У каждого опытного разработчика , наверняка, с опытом формируются негласные правила .. которые.

    Как динамически присвоить стиль кнопке, если стиль описан в файле xaml?
    Здравствуйте, столкнулся с проблемой: нужно в коде присвоить стиль кнопке. Этот стиль находится в.

    Меняем стиль элементов управления на свой стиль
    Меняем стиль элементов управления на свой стиль. Может кому пригодится. watch?v=0EI9_KhHeN0&

    Хороший стиль программирования
    Здравствуйте, меня интересует как правильно создаются архитектуры игр. К примеру: у меня есть.

    Хороший стиль программирования, просто интересно
    Есть несколько вопросов коде ява: 1) Почему к полям классов обычно обращаются через методы.

    15.07.2011, 16:37 2 15.07.2011, 16:40 [ТС] 3 15.07.2011, 16:49 4
    15.07.2011, 16:49
    15.07.2011, 16:53 5

    this нужен в основном, чтобы различить переменную в сигнатуре метода с переменной/свойством в самом классе в случае их совпадения.

    ещё this нужен для того, чтобы вызвать, например, конструктор этого же класса:

    15.07.2011, 16:56 6

    , а иногда в нем действительно нет необходимости. Я, например, всегда использую this, ввиду привычки и мне так приятней/удобней читать код.

    Если же смотреть со стороны IL, то там вообще нет this, всё обращения к полям (методам) идёт через type::field (type::method()).

    15.07.2011, 17:08 [ТС] 7

    он объясняет это именно хорошим стилем

    Redfex, za5, SSTREGG, вопрос не ставился о неупотреблении this вообще. имеется ввиду употребления this именно в контексте хорошего стиля

    16.07.2011, 01:03 8
    16.07.2011, 12:50 9
    16.07.2011, 13:05 [ТС] 10
    16.07.2011, 13:36 11

    скажем так, в «All-In-One Code Framework Coding Standards» (Microsoft) про this, действительно ничего не сказано, но это ключевое слово лично я использую при:

    — обращениям к полям, созданных конструктором Windows Forms, так как по умолчанию (хоть это и настраивается) они начинаются не с нижнего подчеркивания (_myControl), а так myControl1
    — при разрешениях неоднозначностей между полями/методами класса-наследника и родителя (base this). сюда же относится пример za5, но не дай Бог кому-либо при мне живом написать так как он — это совсем дурной тон
    — ну и в редких случаях, для подчеркивания логики: например, когда в пределах одного блока кода в пределах типа сосуществуют экземпляры этого типа

    C++ — Functional-Style Programming in C++

    By David Cravey | August 2012

    C++ is a multiparadigm, systems-level language that provides high-level abstractions with very low (often zero) runtime cost. The paradigms commonly associated with C++ include procedural, object-oriented and generic programming. Because C++ provides excellent tools for high-level programming, even functional-style programming is quite reasonable.

    By functional-style programming, I don’t mean the programming is strictly functional, just that it’s easy to use many of the functional building blocks in C++. This article will focus on one of the most important functional programming constructs: working with values as opposed to identities. I’ll discuss the strong support C++ has always had for working with values, then show how the new C++ 11 standard expands this support with lambdas. Finally, I’ll introduce a method of working with immutable data structures that maintains the speed C++ is known for while providing the protection that functional languages have long enjoyed.

    Values vs. Identities

    Let me first explain what I mean by working with values rather than identities. Simple values such as 1, 2 and 3 are easy to identify. I could also say that 1, 2 and 3 are constant integer values. This would be redundant, however, because all values are actually constants and the values themselves never change (that is, 1 is always 1 and 1 will never be 2). On the other hand, the value associated with an identity may change (x might equal 1 now, but it could equal 2 later).

    Unfortunately, it’s easy to confuse values and value types. Value types are passed around by value rather than by reference. Though I want to focus here on the values and not the mechanism involved in using or copying them, it’s useful to see how value types go part way in preserving the concept of values versus identities.

    The code in Figure 1 demonstrates a simple use of a value type.

    Figure 1 Using a Value Type

    With only a small change, the variable y can become a reference type—which drastically changes the relationship between x and y, as shown in Figure 2.

    Figure 2 Using a Reference Type

    As Figure 3 shows, C++ also provides the const modifier, which prevents the programmer from making changes to a variable and thus further preserves the concept of a value. (As with most things in C++, however, there’s at least one way to defeat that protection. For more information, look up const_cast, which is intended for working with older code that isn’t “const correct.”)

    Figure 3 The const Modifier

    Note in Figure 3 that though y is passed by reference, the value of y is protected at compile time by a const modifier. This gives C++ programmers an efficient method of passing large objects while working with their values as opposed to their identities.

    With the const modifier, C++ has immutable data types that resemble those found in most functional programming languages. However, dealing with these immutable data types is difficult. Furthermore, making deep (full) copies of large objects for every small change isn’t efficient. Nonetheless, it should be clear that standard C++ has always had a concept of working with values (even if it’s not a very pure concept).

    Note that the support for value types extends to user-defined types through copy constructors and assignment operators. C++ copy constructors and assignment operators allow user-defined types to make a deep copy of the object. Keep in mind that while C++ copy constructors can be implemented to make a shallow copy, you’ll have to make sure the value semantics are preserved.

    C++ 11 Support for Functional-Style Programming

    C++ 11 brings a number of new tools for functional-style programming. Perhaps most important, C++ now has support for lambdas (also known as closures or anonymous functions). Lambdas allow you to compose your code in ways that wouldn’t have been practical before. This functionality was previously available through functors, which are powerful but less practical to use. (Actually, C++ lambdas write anonymous functors behind the scenes.) Figure 4 shows how lambdas have improved our code with a simple example that uses the C++ standard library (STL).

    Figure 4 Using Lambdas

    In this case, the for_each function applies a lambda to each element of a vector. It’s important to note that C++ lambdas have been designed to be used inline when possible; thus lambdas can run as fast as handcrafted code.

    While C++ is just one of the many imperative languages that now have lambdas, what makes C++ lambdas special is that (similar to functional programming languages) they can preserve the concept of working with values as opposed to identities. While functional programming languages accomplish this by making variables immutable, C++ does it by providing control over the capture. Consider the code in Figure 5.

    Figure 5 Capturing by Reference

    In this code, everything is captured by reference, which is the standard behavior for lambdas in other languages. Yet capturing by reference complicates things unless the variables being captured are immutable. If you’re new to working with lambdas, you probably expect the following output from the code:

    However, that’s not the output you get—and the program might even crash. This is because the variable ctr is captured by reference, so all of the lambdas use the final value of ctr (that is, 3, the value that made the for loop come to an end) and then access the array beyond its bounds.

    It’s also worth noting that to keep the ctr variable alive to be used by the lambda outside of the for loop, the ctr variable’s declaration has to be lifted out of the for loop. While some languages eliminate the need to lift value type variables to an appropriate scope, that doesn’t really solve the problem, which is that the lambda needs to use the value of ctr as opposed to the identity of the variable ctr. (There are workarounds for other languages that involve making an explicit copy to a temporary variable. However, this makes it a bit unclear as to what’s going on, and it’s error-prone because the original variable is also captured and thus is still available for use.)

    As Figure 6 shows, C++ provides a simple syntax to allow easy control of the lambda’s capture, which preserves the concept of working with values.

    Figure 6 C++ Syntax for Controlling Lambda Capture

    Don’t capture anything

    (exactly what I wanted in the first lambda example)

    Capture everything by reference

    (traditional lambda behavior, though not consistent with functional programming’s emphasis on values)

    Capture everything by value

    (while this preserves the concept of values, it limits the usefulness of the lambdas; also, it can be expensive to copy large objects)

    [&ctr] Capture only ctr, and capture ctr by reference
    [ctr] Capture only ctr, and capture ctr by value
    [&,ctr] Capture ctr by value and everything else by reference
    [=,&v] Capture v by reference and everything else by value
    [&, ctr1, ctr2] Capture ctr1 and ctr2 by value and everything else by reference

    It’s clear from Figure 6 that the programmer has complete control over how the lambda captures variables and values. However, while this preserves the concept of working with values, it does nothing to make working with complex data structures as values efficient.

    Immutable Data Types

    What’s missing are the efficient immutable data structures that some functional programming languages have. These languages facilitate immutable data structures that are efficient even when very large because they share common data. Creating data structures in C++ that share data is trivial—you just dynamically allocate data and each data structure has pointers to the data. Unfortunately, it’s more difficult to manage the lifetime of shared variables (for this reason, garbage collectors have become popular). Luckily, C++ 11 provides an elegant solution for working with shared variables through the std::shared_ptr template class, as shown in Figure 7.

    Figure 7 Sharing Variables

    The code in Figure 7 illustrates a simple use of std::shared_ptr and its helper function std::make_shared. Using std::shared_ptr makes it easy to share data among data structures without fear of leaking memory (as long as circular references are avoided). Note that std::shared_ptr provides the basic thread-safety guarantees, and runs fast because it uses a lock-free design. However, keep in mind that the basic thread-safety guarantee that std::shared_ptr provides doesn’t automatically extend to the object to which it’s pointing. Still, std::shared_ptr guarantees it will not reduce the thread-safety guarantee of the object it points to. Immutable objects inherently provide a strong thread-safety guarantee because once they’re created they never change. (Actually, they never change in an observable manner, which includes, among other things, an appropriate thread-safety guarantee.) Therefore, when you use a std::shared_ptr with an immutable object, the combination maintains the immutable object’s strong thread-safety guarantee.

    I can now easily create a simple immutable class that potentially shares data, as shown in Figure 8.

    Figure 8 An Immutable Class for Sharing Data

    The code in Figure 8 is a bit long, but most of it is boilerplate code for the constructors and assignment operators. The last two functions are the key to making the object immutable. Note that the SetS and SetD methods return a new object, which leaves the original object unchanged. (While including the SetS and SetD methods as members is convenient, it’s a bit of a lie, because they don’t actually change the original object. For a cleaner solution, see the ImmutableVector in Figures 9 and 10.) Figure 11 shows the Immutable class in action.

    Figure 9 Using the Smart ImmutableVector Template Class

    Figure 10 Methods for Operating on the ImmutableVector

    Figure 11 The Immutable Class in Action

    Note that object b shares the same string as object a (both strings are at the same address). Adding additional fields with associated getters and setters is trivial. Though this code is good, it’s a little more difficult to scale to containers when you’re being efficient. For example, a naïve ImmutableVector might maintain a list of shared pointers representing each element of the array. When the naïve Immutable-Vector is changed, the entire array of shared pointers would need to be duplicated, incurring additional cost as each shared_ptr element of the array would need its reference count to be incremented.

    There is a technique, though, that allows the data structure to share most of its data and minimize the duplication. This technique uses a tree of some form to require duplication of only the nodes that are directly affected by a change. Figure 12shows a comparison of a naïve ImmutableVector and a smart ImmutableVector.

    Figure 12 Comparing Naïve and Smart ImmutableVectors

    This tree technique scales nicely: as the number of elements grows, the percent of the tree that needs to be duplicated is minimized. Moreover, by adjusting the branching factor (so each node has more than two children), you can achieve a balance in memory overhead and node reuse.

    I developed a smart ImmutableVector template class that can be downloaded from archive.msdn.microsoft.com/mag201208CPP. Figure 9 shows how you can use my ImmutableVector class. (As previously noted, to make the immutable nature of the ImmutableVector clearer to the users, ImmutableVector uses static member functions for all actions that generate new versions.)

    For read-only actions, the vector can be used much like a regular vector. (Note that for this example I haven’t implemented iterators, but doing so should be fairly trivial.) For write actions, the AppendValue and TruncateValue static methods return a new ImmutableVector, thus preserving the original object. Unfortu-nately, this isn’t reasonable for the array subscript operator, so I made the array subscript operator read-only (that is, it returns a const reference) and provided a SubstituteValueAtIndex static method. It would be nice, however, to be able to make a large number of modifications using the array subscript operator in a single block of code. To facilitate this, ImmutableVector provides a GenerateFrom static method, which takes a lambda (or any other functor). The lambda in turn takes a reference to MutableVector as a parameter, which allows the lambda to work on a temporary MutableVector that can be changed freely like a normal std::vector. The example in Figure 10 shows the various methods for operating on the ImmutableVector.


    The beauty of the GenerateFrom static method is that the code within it can be written in an imperative way, while resulting in an immutable object that can be safely shared. Note that the Generate-From static method prevents unauthorized access to the underlying ImmutableVector by disabling the MutableVector it passed into the lambda as soon as the lambda exited. Please note as well that while ImmutableVector provides a strong thread-safety guarantee, its helper class MutableVector does not (and is intended to be only used locally within the lambda, not passed around to other threads). My implementation also optimizes for multiple changes within the Change method such that there’s minimal restructuring occurring on the temporary tree, which gives a nice performance boost.

    Wrapping Up

    This article gives you just a taste of how you can use functional-style programming in your C++ code. Moreover, C++ 11 features such as lambdas bring a touch of functional-style programming regardless of the paradigm used.

    David Cravey is a Visual C++ MVP who enjoys programming in C++ maybe a bit too much. You’ll find him presenting at local C++ user groups and universities. During the day he enjoys working at NCR, through TEKsystems in Fort Worth, Texas.

    Thanks to the following technical experts for reviewing this article: Giovanni Dicanio, Stephan T. Lavavej, Angel Hernández Matos, Alf P. Steinbach and David Wilkinson

    Лучшие приемы программирования на C

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

    Стили и нормы программирования

    • Необходимо использовать стиль программирования, который делает код читабельным и понятным. Несмотря на то, что некоторые разработчики имеют свой собственный стиль программирования или используют стиль программирования, принятый в их компании, хорошим тоном считается следовать стилю программирования Кернигана и Ритчи (Kernighan и Ritchie), используемому подавляющим большинством программистов на C. Однако, чересчур увлекшись, легко прийти к чему-нибудь такому:

    Dishonorable mention, Чемпионат по самому непонятному коду на C (Obfuscated C Code Contest), 1984 г. Автор кода неизвестен.

  • Всегда в коде можно увидеть главную функцию, называемую main() . По стандарту ANSI эта функция определяется как int main(void) (если не нужно обрабатывать аргументы командной строки) или как int main( int argc, char **argv ) . Не-ANSI компиляторы могут пропускать объявление void или составлять список имен переменных и следовать их объявлениям.
  • Отступы

    Необходимо использовать вертикальные и горизонтальные отступы. Количество и расположение отступов и пробелов должно отражать структуру кода.

    Длинная строка условного оператора должна быть разбита на несколько строк. Например:

    будет лучше выглядеть как:

    Точно так же сложные циклы for должные быть разделены на несколько строк:

    Другие сложные выражения, такие как использующие оператор ?: тоже лучше разделить на несколько строк:

    Комментарии

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

    Такой стиль комментирования является избыточным:

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

    Правила наименования

    Имена с ведущими или завершающими знаками подчеркивания предназначены только для системы целей и не должны использоваться для каких-либо пользовательских имен переменных. Правила определяют следующие требования:

    1. Константы #define должны записываться ЗАГЛАВНЫМИ символами.
    2. Константы enum должны начинаться с заглавного символа или записываться полностью ЗАГЛАВНЫМИ символами.
    3. Слова function , typedef и имена переменных, так же как и struct , union и enum должны быть в нижнем регистре.

    Для понятности необходимо избегать имен, различающихся только регистром, например, foo и Foo . Точно так же лучше избегать одновременного использования имен foobar и foo_bar . Необходимо избегать любых имен, которые похожи друг на друга. На многих клавиатурах и во многих шрифтах l , 1 и I выглядят очень похоже. Переменная с именем l , в частности, плоха потому, что похожа на константу 1 .

    Имена переменных

    При выборе имени переменной не так важна длина имени, как понятность. Длинные имена могут назначаться глобальным переменным, которые редко используются, но индексы массивов, появляющиеся в каждой строке цикла, не должны быть значительно сложнее, чем i . Использование index или elementnumber не только усложняет набор, но и может сделать менее понятными детали вычислений. С длинными именами иногда сложнее понять, что именно происходит в коде. Легко сравнить:

    Имена функций

    Имена должны отражать то, что делают функции и что они возвращают. Функции используются в выражениях, часто в условных операторах if , поэтому они должны читаться соответственно. Например:

    непонятно, так как не говорит о том, возвращает ли функция TRUE при прохождении проверки или наоборот; использование вместо этого:

    делает все понятным.

    Объявление переменных

    Все объявления внешних переменных должны предваряться ключевым словом extern .

    Обозначение указателя, * , должно сопровождать имя переменной, а не ее тип:

    Второй пример объявления переменных не является неправильным, но могут возникнуть сомнения из-за того, что t и u не объявлены как указатели.

    Заголовочные файлы

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

    Следует избегать имен заголовочных файлов, совпадающих с именами стандартных библиотек. Строка #include «math.h» включает заголовочный файл стандартной библиотеки math, если файл с таким именем не будет найден в текущем каталоге. Если такое поведение — именно то, что нужно, то лучше оставить соответствующий комментарий.

    Наконец, использование абсолютных путей для заголовочных файлов — не самая лучшая идея. Опция компилятора C include-path ( -I на большинстве систем) — это предпочтительный метод обработки внешних библиотек и заголовочных файлов; он позволяет изменить структуру каталогов без необходимости изменения исходных кодов.

    scanf

    Не следует использовать scanf в серьезных приложениях. Обработка ошибок в этой функции неадекватна. Рассмотрим такой пример:

    Запустим тест:

    Enter an integer and a float: 182 52.38

    I read 182 and 52.380001

    Теперь другой тест:

    Enter an integer and a float: 6713247896 4.4

    I read -1876686696 and 4.400000

    ++ и —

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

    Нельзя позволять себе видеть то, чего на самом деле нет

    Рассмотрим следующий пример:

    На первый взгляд такой оператор while выглядит корректным кодом на C. Однако использование оператора присваивания вместо оператора сравнения приводит к появлению синтаксически некорректного кода. Так как приоритет оператора = является наименьшим, то данное выражение будет интерпретировано следующим образом (скобки добавлены для наглядности):

    Левая часть оператора присваивания:

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

    Явно выраженные намерения

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

    Иногда можно разрабатывать код, который предупреждает возможные ошибки. Например, можно ставить константы в левую часть оператора сравнения, т. е. вместо:

    можно написать так:

    В этом случае компилятор выдаст предупреждение:

    Такой стиль программирования позволяет компилятору находить потенциальные проблемы; пример кода выше неправилен, так как пытается присвоить значение для \t .

    Ошибки из-за специфики реализации языка программирования

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

    Выражение будет интерпретироваться по правилу максимального оператора. Если комментарии могут быть вложенными, то интерпретация будет следующей:

    Паре символов /* соответствует пара символов */ , поэтому выражение равно 1 . Если комментарии не вкладываются, то на некоторых системах /* в комментариях будет проигнорировано. На некоторых компиляторах будет также выдано предупреждение для вложенной последовательности /* . В любом случае выражение будет интерпретировано следующим образом:

    Сбрасывание буфера на диск

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


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

    Это выражение должно быть выполнено перед записью в stdout . В идеале этот код должен помещаться в начале функции main .

    getchar() — макрос или функция?

    Следующая программа выводит свои входные данные:

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

    Программу можно переписать следующим образом:

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

    Так как вызов функции обычно занимает довольно много времени, getchar часто реализуют в виде макроса. Этот макрос определен в stdio.h , поэтому когда #include удаляется, компилятор не знает, что такое getchar . На некоторых системах полагается, что getchar — это функция, возвращающая int .

    В действительности многие реализации компиляторов языка C имеют свои стандартные функции getchar , частично в целях защиты от таких оплошностей. Таким образом, ситуация, когда включение #include пропущено, влечет использование компилятором собственной версии функции getchar . Дополнительные вызовы этой функции делают программу медленнее. То же самое верно и для putchar .

    Пустой указатель

    Пустой указатель не указывает ни на какой объект. Неправильно использовать пустой указатель для любых целей, кроме присваивания и сравнения.

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

    Переход по пустому указателю может вызвать странные эффекты.

    Что означает a+++++b ?

    Единственный правильный способ интерпретации этого выражения такой:

    Однако правило длинного оператора предписывает разбить выражение следующим образом:

    Это синтаксически неверно, такой код эквивалентен строке:

    Но результат a++ не является lvalue и, следовательно, не может быть операндом для ++ . Таким образом, правила для разрешения логических двусмысленностей не могут в этом примере привести к синтаксически верной конструкции. На практике, конечно, лучший способ избежать таких конструкций — это полная уверенность в том, что код интерпретируется однозначно. Конечно, добавление пробелов помогает компилятору понять цель оператора, но предпочтительнее (в перспективе сопровождения кода) разбить конструкцию на две строки:

    Осторожное обращение с функциями

    Функции обеспечивают наиболее общее структурирование кода на C. Они должны использоваться для решения проблемы «сверху вниз» — для разбиения задачи на ряд более мелких подзадач до тех пор, пока подзадача не будет легко решаться. Это помогает реализовать модульность и упростить документирование программы. Кроме того, программы, составленные из большого числа маленьких функций, значительно легче для отладки.

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

    Если заголовочные файлы не объявляют тип возвращаемого значения библиотечных функций, необходимо сделать это самостоятельно. Окружив объявления конструкцией #ifdef/# , можно упростить портирование своего кода на другую платформу.

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

    «Висячий» else

    Нужно опасаться проблемы «висячего» else , если нет полной уверенности в правильности конструкции:

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

    Границы массива

    Необходимо проверять границы всех массивов, включая строки, так как сегодняшнее fubar ‘ может стать завтра floccinaucinihilipilification . В надежном программном обеспечении не используется gets() .

    Тот факт, что в C элементы нумеруются с нуля, делает более вероятными ошибки подсчета. Однако требуются некоторые усилия на изучение того, как уберечься от них.

    Пустой оператор

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

    Проверка выражений на истинность

    Не нужно оставлять по умолчанию проверку на ненулевое значение, т. е.:

    даже если FAIL имеет значение 0 , которое C рассматривает как ложь (конечно, здесь нужно соблюдать баланс с такими конструкциями как, например, показанная в разделе «Имена функций»). Явное значение поможет избежать ошибок, если вдруг кто-то решит, что при неудачном завершении должно возвращаться значение -1 вместо 0 .

    Частые затруднения вызывает функция проверки равенства строк strcmp , так как нет единого значения, означающего, что строки неравны. Предпочтительный вариант — определение в этом случае макроса STREQ :

    Использовать этот макрос можно в операторах следующего вида:

    Таким образом, функция получает желаемое поведение (не требуется переписывать или переопределять стандартные библиотечные функции типа strcmp() ).

    Не следует сравнивать логические выражения с 1 ( TRUE , YES и другими); вместо этого нужно проверять на равенство 0 ( FALSE , NO и так далее). Большинство функций гарантируют возвращение 0 в случае неудачного завершения, и возвращение лишь ненулевого значения в случае удачного завершения. Таким образом,

    лучше переписать так:

    Вложенные операторы

    А сейчас — время для разговора о вложенном операторе присваивания. В некоторых конструкциях нет лучшего способа присваивания, хотя он и влечет увеличение кода в операторе и ухудшение читабельности:

    Использование вложенного оператора присваивания для улучшения производительности возможно. Однако необходимо искать компромисс между увеличением скорости и усложнением сопровождения кода, которое возникает при использовании вложенных присваиваний в неподходящем месте. Например:

    не должно заменяться на:

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

    Оператор goto

    goto необходимо использовать крайне умеренно. Один из случаев, когда этот оператор полезен — это необходимость прервать многоуровневый оператор switch , for или while , хотя такая необходимость может свидетельствовать о том, что внутреннюю конструкцию лучше вынести в отдельный цикл.

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

    «Проваливание» через switch

    Когда блок кода имеет несколько меток, каждую из них нужно размещать на отдельной строке. Этот элемент стиля программирования согласуется с правилом установки вертикальных отступов и делает перекомпоновку (если она понадобится) сравнений case простой задачей. Использование предоставляемой языком С возможности «проваливания» в операторе switch должно обязательно комментироваться в целях упрощения последующего сопровождения кода. Каждый, кто испытал на себе неприятности от ошибок при использовании этой возможности, знает, насколько это важно!

    Хотя последний оператор break и не является необходимым, его использование предотвращает ошибку в случае, когда потребуется добавить еще один case . В случае, если используется вариант default , он должен быть последним и не требует оператора break .

    Константы

    Символические константы делают код более простым для чтения. Числовых констант, как правило, следует избегать; лучше использовать #define для задания понятного имени. Сосредоточение всех определений в одном месте (лучше всего — в заголовочном файле) также упрощает администрирование изменений в больших проектах, так как позволяет вносить изменения только в директивах #define . Можно рассматривать использование типа данных «перечисление» в качестве улучшенного способа объявления переменных, которые могут принимать только дискретные значения. Использование перечислений также позволяет компилятору выводить предупреждения при ошибках использования типа перечисления. И, наконец, явно перечисленные цифровые константы требуют меньше объяснений о своем происхождении при комментировании.

    Константы необходимо объявлять соответственно их использованию, т. е. необходимо указывать 540.0 для числа с плавающей точкой вместо 540 с прямым объявлением типа float . Есть случаи, в которых константы 0 и 1 могут возникать явно вместо своих объявлений строковыми константами. Например, если цикл for индексирует массив, то код:

    — нет. Во втором примере front_gate — это указатель; когда значение является указателем, то оно должно сравниваться с NULL , а не с 0 . Даже простые значения типа 1 или 0 часто лучше воспринимаются в качестве TRUE и FALSE (или YES и NO ).

    Не нужно использовать переменные с плавающей точкой там, где нужны дискретные значения. Это связано с не совсем корректным представлением чисел с плавающей точкой (можно вспомнить второй пример из раздела scanf выше). Сравнивать числа с плавающей точкой лучше используя или >= ; явное сравнение ( == или != ) может не обнаружить «достаточного» равенства.

    Символьные константы должны быть объявлены как символы, а не как числа. Нетекстовые символы являются более трудными для портирования. Если нетекстовые символы необходимы, в частности, при использовании в строках, они должны быть записаны в виде управляющих последовательностей из трех восьмеричных цифр, а не одной (например, ‘\007’ ). Даже в этом случае такое использование символов является платформозависимым и должно восприниматься таковым.

    Условная компиляция

    Условная компиляция полезна в случаях, когда требуется реализовать машинозависимый код, при отладке и для установок значений во время компиляции. Различные варианты управления могут легко привести к непредвиденным ситуациям. При использовании #ifdef для машинозависимого кода необходимо быть уверенным, что если тип машины не определен, то возвращается сообщение об ошибке, а не используется конфигурация по умолчанию. Директива #error предназначена как раз для этих целей. При использовании #ifdef для оптимизации лучше применять по умолчанию неоптимизированный код, чем некомпилируемый или некорректный. Необходимо тестировать неоптимизированный код.

    Разное

    • Утилиты для компиляции, такие как make , значительно упрощают задачу переноса приложения из одного окружения в другое. В процессе разработки make перекомпилирует только те модули, которые были изменены со времени последней компиляции.

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

    Также необходимо изучить документацию компилятора и выяснить, какие опции сделают его более «разборчивым». Работа компилятора заключается в том, чтобы быть точным, поэтому необходимо дать ему возможность выдать отчет о потенциальных проблемах, используя соответствующие опции компиляции.

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

    Никогда не нужно делать предположений о размере заданного типа данных, особенно указателей.

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

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

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

    Общеизвестно, что сопровождение приложения отнимает значительную часть времени программиста. Частично это происходит из-за использования платформозависимых и нестандартных особенностей, но в большей степени — из-за плохого стиля программирования. В этой статье дается несколько советов, которые помогают сберечь время, требуемое для сопровождения кода. Следование этим советам сделает сопровождение приложений командой разработчиков более простым.

    Илон Маск рекомендует:  PWideChar - Тип Delphi
    Понравилась статья? Поделиться с друзьями:
    Кодинг, CSS и SQL