Предыдущий топик Следующий топик

О учебнике

Изначально этот учебник назывался "Уроки по C++", но потом я понял, что это название ничего не говорит о моих целях и планах, да и к тому же несколько неправильно. Потому я решил дать новое название - "Путь программирования. C++". Возможно, это звучит несколько странно, но по-моему именно это название наиболее близко соответствует моей цели - именно такой путь вам предстоит пройти вместе с этим учебником. Вообще говоря, не совсем правильно также называть его учебником, моей целью является именно описать путь программирования, не во всех подробностях, но по крайней мере так, чтобы вы сами могли смело идти по этой дороге. Но, правда, сами статьи написаны в стиле уроков. Я под этим подразумеваю, то, что учебник разбит на небольшие, строго тематические последовательные уроки. Сам стиль изложения я стараюсь делать как можно более естественным, но в то же время все технические данные излагаются довольно строго, во избежание недоразумений и разногласий.

Как можно догадаться из названия учебника, он посвящён языку C++. Первый вопрос, который встаёт перед каждым, наверно, - почему именно C++. Причина проста - это мой любимый язык. Можно конечно спросить, за что я его люблю. Но разве любят за что-то? LOVE Любят просто так. Вообще тема выбор языка программирования это первый вопрос, который встаёт перед каждым начинающим программистом, перед тем кто только становится на путь программирования. Замечу, вопрос очень важный, но в то же время вопрос без ответа. Некоторые свои рассуждения по этой теме, в том числе насчёт работодателей и будущего языков, я выкладываю в следующем топике.

Кстати, топик означает статья, обсуждение некоторой темы - думаю знаете. Если нет, то читать мой учебник, наверно, будет тяжеловато. Дело в том, что тяжело писать подобные вещи по программированию без использования общекомпьютерной, а также программисткой лексики. Да и глупо, так как многие нормальные ресурсы используют эту лексику по простой причине - это естественный (на сегодня) язык программиста. Но я всё же думаю, что раз вы ещё читаете моё творение, значит название вас не испугало, а значит вы сможете нормально читать мои уроки. Если нет, то запаситесь каким-нибудь словариком программиста, благо сейчас таких в интернете валом. Также неплохо иметь словарик (или переводчик) английского языка. Как минимум по двум причинам. Во-первых, многие книги, мануалы, сайты и документации по программированию проще достать на английском языке, а иногда на русском просто невозможно, или (что бывает очень часто) возможно достать лишь в испорченном и неполном виде. Поверьте мне, нормальный английский текст при небольшой привычке читается гораздо лучше кривого и зачастую ошибочного русского. Достаточно знаний английского на 'программистском' уровне (заверяю: мой уровень не так уж велик :(). Вторая же причина - небольшие знания английского языка упрощают восприятие программисткой и компьютерной лексики. Например: дебаггер, топик, инстансировать, продефинить, сорсы, темплейт, регексп, синглетон и др.

Теперь немного о структуре моего учебника. Как я в итоге понял, главное, что её характеризует это естественная непредсказуемость. Хотя общий план есть. Учебник поделен на разделы, в каждом разделе можно найти сколько-то уроков (про количество пока ничего не могу сказать, так как на момент написания этого топика была закончена лишь первая часть). В конце урока могут быть разные дополнения: файлы к уроку, ссылки на веб-ресурсы, упражнения и т. д. Не секрет, что одним из наиболее обширных источников информации по программированию является майкрософтский ресурс MSDN (Microsoft Developer Network - майкрософтская сеть разработчика). Периодически выпускается оффлайновая дисковая версия с собственным браузером (на иешном движке), который убыстряет поиск информации (за счёт дополнительных инструментов). Рекомендую купить. Последняя версия входит в поставку MS Visual Studio 2005 (о том, что это за зверь - чуть позже). В этой версии значительно усовершенствован поиск (хорошая настройка критериев поиска, удобный вывод результатов в стиле инетовского поисковика, возможность сохранения результатов для последующего быстрого восстановления), поэтому желательно достать именно её. Иногда бывают проблемы с установкой MSDN (как впрочем и VS, т. е. Visual Studio) - инсталлер просит вас вставить диск, жалуюсь что некоторый файл не найден на диске (на самом деле он там есть). Решение для этого простое - скопируйте дистрибутив на винчестер, установите MSDN (или VS), а затем удалите дистрибутив. Во многих уроках я привожу названия статей из MSDN. Замечу, что вначале я приводил адреса, но дело в том, что в 2005-ой MSDN-ке адреса страничек стали не очень читабельные (имена страниц ХТМЛ состоят из бессмысленного набора цифр и букв), поэтому я даю названия, они вроде в основном сохранились. Юзайте Search.

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

  • MSDN - ну про это уже сказал.
  • RSDN (Russian Developer Network) - несмотря на название это не перевод MSDN, а независимый ресурс. Здесь вы можете найти много довольно неплохих статей, есть форум (правда, по-моему, жутко неудобный), раздел проектов и др.
  • Винград - самый крупный русскоязычный форум программистов с оригинальным дизайном. VPF - это про него (Vingrad Programmers Forum). Там же можете встретить и меня COOL
  • Исходники.ру - достаточно обширный русскоязычный портал для программистов. Множество отличных статей, исходников, неплохой форум, а также множество других проектов (например, algolib - неплохая коллекция алгоритмов).
  • Первые шаги - довольно неплохое собрание статей, единственный недостаток - небыстрое обновление (некоторые статьи морально устаревают). Замечу, что на винграде есть проект WorldCPP, включающей множество статей и исходников, в том числе с этого сайта.
  • Алголист - пожалуй, лучшая коллекция алгоритмов на русском языке.
  • CodeGuru - популярный англоязычный ресурс.
  • CodeProject - ещё один англоязычный портал. Более молодой чем CodeGuru, но очень энергично развивающийся. Я лично его бы оценил на пять с плюсом.
  • SourceForge - главный портал движения Open Source (открытые сорсы, т. е. исходники). Здесь вы можете скачать кучу бесплатных программ, а зарегившись их исходники (причём программы любого спектра: от графических утилит и кодеков до программистских библиотек и утилит). Также вы можете присоединиться к одному из проектов.

Ну, для начала, я думаю, хватит. Идём дальше!

Предыдущий топик Следующий топик


Языки программирования: объективный взгляд.

Из предыдущей статьи вы наверно уже поняли, что мой любимый язык программирования - это C++. Поэтому, наверно, название этого топика выглядит нелепо или надумано. Но это вовсе не так. Наверно, у большинства программистов когда-нибудь наступает такое состояние, когда он понимает, что этот язык - его язык. Почему для меня это C++? Очень сложно ответить. Может в этом виновато время, отданное мною этому языку, тогда ещё по субъективным и неубедительным, я бы даже сказал ошибочным убеждениям. Может судьба у меня такая, так сказать, врождённое. Не знаю. Но я уверен, что это - мой любимый язык.

В то же время не следует понимать это состояние как какую-то зависимость или программистскую слепоту. Напротив, ты готов признать и уважать множество других языков. Сколько вообще сейчас объективно существует языков общего программирования? Я исключаю процедурные языки, предназначенные для специфических задач (например, C или FORTRAN), языки веб-скриптинга (вроде PHP или Cold Fusion), языки логического программирования (Prolog), скриптовые языки (Perl, Python, Ruby) и т. д. То есть я не беру во внимание языки, предназначенные для специфических задач, я говорю об основополагающих объектно-ориентированных языках, имеющих право на существование. Наиболее жесткие судьи, посмотрев на текущее состояние рынка ПО, скажут, что таких языков 3: C++, Java и C#. Некоторые скажут, что таких языков 4 (здесь уже два варианта). Но, если смотреть с объективной стороны, забыв о собственных предпочтениях (а настоящему программисту это довольно таки несложно, хотя иногда и обидно), то получается другая цифра - 5. К тройке общепризнанных добавляются ещё два: Delphi и Visual Basic .NET. За последние 3-4 года эти языки изменились довольно сильно (по сравнению со старыми Delphi и VB), и обрели мощную, стройную дотнэтовскую модель ООП (объектно-ориентированного программирования), которая достойна стоять на уровне с уже классическими моделями C++ и Явы (в Delphi можно разрабатывать и не дотнэтовские приложение, но идеи языка были изменены именно из-за .NET).

По этому поводу надо сказать пару слов о .NET (принято читать как "Дот НЭТ".) Одна из идей этой технологии является вариацией на тему байт-кода Явы, т. е. придуман абстрактный набор команд для абстрактной машины. С одной стороны этот байт-код низкоуровневый, так как вычисление элементарного арифметического выражения разбивается на цепочку команд, оперирующих с виртуальными адресами. Но в то же время, в байт-коде содержится структура классов и весь интерфейс программы. На самом деле байт-код - это явовская терминология, а вообще его принято называть управляемым кодом (managed), т. е. выполнение этого кода выполняется отдельной машиной, поэтому легко, скажем запретить выполнение каких-либо операций для этой программы (от доступа к вашим личным файлам и опроса портов сети до манипуляций большими блоками данных памяти и использования математических функций), легко в любой момент зачистить ресурсы, которые забыла освободить программа (поэтому в управляемых языках их просто не принято, а зачастую невозможно освобождать), этот процесс называется чисткой мусора. Ещё один плюс - кроссплатформенность (при условии реализации виртуальной машины на этой платформе). Главный минус, как несложно догадаться, - более низкая производительность. Но спешу заметить, что эти плюсы и минусы - это лишь первый взгляд на проблему, при более детальном рассмотрении убеждаешься, что они не столь значительны. Можно и на яве поднять производительность, можно и на C++, при наличии хорошей библиотеки, использовать внешнюю защищённость, чистильщик мусора и кроссплатформенность. Для последнего, правда, придётся скомпилировать программу (одну и ту же программу) для разных платформ и распространять несколько дистрибутивов. Но, на практике, и на яве приходится очень часто обращаться к специфическим сервисам конкретной платформы. Более того в C# (насчёт Явы не могу сказать) есть так называемый unsafe-режим, то есть вы отказываетесь от чистильщика и прочего ради некоторых других возможностей. В общем, объективно эти плюсы и минусы абсолютно ничего не стоят. Ниже я ещё вернусь к этому.

Ну, вот придумали этот дотнетовский псевдокод (он называется MSIL). А затем, решили, что если для обычного нэтив-кода (native - родной, природный) существует столько много языков, то почему бы не создать несколько языков для .NET. Ну, вот и создали. Их на самом деле чрезвычайно много (из неназванных - Visual J#, JScript .NET, Python .NET, PHP .NET, Perl .NET, и т. д.), но просто большинство получились ненужными (то же было в своё время и с нэтивными языками). Есть ещё такая вещь, как C++/CLI - это спецификация, разработанная комитетом ECMA, предлагает несколько изменённый C++ для абстрактной машины, единственная его реализация на сегодня сделана MS и предназначена для виртуальной машины .NET.

И вот, ты признаёшь равенство (!) всех этих языков с твоим языком, но понимаешь, что все остальные для тебя чужие. Да, ты готов спорить с любым, что Delphi умер, а попытка возрождения в виде Delphi .NET бессмысленна, что Basic вообще не язык, а VB .NET лишь MS-шное извращение, что C# - это только рекламный трюк, обречённый на погибель, что Ява - это жуткие и никому ненужные тормоза и т. д. И в таком споре с обоих сторон будет приведено тысячи неоспоримых аргументов, напечатаны в порыве горы кода, вспомнены множество известных историй, но ... Официальным итоговым решением будет боевая ничью. И логически понимаешь, что это единственный объективный результат. Хотя каждый внутренне будет считать, что несмотря на официальный результат, очевидно, что он победил. Каждый будет гордится, как ловко он обставил оппонента и какой красивый код он написал. Каждый будет доволен и останется при своём мнении. Это практика и статистика.

Да, принято считать, что C++ будет лучшим решением для программ, критичных к производительности и программ, работающих с более низкоуровневыми вещами, C# - для быстрого написания энтерпрайз-приложений (энтерпрайз-приложениями называют приложения, которые в основном занимаются отображением и редактированием данных из БД, на основе SQL или другого языка запросов), Java - для кроссплатформенных и сетевых энтерпрайз-приложений. Но, откровенно, говоря, на практике, всё это полный бред. Наиболее быстро вы будете писать на том языке, который лучше всего знаете и (что немаловажно) любите. Подчёркиваю, именно язык, а не какие-либо библиотеки или технологии, это всё выучивается при необходимости не так уж и долго, а зачастую уже во время работы над проектом.

Не думайте, что написав более-менее приличную программу на C++, который вы не знаете, вы получите более шуструю программу, чем на тормозной Яве, которую вы знаете. Да, вы получите выигрыш на классических общих алгоритмах, но главная задача - проектирование, скорей всего на C++ будет сделана в идеологии Явы, думаю результат понятен. Вспоминается рассказ одного программиста. Дело было ещё во времена старого VB (замечу, что его производительность автоматически была раза в 3-4 (!) ниже, чем обычных языков), какую-то объёмную по вычислениям задачу программер написал на C++, как того требовалось в институте. Затем он вспомнил о родном VB и интереса ради написал это дело в ВБ-шном стиле. Производительность выросла раза в 1,5! Он рассказал об этом преподавателю. На следующий день препод принёс решение этой задачи на Javascript. Напомню, что все преобразование скрипта в бинарный вид (всех команд, чисел и др.) происходит во время выполнения программы, но несмотря на это, как вы, наверно, догадались, программа работала ещё быстрее!

Не надо также думать, что сев за C# вы значительно ускорите разработку энтерпрайз-приложений. Максимум, вы быстро сгенерируете заготовку визардом Microsoft Visual Studio или Borland Developer Studio, затем неделя уйдёт на изучение полученного кода. Ещё две недели на решение общих проблем, не связанных непосредственно с энтерпрайзом, а такие проблемы точно будут и немало. Ещё две недели на создание своего нормального каркаса. После безуспешных попыток вы либо начнёте нормально изучать C#, либо перейдёте на родной C++.

Хотя оба этих примера представляют из себя крайние случаи, но на практике всё происходит очень похоже. Когда-то я, как и многие начинающие, не хотел этому верить. Несмотря на мнения действительно опытных программистов, я упорно говорил о главных фичах C++: скорости и возможности лёгкого доступа к системе. Но, время учит... Понимаешь, что главная его фича - развитая модель ООП. Но это есть во всех языках золотой пятёрки, и во всех языках именно это и есть главная фича. Любой опытный программист, независимо от его любимого языка, согласится со мной в этом вопросе. Но, помните, что это вовсе не означает, что это всё одинаковые языки. Языки очень разные, причём в первую очередь по идеологии, но единство есть - базовые принципы ООП.

Вот в этом и есть единство всех программеров. Но кроме этого гармоничного и доброго мира есть мир иной. Мир глупый, жестокий и неумолимый. Это мир не программеров, а работодателей. Именно, благодаря их предрассудкам существует представление о тройке лидеров (C++, Java, C#). Разве сможет работодатель сказать: "Требуются программисты на VB .NET". Не сможет, боится, что засмеют. А зря... Итак, у нас наиболее востребованы программисты, знающие C++ и C#. Реже - что-то одно из "тройки лидеров". Хотя всё-таки профессионалы и патриоты своего языка смогут объяснить этому работодателю, что им вполне подойдёт программист VB .NET или Delphi. Есть, кстати среди работодателей особо интересные особи, представляющие из себя живой и надоевший анекдот. Этим работодателем требуются (цитирую) "программисты 1С". Не знаю, то ли они хотят переманить к себе в контору программистов из известной фирмы, то ли это аббревиатура для "1 из: C++, C#". Если вы что-то знаете по этому поводу, то обязательно мне сообщите.

Ну, поговорили о высоком, а теперь о главном. После прочтения этой главы у вас есть два пути. Первый - забросить этот учебник, а если когда-нибудь взбредёт в голову открыть его ещё раз, то обязательно перечитать эту главу. Если же вы ещё читаете эти строки, то запомните, что C++ - это самый лучший язык программирования. Помните, что Delphi умер, а попытка возрождения в виде Delphi .NET бессмысленна. Помните, что Basic вообще не язык, а VB .NET лишь MS-шное извращение. Помните, ...


Наши цели и инструменты

Хотелось бы теперь собственно решить - что мы будем изучать. И, что немаловажно, как мы будем это делать. Вообще хотелось бы, чтобы вместе с этим учебником вы могли бы пройти путь от начинающего программиста до знающего, способного легко пользоваться мощью C++. Сразу, замечу, что в плане ОСи я опираюсь на винду, хотя универсальные средства C++, а также POSIX-совместимые вещи я не пропускаю. Зависимость от ОСи проявляется в первую очередь в тех вопросах, где без этого не обойтись. Ну, вдобавок в мои планы точно не входит изучение Xlib или Mac Core Craphics, хотя WinAPI в разумных пределах входит в нашу программу. Ещё один вопрос, где важен выбор системы - используемые компиляторы и IDE. Но, честно говоря, редко встретишь начинающих программистов, сидящих под маками или никсами.

Первый же вопрос, который мне предстояло решить, начиная писать этот учебник - это методика и технология обучения (во, как выразился!). В этом плане очень полезно сказать буквально пару слов по истории языка C++ (я не претендую на историческое повествование, а хочу лишь назвать пару моментов, важных для понимания сути C++). Когда-то в 70-х годах фирмой Bell Labs был разработан замечательный язык C, тогда ещё для личных нужд. Из-за высокой гибкости, но в то же время идеальной, наверно, с точки зрения компьютера модели языка он приобрёл жуткую популярность и в своей области остаётся общепризнанным лидером (в первую очередь, системное программирование). Этот язык до сих пор используется, развивается (последний его стандарт датируется 99 годом - C99) и будет продолжать это делать (в своей области). Чуть позже появился другой язык, предназначенный для прикладного программирования - Simula. Он не получил высокого распространения, но этот язык по достоинству считается первым объектно-ориентированным языком. Он ни капли не похож на C++, но именно его идеи, значительно преобразованные, стали идеологией этого гиганта. Базовый синтаксис же C++ был основан на C. Более того - вся стандартная библиотека языка C была включена и в C++. В связи с этим есть две идеи подхода к изучению C++. Первый подход - изучать C++, как язык объектно-ориентированный, забыв о структурном программировании. Этот подход проповедуется многими авторами книг, но я считаю всё-таки его не лучшим подходом. Во-первых, нам приходится вначале рассуждать о теории ООП без никакой практики, я не считаю это хорошим началом. Во-вторых, использование идеологии C зачастую очень полезно, я не вижу смысла искусственного придумывания классов и объектов в десяти строках кода, элегантно решаемых средствами C. В-третьих, для серьёзного программирования под винду на C++ не знать основ WinAPI по меньшей мере стыдно, а это библиотека чисто сишная, никакого ООП вы здесь не найдёте. В-четвёртых, читая очень хорошие книги по C++ (например, очень понравившуюся мне книгу Брюса Эккеля и Чака Эллисона "Философия C++. Практическое программирование"), зачастую встречаешься с изучением идеологии ООП как противопоставления каким-либо моментам структурного программирования. Можно много ещё причин придумать, было бы желание. Второй подход, основывается на изучение вначале идей структурного программирования и их реализации в C++. При этом я ограничиваюсь либо стандартной библиотекой языка C в её плюсанутой реализации, либо, где это нужно - функциями POSIX стандарта. Этот независимый стандарт расширяет стандартную сишную библиотеку (рантайм), изначально был разработан для никсов, но затем во многом был поддержан MS для винды. Но всё-таки надо заметить, что я рассматриваю приплюснутую реализацию идей структурного программирования, она несколько отличается от сишной реализации несколько большим уровнем абстракции и следовательно большой простотой. В моих уроках вам не придётся возится с malloc/free, не придётся лишний раз писать ключевые слова struct/enum и др., вы будете использовать перегрузку и исключения. Но несмотря на это перейти на чистый C, я думаю, вам будет не сложно.

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

  1. Структурное программирование
  2. Практика структурного программирования
  3. Основы ООП
  4. Стандартная библиотека C++
  5. Практика и трюки ООП
  6. WinAPI

А вот дальше сложнее. Есть множество интересных тем, какие из них войдут в этот учебник, какие нет - не знаю. Также не могу сказать, как это всё чудо будет представлено в виде разделов. Вот некоторые темы (не факт, что все они будут в учебнике):

  1. COM технология (надо бы рассказать, вот только не знаю, где это лучше вставить).
  2. MFC (тема обширная, о её нужности можно спорить много, но не знать MFC, прогая на C++ под винду, по-моему, стыдно).
  3. Qt (тема очень перспективная и красивая, тоже объёмная).
  4. DirectX (есть задумки рассказать о некоторых библиотеках даже этого зверя)
  5. Boost (современная классика C++, некоторые библиотеки буста должны быть включены в новый стандарт C++).

Есть ещё множество других мыслей, но пока совсем не оформившихся.

Понятно, что программы вы при изучении писать будете, причём не на листочке в клеточку, как это делается зачастую в вузах. Для написания программы вам потребуются по крайней мере следующие вещи: текстовый редактор, компилятор, компоновщик, необходимые библиотеки. Общий сценарий при этом таков: вы текстовым редактором пишите программные файлы, сохраняете их с соответствующими расширениями, запускаете с командной строки компилятор, а затем компоновщик. На мой взгляд для новичка не самый простой вариант, вдобавок медлительный. Есть такая штука, как среда разработки, которая включает в себя всю эту гадость в наилучшем виде. Вы создаёте проект, добавляете файлы, редактируете их в интегрированном, обычно очень удобным именно для программиста, редакторе, жмёте команду построения проекта и наслаждаетесь результатом. Как вы позже увидите, вдобавок к этому вы получаете массу очень хороших вещей (например, отладчик). Среды разработки чаще называют по другому - IDE (Integrated Development Environment - интегрированная среда разработки). Наверно, со мной согласится по крайней мере большинство, если я скажу, что самая известная виндовая IDE - это Microsoft Visual Studio, именно её рекомендую достать. Но всё же выбор за вами. Приведу список продуктов, достойных внимания (напомню: онли винда)

В начале я пишу название, если это IDE, то в графе IDE вы увидите плюс. В графе компиляторы для IDE вы можете увидеть компиляторы поддерживаемые этой IDE, если компилятор входит в поставку IDE, то в скобках я ставлю знак плюса, для компиляторов в этой графк стоит собственно компилятор. В графе web-сайт я привожу инфу о сайте с которого можно узнать всё о приобретении этого продукта.
Название | IDE | Компиляторы | Web-сайт | Описание | Моя оценка
Visual Studio .NET 2003 | + | Visual C++ 7 (+) Intel C++ 7-9 | Microsoft Developer Network | Неплохая IDE + неплохой компилер. Все навороты хорошей 'буржуйской' IDE: автокомплит, подсветка синтаксиса, браузер кода, отладчик, редактор ресурсов. IDE расширяется за счёт Add-In-ов. Компилятор довольно неплохо соответствует стандарту. IDE платная (около 800-900$). | Для начинающих довольно неплохое решение. Главный минус - платное.
Visual C++ Toolkit 2003 | - | Visual C++ 7 | Microsoft Developer Network | Бесплатный компилятор от Microsoft. Для бедных любителей Visual-а LOVE . | Сама по себе вещь мала полезная, но... (см. ниже)
Visual Studio 2005 | + | Visual C++ 8 (+) Intel C++ 10 (ещё не вышел) | Microsoft Developer Network | Шикарная IDE + компилятор, довольно близкий к стандарту (кроме основного стандарта также поддерживает спецификацию ECMA C++/CLI). Личное впечатление очень неплохое COOL . Хотя претензии есть, но... Главный минус - ещё платнее 2003-ей (сейчас где-то 1000-1100$). Многие современные замечательные платные библиотеки (Qt Comercical, Ultimate Toolbox, BCGControlBar) интегрируются в VS 2005. | Если вам прогать под винду и денег валом, то мастхевная вещь (по-моему). Но даже без соблюдения этих условий (при использовании не совсем официальной версии, про которую вы от меня не слышали) многим (а начинающим, наверно, особенно) будет приятно насладиться качественной работой MS.
MinGW 32 | - | GNU C++ | MinGW | Виндовая версия, наверно, самого известного бесплатного компилятора, чуть ли не стандарта Open Source (сам он поставляется с исходниками на C). | Если вы поклонник Open Source - качать обязательно. Вообще, компилер довольно качественный и неплохой, иметь не помешает. Замечу, что некоторые Open Source библиотеки (например, Qt Open Source) работают под виндой только с MinGW. Для MinGW существует множество IDE (даже аналог никсового KDevelop).
Digital Mars | - | Digital Mars C++ | Digital Mars | Из бесплатных - самый близкий к стандарту компилятор. На сайте также можно скачать STLPort, оптимизированный для Digital Mars C++, и редактор ресурсов. | Для начинающих вряд ли нужен. А вообще компилятор очень хороший.
Dev C++ | + | GNU C++ из MinGW (+/- - в разных редакциях по-разному) | Dev C++ | Пожалуй, вторая по популярности IDE (после VS). Есть подсветка, браузер кода, автокомплит (правда, требует кэширования кода при первом запуске, обычно это минут 5), дебаггер, редактор форм wxWidgets (в некоторых редакциях). Правда, с VS сравнивать как-то даже не удобно... | Вещь хорошая, известная, но (это чисто моё мнение) ничего особо привлекающего в ней нет.
Intel C++ Compiler | - | Intel C++ (текущая версия - 9) | Intel | Платный компилятор от Intel-а, интегрируется в VS (9-ый - от 6-ки до 2003-ей, планируемый 10-ый будет интегрироваться в VS 2005, но не будет в шестёрку), его использование без последней по-моему в природе не встречается. Главное достоинство компилятора - отличная оптимизация и использование (если разрешить это компилеру) самых последних технологий Intel-а. | Действительно нужен для кода, критичного к производительности. Для начинающих не советую (все равно работать будем скорей всего в той же VS, потому проще использовать Visual - возни меньше и документация лучше).
Code::Blocks | + | Visual C++ 7 GNU C++ (MinGW) Digital Mars C++ Open Watcom C++ Borland C++ чуть помучавшись - любой другой | Code::Blocks | Бесплатная оупен сорсная IDE с возможностью работы со множеством компиляторов (правда, чтобы нормально их использовать придётся незначительно повозиться). Есть визарды проектов, браузер кода, подсветка, автокомплит (правда, плоховато работает с макросами), отладчик (на базе gdb из пакета MinGW), редактор форм wxWidgets (в виде плагина). За счёт плагинов IDE можно неплохо расширять. | Вещь хорошая, но на мой взгляд для начала это не лучший вариант, хотя я не уверен.
Comeau C++ Compiler | - | Comeau C++ | Comeau Technologies | Компилятор, провозгласивший своим девизом стандарт C++. Надо признать, что не зря. Компилятор платный, причём не дешёвый. На сайте есть бесплатная онлайновая версия. Вы пишите программу и входные данные, щёлкаете по кнопке и через некоторое время любуетесь результатами. | Достать действительно тяжело (без использования финансовых средств). Онлайновая версия идеальна для проверки некоторого кода на соответствие стандарту. Тем, кто собирается писать большие книги по C++ очень желательно достать этот компилер.

Также вам скорей всего потребуется справочник по C++. Я предлагаю MSDN. Хотя вы можете приобрести также любые другие справочники (вообще говоря - никогда не помешает). Довольно неплохие карманные справочники (печатные) от O'Reilly, у нас в продаже можно найти русский перевод издательства "Питер" (лично видел две книжки: по языку и по стандартной библиотеке STL, последней книжецей сам иногда пользуюсь). Итак, я ориентируюсь, что у вас есть VS и MSDN. Вместе с примерами кода я обычно даю файл проекта для VS. Первый раздел писался на 2003-ей студии. Теперь у меня стоит VS 2005, поэтому ориентируюсь именно на неё.

Вот пожалуй и всё со вступлениями, если вы ещё не бросили этот учебник, то "ПОЕХАЛИ!!!".


Структурное программирование

Существуют много подходов к программированию. Самый старый - последовательный, т. е. каждая инструкция имеет номер, для организации любого нелинейного выполнения используются условные и безусловные переходы. Классика этого жанра - ассемблер. Этот подход интересен и важен, но не для этого учебника. Существует, скажем, функциональное программирование. С его логикой вы позже познакомитесь (библиотеки C++ реализуют концепции функционального программирования). Кстати, некоторые его идеи перекочевали в электронные таблицы (например, MS Excel). Существует логическое программирование. При этом задача программиста составить модель задачи в виде логических следствий. На вход программы подаётся набор фактов и среда исполнения (наиболее известная - ProLog) проверяет истинность какого-либо нового факта из ваших следствий.

C++ прекрасно воплощает в себе два подхода - структурное и объектно-ориентированное программирование. Структурное программирование основано на построение алгоритмов и, следовательно, включает в себя все алгоритмические конструкции: линейные последовательности, ветвление, выбор, циклы. Для построения определённых алгоритмов удобно использовать вспомогательные алгоритмы, представленные в C++ с помощью функций. Все эти понятия рассматриваются в этом разделе. Кроме того рассматриваются основные структуры данных и пользовательские типы: массивы, структуры, перечисления, объединения, битовые поля. Чтобы ваша программа могла общаться с внешним миром наиболее просто в структурном программировании использовать станадртную бибилотеку C времени выполнения (C Runtime Library - CRT). Я, естественно, не рассматриваю целиком всю эту библиотеку (это глупо и неинтересно), я лишь даю сведения о наиболее часто используемых и наиболее важных функциях, представляющих определённые идеи программирования. Поняв их логику, остаётся лишь вооружится справочником (например, MSDN) и всё в ваших руках. В конце мы также научимся делать собственные библиотеки (как статические, так и динамические).

В добрый путь!


Структура C++ - программы

Чтобы понять многие особенности программирования на C++ неплохо знать то, что происходит с нашим проектом на пути рождения экзешника или другого бинарного файла. В C++ существуют два типа файлов: файлы исходного кода (*.cpp, *.c, *.cxx, *.cc) и файлы заголовков (*.h, *.hpp, *.hxx, *.hh). Первые, как ни странно, содержат код C++, вторые же - объявления и препроцессорные директивы. Когда мы запускаем билд (или мэйк, что в принципе то же самое) нашего проекта, то первым вступает в дело компилятор (в VC++ это cl.exe). Ему поочерёдно передаются все файлы исходного кода. В свою очередь компилятор включает в себя препроцессор, который преобразует исходный код перед компиляцией. Препроцессор не добавляет никаких функциональных возможностей и предназначен исключительно для удобства программиста. После обработки текста препроцессором компилятор генерирует для файла исходного кода файл объектного кода (обычно - *.obj). Надо подчеркнуть, что компилятор работает со всеми файлами исходного кода абсолютно независимо. Наконец, вступает в дело линкер (или компоновщик, в VC++ - link.exe), ему передаётся список файлов объектного кода. Линкер создаёт PE-файл (при программировании в винде) и обеспечивает, чтобы адрес загружаемый при запуске PE-файла передавал управление на точку входа программы. Последняя может быть названа явна в виде параметра или по умолчанию (для /SUBSYSTEM:CONSOLE - main, для /SUBSYSTEM:WINDOWS - WinMain).

Немного о работе препроцессора. Препроцессор, в отличие от компилятора, не генерирует новый текст, а анализирует имеющийся и заменяет некоторые блоки текста на другие блоки. В первую очередь он обращает внимания на строки, начинающиеся с решётки (#), эти строки - директивы препроцессора. Директивы могут быть растянуты на несколько строк с помощью обратного слеша (\). Кроме того препроцессор обращает внимание на макросы - т. е. идентификаторы, которые в данный момент объявлены для препроцессора как макросы. Для возможности универсализации текстов программ и заголовочных файлов C и C++, стандарт указывает, что в препроцессоре C++ есть изначально определённый макрос - __cplusplus. Со многими директивами препроцессора вы встретитесь на страницах уроков.

Итак, напишем простейшую программу. В IDE VS .NET выбираем File => New => Project (по умолчанию - Ctrl + Shift + N). Project type: Visual C++ Projects. Template: Win32 Console Project. Пишем имя проекта, давим OK. На вкладке Application Settings ставим флажок Empty project. Finish. На панели Solution Explorer ('Проводник проекта', View => Solution Explorer) выбираем Source Files (файлы исходного кода) и в контекстном меню: Add => Add New Item (Добавить => Добавить новый элемент). Категория - Code, шаблон - C++ File. Пишем имя. Open (или давим Ентер). В открывшимся окне редактирования файла пишем:

int main()
{
    return 0;
}
Таким образом мы получаем функцию main, которая возвращает значение типа int (целое выровненное число, длина определяется компилятором, на всех нормальных 32-разрядных компилерах - 32 бита). И единственный оператор в функции - возврат нуля. Выбираем Build => Build Solution (Построить => Построить Соллюшен). При этом желательно чтобы в диалоге Tools => Options на вкладке Environment => Projects and Solutions в группе Build and Run Options стоял переключатель Save All Changes, это гарантирует вам, что вы не забудете сохранить всё перед билдингом, на той же вкладке желательно поставить галочки Show Output window when build starts (показать панель Output при запуске билдинга) и Show Task List window if build finished with errors (показать панель Task List, если построение проекта завершено с ошибками). Мы получили рабочий экзешник! Можно выбрать Debug => Start Without Debugging (Отладка => Запуск без отладки, по дефолту - Ctrl + F5).

MSDN

  • Compiling a C/C++ Program - Компиляция C/C++ программ.
  • Preprocessor Directives - Директивы препроцессора.
  • Fundamental Types - Базовые типы C++.

Файлы

Проект


Ввод - вывод

Ввод и вывод исторически два очень важных понятия. Большинство языков программирования предоставляют встроенные в сам язык средства для выполнения этих задач. Но не зря C++ - это универсальный, бесконечно настраиваемый и приспосабливаемый язык (как он задумывался авторами). В C++ для реализации ввода-вывода существует во-первых множество документированных стандартом функций, описанных в соответствующих заголовочных файлов, в VC++, по умолчанию, все эти функции берутся из msvcrx.dll или msvcrxd.dll (в дебаг-версии), где x - номер версии, но можно заставить линкер статически засобачить их к экзешке (в настройках проекта). Наиболее важной является стандартная библиотека ввода-вывода, заголовочный файл - stdio.h (в VC++ он обычно расположен в каталоге %VSINSTALLDIR%\VCx\include).

Функция форматированного вывода printf. Функция printf выводит форматированную строку в поток вывода (output stream). Функция принимает один или более аргументов. Первый аргумент - форматная строка, остальные - данные для вывода. По сути будет выведена форматная строка, со вставками отформатированных данных. Для вставки данных используется управляющий символ - знак процента и спецификация формата. Например, %i выведет целое число, %f - вещественное. Можно также указать точность вывода, например %.3f - с тремя знаками после запятой. Подробности смотрите в вашем справочнике.

Поэкспериментируем! Создаём пустое консольное приложение, добавляем файл исходного кода и пишем:

#include <stdio.h>

int main()
{
    int a,b; // создаём две целые переменные
    a = 10; // присваиваем, то есть устанавливаем значение
    b = 5;
    printf("a=%i\nb=%i\na+b=%i\n",a,b,a+b);
    return 0;
}
Отметьте во-первых, что текст после двух слешей и до конца строки игнорируется компилятором. Подобный текст называется комментарием, они могут быть также вставлены в скобках комментариев: слеш и звёздочка - открывающая, а звёздочка и слеш - закрывающая; в этом случае один комментарий может быть растянут на произвольное количество строк. В первой строчке мы задаём директиву препроцессора include. Директива препроцессора - это строки, начинающиеся с решётки. Эта директива (include) по сути просто вставляет текст указанного файла, а файл stdio.h содержит объявления функций ввода-вывода. Файл может задаваться в кавычках или в угловых скобках. В последнем случае он ищется относительно каталогов, прописанных в переменой окружения INCLUDE. Каталоги для всех стандартных библиотек студия прописывает при установке. При записи в кавычках файл вначале ищется относительно каталога с компилируемым файлом, а затем в INCLUDE-каталогах. В нашем случае вместо этой директивы можно написать
extern "C" int __cdecl printf(const char *, ...);
То есть непосредственно объяснить компилятору, что означает имя printf. А именно: это функция, возвращающая целое значение, её аргументы - константный указатель на символы (char - символ) и опциональный список, вызывается (на ассемблерном уровне) функция согласно спецификатору __cdecl (C Declaration), функция внешняя, то есть её реализация находится в другом объектном файле (мол, пусть линкер сам ищет, а компилер умывает руки) и, наконец, компилеру не следует коверкать имя функции (для перегрузки и прочих гадостей), а надо оставить его в C-стиле. Сомневаюсь, что такой способ будет удобным для вас, для этого на самом деле и придумали заголовочные файлы, в них по сути написано множество подобных объявлений (в частности и это). Просто я хотел показать, что include не выполняет каких-либо 'подключений библиотек' (хотя так очень часто говорят, но при этом не имея ввиду собственно подключения библиотек или каких-то модулей в классическом смысле, вроде VB-шного Declare), а лишь копирует текст файла. Строки в C++ записываются в кавычках, а символы (одиночные) - в апострофах (позже вы увидите принципиальное отличие между строками и символами). Строку можно разорвать на несколько файловых строк, при этом каждая часть строки обрамляется кавычками (на каждой строке). Замечу также, что обратный слеш в строке в C++ (как и в большинстве языков) используется для формирования так называемых escape-символов (\n - новая строка, \r - возврат каретки, \t - табуляция, \0 - ноль - символ, \\ - бекслеш). Происходит их формирование на этапе компиляции, в этом их важное отличие от процентов в форматной строке. Кроме того замечу, что в форматной строке можно задать ширину поля, некоторые дополнительные флаги, точность. Подробно об этом смотрите в MSDN. Возвращает функция printf количество выведенных символов или -1, если что-то у неё не получилось.

Форматированный ввод - scanf. В целом аналогичен выводу. Есть некоторые особенности. Во-первых, чтобы функция могла изменить значение переменных, надо передавать ей адреса переменных, а не их значения, делается это с помощью операции взятия адреса (обозначается амперсандом - &). Во-вторых, если ввод происходит с консоли, то мы можем редактировать вводимую строку. Поэтому функция среагирует только когда мы нажмём клавишу Enter (так уж, впрочем довольно логично, устроены все консоли или терминалы). И ещё, если мы задаём в форматной строке одно, а вводим совершенно другое, то функция изменяет значение только тех переменных, для которых это ещё возможно (согласно строке). Приведу пример:

#include <stdio.h>

int main()
{
    int a,b;
    scanf("%i %i",&a,&b);
    printf("a=%i\nb=%i\na+b=%i\n",a,b,a+b);
    return 0;
}
Если мы введём '45dfhg', то значением a станет 45, а значение b будет заранее неизвестным (что в ОЗУ на момент выделения памяти под b было, то и получится).

Напоследок о фундаментальности понятий ввода-вывода. В UNIX можно было при запуске программы перенаправить потоки ввода-вывода, эта возможность перекочевала в MS-DOS и следовательно и в Windows. Создайте в папке с экзешкой следующий батник:

имя_экзешника < in > out
Создайте в той же папке файл in и запишите в него через пробел два целых числа. Запустите батник. Полюбуйтесь на содержимое файла out! Замечу, что аналогично работает технология cgi на Web-серверах. Это особенно заметно при использовании языков, не ориентированных на web (Perl, C, C++ и др.).

MSDN

  • Format Specification Fields: printf and wprintf Functions - Поля спецификации формата.
  • printf - Функция printf.
  • scanf - Функция scanf.
  • Stream I/O - Потоковый ввод-вывод в C/C++.

Файлы

Проект 1
Проект 2


Алгоритмические конструкции. Массивы

Как и у любого языка программирования (за исключением языков логического программирования, предназначенных для решения определённых задач, а не создания программ) в арсенале C++ есть набор инструментов для создания структуры алгоритма программы. Перечислю во-первых алгоритмические конструкции: if (неполный условный оператор), if - else (полный условный оператор), while (цикл с предусловием), do - while (цикл с постусловием), switch (выбор), for (цикл по типу while, но более красивый). Если нужно в алгоритмических конструкциях пользуются блоками, ограничивающимися фигурными скобками. Важное замечание: переменные, объявленные в блоке, после выхода из него не действительны (память, занимаемая ими освобождается, что вполне логично, хотя в Паскале это не верно).

Во-первых, о типичных конструкциях:

  • if (условие) команда. Команда будет выполнена, только если условие верно.
  • if (условие) команда1 else команда2. Будет выполнена одна из команд, в зависимости от условия.
  • while (условие) команда. Команда будет циклически выполняться, пока верно условие, условие проверяется при входе в цикл.
  • do команда while (условие). Аналогично, но условие проверяется при выходе (т. е. цикл точно будет выполнен один раз).

Условия в простейших случаях образуются с помощью операций сравнений: <, >, == (равенство), != (неравенство), >= (больше или равно), <=. Результат сравнения имеет логический тип - bool (в чистом C - тип int). Этот тип может принимать два значения: true (истина) или false (ложь). Допустимо неявное преобразование из целочисленного типа в логический, при этом ложь получится только из 0.

А теперь поподробней о switch и for.

Конструкция switch.

После switch в скобках указывается выражение, результат которого имеет регулярный тип данных, то есть тип для которого определены все операции сравнения, упорядоченный тип, вещественный тип к таковым не относится, т. к. сравнение для него реализуется на самом деле по приближённым алгоритмам, а про упорядоченность я вообще молчу (какое значение будет следующим после 1.3?). Далее следует блок вариантов, каждый вариант оформляется в виде case <константное выражение>:. Можно через запятую перечислить несколько веток. В конце также может быть ветка default:. Важно заметить, что default ветка будет выполнена всегда и вообще после выполнения любого case будут выполнены проверки остальных веток (это связано с ассемблерной реализацией switch). Классическое решение - break. Пример будет ниже.

Цикл for.

Общий вид:

for (оператор_инициализации; оператор_проверки_входа_в_цикл; оператор_итерации) оператор_цикла;
Любой из операторов может быть пустым. По сути for не вносит ничего нового по сравнению с while (за исключением бесконечного цикла), но любой сишник будет восхищаться красотой такой строки:
for (a = malloc(strlen(b)); b; *(a++) = *(b++));
К слову: эта строчка создаёт a - копию строки b и ставит указатель на её конец средствами чистого C. По эффективности абсолютно равна функции strcpy, но при этом не нужен вызов функции! Операция, обозначаемая двумя плюсами обозначает операцию инкремента, т. е. увеличения на 1, аналогично можно уменьшать на 1 (декрементировать). Есть постфиксная и префиксная формы инкремента и декремента (a++ и ++a), первая возвращает полученное значение(теоретически, выполняется на 2-3 такта быстрей), а вторая - начальное. Более приземлённый пример - заполнение массива a числами от 0 до 9:
for (int i = 0; i < 10; a[i++] = i);
И ещё, цикл по записям MFC-шной БД:
for (rs.MoveFirst(); !rs.IsEOF(); rs.MoveNext()) // rs - RecordSet
{
    // здесь обработка значений, извлекаемых с помощью rs.GetFieldValue
}

Особенно красив for именно для подобных операций, т. е. для итераций (грубо говоря, пробега) по коллекции данных, отличной от массива. Сосредоточение логических этапов цикла в одном операторе при должной привычке придаёт программе значительную красоту и наглядность, хотя while тоже не надо забывать!

Операторы, используемые для создания структуры алгоритма. Во-первых, старый добрый goto. Через пробел указывается метка. Объявляется метка перед любым оператором и от последнего отделяется двоеточием. Вообще, этот оператор принято считать нецензурной лексикой, т. к. он нарушает внешнюю понятность структуры программы. Хотя как и последняя иногда бывает к месту... Во-вторых - break. Осуществляет выход из цикла или из switch. Именно с break-ом используются бесконечные циклы, именно break позволяет грамотно использовать switch:

switch (a)
{
    case 0:
        // a = 0
        break;
    case 1:
        // a = 1
        break;
    case -1:
        // a = -1
        break;
    default:
        // всё прочее
}
Последний оператор - continue. Используется только в циклах (while, do-while, for). Завершает текущую итерацию и переходит к следующий. Следующие два примера аналогичны:
// 1
while (condition)
{
    // операторы 1
    continue;
    // операторы 2
}

// 2
while (condition)
{
    // операторы 1
    goto end_of_cycle;
    // операторы 2
    end_of_cycle: ;
}

Теперь о массивах. Спешу заметить, что массивы не есть новый тип данных (если, конечно, это не 'массив', реализованный в виде класса, вроде STL-вского vector), а последовательность данных одного типа. Если у нас есть массив a, то на самом деле в самой ячейке a хранится адрес начала массива, когда мы пишем a[n], мы обращаемся к значению, лежащему по адресу a+n (в C++, как и в большинстве языков, нумерация массива начинается с 0, это эффективней с точки зрения реализации машинного кода и привычней для большинства программистов). Поэтому запись int a, b[5]; создаст целую переменную a и массив b, состоящий из 5 целых элементов. Массив можно сразу проинициализировать:

int a[5] = {1, 2, 3, 4, 5};
Надо сказать, что если мы сразу инициализируем массив, то размер указывать не обязательно (просто квадратные скобки). Можно создавать двумерные и, вообще говоря, n-мерные массивы (массивы массивов):
int a[5][4];
int b[3][2] = {{1, 2}, {3, 4}, {5, 6}};
int c[3][2] = {1, 2, 3, 4, 5, 6}; // так тоже можно - компилятор автоматически определит границы по указанным размерам
int d[3][] = {1, 2, 3, 4, 5, 6}; // так нельзя, можно не указывать только самый внешний размер
int e[][2] = {1, 2, 3, 4, 5, 6}; // нормально

Строка - это массив символов. Символ - это либо байт, либо слово (Unicode). В первом случае (классическом) - тип char. Допустим, в программе мы часто просим: "Please, input a", "Please, input b" ... Эффективней (и проще модифицировать затем строку-приглашение) сделать так:

const char str[] = "Please, input %s";
// ...
printf(str,"a");
// ...
printf(str,"b");
Слово const заставляет компилятор заботится, чтобы мы не поменяли значение переменной. Хотя, надо признать, что и без использования переменной-строки оптимизатор VC++ 'исковеркал' бы наш код. Ещё замечание насчёт массивов - многомерные массивы. По сути представляют собой массивы массивов, а это значит, что, скажем для двухмерного массива, мы будем иметь адрес блока, где содержатся адреса блоков, в каждом из которых содержатся элементы.

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

#define ARRAY_SIZE 10
// ...
int a[ARRAY_SIZE]
// ...
for (int i = 0; i < ARRAY_SIZE; ++i)
{
// ...
}
Директива define определяет препроцессорное макро (в данном случае без параметров). Везде, где препроцессор встретит это макро, он заменит его на некторый текст (в данном случае - 10). Удобно. Кстати, существует директив undef, которая отменяет определение макро. В VC++ многие макро могут определяться в командной строке, передаваемой компилятору и значит в свойствах проекта. На этом основаны многие фичи VC++. Существует в VC++ конфигурации проекта. Установить активную можно с помощью Build => Configuration Manager. По умолчанию их две: Debug (отладочная, в которой выполняются многие проверки и др.) и Release (оптимизированная для конечного пользователя). В дебаг-конфигурации определяется макро _DEBUG. Напишем такой код:
#ifdef _DEBUG
printf("Debug Configuration\n");
#else
printf("Release Configuration\n");
#endif
Директивы ifdef (если определено), else, endif - директивы условной компиляции кода. То есть в дебаг версии будет скомпилировано одно, а в релиз - другое. Это позволяет организовать в дебаг-версии вывод ответственных переменных в нужное время, в то время как в релизе этого не будет совсем (в смысле в самом экзешнике). Кстати сравните размеры дебаг и релиз экзешек.

В файлах к этому уроку, так сказать, для практики, представлен пример реализации простейшей сортировки массива. Кроме того там приведён пример использование указанных директив препроцессора, директивы error одной из возможностей директивы pragma в VC++ (не забудьте прочитать окошко Output).

MSDN

  • Statements (C++) - Ссылки на разные конструкции C++.
  • The #if, #elif, #else, and #endif Directives - Условная компиляция.

Файлы

Проект


Функции. Ссылки. Указатели

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

тип_возвращаемого_значения имя_функции (параметры_через_запятую)
{
    операторы
}
Для возврата из функции значения используется конструкция return. После которой указывается выражение, значение которого необходимо вернуть. В качестве типа возвращаемого значения широко используется void, то есть пустой тип, в этом случае конструкция return необязательна, хотя её можно использовать, не указывая возвращаемое выражение.

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

Допустим, что нам очень часто в программе необходимо находить модуль чисел. Безусловно в рантайм-библиотеки VC++ (msvcr71.dll) есть уже такая функция, но мы напишем свою:

int abs(int a)
{
    if (a >= 0)
            return a;
     return -a;
}
Теперь мы спокойно можем использовать эту функцию. Теперь допустим, что эту зверскую функцию мы написали, отладили и нам не хочется при каждой компиляции тратить время на компиляцию этой функции текст которой мы уже не меняем (мы меняем теперь собственно программу). Для этого функция должна храниться в отдельном cpp-файле и при сборке (build) проекта не будет генерироваться новый объектный файл для cpp - файла с функцией (если мы, конечно не изменим сам файл с кодом). Выбираем в Solution Explorer пункт Source Files и в контекстном меню Add => Add New Item. Выбираем C++ File. Пишем имя, скажем funcs. Open. Пишем туда наш текст функции. Пишем в main.cpp что-то вроде этого:
#include <stdio.h>

int main()
{
    int a,b;
    const char s_i[] = "Please, input number #%i: "; // Пожалуйста, введите число №%i
    const char s_o[] = "Absolute value of number #%i = %i\n"; // Абсолютное значение числа №%i = %i
    printf(s_i,1);
    scanf("%i",&a);
    printf(s_i,2);
    scanf("%i",&b);
    a = abs(a);
    b = abs(b);
    printf(s_o,1,a);
    printf(s_o,2,b);
    return 0;
}
Собираем проект. И тут компилятор ругается, что, мол, он не нашёл, что такое abs. Объясняется это очень просто, достаточно вспомнить, что компилятор со веми файлами проекта работает независимо. Здесь в пору вспомнить о директиве include. Только не вздумайте написать #include "funcs.cpp". Это сработает, но фактически мы опять будем компилировать обе функции (и main, и abs)! Что нужно знать компилеру об abs, чтобы он смог сгенерировать объектный код для main? А ему необходимо знать просто имя функции, тип возвращаемого значения, типы и порядок аргументов. Ему абсолютно не нужна реализация функции и (в принципе) ему плевать как вы назвали параметры (его задача - сгенерировать в загон параметров в правильном порядке в стек или регистры, зачем ему имена?). Первое решение - напишем в main.cpp где-нибудь до функции main такой текст: int abs(int);. Это так называемый прототип функции. Собираем проект. Работает! Теперь допустим мы решили изменить функцию, чтобы она работала с вещественными числами. Открываем func.cpp и в первой строчке пишем float abs(float a). Билд. И тут мы видим, что теперь заругался линкер. Почему? Дело в том, что компилятор, когда мы ему не предоставляем определения (defenition) функции, а лишь её объявление (declaration), делает в .obj файле пометку, что есть тут внешняя ссылка. Задача линкера - связать (слинковать) всё воедино и разрешить внешние ссылки. Но он не в состоянии этого сделать, так как ни в одном (из двух) объектных файлов он не находит реализации целочисленной функции abs с целочисленным аргументом. Чтобы быть уверенным в том, что внешних ссылок на функции не получится, лучше всего использовать файлами заголовков. Добавляем к проекту новый элемент в категорию Header Files. В качестве шаблона указываем Header File. Пишем имя funcs (в принципе, можно написать любое другое, но есть ужасно хорошая традиция - имена файла с объявлением и соответствующего файла с реализацией должны совпадать). В этом файле пишем float abs(float);. Или даже float abs(float a);. Последнее по сути не отличается от первого, но проще читается, плюс в качестве бонуса более красивая работа всяких auto complete, code competitions и других фич редактора Visual Studio. Вставляем директиву #include "funcs.h" в файлы funcs.cpp и main.cpp. Надо заметить, что в первом файле в данном случае она не играет никакой роли, но всё же будем привыкать приинклюдивать объявления к реализации (это важно уже для функций с дефолт-параметрами и тем более для классов). Билдим. Замечу, что реальное преимущество использования include мы получим, если будем использовать наш abs в нескольких файлах кода (cpp).

Кстати, я не зря захотел поставить float. Теперь мы впервые, что говорится в лоб, столкнулись с преобразованием типов, в данном случае с неявным. В одном случае из int в float (при передаче аргумента), в другом - из float в int (присваивание). Важно помнить, что преобразование выражения к какому-либо типу - это операция. Записывается так: (тип)выражение. Иногда выражение надо указать в скобках (порядок операции преобразования почти самый приоритетный). К этому сишному оператору C++ добавляет ещё некоторые свои, которые приобретают смысл для указателей на объекты, когда имеет значение иерархия наследования. Преобразование типов также может быть вызвано арифметическими операциями над двумя операндами разного типа. В этом случае по-возможности оба типа приводятся к int (если это возможно без потерь), иначе действует иерархия: int - unsigned int - long - unsigned long - float - double - long double. (unsigned - невыровненный тип, long - длинное целое, float - вещественное, double - вещественное двойной точности). Надо заметить, что в Visual C++ (и в большинстве 32-разрядных компилеров) int совпадает с long, unsigned int совпадает с unsigned long, а double совпадает с long double.

Частенько нам хочется поменять местами две переменные (пример - сортировка массива). Напишем для этого функцию:
void swap(int a, int b)
{
    int h = a;
    a = b;
    b = h;
}
Если мы напишем программу, использующую эту функцию, и протестируем её, то увидим, что она не работает. Почему? Всё очень просто. Для передачи аргументов в функцию как временное хранилище используется общий стек или регистры, затем аргументы выгружаются в стек функции и используются как локальные переменные. В итоге, испортить переменные в функции не получится (мы испортим локальные переменные этой функции). Есть два выхода. Первый использовать ссылки. Переменная-ссылка - это синоним другой переменной. Внутри кода объявляются так:
тип& имя_ссылки = имя_переменной_или_другой_ссылки;
Объявление ссылки без знака '=' невозможно (глупо иметь ссылку в никуда). В аргументах функции ссылки объявляются аналогично, но без знака '=', т. к. привязка ссылки происходит при вызове функции. Исправляем:
void swap(int &a; int &b)
Так сработает. Есть другой способ. На асм-уровне они подобны, но синтаксис языка другой. Это указатели. Указатели - это одно из самых больших преимуществ и недостатков C++ над другими языками! Это то, за что его так любят все сишники и ненавидят джависты! Всё это - указатели. Указатель - это переменная, хранящая адрес памяти, её размер не документируется стандартом и может быть разным в зависимости от платформы, оси и компилера. Для объявления указателя используется символ *:
int *p1;
int a = 10;
int *p2 = &a; // в p2 будет адрес a, & - операция взятия адреса
int p3 = p2; // p3 указывает тоже на a
int b = *p2; // в b будет значение a, * - операция разыменования указателя, взятия значения по определённому адресу
int c = *(int*)(0x00ff00ff); // 0x - 16-ричная запись, здесь используется преобразование целочисленного адреса к указателю на int
В последнем случае мы по сути пытаемся прочитать значение по заданному адресу, скорее всего мы получим вылет программы (при выполнении) и сообщение, что память по адресу 0x00ff00ff не может быть прочитана. Просто хотел показать принцип. Замечу, что приведение (преобразование) типов можно использовать не только для указателей. С помощью указателя можно управлять памятью. Для этого используются операторы new (выделение памяти) и delete (освобождение). Следует запомнить, что любую память которую мы взяли, обязаны отдать, иначе - не гуд. Существует две формы этих операторов. Во-первых для выделения/освобождения отдельной ячейки: new тип, delete указатель. Пример:
int *a = new int; // берём
// пользуемся
delete a; // отдаём
Во-вторых - целого блока: new тип[размер_блока], delete[] указатель. Таким образом мы получаем динамические массивы.

Существует также особый указатель - указатель на void, т. е. указатель на неизвестный (пустой) тип данных. К таким указателям нельзя применять операции new/delete (т. к. неизвестен размер выделяемого блока) и операции сложения/вычитания (см. ниже). Такие указатели используются только как универсальные промежуточные указатели. Единственная применимая к ним операция - сравнение с другими указателями.

Напомню, что функция scanf использует как раз указатели. Кстати, символы * и & вовсе не обязательно 'прилепливать' к переменной (как я писал), они могут располагаться в любом месте между типом и именем. В качестве примера работы с динамическими массивами прилагается та же сортировка, но количество элементов заранее неизвестно. Плюс задача разбита на функции. Кстати, о передачи массивов в функцию. По-сути, как я говорил массивы - это тоже указатели (разница между статическими и динамическими: в одном случае данные располагаются в стеке для функций (stack) или в статической области для глобальных массивов (static segment), а во втором - в куче (heap), т .е. в памяти произвольного доступа). Но как вызываемая функция поймёт сколько элементов в массиве? Никак. Поэтому, чтобы мы обработали весь массив и не возникло ошибки доступа к памяти (какая досадная ошибка!), следует в функцию передавать также размер массива.

Итак, переменная, являющаяся массивом - это указатель на первый элемент массива. Для обращения к произвольному элементу можно к указателю прибавлять или вычитать целочисленные значения. Рассмотрим пример:

int *pi = new int;
char *pc = (char*)pi;
if (pc == pi)
    printf("+\n");
else
    printf("-\n");
if ((void*)++pc == ++pi // приведение к void* для возможности сравнения абстрактных указателей, второй будет приведён не явно
    printf("+\n");
else
    printf("-\n");

Как ни странно, вы увидите вначале плюс, а затем минус. Дело в том, что когда мы увеличиваем указатель на 1, мы не увеличиваем адрес на 1, а переходим к следующему элементу массива (т. е. увеличение будет выполнено пропорционально размеру одного элемента данного типа). Более того по стандарту прибавление (или вычитание) чисел к указателям не определено вне пределов массива. Хотя большинство компилеров позволяют это делать (это не противоречит стандарту, по стандарту - не определено), из-за удобств внутренней реализации. Посмотрите второй пример:

int *pi = new int;
char *pc = (char*)pi;
if ((void*)pc == pi)
    printf("+\n");
else
    printf("-\n");
pi = (int*)((unsigned long)pi + 1);
pc = (char*)((unsigned long)pc + 1);
if ((void*)pc == pi)
    printf("+\n");
else
    printf("-\n");

Теперь мы увидим оба плюса, так как прибавление 1 применяется к обычному числу (unsigned long), т. е. к самому адресу.

MSDN

  • Functions(C) - Функции в C.
  • new Operator (C++) - Оператор new.
  • delete Operator (C++) - Оператор delete.

Файлы

Проект 1
Проект 2


Строки

Безусловно, строку простым (т. е. базовым) типом не назовёшь, но тем не менее обработка строк - задача очень часто встречающаяся на практике. Ранее было сказано, что строка - это массив символов. Но часто размер строки может меняться, использовать только динамические массивы, которые надо постоянно пересоздавать - это глупо. Поэтому обычно заранее резервируют столько памяти, сколько хватит для хранения строки, при необходимости, конечно, можно с помощью вспомогательной временной строки увеличить размер. При таком подходе необходимо чем-то ограничивать фактический размер строки. Для этого есть два подхода. Во-первых можно непосредственно хранить длину строки (выделив для этого определённое количество байт), так реализованы паскалевские string. Единственное преимущество этого подхода - узнать длину строки можно чрезвычайно быстро. Недостатки: длина строки строго ограничена (в Паскале строка типа string должна иметь длину не более 256 символов в 8-битной кодировке и 128 - в Юникоде), более медленная работа большинства функций (поиск символов и подстроки, конкатенация и др.). В стандартном наборе функций языков C и C++ есть функции для работа со строками, признак конца которых - ноль-символ. В принципе никто не запрещает вам написать класс, реализующий строку по первому подходу, мощь C++ позволяет красиво это сделать, но на мой взгляд смысл данного действия отсутствует.

Функции для работы со строками, ограниченными ноль-символом (также называемыми строками в C-стиле), проще всего объявить, подключив заголовок string.h. Рассмотри некоторые функции:

  1. char *strcpy( char *strDestination, const char *strSource) - двоично-безопасное копирование строк. Настоятельно рекомендую при инициализации строк пользоваться именно этой функцией, иначе последствия непредсказуемы (есть исключение - инициализация константных строк, её эффективней проводить простым присваиванием). Функция принимает два параметра: указатель на строку, в которую копируем, и константный указатель на строку-источник (константность гарантирует, что функция не изменит нашей строки). Возвращает функция первый указатель, в большинстве случае он не используется, но иногда полезен при цепочных операциях (несколько операций с одной строкой). Замечу, что strpcy только копирует строку, но не выделяет память под неё, это вы должны сделать заранее. Типичная ошибка - попытка 'присваивания' одной строки другой:
    #define MAX_STR_LEN 1000
char *s1 = new char[MAX_STR_LEN];
strcpy(s1,"simple string");
char *s2 = s1; // получим два указтеля на одну область памяти
char *s3 = new char[MAX_STR_LEN];
s3 = s1; // потеряем укаатель на область памяти, получим утечку памяти
  2. size_t strlen( char *string) - определение длины строки (без учёта ноль-символа). Тип size_t - это платформозависимый тип, для хранения размеров. Обычно совпадает с типом int (в 64-битных компилерах совпадает с типом long long - длинное-длинное целое).
  3. char *strcat( char *strDestination, const char *strSource) - конкатенация (присоединение) строк. Возвращает указатель на первую строку.
  4. int strcmp( const char *string1, const char *string2) - сравнение (согласно кодировке) строк. Возвращает значение меньшее 0, если первая строка меньше второй, 0 - если строки равны и значение большее 0, если первая строка больше второй. Типичная ошибка сравнения строк:
    char *s1, *s2;
// ...
if (s1 == s2) // сравниваются указатели, а не символы
  5. int strcoll( const char * string1, const char * string2) - сравнение строк с учётом локализации (кодировки, отличной от умолчальной). Для установки локализации используйте функцию setlocale (заголовок - locale.h). Последняя принимает два аргумента - второй указывает локализацию, а первый - область действия функции (функция может влиять на форматирование чисел, времени, дат и т. д.). Замечу, что, если нам нужно лишь узнать равны ли две строки (какая меньше, какая больше - нас не интересует), то смысла в использовании strcoll нет.
  6. int stricmp( const char * string1, const char * string2) - сравнение строк без учёта регистра (фактически, будут использоваться временные строки, приведённые к нижнему регистру).
  7. char * strchr(const char * string, int c) - поиск первого вхождения символа c в строку. Возвращается указатель на этот символ, можно интерпретировать этот указатель как часть строки от этого символа и до конца. Если символ не найден возвращается 0. При использовании указателей 0 означает указатель на несуществующую область памяти, для повышения читабельности стандарт рекомендует использовать продефиненную (#define) константу NULL (кстати при компиляции для C, NULL определяется как (void*)0).
  8. char * strstr( const char *string, const char *strSearch) - поиск первого вхождения строки strSearch в строке string. Возвращает либо указатель на часть строки string, либо NULL.
  9. char * strpbrk( const char * string, const char * strCharSet ) - поиск первого вхождения одного из символов строки strCharSet в string. Возвращает или указатель, или NULL.
  10. char * strlwr(char * string) - конвертирование строки в нижний регистр. Возвращает указатель на нашу строку.
  11. char * strupr(char * string) - то же, но в верхний регистр.
  12. char * strset(char * string, int c) - заполняет строку string символом c.
  13. char * strrev(char * string) - переворачивает строку и возвращает указатель на неё.
  14. Существует ещё функции strncpy, strncat, strncmp, strncoll, strnicmp, strnset. От соответствующих функций без буквы n отличаются наличием дополнительного параметра (последнего) - целого числа, ограничивающего число символов, участвующее в операции.

Замечу, что в ms-шном рантайме (msvcr71.dll) некоторые функции объявлены с подчёркиванием в начале (это не всегда так было и в 2003-ем VC++ в string.h у соответствующих функций без подчёркивания сделана пометка, мол это из библиотечного файла 'старые имена' - oldnames.lib). Понять глубинный смысл этих тайных посланий не каждому дано и я рекомендую пользоваться функциями, декларированными стандартом - без подчёркивания.

Вывод строк осуществляется очень просто: printf("%s",s). Заманчиво написать так: printf(s), но в этом случае может получиться, что s содержит символ процента и printf будет пытаться обратиться к несуществующей области памяти (чтобы узнать, что же ей подставить вместо процента), а точнее к памяти к которой нет доступа. Весёлый итог - Access Violation (дословный перевод - 'Насилие над доступом'). Более интересна проблема ввода строк. Насчёт этого я кинул линки на MSDN (там рекомендуется всегда задавать максимальную длину строки), я же рекомендую для простого ввода строки пользоваться функцией gets (заголовок - stdio.h), она принимает и возвращает указатель на строку (возвращаемое значение обычно не используется), память под строку должна быть выделена заранее. Хотя иногда удобен и scanf. Ещё одна полезная функция при работе со строками - sprintf (объявлена в stdio.h). Её отличие от printf в дополнительном первом аргументе - указателе на строку, куда будут записаны отформатированные данные.

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

MSDN

  • String Manipulation (CRT) - Строковые функции.
  • gets, _getws - Функция gets.
  • scanf Width Specification - Ввод строк в scanf.

Файлы

Проект


Константы

Предваряя урок, вспомню один случай. Дело было на форуме по C++ Builder (почему вопрос попал именно в эту ветку - претензии к его автору). Уж не помню точно, но суть в том, что была какая-та функция, возвращающая int, пусть это будет IntFunc без параметров (там вроде была какая-то из стандартных функций, причём с параметрами). Была такая строка:

float f = IntFunc() / 10;
Так вот, спрашивают: 'Почему когда IntFunc возвращает число меньшее 10 я получаю в f (типа float) значение 0?'. Занавес.

Итак, в этом уроке я хотел бы осветить 2 темы: символьная запись констант (так называемые литералы) и грамотное применение и понимание ключевого слова const.

Задумывались ли вы какого типа, скажем, 12? Большинство правильно ответят - int. Но почему именно int, а не скажем long, или short, или char. И как достичь, например, именно long? Для этого существуют определённые правила. Рассмотрим основные из них, за подробностями, как всегда, отсылаю к MSDN.

  1. Любая числовая константа, не имеющая точки имеет тип int - 12
  2. Числовая константа с точкой имеет тип double - 12.0
  3. Для задания типа float вместо double следует добавить в конце букву f - 12.0f
  4. Для задания типов long и long double (последнее в современных компилерах аналогично double) нужно тыкнуть в самом конце l - 12l или 12.0l.
  5. Для записи невыровненного целого ставим букву в конце (но перед l, если таковая есть) u: 12u или 12ul
  6. Для записи целого числа в восьмеричной системе ставим вначале числа 0 - 012 (=10 в десятичной)
  7. Для записи хексом ставим 0 и x - 0x12 (=18 в десятичной)
    Замечу, что все буквы в литералах необходимо писать в нижнем регистре (по крайней мере по стандарту), но при записи шестнадцатеричного числа буквы a, b, c, d, e, f разрешается писать в любом регистре (опять-таки по стандарту, за глюкавые компилеры не ручаюсь)
  8. При записи строки в кавычках компилятор преобразует её к C-стилю (нуль-строка), затем запишет её в исполняемый файл, а в выражение подставит её относительный адрес (смещение). Под виндой строки обычно кидаются в секцию .rdata PE-файла. Следует подчеркнуть, что эта секция не имеет атрибута WRITE, к этому я ещё вернусь в следующих уроках.
  9. Для записи символа или строки в юникоде (также можно встретить сочетание wide string, в вольном переводе - 'мировая строка') следует добавить вначале большую L - L'с' или L"Это строка (здесь можно написать ещё что-нибудь по-китайски)".
  10. Для вставки специальных символов в строку используются escape-последовательности: \t (табуляция), \r (возврат каретки), \n (новая строка), \b (забой последнего символа, применение весьма сомнительно), \" (кавычка), \' (апостроф), \\ (обратный слеш)
  11. Для вставки в строку символа от 0 до 7 пишут: \цифра
  12. Для вставки в строку произвольного байта пишут: \0xhex-код

Теперь думаю понятно, что следовало писать

float f = IntFunc() / 10.0f;
Замечу также, что при необходимости не стесняйтесь пользоваться наглым преобразованием типов:
float f = (float)IntFunc / 10;
Теперь про const. Чем отличаются следующие две строки:
int i1 = 5;
const int i2 = 5;
Многие относятся к ним 'по-паскалевски', считая i1 переменной, а i2 константой. Это понятие слишком абстрактно для чёткого C++ и слишком бессмысленно. Итак, и i1, и i2 абсолютно одинаковы для операций чтения и инициализации (некоторые ошибочно называют инициализацию также присваиванием). Значение i2 с гарантией всегда будет иметь один и тот же адрес, выделение/освобождение памяти происходит по всем законам переменных C++. Но вот изменить значение i2 средствами языка невозможно. Но при этом этот контроль происходит на уровне компилятора, а не процессора (как строки из секции .rdata) и тем более ОСи. Это важно. Спешу заметить, что этот контроль стандартом продуман очень хорошо (в отличие, например, от того же прославленного своей кривостью Borland Pascal, он же Borland Object Pascal, он же Borland Delphi), в пределах стандарта мы не в силах никак изменить переменные, дополненные описателем const, это возможно только с помощью различных расширений языка, в VC++ - только вставка ассемблерных инструкций (MASM32), и то с майкрософтским оптимизатором это довольно муторно.

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

Особая тема - константные указатели. Дело в том, что указатель может быть константен с двух сторон: указывать всегда в одно и то же место или указывать куда угодно, но не иметь права изменить значение, на которое указывает. Соответственно описание:

const int * p1;
int * const p2 = p1;
Почему я так упорно не называю это всё константами? Всё очень просто, константами принято называть 3, 12.34, 'a' и т. д. Они обычно хранятся временно и только в регистрах, не залазея в ОЗУ. Другое дело - константные переменные. Рассмотрим типичное неграмотное применение const:
const double pi = 3.1415926;
double r, s;
printf("Please, input radius...\n");
scanf("%f\n",r);
s = pi * r * r;
printf("s = %f\n",s);
В данном случае нам нужна именно константа, поэтому правильней писать именно константу. Но, допустим, к этой константе мы обращаемся много раз. Писать одно и то же не хочется. Пишем:
#define PI 3.1415926
и т. д. Вот это грамотно.

Константным может быть всё. Например ссылки. Классическое применение - в качестве аргументов функции. Допустим у нас есть тип MyType (откуда он взялся пока неважно, в следующем уроке мы рассмотрим один из вариантов его появления), причём переменные этого типа достаточно большие. Пусть надо написать функцию, которая извлекает некоторую информацию из переменных этого типа и выводит её на экран. Рассмотрим её прототип:

void MyFunc (MyType)
На первый взгляд всё хорошо. А теперь представьте вызов этой функции - будет произведено глупое копирование большого объёма данных. Я конечно понимаю, что у вас самая быстрая память, самый мощный процессор и вам дороги секунды, потраченные на набор лишнего кода, но... Глюков и тормозов в различном ПО нам и так хватает, пишем:
void MyFunc (const MyType &)
Просто и изящно. И хотя внутри программы мы часто забываем про столь хорошее const при написании библиотек и пр. мы обязаны (мне, себе и всем программистам C++) пользоваться const. Многие небезосновательно говорят, что хорошо раскиданное по библиотечным функциям и классам const - один из признаков настоящего программиста C++.

MSDN

  • Literals (C++) - литералы в C++
  • const (C++) - ключевое слово const


Структуры

Рассмотрим небольшую задачу. Пускай у нас есть некая анкета со следующими данными: имя, возраст, пол. Необходимо ввести эту анкету, упорядочить по имени или возрасту и вывести. Удобно представлять всю нашу анкету в виде массива записей (в терминологии БД) или структур (в терминологии C++). Структура представляет из себя набор связанных переменных. Фактически, они связаны тем, что хранятся в виде монолитного блока памяти (по крайней мере так можно представлять для нашей программы, как именно будет реализовывать это ОСя - не наше дело), т. е. для обращения к любому элементу структуры достаточно знать её базовый адрес, хотя спешу подчеркнуть что сама структура указателем не является (в отличие от массивов), поэтому при передаче структур в функции следует использовать ссылки и указатели, если надо - константные. Структура - это новый тип данных (опять таки, в отличие от массива, который есть указатель на не новый тип), поэтому её предварительно надо объявить:

struct имя_структуры
{
        тип1 список_переменных_типа_1;
        тип2 список_переменных_типа_2;
        // и так далее
};
Затем (после объявления типа) имя структуры можно использовать для создания экземпляров нового типа, делается это классическим методом: тип имя. Опишем нашу структуру:
struct FormField
{
        char* lpszName;
        unsigned char nAge;
        bool bIsMan; // Мужской - true
};
Кстати, вы, наверно, обратили внимание, что все имена я начинал с 'лишних' букв. Это так называемая 'венгерская нотация'. Один из её принципов - начинай имена с префиксов, которые позволяли бы определить тип данных. В более-менее больших проектах при должной привычке это действительно удобно, особенно если вспомнить, что MS во всех своих библиотеках (таких как WinAPI, MFC, ATL, DirectX и др.) довольно прилежно следует этому правилу (хотя не следует всё-таки считать это правило каноном, об этом подробней смотрите во второй части). Приведу некоторые префиксы, рекомендованные венгерской нотацией:
  • b - байт (byte) или булевское (boolean), т .е. логическое значение
  • n - целое число, иногда просто число (numeric)
  • u - невыровненное число (unsigned)
  • i - индекс массива или другой коллекции (index)
  • f - вещественное число (float), флаг, битовое поле (flag)
  • l - что-то длинное (long)
  • w - невыровненное слово (word)
  • dw - невыровненное двойное слово (double word)
  • c - символ (character)
  • p - указатель (pointer)
  • lp - длинный указатель (long pointer)
  • lpfn - длинный указатель на функцию (long pointer to function)
  • s - произвольная строка (string)
  • sz - строка, заканчивающаяся нулём (string with zero)
  • m_ - член-переменная класса, иногда структуры (member)
Кроме того некоторые библиотеки вводят свои префиксы:
  • wnd - окно в WinAPI (window)
  • h - хендл в WinAPI (handle)
  • C - класс в MFC (Class)
  • Q - класс в QT (QT Class)
  • I - интерфейс в COM (Interfase)
  • gl - графические функции OpenGL (graphic library)
Вопрос использования/неиспользования венгерской нотации на самом деле сложен. Но для классических MS-шных библиотек (исключение - .Net Frameowrk) ради единства стиля, наверно, всё-таки это будет удобно. Немного по этому поводу мы ещё поговорим во второй части, а сейчас по этому поводу старый анекдот:

Начинающий программист приносит своему шефу огромную порученную ему программу. Шеф смотрит и видит там имена в стиле: a, b, c, ... , aa, ab и т. д. Он говорит: 'Что это за такое! Надо использовать длинные мнемонические имена, а не эту чушь!' Через несколько дней программист исправился: длинное_мнемоническое_имя_a, длинное_мнемоническое_имя_b, ...

Вернёмся. После объявления типа структуры можно объявлять переменные этого типа:

FormField ff1;
FormField ff2 = { "Name", 25, false };
Как видите, структуру можно сразу проинициализировать, правда если структура содержит строки (как в нашем случае) и мы эти строки собираемся изменять, то правильней или отказаться от инициализации, или написать:
char* lpszTemp = new char[MAX_STR_LENGTH];
strcpy(lpszTemp,"Name");
FormField ff = { lpszTemp, 25, false };
При этом заметьте, что освобождать память для lpszTemp не надо, так как она будет освобождена, когда мы будем 'зачищать' структуру, при двойном освобождении одной и той же памяти мы получим access violation, т. к. второй раз мы будем пытаться освободить память, к которой сами не имеем доступа.

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

  1. Разыменовать указатель и воспользоваться точкой.
  2. Написать имя_указателя->имя_поля.
Вот такая стилизованная красивая стрелочка! Кстати, при объявлении структуры можно сразу создать переменные этого типа:
struct FormField
{
        char* lpszName;
        unsigned char nAge;
        bool bIsMan;
} ff;
Можно даже не писать имя структуры, а сразу создать один или несколько её экземпляров, это будут так называемые анонимные структуры.

В файлах к этому уроку прилагается проект задачи, поставленной вначале. Немного разберём его новые моменты. Проект состоит из трёх файлов: main.cpp (главная программа), FormField.h (описание интерфейса работы с полями анкеты, то есть типа поля анкеты и функций для работы с полем), FormField.cpp (реализация функций работы с полями анкеты). Рекомендую начать именно с рассмотрения интерфейса. Довольно удобно вначале писать интерфейс, а затем реализацию, следует привыкать. Можно заметить, что у функций для работы со структурами полей анкеты я использовал префикс FF (FormField), тоже типа цевильно. Ещё одна интересная новинка - параметры по умолчанию. Иногда удобно вызывать какую-то функцию, не указывая значения, некоторых параметров, используя некоторые умолчальные значения. В сишной реализации вызова функций эти параметры должны стоять в конце списка. Для установки умолчальных значений пишется знак равенства и дефолтное значение. Замечу, что если пишется отдельно объявление (declaration), а отдельно определение (defenition), т. е. реализация функции, то дефолтные значения указываются только при определении, иначе получится мнимая перегрузка одинаковых функций, то есть ошибка компилятора. И последнее - оператор '? :', также называемый условным оператором. Это единственный трёхоперандовый оператор в C++, знакомым с MS Excel он напоминает функцию IF (ЕСЛИ в русской версии). Первый операнд интерпретируется как условие, результат вычисления оператора - это второй операнд, если выражение истинно и третий операнд - иначе.

MSDN

  • struct (C++) - Структуры в C++.
  • Conditional Operator: ? : - Оператор '? :'
  • Default Arguments - Параметры по умолчанию в функциях

Файлы

Проект


Работа с консолью, системные вызовы, переменные среды

До сих пор во всех наших проектах мы пользовались интерфейсом командной строки (то есть в ответ на приглашение программы мы набирали команду, жали Enter и ждали результата). Вещь это хорошая и довольно полезная (множество излюбленных утилит программистов использует интерфейс командной строки, как удобный в реализации и чрезвычайно удобной для дальнейшей автоматизации с помощью батников, перловых скриптов, а также WSH (Windows Script Host)), но всё же, не отходя от консоли, хотелось бы развить интерфейс, примером популярнейших программ с неплохим консольным интерфейсом может служить FAR и HIEW. Для этого существует множество функций управления консолью. Во-первых рассмотрим функции консольного ввода-вывода, определённые в заголовочном файле conio.h (console input and output). Этот заголовочный файл не предусматривается стандартом, но есть во всех компиляторах для windows (Visual C++, MinGW (GNU C++ for Windows), Intel C++, Borland C++, CygWin, Open Watcom). Рассмотрим эти функции:

  1. _cprintf и _cscanf - аналогично printf и scanf, но используется консоль вместо STDIN и STDOUTPUT. Замечу, что _cscanf реагирует на любой разделитель, например пробел (а не только Enter), поэтому иногда приходится использовать просто _scanf, кроме того _cscanf после ввода стирает, то что вы написали с экрана.
  2. int _getch() - читает один символ с консоли, при этом он не выводится в консоль, удобно для надписей вроде 'Press any key to continue', а также считывания навигационных и функциональных клавиш. В последнем случае функцию придётся вызвать 2 раза, в первом случае она вернёт значение 0xE0 для навигационных (стрелки, insert, delete, home и т. д.) и 0 для функциональных (F1, F2, F3 ...), а при втором вызове собственно код клавиши.
  3. int _getche() - аналогична предыдущей, но она автоматически выводит считанный символ в консоль.
Иногда необходимо выполнить очистку консоли. Надо заметить, что реализация работы с консолью в разных ОСях разная и потому этих функций нет в стандарте. Позже мы будем использовать для этого функции Win32 API (Application Programming Interface), а пока сделаем по другому. В большинстве ОС есть команда оболочки cls (clear screen), которая очищает активную консоль. Для запуска произвольной системной команды существует функция system, объявленная в stdlib.h (standard library - станадартная библиотека). Рассмотрим сразу ещё несколько функций из этого заголовочного файла:
  1. int system(const char *command) - выполняет системную команду (аналогично команде, выполненной в системном интерпретаторе командных строк или команде Пучк => Выполнить под виндой), заданную в виде нуль-строки command. Эта функция может изменить внешнюю переменную errno типа int. Возможны 2 варианта: строка command задана, либо не задана (NULL). В последнем случае функция возвращает не 0, если найден интерпретатор командных строк (в Windows NT при стандартных настройках - cmd.exe) и 0 - в противном случае (возвращённое значение можно непосредственно использовать как условие). Если команда задана, то возвращается либо значение, являющееся результатом выполнения команды (это значение для своей программы вы можете вернуть из функции main), либо -1 - при ошибке выполнения команды. В последнем случае в переменной errno остаётся тип ошибки. Это может быть одно из 4 значений, продефиненных в stdlib.h:
    • E2BIG - слишком большой список аргументов
    • ENOENT - не найден интерпретатор командных строк
    • ENOEXEC - не найдена команда, либо файл для запуска
    • ENOMEM - ошибка памяти (иногда возникает при попытке windows увеличить размер файла подкачки)
  2. void exit(int status) - аналогично return status в main, но вызов возможен из любой функции.
  3. int atexit( void (__cdecl *func)(void) ) - (не пугайтесь надписи в скобках ;)) функция позволяет установить какую-то функцию, как обрабатывающую завершение программы. Удобно для показа окошек вроде 'Отправить отчёт куда подальше? Или сам дойдёт...'. Устанавливаемая функция должна иметь такой прототип: void (void) (аналогичный, но ныне не модный прототип - void ()).
  4. char *getenv(const char *varname) - считывает переменную среды и возвращает указатель на строку, являющуюся её значением, замечу что переменные среды определены в контексте конкретного процесса, то есть изменив эту строку, вы не измените самой переменной среды (переменной окружения)
Итак для очистки экрана достаточно написать
system("CLS");
В файлах к уроку прилагается усовершенствованный пример предыдущей программы. По F1 показывается хелп, F2 - открывается меню, представляющее собой список команд со стилизованной стрелкой перед выбранной командой, стрелками можно перемещаться по меню, enter-ом - выполнение команды, по F4 - выход. Возможно редактирование конкретной записи. Консольное меню, ввод функциональных клавиш и очистка экрана реализованы в файле conint.cpp, его интерфейс объявлен в conint.h. Ещё, вроде раньше это не использовалось: в for любую секцию можно расширить с помощью запятой. Обычно это не нужно и не очень красиво, просто решил на всякий случай показать.

MSDN

  • Console and Port I/O - Консольный ввод-вывод.
  • system, _wsystem - Функция system.
  • exit, _exit - Функция exit.
  • getenv, _wgetenv - Функция getenv.
  • atexit - функция atexit

Файлы

Проект


Перечисления. Работа с типами (typedef, sizeof)

В прошлом уроке мы использовали некоторый дискретный набор значений для кодов клавиш. Я для этой цели применял константы, реализованные с помощью define. Есть другой способ - использование специального типа, со строго ограниченным набором значений. Эти значения задаются перечислением, отсюда название типа - перечислимый тип. Для объявления перечислимого типа используется ключевое слово enum:

enum имя_типа
{
    список_значений
};
Значения перечисляются через запятую, например:
enum NavigationalKey { nkUpArrow, nkDownArrow, nkRightArrow, nkLeftArrow };
Все значения перечислимого типа по сути становятся целочисленными константами (int), первая из них по умолчанию принимает значение 0, а остальные значения на 1 больше предыдущего, как они будут реализоваться на внутреннем уровне нас не интересует - каждый компилер волен решать так как ему угодно. Дефолтные значения можно явно изменить:
enum NavigationalKey { nkUpArrow = 0x50, nkDownArrow = 0x48 };
Если мы изменили лишь одно значение, то следующее примет значение на 1 большее, заданного нами и т. д. Замечу, что nkUpArrow, nkDownArrow сами относятся к типу NavigationalKey, что вполне логично, так как именно эти значения мы будем присваивать переменным типа NavigationalKey. В любых выражениях допустимо неявное преобразование из перечислимого типа в целочисленный. Для обратной операции необходимо явное приведение типов, при этом переменная перечислимого типа может принять значение, отсутствующее в перечислении, так что будьте осторожны.

Как и со структурами можно сразу создать переменные перечислимого типа и создать анонимные перечисления. Можно также написать что вроде этого:

enum {MyConst1 = 16, MyConst2, MyConst3 = 0x32};
По сути это элегантный способ выделить группу констант, альтернатива использованию define. Но надо не перемудрить, следует помнить, что константы перечисления забивают имена в контексте, где объявлено перечисление (а обычно это глобальный контекст).

Но и при использовании define часто хочется, ввести новый тип, по сути аналогичный уже существующему. Для этого существует оператор typedef, его использование:

typedef описание_типа новый_тип;
Иногда этот оператор удобен также для укорачивания имён типов, например:
typedef unsigned char byte;
typedef unsigned short word;
typedef unsigned long dword;
Также этот оператор используется для создания платформо-зависимых типов. Например, нам нужен тип, способный хранить максимальное по значение, но оптимальный для нашего процессора. Используя, MS-шную спецификацию, можно сделать так:
#ifdef _M_IA64 // Компилеры, следующие MS-шной спецификации, определяют это макро при компиляции для процов IA64
typedef unsigned long long my_int; // long long - ANSI-тип, аналогичен MS-шному __int64
#else
typedef unsigned long my_int;
#endif
Ещё одна интересная задачка - узнать размер занимаемый некоторым типом. Есть две его формы: в первой через пробел (или несколько пробелов) указывается унарное (однооперандовое выражение), а во второй - тип в скобках. Значение возвращается как тип size_t. Замечу, что для статического массива мы получим количество элементов, а для динамического - длину указателя (для 32-разрядных платформ, обычно 4 байта), это не удивительно, т. к. sizeof - оператор времени компиляции, а сколько и где вы выделите памяти по динамический массив компилер заранее не знает.
printf("Size of my_int = %u\n",sizeof my_int);
Проект "Анкета" я переделывать не стал, думаю разберётесь.

MSDN

  • C++ Enumeration Declarations - Перечисления в C++.
  • typedef Specifier - Оператор typedef.
  • sizeof Operator - Оператор sizeof.


Файлы. Потоковый ввод-вывод

Работа с файловой системой и файлами - одна из базовых тем программирования. В этом уроке мы рассмотрим файловый ввод-вывод с использованием потоков (стандартный ввод-вывод), т. е. нам понадобится заголовок stdio.h. Любая ОСя хранит для каждого приложения так называемую таблицу дескрипторов открытых потоков. В ней есть по крайней мере три элемента - стандартный поток ввода, стандартный поток вывода и стандартный поток вывода ошибок. Все эти потоки создаются при запуске приложения. В stdio.h для этих трёх потоков объявляются соответствующие макросы: stdin (standard input), stdout (standard output), stderr (standard error output). Для ваших программ на C или C++ для доступа к файлам используется указатель на структуру FILE, объявленную в stdio.h. О связи этой структуры с дескрипторами вам заботится не надо. Ещё одним важным понятием является потоковый указатель (также называемый файловым указателем (следует не путать его с указателями C/C++). Этот указатель хранит некоторую позицию в потоке. При любой операции считывания или записи он передвигается далее, с помощью специальных функций можно заниматься его позиционированием. В заголовке stdio.h (стандартный ввод-вывод) также объявляется (обычно директивой define) целочисленная константа EOF, изначально означающая End Of File (конец файла), эта константа используется во многих функциях потокового ввода-вывода.

Итак, рассмотрим функции из stdio.h. Замечу, что выделять и освобождать память под структуру FILE не надо, ОСя, либо C Runtime (msvcrt.dll или msvcrxx.dll для винды) сделают это. Во-первых функции для открытия/закрытия потоков.

  1. FILE *fopen (const char *filename, const char *mode). Функция открывает некоторый объект для потоковой работы с ним, обычно это файл. Первый параметр - нуль-строка, обозначающая ресурс потока. В разных ОСях он имеет разную интерпретацию. Рассмотрим его в ОС Windows. Он может представлять собой относительный путь к файлу. При этом в качестве разделителя может выступать как прямой, так и обратный слеш (в последнем случае при записи строки не забудьте поставить 2-ой обратный слеш, т. к. обратный слеш в C и C++ используется для формирования Escape-последовательностей). Может также быть абсолютным путём (с указанием диска) или UNC-путём (с указанием двойного обратного слеша, имени компьютера в сети и имя ресурса, например: "\\\\COMP1\\SharedDocs\\MyDoc.txt"). Наконец таки может быть задан порт ввода-вывода, вроде "COM1" или "LPT1" (кстати именно поэтому в винде невозможно создать файл с подобным именем). Замечу, что максимальная для данной среды и данного компилятора длина строки filename доступна в препроцессорной константе FILENAME_MAX, также объявленной в stdio.h. Теперь о втором параметре, он задаёт режим доступа к открываемому объекту. Во-первых необходимо указать в этой строке букву r, w или a. Первая открывает файл (или иной объект, в дальнейшем буду писать просто файл, т. к. этот случай наиболее используем) для чтения и устанавливает файловый указатель в начало файла. w - открытия файла для записи, если файл не существует, то он создаётся заново, в любом случае всё его содержимое очищается (если быть точным на диске оно остаётся тем же, пока вы не закроете поток или не вызовите функцию записи данных на диск). a - открытие файла для дозаписи, файловый указатель - на конец файла. К любой из трёх букв можно добавить символ +, означающий, что файл открывается как для чтения, так и для записи, так называемый режим обновления (различие между r+, w+ и a+ в позиционировании файлового указателя и очистки или нет предыдущего содержимого). Кроме того можно задать ещё букву t или b, означающие соответственно текстовый и бинарный режимы. В последнем случае содержимое файла будет читаться таким, какое оно есть, в первом же при записи \n будет записано \r\n, а при чтении наоборот. В случае ошибки возвращается NULL.
  2. int fclose (FILE *stream). Закрывает поток, открытый функцией fopen, очищает буфер, выделенный для чтения-записи и освобождает память, выделенную под структуру FILE. При успешном закрытии потока функция возвращает 0, иначе - EOF (простой пример последнего: вы открываете файл на внешнем носителе для записи, пишете некоторую информацию, удаляете носитель куда подальше, fclose возвращает EOF). Замечу, что файлы обязательно надо закрывать. При записи в файл именно в этот момент осуществляется сброс информации из буфера (размер которого различен для разных систем и платформ) на диск. К тому же максимальное количество файлов, которые может открыть приложение ограничено. Кстати это значение доступно в FOPEN_MAX.
  3. FILE *freopen (const char *path, const char *mode, FILE *stream). Функция предназначена для переназначения файловому дескриптору нового значения. Замечу, что её вызов эффективней, чем fclose и затем fopen, т. к. при этом не будет лишний раз освобождаться структура FILE (выигрыш, конечно, зверский, но по крайней мере грамотней, что немаловажно).
  4. int fflush(FILE *stream) - форсирует сброс информации из буфера на диск.
  5. FILE *tmpfile (void) - эта функция не требует никаких параметров и создаёт временный файл, доступный как для чтения, так и для записи, с флагом "wb+". Под виндой этот файл по умолчанию создаётся в корне диска, на котором расположена ваша программа. Максимально допустимое количество временных файлов ограничено TMP_MAX. Этот файл будет автоматически удалён (закрывать его не надо).
  6. char *tmpnam (char *name). Функция предназначена для генерации уникальных имён для временных файлов. Подчеркну, что эта функция только генерирует имя, но не создаёт сам файл. Если строка name задана как NULL, то сгенерированное имя сохраняется в name, иначе - в внутреннем статическом буфере (его очисткой занимается операционная система или сишный рантайм). Если имя не удаётся сгенерировать, то функция возвращает NULL.

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

  1. fprintf и fscanf - их отличие от уже знакомых вам функций в дополнительном первом параметре - указателе на структуру файл. Замечу, что printf и scanf аналогичны fprintf(stdout,...) и fscanf(stdin,...) соответственно.
  2. void perror (const char *string) - записывает в stderr строку string и описание последней ошибки. Обычно, stderr совпадает с экраном консоли. Для переназначения (например, для вывода ошибок в .log файл) используйте функцию freopen (её, кстати, также можно использовать для переназначения stdout и stdin).
  3. int fgetc(FILE *stream) - читает один байт (символ) из файла и возвращает его. В случае ошибки или достижении конца файла возвращает EOF.
  4. int fputc(int c, FILE *stream) - записывает байт в файл, возвращает либо записанный символ, либо EOF (в случае ошибки).
  5. char *fgets( char *string, int n, FILE *stream ) - считывание строки файла. При этом длина строки ограничивается: концом файла, началом новой строки, параметром n, который задаёт максимально допустимое число символов для чтения. Возвращает либо string, либо NULL (в случае ошибки).
  6. int fputs (const char *string, FILE *stream ) - запись строки в файл. В случае ошибки возвращает EOF. Замечу, что эта функция не добавляет автоматически символ конца строки.
  7. size_t fread( void *buffer, size_t size, size_t count, FILE *stream ) - чтение данных в произвольный буфер. При этом buffer - указатель на буфер (обычно массив), count - максимально допустимое количество читаемых элементов, size - размер отдельного элемента. Для определения последнего принято использовать оператор sizeof, предназначенный для вычисления количества байт занимаемым некоторым типом, например: sizeof(float). Замечу, что при использовании (разумном использовании) этой функции обязательно открывайте файл в бинарном режиме, иначе вы гарантируете себе головную боль из-за обычной буковки b. Функция возвращает количество фактически прочитанных байт (может быть меньше count при ошибке или достижении конца файла).
  8. size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream ) - аналогичная функция для записи данных

Наконец-таки функции для работы с файловым указателем.

  1. int feof (FILE *stream) - проверка на достижение конца файла. Возвращает не 0, если файловый указатель достиг конца файла. Рекомендуется вызывать эту функцию при каждой операцией чтения.
  2. int ferror (FILE *stream) - аналогичная проверка на ошибку, вызывается как альтернатива feof, но после операций записи/чтения.
  3. void clearerr (FILE *stream) - сбрасывает флаг ошибки для файлового потока (после этого ferror вернет 0).
  4. int fgetpos( FILE *stream, fpos_t *pos ) - считывание в переменную, на которую указывает pos текущей позиции файлового указателя. В случае ошибки возвращает не 0. fpos_t - платформо-зависимый целочисленный тип, определяемый в stdio.h.
  5. int fsetpos( FILE *stream, const fpos_t *pos ) - установка текущей позиции файлового указателя. В случае ошибки возвращает не 0. Устанавливаемая позиция по непонятным мне причинам передаётся в константном указателе (а не просто значение позиции), но так уж сложилось...
  6. void rewind( FILE *stream) - устанавливает файловый указатель на начало файла.
  7. int fseek( FILE *stream, long offset, int whence ) - позиционирование указателя. offset задаёт смещение, а whence задаёт точку отсчёта, может принимать одно из 3 значений: SEEK_CUR (относительно текущей позиции файлового указателя), SEEK_SET (относительно начала файла), SEEK_END (относительно конча файла, с отсчётом в обратном направлении). В случае ошибки возвращает не 0.

В качестве примеров приведён усовершенствованный вариант проекта "Анкета" с возможностью сохранения и загрузки информации в/из файла, а также простой вариант hex-вьювера.

MSDN

  • Stream I/O - Потоковый ввод-вывод.
  • perror, _wperror - Функция perror.
  • Error Handling (CRT) - Функции обработка файловых ошибок и конца файлов.

Файлы

Анкета
Хекс-вьювер


Работа с файловой системой

Этот урок является логическим продолжением предыдущего. Здесь мы рассмотрим манипуляции с файлами, каталогами (папками, директориями) и дисками как объектами файловой системы. Сразу замечу, что из-за различного подхода к файловой системе на разных платформах (наиболее известны три подхода: MS-шный, nix-овый и подход используемый в мобильных и встроенных системах) на каждой из них функции для работы с файловой системой несколько различны. Мы рассмотрим функции, которые реализуют большинство виндовых компиляторов C++ (в т. ч. и Visual), большинство из них POSIX-совместимы. Есть правда 2 функции, документированные стандартом, они объявлены в уже знакомом вам stdio.h:

  1. int remove(const char *filename) - удаляет файл, возвращает 0 в случае успешного завершения. Иначе во внешней переменной errno сохраняется код ошибки: EACCES, если операция удаления невозможна из-за прав доступа, и ENOENT, если файл не существует. Замечу, что для того чтобы компилятор понял что такое errno вам, возможно, придётся подключить заголовок errno.h (если, он не будет подключен через другие заголовки, например stdlib.h).
  2. int rename (const char *oldname, const char *newname) - переименование файла или директории, в случае ошибки возвращает не 0 и сохраняет в errno код ошибки: EACCES, ENOENT или EINVAL (задано недопустимое новое имя файла/директории). Замечу, что этой же функцией производится и перемещение.

В прошлом уроке мы обращались к файлам, использую указатель на структуру FILE. Многие виндовые функции требуют непосредственно дескриптор файла, т. е. его номер в таблице открытых потоков в контексте вашего приложения. Рассмотрим функции работы с конкретным файлом через файловый дескриптор, большинство этих функций подключаются через io.h (ввод-вывод):

  1. int _fileno(FILE *stream) - возвращает дескриптор по указателю на FILE, если stream является недействительным указателем, то результат не определён (может быть разным в завистмости от реализации), объявлена в stdio.h
  2. int _chsize( int fd, long size ) - изменение размера файла. fd - дескриптор открытого с правом записи файла, size - новый размер. При увеличении размера добавляются нулевые байты. Возвращает 0 в случае удачи, иначе - -1, при этом в errno сохраняется код ошибки: EACCES, EBADF (неверный дескриптор или файл открыт только для чтения), ENOSPC (нет места на диске).
  3. long _filelength (int fd) - возвращает размер файла, при ошибке возвращает -1.
  4. __int64 _filelengthi64 (int fd) - аналогична предыдущей, используется для больших файлов. Возвращаемое значение имеет MS-шный тип __int64 (большинство виндовых компиляторов для совместимости также реализуют этт тип), т. е. 64-разрядное целое, на 32-разрядных системах реализуется средствами операционной системы.
  5. int _fstat( int fd, _stat *buffer) - получение информации о файле, возвращает 0, а при ошибке - -1. Тип _stat представляет из себя структуру, определённую в заголовочном файле stat.h из папки sys (подключается так: #include ), в нём же определена и сама функция. За подробностями отсылаю к MSDN.

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

  1. int _access( const char *path, int mode ) - определяет возможность доступа к файлу или каталогу path для определённых операций mode. mode может иметь значения: 0 - проверка на существование, 2 - право на запись, 4 - право на чтение, 6 - право и на чтение, и на запись. В случае наличия прав возвращает 0, в противном случае - -1, пи этом errno може принимать значения: ENOACCES, ENOENT. Объявлена в io.h.
  2. char *_fullpath( char *absPath, const char *relPath, size_t maxLength ) - преобразует онтносительный путь к файлу в абсолютный. Параметры, думаю, понятны из контекста. Функция удобна, чтобы сохранить путь к файлу, находящемся в рабочей папке программы (скажем, файл настроек) и затем после смены текущего каталога обращаться к этому файлу. Заголовок - stdlib.h.
  3. void _splitpath( const char *path, char *drive, char *dir, char *fname, char *ext ) - разбивает путь к файлу на его составляющие: диск, папку, имя и расширение. Заголовок - stdlib.h.
  4. void _makepath( char *path, const char *drive, const char *dir, const char *fname, const char *ext ) - функция обратная предыдущей, конструирует имя файла. Заголовок - stdlib.h.
  5. int _stat( const char *path, _stat *buffer ) - функция аналогична _fstat, но работает с путём к файлу. Функция из sys/stat.h.
  6. void _searchenv( const char *filename, const char *varname, char *pathname ) - поиск файла filename в каталогах, указанных в переменной окружения varname, результат записывается в pathname. Так ищется например компилятор cl.exe в Visual Studio, используя переменную окружения PATH, а также все заголовочные файлы, указанные в угловых скобках, используя переменную окружения INCLUDE (правда, важно заметить, что при запуске Visual Studio конфигурирует многие переменные окружения в контексте своего процесса, поэтому, скажем INCLUDE, в контексте вей системы у вас может даже не существовать).

Следующий блок функций - для работы со структурой папок. Объявлены в direct.h (от слова directory).

  1. int _chdir (const char *dirname) - смена текущей папки. Возвращает 0 при нормальном завершении и -1 в случае ошибки.
  2. char *_getcwd( char *buffer, int maxlen ) - поучение текущей директории на текущем диске (думаю, параметры в пояснениях не нуждаются), в случае ошибки возвращает NULL и устанавливает errno в одно из 2 значений: ENOMEM (нехватка памяти для записи maxlen байт, т. е. buffer равен NULL или под buffer был выделен блок размером меньше maxlen) или ERANGE (текущий путь длиннее чем ваш maxlen).
  3. char *_getdcwd( int drive, char *buffer, int maxlen ) - получение текущей папки на указанном диске drive, задаваемым числом в стиле: 1=A, 2=B ...
  4. int _chdrive (int drive) - смена текущего диска. Возвращает 0 или -1 (что когда, думаю, уже понятно).
  5. int _getdrive( void ) - возвращает текущий диск (функция без параметров).
  6. unsigned long _getdrives( void ) - возвращает все диски доступные в системе в виде битовой маски, т. е. бит 0 - диск A, 1 - B и т. д. Для определения доступности конкретного диска используют, например такую конструкция if ((0x01 << n) & _getdrives()), где n - номер диска, начиная от 0.
  7. unsigned int _getdiskfree( unsigned int drive, _diskfree_t *driveinfo ) - получение информации о месте на диске. Возвращает 0 в случае успешного завершения. _diskfree_t - структура из 4 членов типа unsigned long: total_clusters (всего кластеров), avail_clusters (свободно кластеров), sectors_per_cluster (секторов в кластере), bytes_per_sector (байтов в секторе).
  8. int _mkdir (const char *dirname) - создаёт новую папку, при этом dirname может представлять как абсолютный, так и относительный путь. Возвращает 0 или -1, возможные значения errno: EEXIST (папка уже существует), ENOENT (недопустимый путь).
  9. int _rmdir (const char *dirname) - удаление пустой папки. Эта функция не удаляет данные, а лишь удаляет запись из файловой таблицы. Возвращает 0 или -1, возможные значения errno: ENOTEMPTY (папка не пуста), ENOENT, EACCES.

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

  1. _MAX_PATH - полный путь
  2. _MAX_DRIVE - имя диска
  3. _MAX_DIR - путь до каталога
  4. _MAX_FNAME имя файла
  5. _MAX_EXT - расширение

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

  1. char name[_MAX_PATH] - имя файла или папки (только имя + расширение, без пути)
  2. _fsize_t size - размер файла. _fzie_t - невыровненный целочисленный тип, размер которого может быть разным в зависимсоти от реализации (у MS - 32 бит).
  3. time_t time_write - время последней модификации файла. time_t - тип для работы со временем, более подробно о нём во второй части, сейчас же рекомендую пользоваться функцией ctime из time.h, эта функция принимает указатель на time_t и возвращает время в виде строки, с учётом локализации.
  4. time_t time_creation - время создания файла, действительно только для NTFS (на FAT принимает значение -1)
  5. time_t time_access - время последнего доступа (возможно, без модификации), действительно только на NTFS.
  6. unsigned int attrib - файловая маска атрибутов. Допустимые флаги: _A_ARCH (архивный), _A_HIDDEN (скрытый), _A_NORMAL (обычный файл, флаг = 0), _A_RDONLY (только чтение),_A_SUBDIR (указывает, что данный элемент является папкой), _A_SYSTEM (системный). Для проверки флага используется побитовое and (&).

Ну и собственно функции:

  1. intptr_t _findfirst( const char *filespec, _finddata_t *fileinfo ). filespec - шаблон для перечисления файлов (например, *.txt). Поиск производится в текущей папке. intptr_t - платформо-зависимый тип для хранения указателей на элементы системных таблиц, называются также хендлами (handle). В случае если был найден файл или папка, удовлетворяющая шаблону, то возвращается хендл поиска и структура, на которую указывает fileinfo заполняется данными о файле/папке. В противном случае возвращается -1 и errno получаем одно из 2 значений: ENOENT (файл не найден), EINVAL (недопустимый шаблон).
  2. int _findnext( intptr_t handle, struct _finddata_t *fileinfo) - продолжает поиск, начатый предыдущей функцией. Первый параметр - хендл поиска. В случае успеха возвращает 0 и заполняет структуру, иначе возвращает -1.
  3. int _findclose (intptr_t handle) - закрывает поиск и освобождает память выделенную под системные ресурсы, в случае ошибки возвращает -1, иначе - 0.

Что ж теперь файловая система в наших руках! Замечу, правда, что все эти функции не работают с виртуальными виндовыми папками оболочки (Мои документы, Корзина, Temporary Internet Files, %WinDir%\Tasks и др.), для работы с этими папками используются специальные функции WinAPI. Замечу также, что нет функции копирования файла. Для этого обычно открывают первый файл для чтения, создают массив _filelength байтов, читают их с помощью fread, открывают второй файл для записи, пишут с помощью fwrite, освобождают память, выделенную под массив. Также можно предварительно проверить на существование копируемый и на запись целевой файлы. Ещё один алгоритм, скажем, удаление непустой папки. Для этого вначале надо удалить все файлы и папки внутри неё, а затем саму папку. Скажем, так:

bool DeleteFolder (const char *path)
{
    int rd = _rmdir(path);
    if (rd == 0)
        return true;
    if (errno != ENOTEMPTY) // какая-то другая ошибка, либо ошибочный path, либо права доступа
        return false;
    _chdir(path);
    _finddata_t fd;
    intptr_t hf = _findfirst("*",&fd);
    if (hf == -1)
        return false; // у нас нет прав на чтение папки
    do
    {
        if (fd.attrib & _A_SUBDIR)
        {
            if (!DeleteFolder(fd.name))
                return false;
        }
        else
        {
            if (remove(fd.name) != 0)
                return false;
        }
    }
    while (_findnext(hf,&fd) != -1);
    _findclose(hf);
    _chdir(".."); // Выходим из удаляемой папки
    if (_rmdir(path) !=0)
        return false;
    return true;
}
Такая функция, т. е. функция, вызывающая сама себя называется рекурсивной. Рекурсивные функции очень часто используются при обработке различных деревьев, в т. ч. файловой системы.

В качестве примера - cpp и h файл, реализующие некоторые удобные функции работы с файлами. Добавить эти файлы в ваш проект очень просто: скопируйте их в папку проекта (это необязательно, но удобно), в Solution Explorer из контекстного меню проекта выберите Add => Add Existing Item (Добавить => Добавить существующий элемент). Выделите оба файла и жмите Open. Если вы сделаете всё именно так, то файлы автоматом будут раскиданы на категории (Source Files, Headers ...). Признаюсь, функции делались наспех и несколько недоработаны. Вот основные, так сказать, TODO:

  1. При смене каталога сохранить текущий и затем восстанавливать его, а не уровень выше
  2. При смене каталога, также менять диск (получить его удобней всего так: _splitpath и затем _chdrive(drive[0] - 'A' + 1), где drive - строка, переданная в _splitpath)
  3. Обеспечить вызов IsSubfolder в CopyFolder и MoveFolder единожды (не при каждом рекурсивном вызове), т. е., скажем CopyFolder разбивается на две функции: собственно копирование (рекурсивная, не объявленная в fs.h) и оболочка, вызывающая проверку, а затем первую функцию (объявленная в fs.h).
  4. Общие части CopyFolder и MoveFolder вынести в одну функцию с целью уменьшения размера
  5. При возврате false мои функции не делают зачистки (смена на начальную папку, _findclose)
Что ж, могу предложить сделать это всё вам!

MSDN

  • File Handling - Функции для работы с файлами.
  • Directory Control - Функции для управления дисками и каталогами.
  • System Calls - Перечисление файлов.
  • Path Field Limits (CRT) - Лимиты файловых имён.

Файлы

fs.h и fs.cpp


Объединения. Битовые поля. Перегрузка функций

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

Итак, начнём в лоб - описание конкретного объединения:

union MyUnion
{
    int item_int;
    double item_double;
};
Пожалуй, вам это больше всего напоминает структуру. Вспомним, при использовании структуры в памяти будет храниться каждый её элемент, при использовании объединений одновременно мы имеем лишь один элемент. Память резервируется под размер максимального элемента (в нашем случае - double). Отсюда вывод - в объединение не должно быть двух однотипных элементов (это допустимо, но глупо). Рассмотрим несколько строчек кода (читайте комментарии):
MyUnion u; // создаём объединение
u.item_int = 5; // инициализируем int элемент
printf("%i\n",u.item_int); // печатаем int элемент
u.item_double = 10.5; // заменяем на вещественное значение
printf("%.2f\n",u.item_double); // печатаем вещественное значение
printf("%i\n",u.item_int); // результат вообще по стандарту не определён,
                           // но по сути будет напечатана int-интерпретация
                           // двоичной записи в вещественном виде числа 10.5
                           // (т. е. 0, лучше всего это наблюдается в дебаггере)
Итак, как вы поняли, объединения предназначены для компактного хранения данных. Используются одни и те же физические байты, но интерпретируются они могут как угодно. Понятно, что о типобезопасности (type safety) говорить не приходится: пишем одно, а прочитать можем другое. Одно из наиболее красивых применений - это различные универсальные типы (variant-типы), т. е. объекты, фактический тип которого определяется в рантайме (неизвестен во время компиляции. В одно время такие объекты хранят число, затем оно может быть динамически преобразовано в строку, которая потом будет интерпретироваться как дата или время и т. д. Немого огрубляя, можно сказать, что универсальный тип представляет из себя структуру из 2 элементов. Один из них - объединение, а другой - целочисленный тип, хранящий текущий тип. С помощью определённого набора функций и перегруженных операций многие библиотеки позволяют эффективно работать с абстрактными типами. Наиболее красоту объединений мы увидим позже, после знакомства с классами.

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

При работе с объединениями очень удобной оказывается перегрузка функций, некоторые жёстко относят перегрузку к ООП, но я считаю, что с одним из видов перегрузки можно познакомиться уже сейчас. Под перегрузкой в принципе понимают такую возможность языка, когда под одним именем может скрываться несколько функций. Приведу небольшой пример. Допустим мы решили написать функцию вычисления квадратного корня (я понимаю, что она уже существует, но мне важен пример). Существуют различные алгоритмы для вычисления целого корня из целого числа и для вещественного из любого числа. Первая группа, естественно, работает быстрее (но с более низкой точностью). Хотелось бы реализовать оба алгоритма в одной функции. Для этого пишем:

long MySquareRoot (long x)
{
    // реализация целочисленного алгоритма
}

double MySquareRoot (double x)
{
    // реализация вещественного алгоритма
}
При вызове функции компилятор посмотрит на тип аргумента и сгенерирует код для вызова одной или второй функции. А если мы хотим для целого a посчитать вещественный корень (для большей точности)? Очень просто, пишем: MySquareRoot((double)a), т. е. явно указываем тип. Замечу, что на перегрузку не влияют:
  1. Имена параметров
  2. Тип возвращаемого значения

Т. е. невозможно создать две перегруженных версии функции с одинаковым количеством и типами параметров, но с разными возвращаемыми типами или разными именами параметров. Кроме того будьте аккуратны с дефолтными параметрами. Скажем, если обе перегружаемые функции будут иметь только дефолтные параметры, то код не скомпилируется, т. к. если вы вызовете эту функцию без параметров, то компилер не в состоянии будет определить нужную функцию. А вообще, перегрузка - вещь чрезвычайно полезная. Одно замечание: даже не пытайтесь перегружать функции в C, хоть это и не совсем ООП, но один из элементов и потому работает только в плюсах. Также не пытайтесь перегружать функции, объявленные в блоке extern "C" (что это такое - узнаете позже).

Итак, теперь о битовых полях. Битовые поля предназначены для чёткого указания размера определённых данных с точностью до бита, есть при этом одно правила, эти данные должны иметь тип, который может интерпретироваться как целочисленный (в т. ч. и перечисления). Синтаксис объявления битовых полей похож на объявление структуры:

struct имя
{
    тип1 поле1  : размер1;
    тип2 поле2  : размер2;
    // и т. д.
};
Размер указывается в битах и не может превышать размера типа (sizeof(тип)), работа с битовыми полями ведётся также, как со структурами. Для выравнивания цепочки битов могут использоваться поля без имён, они будут занимать память, но доступ к ним (с использованием только синтаксиса языка) будет невозможен. Можно даже задать поле без имени и с нулевой длиной при этом его длина, фактически, дозаполнит предыдущий элемент до полного размера (sizeof(тип)). Для чего это делается? Дело в том, что не следует забывать, что архитектура современных процессоров (в первую очередь адресация) направлена на работу с байтами, словами, двойными словами и др. но преимущественно не с битами. Поэтому выравнивание битовых полей значительно увеличивает производительность. Одно из типичных использований битовых полей - двоично-пакованные данные. Безусловно, битовые поля удобны для работы с наборами флагов, но не следует увлекаться. В данном случае производительность всё же важней. Я для хранения флагов рекомендую использовать обычные невыровненные целочисленные типы. Для добавления флага типичным приёмом является использование побитового OR, а для проверки установлен ли флаг - побитового AND.

В первом примере приведена простая реализация универсального типа - int, float, double или массив char. Реализован перевод числа в строку. При обратном переходе число становится равным 0. Второй пример - использование двух методов для работы с флагами - битовых полей и типа unsigned char. В этом примере также полезным является использование макросов с параметрами. Например:

// следующий макрос может быть использован для объявления int-переменной и сброса её в ноль
#define ZERO_INT(x) int x = 0
ZERO_INT(a); // препроцессор раскроет это в int a = 0
ZERO_INT(b); // а это в int b = 0

// ещё один макрос - объявление любой переменной и сброс её в ноль
#define ZERO_ANY(x, type) type x = 0
ZERO_ANY(i, int); // int x = 0
ZERO_ANY(f, float); // float f = 0
ZERO_ANY(p, int*); // int* p = 0

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

  • # - позволяет заключить параметр в кавычки, например:
        #define M(p) printf(#p)
    M(qwerty);
    Это эквивалентно printf("qwerty");.
  • #@ - то же, но заключает в апострофы
  • ## - для присоединения (конкатенции) строк, например:
         #define M(a, b) a##b = 0
     M(a,1); // a1 = 0
     M(a,2); // a2 = 0
     M(int _, 1); // int _1 = 0
    Я думаю из последнего примера вы поняли, что препроцессор - это источник питания для програмистских извращенцев. И ещё не думайте, что следующий код дают доступ к переменным a1, a2, a3, ..., a10:
         #define V(n) a##n

     for (int i = 1; i <= 10; ++i)
         V(i) = 0;

     // препроцессор сделает из этого:
     for (int i = 1; i <= 10; ++i)
         ai = 0; // переменная с именем ai!

MSDN

  • Union Declarations - Объединения.
  • C Bit Fields - Битовые поля.
  • Function Overloading - Перегрузка функций.

Файлы

UniType
Flags


Указатели на функции. Обработка исключений

Рассмотрим довольно типичную задачу - приближенное решение некоторого уравнения вида f(x)=0. Алгоритм следующий (он называется методом деления отрезка пополам): задаются границы отрезка и функция. Если на концах отрезка функция принимает значения разного знака, то, вероятно, уравнение на этом промежутке имеет корень (с гарантией - для непрерывных функций). Затем отрезок делится пополам и одна из границ заменяется на середину (конечно, если в ней значение функции не равно 0), причём таким способом, чтобы знаки опять были разные. Операция повторяется до тех пор, пока разница между границами отрезка не станет меньше некоторого заданного числа. Любая из границ тогда считается корнем. Сделать универсальную программу можно, но при этом нам придётся парсерить выражения функций (т. е. разбирать строку на составляющие элементы выражения). Мы сейчас займёмся другим. Если эту задачу мы решаем только для себя, то мы можем позволить себе написать все нужные для нас функции (f1, f2, ...) непосредственно в программе. Одна проблема, сам алгоритм хочется сделать универсальным. Какие параметры будут передаваться функции, ищущей корень? Во-первых, границы интервала. Во-вторых, точность. А в-третьих, работающая функция. Саму функцию передать нельзя, передаётся указатель на неё. Фактически, он представляет адрес функции в сегменте кода. Объявляется указатель на функцию следующим страшным синтаксисом:

тип_возвращаемого_значения (деклараторы_вызова *имя_указателя) (список_типов_аргументов);
О деклараторах вызова мы пока молчим. Скажу лишь, что их может просто не быть. Если вы напишите просто имя любой функции без скобок, то это тоже будет указатель на функцию, константа-указатель. Именно такие имена без скобок присваиваются переменным, являющимися указателями на функцию.

Для удобства тип указателя на функцию можно загнать в typedef:

typedef тип_возвращаемого_значения (деклараторы_вызова *имя_типа) (список_типов_аргументов);
Теперь, используя этот тип, можно классическим способом объявлять переменные.

В примере реализован приведённый вначале алгоритм (обязательно посмотрите перед дальнейшим чтением). Сразу замечу, что в нём используются некоторые математические функции, они объявлены в math.h в особых дополнительных пояснениях, я думаю, не нуждаются (в качестве справочника кидаю линк на MSDN). Рассмотрим простое уравнение: x-1/[x]=0 (квадратные скобки означают целую часть). Несложно догадаться, что оно имеет два корня (1 и -1). Допустим, мы задали границы -5 и +5. Значения функции имеют в этих точках разные знаки. Но при запуске программы мы не увидим не первого ответа, ни второго. То, что мы увидим, зависит от операционной системы и используемого компилятора. Большинство компилеров просто доверяют операционке. Windows XP при этом, скажем, предлагает отправить отчёт в MS, Windows 9x и ME - говорят об ошибке в программе и, если что предлагают перезагрузить компьютер. Очень интересный результат мы получаем при компиляции в Open Watcom C++, мы видим в консоли надпись об ошибке и дамп регистров и стека.
Результат в XP
Теперь в ME
А это от экзешки, сгенерированной OpenWatcom C++

Именно последний случай навевает вопрос о том, нельзя ли нам самим обработать эту ошибку? Естественно, можно, и C++ предоставляет для этого специальный синтаксис, связанный со специальным понятием языка, называемым исключениями. Для работы с исключениями используются специальные управляющие структуры - блоки try и catch. В первый блок заключаются потенциально опасные операторы. Блоков catch может быть несколько, в каждом из них в скобках указывается исключение, которое берёт на себя этот блок. Наиболее простой случай - в скобках указывается три точки, в этом случае этот catch берёт на себя обработку всех исключений. Например:

try
{
    a = b / c;
}
catch (...)
{
    printf("Division by zero!\n");
}
В каких случаях генерируются исключения? Для этого есть специальный оператор - throw. Он имеет следующий вид:
throw выражение;
Выражение может иметь любой тип, именно его называют типом исключения, а сам оператор - выбросом исключения. В рассмотренном только что случае, как вы видите оператора throw нет. Можно даже сказать, что это вырожденный случай. Дело в том, что исключения также генерируются для некоторых прерываний процессора, причём судя по всему тип этих исключений не определён (по крайней мере я не могу ничего сказать по этому поводу). Классически же предполагается, что программа (или библиотека, используемая программой) генерирует исключение после проверки некоторого условия. Для передачи информации о исключении используется передаваемое выражение. Это может быть, например, код ошибки. Блок catch может взять на себя перехватку исключений определённого типа, для этого в скобках указывается обрабатываемый тип. Для использования выражения, выброшенного throw, следует в скобках catch указать кроме типа, ещё и имя некоторой переменной. Замечу, что передача этой переменной в блок catch, аналогична передаче параметров в функцию, то есть будет создана локальная копия, действительная только в блоке catch. Можно, конечно, использовать ссылки, но я не вижу в этом смысла в большинстве случае (есть исключение - использование в качестве типа исключения (невольный каламбур!) больших структур, в этом случае используем константные ссылки). Можно также передавать указатели, в этом плане нам интересно, что мы можем написать блок catch, перехватывающий указатель на void, при этом он возьмёт на себя все указатели (для использования, правда, придётся приводить указатель на void к другому типу, например: pi = (int*)pv;). Отсюда вывод, что блок, перехватывающий указатель на void должен идти после всех остальных типов, перехватывающих указатели (самый последний - блок catch с тремя точками).

Оператор throw может использоваться без выражения, в этом случае он повторно выбрасывает последнее исключение. Типичное применение:

try
{
    // потенциально опасные действия
}
catch (type1 v1)
{
    // обработка type1
}
catch (type2 v2)
{
    // обработка type2
}
catch (...)
{
    throw; // а это пусть обрабатывают более высоко стоящие функции
}

Сразу, скажу, что доработка кода примера ложится на ваши плечи. Что-то вроде такого:

for (;;) // ищем точку, где значение вычислимо
{
    try
    {
        fx = f(x);
        break; // выполнится только, если не будет выброшено исключение
    }
    catch (...)
    {
        // TODO: генерация очередного значения для x, скажем x = (a + x) / 2;
    }
}
При этом надо следить, чтобы x не дошло до границы диапазона (в этом случае просто нагло возвращаем false).

Вам, наверно, интересно, почему я не взял для примера f(x)=x-1/x, т. е. почему я использовал целую часть. Дело в том, что такова уж архитектура процов Intel-а - операции с вещественной точкой производятся отдельным компонентом - сопроцессором. А там свои правила, и вещественное число может принимать специальные значения, такие как бесконечность или неопределённое значение. Для того, чтобы заставить всё-таки процессор генерировать исключение, надо установить значение специального регистра. В VC++ для этого используется функция _controlfp (заголовок - float.h). Функция принимает два параметра типа unsigned int. Второй параметр означает маску, показывающую, какую именно опцию процессора мы хотим настроить, в нашем случае (для обработки ошибок обработки вещественных чисел) он принимает значение _MCW_EM. Первый параметр - собственно опция, у нас - _EM_ZERODIVIDE.

Напоследок, замечу, что в VC++ для возможности перехвата исключений при компиляции должна быть добавлена опция /EHsc. В IDE это делается так: открываем свойства проекта (правый клик по проекту в Solution Explorer и пункт Properties). Затем C++ => Code Generation => Enable C++ Exceptions => Yes.

MSDN

  • К сожалению, не нашёл инфы по указателям на функции
  • C++ Exception Handling - Исключения в C++.
  • _control87, _controlfp, _control87_2 - Функция _controlfp.

Файлы

Проект


Описатели static и extern

Работа с исключениями - это обработка ошибок в стиле C++. В C же существовали свои приёмы обработки ошибок. Типичный пример - возврат функцией определённого значения в случае ошибки (вспомните, практически все функции работы с файлами из сишного рантайма), этот способ не утратил своего значения и в C++, правда эстетичней возвращать либо bool, либо некоторый enum. О том, что именно выбрать в конкретной ситуации мы поговорим позже. А сейчас вспомним, что функции рантайма сохраняют ещё в переменной errno код ошибки. Казалось бы ничего особенного, но... Напишем простой пример (некоторые строки помечены для дальнейшей удобной ссылки на них).

// Файл funcs.h
//
enum ErrorType {etGeneric, etUser, etSystem, etReserved}; // *1

bool TestFunc1(void);

bool TestFunc2(void);
//

// Файл funcs.cpp
//
#include "funcs.h"

ErrorType et; // *3

bool TestFunc1(void)
{
    et = etSystem;
    return false;
}

bool TestFunc2(void)
{
    et = etGeneric;
    return false;
}
//

// Файл main.cpp
//
#include <stdio.h>
#include "funcs.h"

int main()
{
    const char szCompleted[] = "Test %i completed!\n";
    const char szFailed[] = "Test %i failed! Error: %s\n";
    const char * szErrors[etReserved] = {"Generic", "User", "System"}; // *2

    if (TestFunc1())
        printf(szCompleted,1);
    else
        printf(szFailed,1,szErrors[et]);

    if (TestFunc2())
        printf(szCompleted,2);
    else
        printf(szFailed,2,szErrors[et]);

    return 0;
}
//
Сразу обращаю ваше внимание на инициализацию константного строкового массива szErrors в строке *2, а также на использование индексов, являющихся перечислениями. Фактически, компилятор заменит etReserved на значение этой константы (в данном случае на 3) и обращения будут идти как к обычному массиву (которым он и является), но такая запись эстетически приятней и понятней и потому часто используется.

Вернёмся к нашей теме (рекомендую кстати самим проследить с вашим компилятором следующие шаги). Наши функции возвращают false в случае ошибки (постоянной ошибки в данном случае), в переменной et сохраняется тип ошибки. Думаю идея понятна. Билдим. ФигВам. Компилятор при компиляции файла main.cpp не находит объявления имени et. Первая идея - исправим строку *1 на

enum ErrorType {etGeneric, etUser, etSystem, etReserved} et;
Таким образом мы заинклюдим объявление. Билдим. Отлично! Теперь мы вылетаем на funcs.cpp. Что мы видим? Переопределение переменной et. Стираем или комментируем строку *3... Ура! Компилится,.. но не линкуется. Что говорит линкер? Цитирую: "enum ErrorType et (?et@@3W4ErrorType@@A) уже определена в main.obj", т. е. мы имеем имя et определённым в двух объектных файлах. Исправляем *1:
enum ErrorType {etGeneric, etUser, etSystem, etReserved} extern et;
Что означает такая запись (точнее запись extern ErrorType et)? Она даёт понять компилеру, что et - внешнее имя, соответственно компилятор сделает для линкера в obj файле специальную пометку, такую же пометку он делает для всех объявлений (но не определений, т. е. реализаций) функций. Билдим. Получаем уже знакомую нам ошибку - линкер не может связать внешнюю ссылку, это и понятно переменная с описателем extern не создаётся, компилер лишь пихает внешний линк на неё, мол пускай компоновщик (линкер) потом сам подправит объектный код. Саму переменную надо завести в funcs.cpp: возвращаем строку *3. Всё работает!

Возможно, представив как раскроется инклюд funcs.h вы подумали не возникнет ли в funcs.cpp конфликта, текст funcs.cpp ведь аналогичен такому:

enum ErrorType {etGeneric, etUser, etSystem, etReserved};
extern ErrorType et; // *5
// ...
ErrorType et; // *6
// ...
Как видите не возникнет. Проводя грубую аналогию можно сказать, что *5 - объявление, а *6 - реализация (выделение реальной памяти). Напомню, что объявление не является обязательном для файла где есть реализация, потому можно было не изменяя *1 (с самого начала) поместить в main.cpp строку вида *5. Но енто не есть культурно. Представьте, что у нас добавился ещё один файл с кодом, мы опять должны писать эту строку... Ну, думаю, вы поняли (используем заголовки по их назначению - скопление объявлений).

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

#include <stdio.h>

int GetNext()
{
    static int next = 0; // *1
    return ++next;
}

int main()
{
    for (int i = 0; i < 5; ++i)
        printf("%i\n",GetNext());

    return 0;
}
Напишите это без статика и вы получите ряд единиц. Вынесите next в глобальные переменные (вместо *1 будет next = 0) и вы получите ряд единиц. Но в этом случае вы получите ряд от 1 до 5. То есть выделение памяти для static-переменных работает по правилам глобальных переменных. А это означает два момента. Во-первых, после выполнения функции значение next сохраняется. Во-вторых, инициализация (некоторый неправильно называют эту операцию присваиванием при объявлении) выполняется единожды. Из определения статика следуют тавтологичность использования его в глобальном контексте по отношению к функциям (некоторые компиляторы выдадут при этом варнинг). Для глобальных же переменных использование статика возможно, в этом случае мы гарантируем, что данные будут проинициализированы до выполнения программы, то есть они намертво будут загнаны в бинарный файл и вместе с ним при запуске спроецированы в оперативную память. Правда, замечу, что в таких случаях лучше положиться на оптимизатор, а не замудрять код. В данный момент (без использования наворотов ООП) очень нужного применения статическим переменным я не вижу, но если где-нибудь вы увидите удобную возможность для статика (кстати, если статик действительно к месту, то код обычно получается эстетически приятный - проверено опытным путём) - используйте, не стесняйтесь (приходилось, правда, видеть людей, которые начинали пихать static перед объявлениями функций или структур, последнее в одном учебнике (!) даже рекомендовали делать, что сказать: и скучно, и грустно, и хочется руки им поотрывать...).


Статические и динамические библиотеки в структурном программировании

Мы часто писали наборы функций для какой-либо задачи (например - conint.h и conint.cpp из урока по работе с консолью). Возможно, вы используете и ещё большие наборы. Что нам даёт вынесение их в отдельные файлы? Это позволяет при компиляции проекта не перекомпилировать каждый раз эти функции (они перекомпилируются только после модификации исходного файла). Отсюда возникает идея библиотеки, т. е. некоторого набора готовых алгоритмов, типов и пр. для использования в различных программах. Но для настоящей библиотеки хотелось бы, чтобы она сама могла состоять из нескольких файлов. Наверно, любая ОСя предоставляет для этого какие-то средства (они должны войти в разрабатываемый сейчас стандарт C++). Мы рассмотрим их в реализации винды, причём с синтаксисом Visual-а.

Для создания обычной библиотеки из IDE Visual Studio .NET в окне создания нового проекта выберите категорию C++ => Win32. Затем выберите любой (из 2) шаблон. В открывшемся окне Win32 Application Wizard (мастер приложения Win32) выберите вкладку Application Settings, т. е. "Настройки приложения" (для этого щёлкните по соответствующей ссылке). Переключатель Application Type (тип приложения) установите в Static Library (статическая библиотека). Флажок Precompiled header (прекомпилированный заголовок) рекомендую оставить. Эта опция добавит в ваш проект файлы stdafx.h и stdafx.cpp. В первый вы можете пихать наиболее глобальные (для проекта) инклюды, дефайны и пр. При первой компиляции проекта (а также после модификации этих двух файлов) вам придётся чуток подождать, зато потом это реально ускорит процесс билдинга проекта, но не забудьте во все файлы кода вначале заинклюдить stdafx.h. Жмём Finish. Самое главное отличие этого проекта от всех предыдущих в настройке на вкладке General (основное) свойств проекта: Configuration Type (тип конфигурации) - Static library. А это означает, что пробилдив проект мы получим вместо экзекшки файл с расширением lib (library - библиотека). Этот lib по внутренней структуре аналогичен файлам объектного кода (obj), т. е. он содержит объектный код всех функций и информацию о внешних именах для линковщика (т. е. в каком адресе какая хранится переменная или начинается функция). Отличие в том, что обычный файл объектного кода генерируется для отдельного файла исходного кода (обычно - *.cpp или *.c), а файл статической библиотеки - для всего проекта. Можно сказать, что статическая библиотека - промежуточный итог.

Встаёт вопрос о том как нам теперь использовать эту библиотеку в другом проекте. Во-первых, желательно поместить библиотеку либо в папку проекта, либо в одну из папок для lib-файлов. Последнии прописываются либо в переменной окружения LIB, либо в IDE Visual Studio. Для этого выбираем Tools => Options (Сервис => Параметры). В дереве слева: Projects => VC++ Directories (Проекты => Каталоги VC++). Затем в списке Show directories for (показать каталоги для) ставим Library files (файлы библиотек). Ниже мы видим список папок, в которых будут искаться lib-файлы (порядок папок тоже важен). Для добавления давим Ctrl+Insert, для удаления - Delete, для редактирования - Insert (можно также использовать кнопки рядом, но обычно клавиатурно удобней). Щёлкнув по кнопке с тремя точками в строке редактирования можно выбрать папку в стандартном диалоге. Наиболее удобно использовать папку с lib файлами в пределах одного проекта. Такие папки через пробел можно прописать в пропертях проекта. Linker => General => Additional Library Directories (Линкер => Основное => Папки дополнительных библиотек). Итак, папки настроили, библиотеку скопировали (если мы её не скопируем, то надо будет прописать путь к ней либо абсолютный, либо относительно каталога проекта, что иногда тоже кстати удобно). Теперь надо подключить заголовки. Технология вам уже знакома, замечу только, что можно создать для своих библиотек специальную инклюд-папку и аналогично lib-папкам прописать её в студии. Наконец-таки надо заставить линкера прилинковать кроме всего прочего нашу библиотеку при сборке проекта. При билдинге с командной строки всё очень просто - необходимо лишь добавить нашу библиотеку в список линкуемых файлов при вызове линковщика (link.exe), например: "link main.obj MyFuncs.obj MyLib.lib /OUT:MyApp.exe". В IDE надо настроить свойства проекта. Ветка Linker => Input (Линкер => Входные данные). В поле Additional Dependencies (Дополнительные зависимости) пишем через пробел все библиотеки (если путь содержит пробелы, то его пишем в кавычках).

Кроме статических существуют ещё динамические библиотеки. Главное их отличие во времени связывания (линковки). Статические библиотеки линкуются на этапе сборки проекта и следовательно каждая программа использующая библиотеку содержит код этой библиотеки. Любая нормальная ОСя (в т. ч. винда) предоставляет технологии для run time связывания. В винде это технология получила название Dynamic link library (динамически линкуемая библиотека) или коротко dll. Для создания динамически линкуемой библиотеки выберите мастер Win32 Application, а в качестве типа проекта поставьте DLL. Затем для создания того шаблона, который я хочу сейчас создать есть два пути: либо создать обычный проект и затем удалить из него добрую половину, либо создать пустой проект и затем кое-что добавить. Мы сейчас выберем второй путь (в первом ничего нового и интересного нет). Для этого поставьте галочку Empty Project (пустой проект). Затем скопируйте файлы conint.h и conint.cpp и добавьте их к проекту. Теперь откройте в свойствах проекта страницу C++ => Precompiled Headers (C++ => Предкомпилированные заголовки). Поставьте в поле Create/Use Precompiled Header (Создавать/Использовать предкомпилированные заголовки) - Use Precompiled Header (использовать ...). Нажмите кнопку OK. Замечу, что по дефолту изменения свойств проекта касается только активной конфигурации, поэтому следует предварительно в списке Configuration (Конфигурация) выбрать All Configurations (Все конфигурации). Теперь добавьте в проект новый заголовок - stdafx.h и новый файл исходного кода - stdafx.cpp, в последнем напишите инклюд stdafx.h. Также заинклюдьте его в conint.cpp. В Solution Explorer выберите свойства StdAfx.cpp (в контекстном меню - Properties). Затем C/C++ => Precompiled Headers. И поставьте здесь Create Precompiled Header. Можете пробилдить проект!

Наверно, вы спросите, зачем нужны эти хитрые манипуляции. Немного поясню. Дело в том, что для любых больших проектов (а библиотеки в неизварщенских случаях всегда более менее большие) очень желательно вынести наиболее глобальные вещи в прекомпилируемый pch-файл (как я уже говорил, это ускоряет процесс построения проекта). Можно конечно, поручить это дело самой VS (Automaticalluy Generate - автогенерация), но я этим часто не пользовался и предпочитаю сам указать, что я хочу вынести в pch. Для этого создаём cpp и h файл, из которых будет создаваться этот pch. По дефолту они имеют имя StdAfx. Для остальных файлов в настройках компилера надо указать использование этих pch, удобно это ставить на весь проект (гарантированное применение ко всем новым файлам). Но для самого StdAfx.cpp при компиляции не надо использовать pch-файл (иначе получим, так сказать, бесконечную рекурсию), а надо из него создать (Create) этот самый pch. Вообщем-то всё. Замечу, что все визарды VC++ при создании более менее серьёзных проектов автоматом создают StdAfx файлы и правильно (как ни странно) ставят настройки.

Технология динамически линкуемых библиотек естественным образом предполагает, что имена функций будут известны во время выполнения программы. Для этого в специальном разделе PE файла (в таблице экспортов - EXPORT TABLE) записывается таблица функций и их адреса (точнее смещение в секции .code). Понятно, что такая информация нужна только для тех функций, которые не являются внутренними для библиотеки, соответственно по умолчанию линкер не генерирует вообще таблицу экспортов. Для добавления чего-либо в эту таблицу (для его экспортирования), например функции или переменной, различные компиляторы предоставляют различный синтаксис (он должен ещё согласовываться с синтаксисом линкера). Мы рассмотрим синтаксис Microsoft Visual C++ (его также поддерживает Intel C++ Compiler). Для экспорта произвольной функции достаточно при её объявлении написать вначале __declspec(dllexport). Пишем этот описатель перед каждой функцией в conint.h (в реализации его ставить не надо). Итак, наша dll нормально билдится. Открыв папку Debug (или Release, если вы билдили релиз-версию) вы увидите не только dll, но и lib файл. Этот lib представляет из себя заглушку для длинного перехода (long jump) в пространство памяти вашей DLL (об организации памяти в винде и её разделении между процессами и потоками мы поговорим в части, посвящённой WinAPI). Самый простой способ использовать вашу dll - это использование этого либа. Подключаем его также, как и раньше. Подключаем хейдер (#include), который надо несколько исправить. Дело в том, что аналогично таблице экспортов существует таблица импорт и соответственно __declspec(dllimport). Но разные заголовки писать не хочется. Откроем свойства dll-проекта. C/C++ => Preprocessor. В поле Preprocessor Definitions (препроцессорные объявления) записаны макросы, передаваемые компилятору с командной строки (ключом -D). Последний из них типа как уникальный для нашей dll. Копируем его. Пишем в хейдере:

#ifdef пастим_сюды
#	define CONINT_API __declspec(dllexport)
#else
#	define CONINT_API __declspec(dllexport)
#endif
Т. е. макрос CONINT_API раскроется для этой dll-ки как экспорт, а для всех других проектов как импорт. Вставляем этот макрос (CONINT_API) перед всеми функциями. Всё билдится. Пытаемся запустить проект, использующий dll-ку, для смены активного проекта в солюшене выберите пункт Set as StartUp Project (установить как запускаемый проект) в Solution Explorer. Вываливается страшное окно с просьбой о переустановки приложения. Всё очень просто, при загрзуке dll винда ищет его следующим способом:
  1. Папка, из которой запущен модуль, требующий dll-ку
  2. Текущая папка
  3. %SystemRoot%\System32
  4. Все пути, указанные в переменной окружения PATH (по непонятным для меня причинам на самом деле проверяются не все пути из PATH).
Кидаем dll-ку в папку к экзешки (или в system32, если dll-ка используется не в одном проекте) и радуемся.

Возможно (хотя не факт) у вас возник вопрос, как работать с перегруженными функциями - мы ведь имеем несколько функций с одинаковым именем, при использовании их внутри проекта, естественно, инфа о именах не сохранится, но в библиотеках, а тем более dll, где это имя прописывается явным образом... На самом деле всё гораздо проще. Линкер C++ генерирует при линковке украшенное (decorated) имя, например, для ciDoMenu: __imp_?ciDoMenu@@YAHQBQBDIQBD@Z (для линкера VC++ 7.1). Это имя генерируется на основе реального имени функции и её прототипа (т. е. по этой ерунде на самом деле можно восстановить список параметров). Вот вам и перегрузка! Такая линковка называется линковкой в стиле C++. Если вы предполагаете использовать вашу функцию где-то ещё (например в C), то вам придётся использовать линковку в стиле C. Для этого заключите объявления функций в блок extern "C", например:

extern "C"
{
    void Func1(void);
    int IntFunc(int param);
}
Аналогично существует extern "C++" (его имеет смысл использовать лишь внутри extern "C". Практически, все заголовки сишного рантайма имеют такую структуру:
#ifdef __cplusplus // напомню, макро __cplusplus по стандарту должен определять автоматически любой препроцессор C++
extern "C"
{
#endif

// ...

#ifdef __cplusplus
}
#endif
Ещё можно экспортировать/импортировать переменные (они будут находится в модуле библиотеки, но из основной программы к ним будет предоставлен способ). Никакие описатели extern при этом не нужны, о взаимосвязи переменной, объявленной в библиотеки и основной программе заботится ОСя, а не C++.

MSDN

  • dllexport, dllimport - dll импорт/экспорт.
  • Using extern to Specify Limkage - extern "C" и "C++".
  • Two Choices for Precompiling Code - Прекомпилированные заголовки.

Файлы

Солюшен с проектами


Параметры командной строки и некоторые дополнительные функции

Говоря о интерфейсе командной строки, обычно подразумевают две его реализации. В первой программа представляет из себя интерпретатор командных строк, вводимых с клавиатуры, для выхода используется специальная команда (вроде quit или exit). Типичные виндовые утилиты, использующие такой принцип: cmd, telnet, edline. Существует второй подход - командная строка передаётся сразу при запуске приложения, приложение её обрабатывает и завершает работу. Типичные примеры: format, taskkill, upx. Например, распаковка upx-шного PE файла выглядит так: "upx -d PackedFile.exe". Существуют программы, в которых реализованы оба способа. Например, утилита из стандартной поставки WinNT - cmd, которая обрабатывает в качестве команды строку и переходит в режим интерпретатора командных строк. Наконец, таки любая программа работающая с некоторыми документами принимает открываемый файл в виде командной строки. Например, "notepad MyBook.txt" - открытие файла MyBook.txt в блокноте, или "notepad /p MyBook.txt" - печать файла MyBook.txt в блокноте.

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

int main(int argc, char* argv[])
Замечу, что первый параметр при обычном запуске программы из оболочки - это путь к вашему экзешнику (это не верно, если ваша программа была запущена другим способом, например функцией CreateProcess из WinAPI, но в любом случае argv[0] не стоит учитывать). Для примера разбор командной строки (парсинг) для upx-а может быть таким:
int main(int argc, char* argv[])
{
    if (argc == 0)
    {
        // TODO: вывод сообщения о правилах использования
        return 0;
    }

    char szFileName[MAX_PATH];
    strcpy(szFileName,argv[argc-1]); // последний параметр - путь к файлу
    if (_access(szFileName,4) == -1)
    {
        // TODO: вывод сообщения о невозможности чтения файла
        return 0;
    }

    if (argc > 1)
        if (strcmp(argv[argc-2],"-d") == 0) // совпадают
        {
            Decompress(szFileName); // внутренняя функция
            return 0;
        }

    Compress(szFileName); // внутренняя функция
    return 0;
}
Для того чтобы передать программе аргументы командной строк есть несколько способов. Рассмотрим как это делается под виндой. Первый способ - использование окна запуска программы ("Пуск => Выполнить" или "Start => Run"). В этом окне можно указать путь к файлу программы, а также параметры командной строки. Второй способ - стандартный интерпретатор командных строк из Windows NT (cmd). Синтаксис базовых команд напоминает синтаксис команд MS-DOS (cd, dir и др.). Для выбора папки или файла можно пользоваться клавишей Tab (автозавершение имени). Вдобавок к тому вы увидите результаты работы программы, выводимые в STDOUT (множество консольных утилит, например, тот же upx, автоматически завершают работу после выполнения действий). Третий способ - использование пакетных файлов обработки (батники). Создайте в папке с PackedFile.exe текстовый файл с расширением .bat и следующим содержанием:
upx -d PackedFile.exe
pause
Последняя строка позволяет увидеть результат работы программы (в данном случае отчёт), фактически вызывает _getch. Многие файловые менеджеры (например, TC или FAR) предоставляют свои способы для передачи аргументов командной строки. Из своих программ вы можете это делать с помощью функции system. Кроме того в винде можно запускать программу с параметрами командной строки с помощью контекстного меню и манипуляций с реестром, но эта тема не входит в наш урок.

Очень часто в качестве аргументов командной строки передаются числа. Например утилита удалённого выключения или перезагрузки системы shutdown из Windows XP (её идея взята из одноимённой никсовой утилиты) может быть запущена так:

shutdown -r -t 40 -m \\COMP1 -c "!!!"
Данная строка перезагружает компьютер с именем COMP1, до перезагрузки 40 секунд будет показано сообщение из трёх восклицательных знаков (чтобы рядовой пользователь понял причину перезагрузки). Так вот, вопрос в том, как принять эти числа. В данном случае мы лишь получим в массиве argv строку из двух символов (не считая ноль-символ), но не число. В одном из примеров (калькулятор) нам уже приходилось переводить данные из строки в число, но, так как эта задача довольно частая, в стандартной библиотеке есть готовые средства. Как и многие функции-утилиты, эти функции объявлены в заголовке stdlib.h. Этих функций три, все они в качестве параметра получают константную нуль-строку, а возвращают:
  • atoi - int
  • atol - long
  • atof - double
Последняя функция дополнительно объявлена также в math.h (как функция работы с вещественными числами). В случае ошибки конвертирования все функции возвращают 0 (соответственно: 0, 0l, 0.0).

Рассмотрим ещё некоторые функции-утилиты. Во-первых - генератор псевдослучайных чисел. Все средства для работы с ним объявлены в stdlib.h:

  • int rand() - возвращает очередное псевдослучайное число. Это число находитя в диапазоне от 0 до RAND_MAX, где RAND_MAX - константа, объявленная в stdlib.h (обычно это максимально допустимое целое число, но не обязательно).
  • void srand( unsigned int seed ) - устанавливает базу для генерация очередного случайного числа, при каждом вызове rand, эта база заменяется результатом rand, изначально её значение - 0.
Для генерации целого числа от 0 до N обычно используют остаток от деления на N (rand() % N). Для генерации вещественного числа от 0 до 1 (включая 1), проще всего сделать так:
float f = rand();
f /= RAND_MAX;
Оба этих способа (в первую очередь второй, а также первый при достаточно больших N) не обеспечивают равномерного распределения генерируемых чисел (при условии, что rand даёт равномерное распределение чисел от 0 до RAND_MAX), но зато наиболее просты и быстры, в следствие чего чаще всего применяются.

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

Итак, работа со временем. Все необходимые объявления - в time.h. Во-первых, структура tm, которая включает в себя следующие целочисленные поля (int):

  • tm_sec - секунды (0-59)
  • tm_min - минуты (0-59)
  • tm_hour - часы (0-23)
  • tm_mday - день месяца (1-31)
  • tm_mon - месяц (1-12)
  • tm_year - год (начинается с 1900, т. е. 2006 год = 106)
  • tm_wday - день недели (0-6, начиная с воскресенья)
  • tm_yday - день года (0-365)

Для представления времени используется тип time_t (фактически - целочисленный тип, объявленный с помощью typedef), многие компиляторы предоставляют также расширенный набор дат, реализуемый с помощью 64-битных типов и функций, но их описание не входит в цель наших уроков, при необходимости пользуйтесь документацией вашего компилятора. Ещё есть тип clock_t, предназначенный для промежутков времени. Количество секунд в одной единице clock_t определено константой CLOCKS_PER_SEC.

Функции работы со временем:

  • time_t time( time_t *timer ) - возвращает текущее время, если timer не NULL, то это значение также сохраняется в ячейке, на которую указывает timer.
  • clock_t clock( void ) - возвращает время, прошедшее после запуска программы.
  • tm* localtime( const time_t *timer ) - позволяет из типа time_t получить структуру tm, в случае невозможности представить время возвращает NULL. Замечу, что хотя функция возвращает указатель, память под структуру tm выделяется и освобождается библиотекой рантайма.
  • time_t mktime( tm *timeptr ) - обратная по отношению к предыдущей функции, при невозможности конвертирования возвращает -1.
  • double difftime( time_t time1, time_t time0 ) - возвращает разницу между двумя моментами времени в секундах (возможно, с долями секунд).
  • char* asctime( const tm *timeptr ) - получает строку из структуры tm (опять таки удалять строку не надо, поэтому, если вы собираетесь модифицировать строку, то лучше вызовите strcpy).
  • size_t strftime( char* s, size_t maxsize, const char* format, const tm *timeptr) - форматирование времени. Результат сохраняется в строке s, maxsize задаёт предел размера строки (память под строку вы должны выделить сами). В форматной строке могут использоваться следующие управляющие символы:
    • %a, %A - сокращённое и полное название дня недели
    • %b, %B - сокращённое и полное название месяца
    • %c - дата и время в стандартной форме
    • %d - день месяца (01-31)
    • %H - часы в 24-часовом формате
    • %I - часы в 12-часовом формате
    • %j - день года (001-365)
    • %m - номер месяца (01-12)
    • %M - минуты (00-59)
    • %p - время суток (до/после полудня)
    • %S - секунды (00-59)
    • %U - номер недели года (первым днём считается воскресенье)
    • %w - номер дня недели (первым днём считается воскресенье)
    • %W - номер недели года (первым днём считается понедельник)
    • %x - стандартная запись даты
    • %X - стандартная запись времени
    • %y - год без учёта века (00-99)
    • %Y - год полностью
    • %z, %Z - сокращённое и полное название временной зоны
    • %% - знак процента

Замечу, что две последние функции работают с учётом локализации (региональных настроек). Мы уже немного говорили об этом в уроке про строки, но сейчас рассмотрим эту тему более подробно. Во-первых для работы с локализацией следует подключить заголовок locale.h (естественно, если вы не хотите вручную всё объявлять). Есть две функции для рботы с локлизацией, но одна из них (localeconv), позволяющая узнать детальную информацию по всем локальным настройкам вряд ли интересна (при необходимости пользуйтесь документацией). Вторая функция - setlocale, она предназначена для изменения текущей локализации (в пределах вашей программы, на системные настройки это никоим образом не влияет). Эта функция возвращает строку с информацией об установленных настройках и NULL при ошибке, обычно делается только проверка на NULL (и то не всегда). Функция принимает два аргумента: целочисленный category (к чему применять локализацию) и константную строку locale (собственно локализация). Первый аргумент может принимать следующие значения (они продефинены в locale.h):

  • LC_ALL - все категории
  • LC_COLLATE - приоритет символов при сравнении строк
  • LC_CTYPE - соответствие больших и маленьких букв
  • LC_MONETARY - денежные настройки (используется только в функции localeconv)
  • LC_NUMERIC - десятичный разделитель и другие параметры чисел (например, отделение разрядов)
  • LC_TIME - временные настройки

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

Первый сегодняшний пример представляет из себя уже до боли знакомую вам сортировку массива, но теперь массив генерируется автоматически и многие параметры для программы могут быть переданы с командной строки (об этом сказано в комментариях к программе). Здесь вы увидите довольно типичные приёмы парсинга (разбора) командной строки. В этом примере используется один приём, официально ранее не упоминавшийся - ссылка на указатель (int* &x). Такой приём позволяет изменить значение самого указателя в функции (не только указываемого значения). Второй пример очень прост, он просто сохраняет в файл info.txt информацию о текущей дате и времени, учитывая русскую локализацию (вывод на консоль не делается из-за проблем отображения кириллицы Windows-1251 в консоли).

MSDN

  • Argument Definitions - Аргументы командной строки.
  • atof, atoi, _ato64, atol - Функции перевода строки в число.
  • Compatibility and Compliance Issues in Visual C++ - Отличия VC++ от стандарта и исправленные ошибки (по сравнению с прежними версиями).

Файлы

Sort
DateTime


Работа с расширенными строками

Ну, что такое юникод, я думаю все знают. А если вы и не знаете - ничего страшного. Это просто универсальная кодировка для всех символов всех языков (по крайней мере используемых ныне) и кучи всяких на самом деле бесполезных символов (кому интересно - в винде есть стандартная утилита charmap.exe, можете посмотреть). Классическое решение для обеспечения всех этих символов кодами - это кодировать их не одним, а двумя байтами. Эта система получила название UTF-16. Существует другой принцип: английские буквы и цифры (точнее - первая половина ASCII-кодировки) кодируется одним байтом, русские буквы, греческие, немецкие и др. кодируются двумя байтами, а всякая дрянь - 4мя (кодировка составлена достаточно разумно: ситуации, когда некоторую последовательность можно отнести, скажем, к однобайтовому и двубайтовому варианту одновременно, невозможны). Эта кодировка получила название UTF-8 (вообще говоря, нелогичное название). К сожалению, работа с этой кодировкой не тривиальна (то есть внаглую интерпретировать символы как одно- или дву-байтовые числа не получится) и для работы с ней вам придётся заюзать стороннюю библиотеку, не входящую непосредственно в C++. В этом же уроке мы поработаем с UTF-16.

Во-первых существует специальный тип wchar_t, который предназначен для представления одного символа расширенной кодировки (как я уже сказал, обычно это UTF-16, т. е. wchar_t обычно соответствует unsigned short). Правда, в изначальный стандарт C++98 этот тип не вошёл, поэтому в некоторых компиляторах этот тип непосредственно отсутствует (объявляется в wchar.h с помощью typedef). Бегло пробежавшись по своему компутеру имеем: GCC, Digital Mars - поддерживают, Open Watcom - не поддерживает (онли инклюд wchar.h), Borland - не нашёл на компутере ( :( ), Visual - ... здесь самое хитрое. По дефолту в VC++ 7.1 тип не определён (при компиляции получаем целых две ошибки вместо одной в Open Watcom), но его можно включить специальным флагом компилятора: /Zc:wchar_t (в VC++ 8.0 этот флаг стоит по умолчанию). В VS открываем свойства проекта, затем ветку C++ => Language, наконец ставим поле "Treat wchar_t as Built-in Type" в Yes. Теперь у нас есть встроенный тип wchar_t (wide character type). Присваивать константные символы (литералы) переменным этого типа можно обычным способом, при этом произойдёт неявное приведение типа из char в wchar_t. Но допустим вы захотите присвоить именно юникодовский символ, если вы ещё помните 6-ой урок, то смело добавите перед апострофом большую L (вообще говоря, если и не помните, вам придётся сделать то же самое). К слову прикол: в VC++ без L всё компилируется, но при этом это чудо интерпретируется как int (судя по warning-ам о возможной потере данных при преобразовании из int в wchar_t) и в итоге (это уже по дебаггеру) вместо символа с кодом 8542 (изображает дробь семь восьмых) получаем неопределённый символ (по крайней мере не один системный шрифт не рисует это) с кодом 34206. Аналогичным способом (буквой L) можно и строки сохранять в юникоде (соответственно имеем массивы wchar_t или указатели на wchar_t). Кстати из следующих двух строк:

wchar_t wc1 = L'a';
wchar_t wc2 = 'a';
первая экономит время выполнения программы (не нужно ран-тайм конвертирование), а вторая - место (теоретически - на 1 байт, практически - этот байт съедается алигном секций исполняемого файла). Интересен также набор юникод-строк с клавиатуры: для некоторых символов курсор будет двигаться не вправо, а влево.

Для работы с юникодовскими строками используются свои функции. Образуются они двумя способами: либо добавлением буквы w (например, fwprintf), либо заменой str на wcs (например, wcscpy). Перечислять все функции я не буду, это не слишком интересно и не дюже полезно (держать в голове список не очень приятных для чтения функций рантайма). Замечу лишь, что в MSDN они описаны в одном топике с соответствующими ANSI-функциями.

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

int wmain( int argc, wchar_t* argv[])

Многие компиляторы предлагают ещё одну красивую фичу. Допустим вы хотите иметь и ANSI и UNICODE версии вашей программы, наиболее логично использовать препроцессор, особенно если макросы написаны за вас! Инклюдить надо tchar.h. Для того, чтобы показать, что это UNICODE версия надо объявить препроцессорную константу _UNICODE. Чтобы не переписывать файлы кода, обычно этот макрос определяют в командной строке компиляции, например: "cl main.cpp -D_UNICODE". В студии делаем так: свойства проекта, вкладка General, опция Character Set (кодировка). Там есть ещё одна интересная опция - Multi-Byte Character Set, она определяет такой набор констант препроцессора, что функции и типы из tchar.h раскроются для работы с мультибайтовыми азиатскими кодировками, если вам это понадобится, то подробности смотрите в MSDN. Так вот, что мы имеем:

  • Тип TCHAR (обратите внимание на регистр).
  • Функции вроде _tcscpy.
  • Функция _tmain
Просто и изящно. Юзайте! Но не переборщите, помните, что хотя tchar.h есть в большинстве компилеров, он не входит в стандарт (и не войдёт, в стандартной библиотеке C++ есть более мощные и элегантные решения, основанные на шаблонах классов).

MSDN

  • ms-help://MS.MSDNQTR.2003JUL.1033/vclang/html/_pluslang_Argument_Definitions.htm - Использование tchar.h.
  • ms-help://MS.MSDNQTR.2003JUL.1033/vccore/html/vcrefZcforScope.htm - Опция /Zc:wchar_t.
  • ms-help://MS.MSDNQTR.2003JUL.1033/vclang/html/vclrfStandardComplianceIssuesInVisualC.htm - Отличия VC++ от стандарта и исправленные ошибки (по сравнению с прежними версиями).


Практика структурного программирования

Итак, вы познакомились с основным инстрментарием структурного программирования на C++ (во многом это относится и к C). Вы можете использовать различные алгоритмические конструкции (ветвление, циклы с пред- и постусловием, циклы for, выбор), писать и использовать вспомогательные алгоритмы (функции), работать с различными структурами и типами данных (массивы, структуры, перечисления, объединения, битовые поля, typedef), работать с ссылками и указтелями, работать с основными библиотеками рантайма C (точнее частями одной библиотеки CRT - C Runtime Library). Вы также знаете некоторые иные интересные вещи: указатели на функции, внешние и статические переменные, описатель const и т. д. Но надо заметить, что за время существования структурного программирования вообще, а также языков C и C++ сформировалось много различных (и довольно важных) идиом программирования. Именно их я в первую очередь отношу к практике программирования.

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

Наверно, вы заметили (да и я говорил об этом вначале), что хотя я в этом учебнике пишу в первую очередь именно о C++ (а не Borland C++, или Visual C++, или GNU C++), я рассматриваю многие операции и возможности специфичные для компилятора Visual C++ и IDE Visual Studio .NET, хотя вы можете пользоваться другим копилятором и другой IDE (или вообще не пользоваться IDE), я не против. IDE (Integrated Development Environment - интегрированная среда разработки) VS .NET включает множество удобных инструментов, использование которых действительно ускоряет вашу работу. Но чтобы работать с этими инструментами, надо знать, что они из себя представлят. Я, конечно, не беру целью описать все возможности этой замечательной среды разработки (со временем, я думаю, вы сами откроете многие из них - так показывает статистика), но всё же расказать об основных возможностях редактора и отладчика, я считаю, стоит.

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

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

В конце части вас ждёт практикум по структурному программированию, но не буду разглашать всё сразу :) .

Ну, что ж - дерзайте!


Оформление кода

Это так сказать урок для затравки - что отличает настоящего (пусть и глупого) программиста от умного, но не настоящего. Не надо относится к оформлению кода также как к оформлению в принципе. Оформление кода - это не дизайн, а культура (есть такое популярное словечко - C++ulture) и возможность обеспечить чтение кода (на самом деле оформление кода - это одна из мелких задач проектирования). Во многих вопросах разные программисты расходятся во мнениях (например, использование венгерской нотации), замечу по этому поводу, что во-первых вы вольны в выборе использования тех или иных рекомендаций, а во-вторых рекомендую всё же придерживаться (в разумной мере) стиля в котором написана библиотека, которой вы в основном пользуетесь в вашем проекте (движок). Из последнего правила есть исключение - при программировании на основе CRT Library. Эта библиотека основана на стандартной библиотеке языка C, в C++ же, грубо говоря, возвращать в качестве ошибки что-то вроде -1 (или 0, или 1) не есть гуд, юзайте bool или enum. Кроме того имена вроде wsprintf меня лично не прельщают. Недавно слышал, как одному программеру пришлось рефакторить (то есть переписывать код с целью соответствия стандартам оформления и идиомам нормального программирования) одну библиотеку, так вот, догадайтесь, что такое sexi. Не знаете? Объясняю - standard exception id (стандартный идентификатор исключения). Или, по Булгакову - "пампуш на твербуле" (памятник Пушкину на Тверском бульваре).

Итак, первый вопрос имена. Во-первых, использование префиксов и венгерской нотации. Здесь могу сказать, что в структурном программировании рекомендую использовать почти всегда, исключение - небольшие локальные блоки, где удобней использовать temp, а не nTemp. Хотя, если лишние префиксы вам не нравятся и (как вы считаете) портят программу - ваше право. Главное - не мешайте всё в кучу (в одинаковом контексте раз делаем по одному, другой - по другому). Для функций, всё-таки рекомендую пользоваться префиксами (Get - получить значение, Set - установить значение, Do - выполнить действие, Is - проверка на соответствие условию и др.). Второй момент - деление имён на слова. Существует два способа: стиль InfixCaps (новое слово выделяется большим регистром) и использование underscape-ов (пишут в нижнем регистре, а между словами ставят символ подчёркивания). При использовании венгерской нотации (но не вообще префиксов, вроде get) второй способ не приемлем. Так как в локальных блоках префиксы обычно не удобны, то там чаще используют второй способ. Есть модификация стиля InfixCaps - первый символ пишут в нижнем регистре, при этом также отметается венгерская нотация. Это делается, чтобы отделить имена типов (пишущихся в классическом InfixCaps) от остальных имён, что позволяет переменные типа MyType назвать myType (такое оформление используется почти во всех пакетах языка Java, а также в библиотеке Qt фирмы Trolltech и в очень многих других библиотеках). Для элементов же перечислений всё-таки большинство программистов использует венгерскую нотацию (enum MyEnum {meItem1, meItem2, meItem3};). Кроме того макросы препроцессора обычно пишут только в верхнем регистре с использованием подчёркивания.

Теперь об отступах. Структурное программирование в отличие линейного (со множеством переходов) позволяет выделить вложенную структуру программы. Для этого используют либо табуляцию, либо несколько пробелов (разница не принципиальна, но первое легче удалять :-) ). Существует извращенское понимание этой концепции. Скажем, приходилось видеть код когда фигурные скобки (если быть точным, это был Pascal, и там были begin/end) размещались далеко вправо, а основной код был слева. Видел и другие страшные сцены (особенно, это неприятно, когда просят разобраться, что же именно у них видите ли не работает в задаче, тихо поплевавшись, обычно выпытываешь условие, давишь Ctrl+A, Delete, Ctrl+S и начинаешь работать). Есть спор насчёт расстановки открывающей скобки: на следующей строчке (так считает большинство) или в конце предыдущей (иногда это называют UNIX стилем). Я рекомендую пользоваться первым способом. Он действительно удобней. Правила нормальных отступов довольно просты, проще понять на примерах:

// одиночный if
if (isStarting())
    doAction();

// блок if-а
if (isStarting())
{
    doAction1();
    doAction2();
}

// if - else
if (isCondition())
    doAction1();
else
    doAction2();

// if - else if (внимание, не else и затем if, а else if, так значительно понятней)
if (isCondition1())
    doAction1();
else if (isCondition2())
    doAction2();
else
    doAction3();

// switch
switch (my_enum)
{
    case meItem1:
        doAction11();
        doAction12();
        break;
    case meItem2:
        doAction2();
        return; // заметьте break уже не нужен
    default:
        break; // пустой дефолт-блок полезен в некоторых компилерах
}

// while
while (condition)
    doAction();

// for
for (int i = 0; i < n; ++i)
    doAction();

// бесконечный for (точки запятой пишем вместе)
for (;;)
    doAction();

// for с одной пропущенной секцией
for (int i = 0; ; ++i)
    doAction();

// do - while
do
{
    doAction();
} while (condition);
Также используйте пробелы (например, для разделения параметров функции), пустые строки (отделение функций от остальной программы, выделение логических блоков), разрывные операторы (при использовании длинных выражений).

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

Файлы

1
2


IDE Visual Studio .NET. Основы

VS (Visual Studio) - майкрософтская среда для самых различных областей программирования. Одним из ключевых её продуктов всегда был Visual C++. С 2002 года все продукты Visual Studio получили общую IDE с гордым названием Visaul Studio .NET (некоторые модули VS действительно выполнены с использованием технологии .NET, но в первую очередь эта приставка - коммерческий трюк). Появилось понятие солюшена, т. е. группы из разных проектов (возможно, что и на разных языках). Немного напишу о версиях VS, которые ещё можно найти у кого-нибудь на компьютере:

  • Visaul C++ 6 (тогда ещё по сути отдельный продукт). IDE 98-го года, по-моему, морально устаревшая. В добавок к тому компилятор в ней не дюже радует (в плане соответствия стандарту ISO C++). Тем, у кого стоит эта IDE, рекомендую сделать uninstall. Правда, если вы фанат именно 6-ки, то вам сложно будет перейти на что-либо другое. Но это лечится...
  • Visual Studio .NET 2002 (7.0). Первая VS .NET. Несколько глюкавая и недоработанная. Из достоинств: компилер довольно близок к стандарту, может конвертить проекты 6-ки (dsp -> vcproj, dsw -> sln) в новый XML-формат, приятный интерфейс. Из недостатков: во-первых недоработана, во-вторых некоторые приятные, хотя и страшные, вещи шестёрки (например, Component Gallery) исчезли, исчезли навсегда...
  • Visual Studio .NET 2003 (7.1). Одумавшись, в MS поработали и через год выпустили новую, довольно неплохую студию. Плюсы: стабильность работы, язык ещё несколько доработан, хорошая интеграция с MSDN и многое другое. Минусы: сделали Managed C++ (MC++), иначе называемый Managed Extensions for C++. Если в MS думали, что этим они прибавят популярности .NET, то они не угадали. Это расширение ужасно неудобное и страшное. Духом настоящего C++ как-то не пахнет. Ещё одна глупость, также реализованная в 2003-ей студии - язык J# (читается J Sharp), т. е. Java для .NET. Опять таки не слишком заметны проявления явовской идеологии.
  • Visal Studio 2005 (8.0). Приставка .NET в полном названии на самом деле не была убрана, но всё же 2005-ую студия обычно называют просто 2005-ой (не говоря о .NET), в т. ч. так делает и MS. С недавнего времени у меня стоит именно эта студия. Вещь довольно приятная и мощная. Появилось множество интересных вещей в дотнетовском фреймворке, значительно развит VB .NET, появился C# 2.0, отошёл на второй план MC++, появилась поддержка стандарта ECMA C++/CLI. Последнее уже интересно, хотя настоящие сипипишники всё-таки ждут выхода нового стандарта C++0x (судя по названию, это в худшем случае 2009 год - C++09). О различных фичах, интересных нам - чуть ниже.
У меня стоит 2005-ая студия, поэтому описываю я именно её. Да и вам рекомендую пользоваться именно ей (если вы не противник VS в принципе). Немного о новых фичах именно 2005-ой студии:
  • Во-первых - собственно среда разработки как таковая:
    • Появилась возможность сохранения/загрузки настроек студии. Те, кто перетаскивал панельки, настраивал хоткеи и многое-многое другое после очередной установки студии или глюка системы (бывало настройки сбрасывались) меня поймут...
    • Появилось новое окошко Error List - для вывода ошибок, предупреждений и сообщений. Ранее всё это отображалось на Task List, который всё-таки более подходит для других целей. Что ж сказать - удобно.
    • При перетаскивание панелей вы увидите вот такую гадость. Перетащив панельку на соответствующей значок этого менеджера размещения вы закрепите её там, где надо. Рулез!!!
    • Зажмите Ctrl и нажмите Tab, не отпуская Ctrl, вы увидите удобный IDE Navigator по всем открытым документам и панелям. Щелчком по нужному документу/панели переходим к нему/ней (также можно пользоваться стрелками или табать/шифт-табать дальше). Вначале было немного странно, а когда привык - понравилось.
    • Также список открытых документов можно увидеть, щёлкнув по кнопке Active Files рядом с крестиком для закрытия активного документа.
    • Тем у кого есть постоянный коннект к интернету, также приятно будет посмотреть на новую Start Page, в которой вы всегда увидите в сжатом виде свежие новости из MSDN-Online, а также на новое меню Community, позволяющее интерактивно общаться с другими разработчиками.
    • Букмарки (закладки) и соответствующая панелька. Можно растаскать букмарки по папочкам (виртуальным). По идее удобно, но пока не привык.
    • При дебаге (отладке) подведите мышку к имени переменной... Вместо обычного тултипа вы увидите довольно занятный тултип + дерево. Удобность я думаю вы оцените, когда мы изучим дебаггер.
    • Хороший поиск в MSDN-ке: возможность фильтра по языку, технологиям и типу страниц, а также удобная выдача результатов (на манер интернетовских поисковиков).
    • Повсеместная поддержка 64-битных систем (компилятор, дебаггер, инсталлер и др.)
    • Утилита для билдинга проектов (в XML-формате) из командной строки.
    • Разные новые фичи для создания Add-In-ов (расширений студии).
    • Автовосстановление файлов (система аналогична офисной) - наконец-таки.
  • Далее - некоторые фичи в IDE конкретно для VC++:
    • Окошко Call Browser - позволяет легко посмотреть какая функция где вызывается. Не придумал правда, где это впихнуть бы... (хотя уверен, что можно)
    • Возможность просмотра наследуемых классов в Class View.
    • Окошко Find Symbols Result для поиска всех обращений к какому-либо имени. Удобно. Надо привыкать (честно признаться, сам ещё до сих пор юзаю Find in Files, когда рациональней Find References - привычка...)
    • Улучшен автокомплит кода (по крайней мере так говорит MS).
    • Новые визарды для создания проектов, в том числе визард для создания проекта на основе существующего кода.
    • Property Sheets. Для чего это нужно пока толком не разобрался.
    • Rule-файлы. Позволяют использовать для билдинга любые средства. Ранее существовал (есть и сейчас) Custom Build Tool - вещь понятная, но не удобная. Rule-файлы непонятны, но по первому впечатлению достаточно удобны. Наверно, самый правильный способ для использования сторонних компилеров из студии или же Qt-шного метакомпилера (moc).
    • Поддержка юникода везде, где только можно. Даже для имён в коде. Теперь будем писать МойКласс вместо MyClass :) (только не вздумайте так делать, это как минимум не эстетично + ваш код пахнет нацизмом + ни по одному стандарту (ISO C 99, ISO C++ 03, ECMA C++/CLI 05) не работает).
    • Многопоточный и многопроцессорный билдинг. Второго испробовать негде, а вот первое понравилось (для солюшенов из нескольких проектов).
  • Немного из языковых и библиотечных фич (я не касаюсь кучи новшеств, связанных с поддержкой стандарта C++/CLI).
    • Новая защищённая версия сишного рантайма.
    • Отдельно вынесены все функции, направленные на работу со специфическими технологиями процессоров (intrin.h).
    • Отладочные и отмеченные итераторы в STL. Что это такое толком пока не могу сказать.
    • for each для работы с STL-овскими коллекциями. Изврат, но факт.
    • Куча новых опций компилера (а соответственно, новых возможностей).
    • Поддержка компиляции для смартфонов и КПК, а также соответствующие версии MFC и ATL. Причём по сторонним отзывам (сам не могу сказать) эмулятор VS превосходит многие другие эмуляторы.
    • Поддержка компиляции под 64-битные платформы. Причём как x64 (AMD64/EM64T), так и Itanium.
    • Можно указать, какой фактически тип будет использовать для хранения перечислений: enum MyEnum: unsigned char {meItem1, meItem2};.
    • Ключевые слова abstract, override и sealed, в том числе для нэтивного кода (вообще говоря, в этом косвенно виноват C++/CLI).
    • Поддержка спецификации OpenMP для компиляции под многопроцессорные вещи. Звучит красиво...
    • Новые прагмы (#pragma): region и endregion. Можно указать также имя региона. Позволяют сворачивать/разворачивать куски кода в студии с подписью имени. Надо влепить эту штуку во многие визарды.
    • Все DLL-ки библиотек VC++ теперь прилинковываются с помощью манифестов. Если ваша программа будет запущена под виндой меньше XP, то DLL-ки (для вас пока только msvcrxx.dll и msvcrxxd.dll, где xx для 2005-ой студии - 80) будут искаться классическим способом (урок про DLL). Но в икспюхе сделаны попытки победить DLL Hell. Есть у вас папочка %WinDir%\WinSxS в ней куча папочек, вроде x86_Microsoft.Windows.Common-Controls_6595b64144ccf1df_6.0.2600.2180_x-ww_a84f1ff9. Как видите название папки состоит из следующих вещей: архитектура процессора, имя библиотеки (MS рекомендует включать сюда компанию, раздел и само название), версию (6.0.2600.2180) и хеш-ключ (берётся на основе сертификата безопасности с помощью утилиты Pktextract.exe). Вы включаете в ресурсы своего приложения манифест (по сути - XML-файл), где пишете, какие библиотеки и каких версий вам требуются. При запуске вашего приложения под икспюхой будет загружена нужная библиотека. Замечу, что манифест тыкается к вашему приложению теперь автоматом с помощью Manifest Tool в свойствах проекта есть соответствующая ветка. Если вам интересна информация о технологии манифестов и сборок, рекомендую топик How To Build and Service Isolated Applications and Side-by-Side Assemblies for Windows XP из MSDN.

Вообще говоря многие новые возможности новой VS остались за кадром, но я думаю, что вы и так поняли, что я от вас хочу. Более подробно смотрите в MSDN-ке (What's New). Также не бойтесь, если что-то из этого списка вам не понятно

Несколько замечаний по установке VS .NET. На определённом шаге вас спросят о типе установки, ставьте custom. (даже если вы поставите в итоге всю студию, интересно хоть посмотреть, что ставить-то будем). После этого появится шаг настройки устанавливаемых компонентов. Самый первый элемент - устанавливаемые языки и их компоненты. Обязательно поставьте галку C++ ;) (я имею в виду VC++ + все компоненты) . Остальное - по желанию. Также в группе Enterprise Development Tools => Visual Studio SDKs рекомендую отметить HTML Help SDK (чтобы использовать в своих программах HTML Help - chm файлы), ещё поставьте в конце Tools for Redistrutabing Applcation (простой, но полезный инсталлер). Выделив коренной элемент дерева компонентов, вы получите возможность в поле справа изменить папку, в которую вы хотите ставить VS, во многих местах вам придётся встретиться с надписью %VSINSTALLDIR%, которая и означает эту папку. Жмём Install. Установка автоматически создаст в меню группу программ для VS.

Теперь ещё несколько важных папок (* означает номер версии: 7, 8 и т. д., может быть пустой в некоторых дистрибутивах):

  • %VSINSTALLDIR%\Common*\IDE - здесь лежат основные элементы IDE.
  • %VSINSTALLDIR%\Common*\Tools (%VS71COMNTOOLS%) - общие утилиты Visual Studio.
  • %VSINSTALLDIR%\Common*\Graphics - небольшая коллекция рисунков и значков :)
  • %VSINSTALLDIR%\VC* - папка Visual C++.
  • %VSINSTALLDIR%\VC*\crt\src - исходники C Runtime Library.
  • %VSINSTALLDIR%\VC*\atlmfc - папка библиотек ATL и MFC, там можно найти либ-файлы, инклюд-файлы и исходники
  • %VSINSTALLDIR%\VC*\PlatformSDK - папка Windows SDK (инклюды и либы)
  • %VSINSTALLDIR%\VC*\bin - компоненты VC++ (компилер, линкер, компилер ресурсов и др.)

Теперь об интерфейсе. Во-первых у нас есть меню (как ни странно). В этом меню есть пункт Tools => Options, из которого доступны все настройки VS. Настройки поделены на группы, собранные слева в виде дерева. В дальнейшем я буду говорить о настройке некоторых вещей в этом окошке. Кроме того мы имеем множество панелей инструментов. Они настраиваются либо с помощью контекстного меню, либо с помощью пункта Tools => Customize. Думаю разберётесь. Далее есть в VS множество панелей (Tool Window), например Solution Explorer. Панели группируются в блоки панелей, при этом внизу вы можете найти вкладки для переключения между панелями одного блока. Для того, чтобы убрать некоторую панель из блока достаточно перетащить её за заголовок вкладки (при перетаскивании за заголовок панели будет перемещён весь блок). Аналогично можно втащить любую панель в блок. Если панель одна, то вкладки не отображаются (что вполне логично). При перетаскивании панели или блока к границе экрана её можно закрепить там, это VS делает автоматом, надо лишь подобрать подходящее место, при перетаскивании вы увидите предполагаемые границы панелей, а в 2005-ой - мнеджер размещения (см. выше). По дефолту все панели прикреплены к границам. Закрывается панель крестиком наверху (как ни странно, опять таки), рядом вы увидите ещё одну кнопку, символизирующую канцелярскую кнопку (она доступна только в закреплённом состоянии панелей). Нажав её, вы увидите что блок панелей сползёт к краю экрана, превратившись в заголовок. Достаточно подвести мышку к заголовку соответствующей панели (для каждой панели блока отдельно), и она опять развернётся, если же в течение небольшой паузы на панели нет фокуса и над ней нет мышки, она свернётся обратно. Достаточно удобно для экономии места на экране. Чтобы сделать панель обычной, разверните её и ещё раз кликните по канцелярской кнопке (она называется Auto Hide - автоскрытие).

Очень важной является панель свойств (Properties), часто называемая просто панелью пропертей (ударение на о). Эта панель доступна пунктом View => Other Windows => Properties Windows, а также (по дефолту) по Alt + Enter. Панель состоит из четырёх частей. В верхнем списке можно увидеть (а также выбрать) элемент, свойства которого мы хотим поменять или посмотреть. Элемент динамически меняется при выборе его на одной из других панелей или в редакторе, поэтому этим списком обычно пользуются лишь для просмотра (хотя иногда, когда один элемент накрывает другой, приходится пользоваться списком). Обратите внимание, что там же отображается используемый редактор свойств, фактически - COM объект, а это означает, что можно создавать для каких-то элементов свои собственные редакторы свойств. Под списком расположен ряд кнопок, зависящий от текущего редактора свойств. Сейчас нам важны лишь две кнопки-переключателя: одна в виде списка с плюсиками и другая в виде букв A и Z со стрелкой. Эти кнопки позволяют переключить тип отображения свойств: с группировкой по категориям или только по алфавиту. Удобнее обычно первое, поэтому эта кнопка отмечена по дефолту. Далее мы видим список категорий и свойств. Свойства одной категории можно сворачивать/разворачивать кнопкой плюса/минуса. Каждое свойство имеет имя, отображающееся слева, и значение - справа. Некоторые свойства задизейблены (серые), то есть рид-онли. Изменяются свойства по-разному. Наиболее типичных (встроенных в общий редактор свойств) три: простой ввод значения, выбор из списка или настройка в каком-либо специальном диалоге после нажатия кнопки с тремя точками.

Основное место на экране занимают открытые документы. Они могут быть открыты в одном из редакторов VS (редактор исходного кода, HTML/XML редактор, HEX- редактор и т. д.) или во встроенном браузере (на движке IE), редакторы могут быть добавлены в VS (разработка расширений для VS не входит в задачи этого урока). Все документу по дефолту разбиваются по вкладкам, довольно удобно. Для закрытия - выбираем вкладку и давим крестик. Если все вкладки не помещаются на экране, тогда справа появляются кнопки для перемотки списка вкладок влево и вправо, в 2005-ой студии есть кнопка Active Files, а также кульный IDE Navigator (про это смотрите в начале этого топика).

При первом запуске VS мы видим в браузере окно Start Page (его можно открыть в любой момент: Help => Start Page). Наверно, оно должно быть удобным, но лично я его не дюже люблю. Чтобы его уничтожить в настройках студии на вкладке Environment => General (Среда =>Основное) из списка At startup (при запуске) ставим Show empty environment (показать пустую среду разработки). Можно также выставить загрузку последнего солюшена, открытие диалога открытия проекта, открытия диалога создания проекта и показ Start Page. На этой же вкладке вы найдёте ещё некоторые общие настройки (например, скорость анимации свертывания/развёртывания панелей, поставьте её на минимум - посмотрите как 'тащатся' панели ;) ).

Ещё пару важных настроек. Environment => Projects and Solutions: на самом верху можно установить базовую папку для ваших проектов (по дефолту - папка Visual Studio Projects в папке Мои документы). Environment => Keyboard - настройка кейборды. Во-первых можно выбрать готовую базовую схему, рекомендую для начала поставить Visaul C++ 6 (во многих источниках разработчики VC++ ссылаются именно на хоткеи VC++ 6). Во-вторых у нас есть список команд (подробней о командах - ниже). Выбираем команду (можно ввести часть команды в поле выше для ускорения поиска), видим список имеющихся хоткеев, любой из них можно удалить (Remove). Можно назначить новый хоткей: нажимаем нужную комбинацию в поле Press shortcut key и давим кнопку Assign. При этом внизу мы видим, где этот хоткей уже используется (если используется). Хоткеи могут состоять из нескольких комбинаций: например, нажимаем Ctrl + 1, затем Ctrl + 2, затем Ctrl + 3 и после этого выполняется команда. Изврат, но иногда удобно (если разумно использовать). Можно назначить хоткей, действующий в определённом контексте (например, только в браузере, или только в редакторе диалогов), для этого есть соответствующий список, где надо выбрать значение перед нажатием Assign (по дефолту стоит глобальный контекст). Раздел Source Control позволяет настроить параметры системы контроля версия (Control Version System - CVS). Сама CVS ставится независимо от студии, но лучше всего использовать Visual SourceSafe (он часто, особенно в DVD-версии, идёт в поставке VS) из-за его отличной интеграции в VS. Debugging => Just-In-Time: можете поставить типы приложений, которые (при ошибке) VS будет пытаться загрузить в свой дебаггер, т. е. отладчик (даже если VS не запущена), а именно: CLR (.NET приложения), Native (машинный код), Script (JS и VBS, выполняемые MS-шным движком). Debugging => Native: дополнительные настройки для нэтив-дебага. Load DLL exports - загрузка таблицы экспорта DLL в дебаггер (по заверениям MS влечёт 'некоторое переполнение', поэтому по дефолту отключено, рекомендуется при достаточно низкоуровневой отладке). RPC debugging - дебаг удалённых COM процедур (Remote Procedure Calling), если у вас нет локальной сети смысла в этой опции нет. Projects => VC++ Directories: как вы помние, здесь можно настроить список различных папок VC++, замечу лишь для чего ставят папки для исходных файлов (Source files) - для возможности при дебаге захода в библиотечные функции в нормальном виде (если исходников нет, то только окошко Disassembly вам в помощь).

Как уже было сказано верхним объектом в VS является солюшен (Solution - Решение). Солюшен состоит из проектов. Для создания проекта выбираем File => New => Project (или же соответствующую кнопку панели инструментов, или же хоткей, по дефолту - Ctrl + Shift + N, далее для всех действий я пишу только один способ). Слева дерево, где мы выбираем тип проекта, справа - список шаблонов проектов. Ниже есть поле имени проекта и базовая папка для проекта (по дефолту - та, что вы выставили в настройках студии). В самом низу вам указывается, где, фактически, будет расположен ваш проект (этот путь складывается из базовой папки и имени). В этой папке будет создан также файл солюшена. Если у вас будет солюшен из нескольких проектов, можно создать отдельную папку для солюшена, но обычно я этим не пользуюсь. Лучше создать вначале пустой солюшен, для этого есть тип Visual Studio Solutions и шаблон Blank Solution.

Для работы с солюшеном удобней всего использовать панель Solution Explorer (проводник солюшена). Она активируются командой View => Solution Explorer (если её не было на экране, то она появится). Эта панель представляет из себя дерево, коренной элемент которого - ваш солюшен. Все операции реализованы с помощью контекстного меню. Во-первых, о командах меню солюшена (корень дерева). Add => New Project (Добавить => Новый проект) - открывает окно создания проекта, при этом базовая папка ставится в папку солюшена. Add => Existing Project (Добавить => Существующий проект) - позволяет добавить в солюшен ранее созданный проект (откроется стандартное окно открытия файла), желательно предварительно скопировать его в папку солюшена (чтобы не вышло путаницы). Тут же мы найдёп пункты для билдинга и ребилдинга всего солюшена. Ребилдинг отличается от билдинга тем, что при этом заново будт перекомпилированы все файлы проекта, а при билдинге только изменённые после последнего билда. Можно также переименовать солюшен (Rename или F2). Теперь о меню проекта. Add => Add New Item - открывает окно добавления файла к проекту, окно напоминает окно создания проекта (по структуре). Add => Add Existing Item - добавление к проекту существующего файла (опять таки желательно скопировать его в папку проекта), можно добавить сразу группу файлов. В меню Project Only (только проект) можно найти пункты билдинга и ребилиднга одного проекта. Пункт Properties открывает очень полезное окно свойств проекта, неоднократно используемое нами ранее. Интересный пункт Project Dependencies (зависимости проекта) - можно установить от каких проектов зависит этот проект (например, основная программа от библиотек), от этого зависит порядок билдинга солюшена. Внутри проекта вы скорей всего найдёте несколько виртуальных папок (это верно по крайней мере для проектов Visual C++). Для вашего удобства VS раскидывает отдельно сорсы (исходный код), хейдеры (заголовки) и ресурсы. Хотя если вы выберите один из подпунктов меню Add папки (а не проекта), то независимо от типа файл ляжет в эту папку. Виртуальные папки можно переименовать, а также создать новые (Add => New Folder из меню проекта). Наконец таки насчёт меню отдельного файла. Пункт Open, или дабл-клик, или Enter открывает файл в дефолтном редакторе. Open With - позволяет выбрать редактор для открытия файла. Например, для корректного отображения русских букв в консоли проще всего писать файлы в кодировке CP866. Для этого выбираем Open With, затем C++ Source Editor (with encoding) (Редактор кода C++ (с кодировкой)), Open. Выбираем "Кириллица (DOS)" и жмём OK. При выборе редактора кнопкой Set as Default можно установить выбранный редактор дефолтным. Ещё есть важный пункт Properties, открывающий свойства файла. Обратите внимание, что свойства для каждой конфигурации (например, дефолтные Debug и Release) могут быть свои. В свойствах мы можем указать, что файл надо исключить из билдинга (Excluded From Build) в данной конфигурации, а также выбрать чем его билдить собственно говоря. Можно ручками настроить командную строку билдинга (например, для сторонних компилеров), для этого выбираем Custom Build Tool. В зависимости от используемого инструмента для билдинга появится ещё ряд настроек, доступных в дереве слева.

Насчёт конфигураций проектов. Открыть их можно с помощью команды Build => Configuration Manager. В появившимся окошке наверху есть список конфигураций. В этом списке есть два дополнительных элемента: New и Edit. Выбор New влечёт за собой открытие окна создания конфигурации солюшена. В этом окне мы вводим имя конфигурации, а также можем выбрать конфигурацию на основе которой надо создать новую (все настройки будут скопированы). Если мы поставим галочку Also create new project configuration(s), тогда для всех проектов будет создана одноимённая конфигурация (зачастую это не нужно, и мы хотим оставить для всех проектов, скажем, Debug, а для одного создать какой-нибудь Extra Debug). Пункт Edit открывает окно, в котором можно удалить (Remove) или переименовать (Rename) конфигурации. Ниже активной конфигурации солюшена есть таблица конфигураций проектов (для данной конфигурации солюшена). Там есть аналогичные New и Edit. Есть также список платформ (в 2005-ой студии там также есть New и Edit - можем выбрать Win32 x86, Win64 x64, WinCE Smartphone (Pocket PC)). Также можно не билдить какие-то проекты в одной из конфигураций солюшена (галочка Build). Итак мы можем настроить конфигурации и выбрать активную (её можно также выбрать на панели инструментов). Все окна свойств (проектов, файлов) имеют список, в котором мы выбираем к чему мы применяем настройки. Этот список содержит все конфигурации, а также элементы Active (текущая конфигурация) и All Configurations (все конфигурации). У каждого проекта в свойствах на вкладке General можно задать Output Directory, то есть папку, где будет итоговый файл. Для солюшена из нескольких проектов удобно задать эту папку вроде "../Debug", в этом случае в папке солюшена у вас будут папки проектов и папка Debug, в которой будут лежать все готовые компоненты (exe, dll и т. д.). На вкладке Linker => General можно также в поле Output File ввести путь к итоговому файлу, обычно что-то вроде $(OutDir)/MyProgram.exe. $(OutDir) означает папку Output Directory (VS подставляет её автоматически). Можно, кстати, непосредственно здесь написать "../Debug/MyProgram.exe", но при этом возникнет проблема с DLL, т. к. для DLL мы в итоге получаем не один файл (ещё, как минимум, lib), поэтому лучше отдельно задать выходную папку, а отдельно выходной файл с использованием макроподстановки выходной папки.

Итак, в VS реализованы на высшем уровне все элементы интерфейса MS-шных рекомендаций по юзабилити (построению интерфейса, дружественного по отношению к пользователю): меню, панели инструментов и хоткеи. Для любой операции можно воспользоваться любым из этих трёх способов (можно ведь создать свои кнопки панелей инструментов). Но ведь всё-таки VS предназначена для программеров, потому есть ещё один, иногда довольно удобный способ. Это панель Command Window (окно команд), открываемая по View => Other Windows => Command Window (Вид => Другие окна => Окно команд). Дело в том, что в VS каждая операция реализуются через специальный менеджер команд, и каждая команда имеет имя. Можно сказать, что команды объединены в структуры, т. е. в группы. Например команда открытия солюшена выглядит так - File.OpenSolution (как вы заметили используется стиль InfixCaps). Набрав эту команду в окне команд (после символа >, означающего приглашение для ввода) и нажав Enter, вы увидите стандартное окно открытия солюшена. Но вряд ли удобно набирать длинные команды целиком вручную, поэтому VS позволяет упростить вашу задачу: нажмите первую букву и вы увидите список команд, введя ещё несколько букв можно значительно сократить список. Выбираете элемент и жмёте Enter (для немедленного выполнения команды) или пробел (для дальнейшего её ввода). Кроме того стрелками вверх и вниз можно ездить по истории команд. Можно кроме того назначить псевдонимы для определённых команд. Для этого используется специальная команда alias. Для назначения псевдонима наберите alias, затем пробел, имя псевдонима, пробел, имя команды (можно использовать автозавершение) и нажмите Enter. Например:

>alias OS File.OpenSolution
Теперь выполните команду OS. Результат будет аналогичен команде File.OpenSolution. Кстати сама команда alias - это псевдоним для Tools.Alias. Если вы наберёте только alias (или Tools.Alias) и имя псевдонима, то увидите текущую команду для этого псевдонима. Набрав просто alias вы увидите весь список псевдонимов. Для удаления псевдонима наберите alias, пробел, /delete и имя псевдонима. Ещё есть интересная команда - shell (полное название Tools.Shell), позволяющая выполнить любую командную строку системы. Например, команда "shell notepad c:/boot.ini" откроет файл boot.ini для редактирования в блокноте. А команда "shell regsvr32.exe MyCtrl.ocx" - попытка регистрации ActiveX компонента MyCtrl.ocx.

Вот, наверно, и всё по основам VS. Ссылки на MSDN не даю, так как, по-моему, в данном случае легче самому по кнопкам потыкать, чем читать умную документацию (к тому же не по-русски). Итак, рекомендую всё вышеизложенное пощупать руками.


IDE Visual Studio .NET. Редактор кода

Хороший редактор кода значительно ускоряет написание программ. Существуют даже специальные редакторы для программистов. Наиболее известны из них: Emacs, Vim, Notepad++ (эти три редакторы распространяются по лицензии GNU GPL, последний, правда, только для винды). Естественно, в VS есть довольно приличный редактор кода. Удобней рассмотреть его на примерах. Поэтому я рекомендовал бы открыть, например, два проекта: из 8-го урока и UniType. В последнем вам придётся создать новый проект и добавить туда файлы UniType.h и UniType.cpp, затем создать файл кода с функцией main и написать там две строки:

SetVal(ut,0);
return 0;

В этом же файле объявите глобальную (вне main) переменную ut типа UniType (не забудьте про инклюд UniType.h).

Первый инструмент редактора VS - Class View. Эту панель можно открыть одноимённой командой из меню View. Отмечу, что в 2005-ой студии (про которую я пишу) она несколько изменилась, поэтому, если у вас более старая версия, то всех несоответствия между тем, что я пишу, и тем, что есть у вас, валите на версии. Основу нашей панельки её составляет дерево из проектов, как и в Solution Explorer. Но в отличии от последнего с помощью этой панельки мы можем бродить по коду проекта, а не файлам. Развернув проект, вы увидите по крайней мере два элемента:

  • Global Function and Variables (глобальные функции и переменные) - в этой ветке вы найдёте все ваши функции и глобальные переменные (в том числе функцию main).
  • Macros and Constants (макросы и константы) - всё что вы объявили с помощью директивы define (в шестёрке эта ветка отсутствовала).

Замечу, что в 2005-ой студии у вас не удастся развернуть эти ветки. Вместо этого просто выделите нужную из них, и на нижней половине Class View (она есть только в 2005-ой студии) вы найдёте нужные элементы. Заметьте, что слева от имени для разных типов имён отображаются разные значки (функции - розовый кирпич, переменные - синий кирпич, для протипедефенинных типов - белый квадрат над листом и т. д.). Также заметьте, что функция main записана как main(void), т. е. с пустым параметром, MS так нравится больше. Двойным щелчком по функции (или Enter-ом) мы перейдём к её реализации (нужный файл будет открыт в редакторе кода и позиционирован на код функции). Аналогично можно 'открыть' переменные, т. е. их объявление. Кроме вышеназванных веток вы можете увидеть:

  • Global Typedefs - здесь собираются глобальные типедефы.
  • Ветки для всех ваших "сборных" типов: структуры, объединения, битовые поля, перечисления. Для каждого типа есть отдельная ветка. Например, UniType для соответствующей структуры. Заметьте, что в этой ветке есть подветка для анонимного объединения. Также посмотрите на его имя: __unnamed_0001_1. Вот такой у MS способ для создания анонимных типов. Самое интересное, что вы можете добавить в структуру, скажем целочисленное поле __unnamed_0001_1 (в вашей студии может несколько отличаться), никаких конфликтов не будет, но в то же время этого поля вы не увидите в Class View.

Таким образом, как вы поняли можно проследить всю структуру кода. Двойным щелчком (или enter-ом) по имени типа или переменной вы можете перейти к соответствующему объявлению. Для функции - к её реализации. Для перехода к объявлению используйте команду контекстного меню Go To Declaration. Рассмотрим другие команды контекстного меню из этой группы:

  • Go To Defenition - переход к реализации функции.
  • Go To Declaration - переход к объявлению.
  • Find All References - поиск всех ссылок на это имя. Результаты поиска отображаются на специальной панельке Find Symbol Results. Там вы увидите файл, номер строки и содержание этой строки, двойным щелчком можно перейти к этому месту в редакторе.

Следующие пункты позволяют настроить сортировку и группировку элементов (применяются к верхней и нижней панельке отдельно):

  • Sort Alphabetically - тупая алфавитная сортировка.
  • Sort By Member/Object Type (первое - для нижней, а второе - для верхней панели) - сортировка вначале по типу элемента, а затем по алфавиту. Стоит по дефолту.
  • Sort By Member Access - сортировка по доступу к элементам (это мы пока не изучали)
  • Group By Member/Object Type - группировка в отдельные ветки по типам (переменные, функции, структуры, перечисления и т. д.). На мой взгляд довольно удобно для нижней панели.

Пункт Properties переведёт вас на панель пропертей (зачем это делать отдельно на знаю, наверно, умнее выглядит). Здесь вы можете изменить (не открывая код в редакторе):

  • (Name) - имя элемента.
  • TypeString - тип элемента (для функций - тип возвращаемого значения).
  • IsConstant - константность (список из двух элементов: True и False).
  • IsVolatile - аналогично можете поставить описатель volatile (что это такое, вы узнаете чуть позже).
  • IsInline - аналогично делаем инлайновые функции (об этом тоже чуть позже).

Теперь поговорим о настройках редактора кода. Как вы, наверно, заметили редактор выделяет разным цветом простой текст, комментарии, ключевые слова, директивы препроцессора, строки и т. д. Эти цвета, а также шрифты можно настроить на вкладке Environment => Fonts and Colors. Если сделаете что-то некрасиво, то всегда есть кнопка Use Defaults. На этой вкладке можно задать абсолютно все цвета и шрифты (вплоть до тултипов). Нас интересует сейчас лишь группа Text Editor. Ну, думаю, что такое foreground и background (сам объект и фон) знают все, с переводом остальных элементов интерфейса проблем не должно быть (ну, например, что такое Font? а Size?). Итак, рассмотрим для чего можно всю эту гадость настроить:

  1. Plain Text - простой текст.
  2. Selected Text - выделенный текст.
  3. Inactive Selected Text - неактивный выделенный текст (активна какая-либо панель или что-то ещё, отличное от редактора).
  4. Indicator Margin - поле слева, на котором ставятся метки для букмарков и точек останова.
  5. Line Numbers - номера строк.
  6. Bookmark - букмарк.
  7. Brace Matching (Highlight) - скобки (любые: круглые, квадратные, фигурные) во время написания. Когда вы закрываете скобку, редактор VS выделяет эту скобку и соответствующую ей открывающую. По умолчанию - просто жирным шрифтом. Выделение убирается, когда вы напишите следующий символ или переместите курсор. Очень удобно для контроля уровня вложенности скобок.
  8. Breakpoint (много разных) - точки останова (брейкпоинты) отладчика (дебаггера), подробнее о них - в следующем уроке.
  9. Call Return - строка, на которую будет осуществлён возврат после выполнения текущей функции (это тоже для отладчика).
  10. Comment - комментарий.
  11. Function Block Start - начало функции.
  12. Identifier - идентификатор (имя).
  13. Keyword - ключевое слово.
  14. Number - число.
  15. Operator - оператор (знак операции).
  16. Preprocessor Keyword - ключевые слова препроцессора.
  17. String - строка.
  18. Track Changes after Save - так VS 2005 выделяет строки, которые были сохранены в текущей сессии и не модифицированы (выделение происходит на полях).
  19. Track Changes before Save - аналогичное выделение для изменённых, но не сохранённых строк.

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

printf("Press the quote (")!");

Синтаксическая подсветка сразу укажет вам, что ваша строка закончилась не там, где надо, т. е. вы забыли экранировать кавычку бекслешем. Что и говорить - удобно. Естественно есть и другие приятные вещи в редакторе VS. Все остальные настройки расположены в категории Text Editor (текстовый редактор) => C/C++, поэтому далее я лишь называю подкатегорию настроек, подразумевая, что искать их надо в этой ветке.

Ещё одна удобная вещь, которую вы, вероятно, также уже заметили - это сворачивание/разворачивание блоков кода - Code Outlining. Для этой цели слева от текста отображается линия с кнопками в виде плюсов (для разворачивания) и минусов (для сворачивания). Кнопки располагаются на первой строке некоторого блока, а на последней показывается метка (небольшой порожек). Напишите, например, некоторый блок if-else. Тут же появятся соответствующие минусы, кликнув по ним, вы вместо длинных блоков кода получите компактные две строки: на одной if с условием, а на другой - else. А вместо самого блока, вы увидите (на той же строке, что и заголовок блока) многоточие в фигурных скобках. Надо заметить, что это многоточие (вместе с фигурными скобками) будет заключено в рамочку и оформлено несколько иначе, чем остальной текст, как именно - настраивайте параметры Collapsible Text (свёрнутый текст) в ветке, указанной ранее. Подведя мышку, к этому "заколлапсенному" тексту, вы в тултипе (всплывающей подсказке) увидите текст самого блока (если он слишком большой - то его начало). Идея эта (Code Outlining) уже довольно старая, но по прежнему красивая - значительно удобней смотреть на код с разной "глубины". Обратите внимание, что подпись в свёрнутом тексте может быть разной. Например, сверните инклюды и дефайны в начале файла, и вы увидите подпись Declarations (объявления). Вы можете запретить сворачивание любого блока, для этого выделите его и из контекстного меню выберите пункт Outlining => Stop Hiding Current. Можно, напротив, скрыть любой блок (хоть он логически блоком и не будет). Для этого надо выделить нужный блок и выбрать в контекстном меню Outlining => Hide Selection. Есть у этого метода два недостатка. Во-первых, в качестве свёрнутого текста вы увидите лишь многоточие - не дюже конкретно. Во-вторых, информация о том, какие куски вы выбрали для сворачивания, сохраняется только в файле проекта. А это значит, что, открыв этот файл вне проекта или в другом проекте, вы не увидите на вашем блоке ни минуса, ни плюса. 2005-ая студия предлагает другой вариант, пришедший из C#. Напишите в начале блока директиву препроцессора pragma region имя_блока, а в конце - pragma endregion комментарий. После этого вы сможете сворачивать и разворачивать этот блок, причём с вашими (опциональными) именем блока и комментарием. Ещё одна полезная командочка в контекстном меню - Outlining => Collapse Block. Она пригодится вам, если вы убрали (с помощью Stop Hiding Current) сворачивание логического блока, а затем решили его вернуть, а также если у вас отключено автоматическое генерация блоков для сворачивания. В подветке Formatting настроек текстового редактора есть три галочки для Code Outlining:

  1. Enter outlining mode when files open - автоматическое включение режима Outlining (аутлайнинг) при открытии файла (то есть показ необходимых минусиков). Если эта галочка не стоит, то аутлайнинг не будет включаться автоматически совсем (до выбора команды колапсинга блока) независимо от следующих галочек.
  2. Automatic outlining of #pragma region blocks - автоматическое включение аутлайнинга для регионов.
  3. Automatic outlining of statement blocks - автоматическое включение аутлайнинга для блоков внутри функций.

Естественно, я рекомендую поставить все эти галочки (все они стоят по дефолту).

Попутно поговорим о других общих настройках. Во-первых - автоматическая табуляция кода. Настройки для неё в основном расположены на вкладке Tabs. В группе Tab можно настроить размер табуляции (эквивалентное количество пробелов), а также заставить студию использовать пробелы вместо табов (при сохранении файла). Для последнего надо поставить переключатель в Insert spaces вместо Keep tabs. Лично я не рекомендую делать последнего (ну, не люблю я пробелы...), хотя - ваше дело. Самое интересное - группа переключателей Indenting. Они управляют автоматическими отступами. Самое бесполезный вариант - None, то есть фиг вам автоматического отступа. По умлочанию, если не ошибаюсь, стоит опция Block. Эта опция наиболее привычна для большинства программистов C/C++. При этом при переходе на новую строку (ентером) уровень вложенности сохраняется по предыдущей строке. Есть также интересный вариант - Smart. Для тех, кому совсем лень табать ручками. Редактор автоматически будет вставлять или убирать отступ при переходе на новую строку. Правда, иногда он делает это не так, как хотелось бы - приходится исправлять. Большинство поэтому эту опцию не используют. Для смарт-табуляции есть ещё одна ненужная опция. Расположена она на вкладке Formatting и называется Indent Braces. Поставив эту галку, вы увидите, что фигурные скобки начнут дополнительно отъезжать вправо. Полезно для извращенцев... Остальные общие опции расположены на вкладке General. Во-первых, Word wrap - перенос длинных строк, достаточно удобно. Хотя в хорошо оформленном коде длинных строк просто не может быть. Ещё одна фича, появившаяся в 2005-ой студии - автоматическое выделение другим цветом блоков, которые будут исключены препроцессором (из-за директив условной компиляции). Разрешить или запретить это можно галкой Colorize inactive blocks in a different color на вкладке Formatting. В группе Display есть ещё две интересные опции. Во-первых, Line numbers - показ номеров строк в поле слева. Во-вторых - Enable single-click URL navigation. Поставив эту галку и написав в редакторе URL (например http://msdn.microsoft.com/library/default.aspx), вы сможете перейти по этому адресу, зажав Ctrl и кликнув по ссылке. Удобно, правда, по-моему, для редактора кода не особо нужно.

Последняя фича, о которой мы поговорим называется IntelliSense. В меню Edit есть одноимённое подменю, которым мы сейчас и займёмся. Во-первых, команда Complete Word. Она позволяет ускорить ввод различных имён. Напишите первые буквы имени и выберите Edit => IntelliSense => Complete Word. Если с ваших букв начинается только одно имя (учитывается также контекст, в котором вызывается команда), то оно автоматически будет дописано. Если больше - вы увидите список, стрелками выбираем нужный элемент и жмём Tab (можно и Enter - для C/C++ редактора всё равно, но в некоторых редакторах это вызовет нежелательный переход на следующую строку). Естественно, можно пользоваться и мыхой, но вряд ли удобно. Чтобы не лазить по меню, можно использовать хоткеи. По дефолту их два: Alt + Right Arrow (стрелка вправо) и Ctrl + Space (пробел). Замечу, что сузить область поиска можно уже после открытия списка IntelliSense, вы по прежнему можете писать или стирать буквы. Можно также использовать команду Complete Word, не начиная писать имени - тогда вы увидите все имена. До 2005-ой студии - в том числе лишние имена (из стандартных заголовочных файлов). В 2005-ой это дело вроде исправили - вы видите лишь то, что заинклюдили (или непосредственно задали в коде). Так говорит MS, правда врёт. Вы не будете видеть не подключенные STL, WinAPI, ATL и MFC, но CRT вы увидите целиком! Как вы, наверно, заметили, имена здесь (как и в Class View) снабжаются значками. Следующая командочка - List Members. Пользоваться мы ей не будем. Вместо этого откройте настройки редактора и проверьте, что на вкладке General стоит галочка Auto list members. Теперь объявите структуру MyStruct, переменную-структуру myStruct этого типа и указатель на структуру pMyStruct. Напишите myStruct, нажмите точку, и вы увидите список полей структуры. Напишите pMyStruct затем стрелочку (->) - вы опять таки увидите этот же список. Как вы позже увидите, эта фича распространяется также на оператор :: (доступ к области видимости). Следующая команда - Parameter Info, которой мы также не будем пользоваться. Вместо этого ставим галку Parameter information. Вводим имя функции, открываем скобку и видим в тултипе заголовок этой функции (имя, возвращаемое значение и параметры). Текущий вводимый параметр отображается жирным шрифтом. Если функция имеет перегруженные версии, то вы увидите надпись, вроде 1 of 5 и две стрелки, с помощью которых можно промотать список функций. Удобней, правда, проматывать их с помощью стрелок вверх/вниз на клавиатуре. Последняя команда IntelliSense - Quick Info. Как вы поняли, мы ей пользоваться не будем. Ставим на вкладке Formatting галку Enable automatic Quick Info tooltip. Подведите мышку к произвольному имени и вскоре вы увидите тултип, в котором будет написана информация о том, что это имя означает. Другой пример использования - в списках Complete Word и List Members. Если вы задержите нанемного курсор (клавиатурный) на одном из элементов, то вы увидите информация о нём. Замечу, что в Quick Info входят комментарии к некоторой переменной или функции, что весьма удобно. Так сказать, встроенная документация, причём реально удобная. Комменты рулят! ;)

Перейдём теперь к букмаркам, то бишь закладкам. Вещь это простая и интуитивно понятная, но при этом достаточно удобная. Работать с ними можно тремя способами. Самый неудобный - подменю Bookmarks в меню Edit. Это очень медленный способ, поэтому рекомендую настроить хоткеи (чтобы сузить поиск, при задании хоткея напишите bookmark) - это и есть второй способ. Третий способ (в некоторых случая более удобный) - окошко Bookmarks. Перейти к нему можно командой View => Bookmark Window. Наверху панели есть ряд кнопок. Первая (Toogle a bookmark on the current line) переключает букмарк на текущей линии (ставит или убирает), правда это удобней делать хоткеем. На строке с букмарком в поле слева появляется соответствующий значок. Добавленный букмарк также появляется в списке на панели. В списке указывается имя букмарка, файл и номер строки. Для переименования букмарка достаточно выбрать Rename из контекстного меню (можно использовать также F2). Для удаления - клавишу Delete. Для перехода на строку с букмарком (если нужно, будет в редакторе будет открыт необходимый файл) достаточно дважды щёлкнуть по нему (можно также использовать Enter). Можно создать папочку (виртуальную) с помощью соответствующей кнопочки (New Folder) и затем перетащить туда любые букмарки. Папку также можно переименовать или удалить. Можно назначить хоткеи (можно также использовать меню или кнопки на панели букмарков) для перехода на следующие/предыдущие букмарки. Причём это можно делать глобально, в пределах текущего файла или в пределах текущей букмарковской папки. Чтобы исключить букмарк из переходов достаточно убрать галочку около его имени в списке, при этом значок на строке с букмарком несколько побледнеет. Эта операция называется запретом букмарка. Можно запретить/разрешить все букмарки соответствующей кнопочкой на панели.

Ещё одна панелька, похожая на букмарки - Task List (список задач). Она способна работать в двух режимах: на основе комментариев и на основе пользовательских задач. Поговорим вначале о первом режиме. Для его настройки откройте в опциях студии вкладку Environment => Task List. Здесь есть Token List - список ключевых комментариев. У каждого указывается имя и его приоритет (для списка задач). Для этого есть соответствующие поля. Для добавления, удаления и изменения элементов списка есть соответствующие кнопки (Add, Delete, Change). Например, здесь (по дефолту) есть классический элемент TODO (сделать). Теперь напишите в любом файле комментарий, начинающийся с TODO. Перейдите на панель Task List (View => Other Windows => Task List). Из верхнего списка выберите режим коментариев (Comments). И вы увидите в списке задач элемент с вашим комментарием. Двойным кликом переходим на нужное место. Сортировка задач по дефолту идёт по приоритету, но можно выбрать нечто иное (через контекстное меню). Выбрав пользовательский режим (User Tasks), можно кнопкой Create User Tasks (она находится на панели задач) создать новую задачу, ввести затем её описание и выбрать приоритет. В любой момент можно поставить галочку - мол, задача решена. По сути, это не более чем список напоминаний.

Следующая наша тема - поиск и замена. Существует быстрый поиск, поиск в файлах, поиск символов (имён), быстрая замена и замена в файлах. Всё это на самом деле собрано в одной панельке Find and Replace. Текущей режим выбирается двумя кнопочками (одна для поиска, другая для замены). Для каждого режима есть соответствующая команда в подменю Find and Replace а также дефолтный хоткей. Я не буду подробно останавливаться на всех опциях, так как они, по-моему, довольно понятны. Быстрый поиск (Quick Find - Ctrl + F) - это самый простой поиск. Перечислю некоторые интересности. Во-первых, можно выбрать место поиска (по дефолту - текущий документ), например, текущий блок. Во-вторых, можно развернуть дополнительные опции кнопочкой с плюсом. Там можно поставить или убрать галочку Search hidden text - поиск в скрытом (свёрнутом) тексте. Можно также поставить галочку Use (использовать) и затем выбрать из списка Wildcards (подстановочные символы) или Regular expressions (регулярные выражения). При этом возле поля с искомым текстом станет доступна кнопка со стрелочкой. Нажав на неё, вы получите меню с доступными подстановочными символами или управляющими конструкциями регулярных выражений. Кнопкой Find Next (или клавишей F3) можно искать далее, а кнопкой Bookmark All - забукмаркить все найденные строки. Следующий режим - поиск в файлах (Find in Files - Ctrl + Shift + F). По умолчанию идёт поиск по всем файлам текущего солюшена (Entire Solution), но на самом деле можно искать в любых папках (для этого есть кнопка с тремя точками). При этом можно будет поставить галку для поиска в подпапках (Include sub-folders). Можно указать типы искомых файлов (в списке вы найдёте также множество заготовок по типам файлов, воспринимаемых студией). Результаты поиска по файлам появляются либо на панельке Find results 1, либо на Find results 2 (просто дубликат). Для этого есть соответствующей переключатель. На панельке вы найдёте имя файла, номер строки и содержимое этой строки (цитата). Поиск запускается кнопкой Find All. Оставшиеся режимы, я думаю, не нуждаются в особых пояснениях. Логика работы та же, просто знайте, что они есть.

Напоследок, команды из подменю Advansed в меню Edit (большинство из них имеют кнопки на панели инструментов Edit):

  1. Format Selection - применить автоотступы к выделенному тексту.
  2. Tabify Selection / Untabify Selection - добавить/убрать уровень табуляции для текущего выделения.
  3. Make Uppercase / Make Lowercase - первести выделенный текст в верхний/нижний регистр.
  4. View White Space (переключатель) - показать скрытые символы (пробелы в виде точек, табуляция в виде стрелок).
  5. Word Wrap (переключатель) - перенос длинных строк.
  6. Comment Selection / Uncomment Selection - закомментировать/раскомментировать выделенный текст. Второе, правда, иногда не срабатывает.
  7. Increase Line Indent / Decrease Line Indent - увеличить/уменьшить уровень вложенности выделенных строк. Если у вас в опциях размер табуляции равен размеру автоотступа (что желательно и стоит по умолчанию), то эти команды аналогичны командам из пункта 2. Единственное отличие - есть (по дефолту) кнопки на панели инструментов.

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


IDE Visual Studio .NET. Борьба с ошибками и отладчик

Редко бывает, чтобы написанная программа сразу работала, причём именно так, как хотелось бы. Поэтому очень важным этапом в программировании является борьба с ошибками. Ошибки бывают разные, способы их выявления и устранения соответственно тоже разные. Первый тип ошибок - нелепые описки. Предотвратить их помогает ещё редактор, показывая незакрытые скобки или помогая без ошибок ввести имя (IntelliSense). Иногда, правда доходит дело до компилятора. Тогда на панели Error List (до 2005-ой - в Task List) появляется список ошибок. У этой панельки есть три кнопки-переключателя, фильтрующие элементы списка: ошибки (Errors), предупреждения (Warnings), и сообщения (Messages). Около каждой кнопки пишется количество ошибок, предупреждений или сообщений. Для каждого элемента списка указывается значок (соответствует типу элемента), его номер (в процессе всего билдинга), описание, в которое включается код ошибки, файл и номер строки. Двойным щелчком по элементу открываем это место в редакторе. Все коды ошибок компилятора начинаются с буквы C. Для ошибок-описок (вроде пропущенной точки с запятой) описания обычно довольно понятны и правильны. Устранение этих ошибок заключается в двойном щелчке по элементу панели Error List и незначительном редактировании. Ну и, конечно, затем повторный билдинг для проверки.

Следующий тип ошибок - неправильное использование правил языка. Самый простой пример - попытка переопределить некоторое имя в том же контексте (в том же блоке). Например:

int main()
{
    char c;
    // ...
    int c; // типа сount - количество
}

Этот пример сгенерирует ошибку C2371 с описанием 'c': redefinition; different basic types. Если же вы дважды напишите один и тот же тип, то получите уже C2086. Другие типичные примеры: использование в case-метках непорядкового типа (например, double), блок try без catch, разыменование (*) для не указателей или для указателей на void, попытка сложения двух строк ("Str1" + "Str2"), пропуск внутренних измерений многомерного массива (char* s[] = {"111", "222", "333"}) и другие. Бывают (и не редко) случаи позанятней. Например:

void func(char c) {}
void func(int i) {} // перегруженная версия

int main()
{
    func(1.0);
    return 0;
}

Данный пример вызовет ошибку C2668 - невозможность выбора конкретной версии перегруженной функции. Компилятор не знает, во что правильней преобразовать из double: char или int. Уберите любую из этих функций или добавьте третью с аргументом типа double и пример скомпилируется! Данный пример явно надуман, но возможны более реалистичные варианты. Допустим, у вас есть функции для вывода символа (char) и целого числа (int). Где-то в программе вы считаете, некоторую вещественную функцию (double), но результат её вам достаточно напечатать с точностью до целого (такое бывает действительно часто, тем более возможно, что при это в какой-нибудь файл сохраняются абсолютно точные данные). Другой пример - вычисление математических функций для целых чисел (математические функции имеют две версии - для float и для double). Классическое (и наиболее верное) решение - использование явного приведения типов. Можно придумать множество интересных (причём из опыта) примеров. В конце этой части я думаю подкинуть вам кусков кода для поиска ошибок (естественно, при этом запрещается пользоваться компилятором).

Как бы то ни было во всех этих ошибках компилятор ясно покажет ошибочную строку (или строку, имеющую непосредственное отношение к ошибке), а также довольно понятное (обычно) описание. Если вам не понятно, почему компилер не доволен, то посмотрите описание ошибки (обычно с примерами) в MSDN. Проще всего найти нужный топик на панели Index, набрав код ошибки (например, C2668). Хуже бывает с ошибками компоновки. Если вы видите ошибку Unresolved external symbol, то скорей всего вы забыли подключить один из lib-файлов, в котором находится реализация указанной функции. Также часто вы увидите злосчастную LNK2019, если забыли объявить (единожды) внешнюю переменную без extern. Другая типичная ошибка компоновки - подключение двух версий одной библиотеки. Часто это получается неявно. Например, вы написали некоторую библиотеку (неважно статическую или динамическую), отладили её и довольные скомпилировали релиз-версию. Теперь вы подключили эту библиотеку к некоторой программе, начали билдить дебаговую версию и с ужасом увидели кучу ошибок LNK2005, мол некоторое имя (надо заметить, не дюже знакомое) уже объявлено в некоторой либе (чаще всего LIBCMTD.LIB). Не знаю, как у кого, а у меня в своё время такая куча ошибок вызвала полное непонимание. А на самом деле всё очень просто. Несмотря на то, что обычно CRT (сишный рантайм) подключается динамически (msvcrxx.dll или msvcrxxd.dll), часть важных внутренних функция будет подключена статически. Для дебаговой версии они будут в LIBCMTD.LIB, а для релиза - в LIBCMT.LIB. В нашем же слчае получились подключены обе либы, а в итоге - несколько часов (если не дней) мучений. Давайте заодно посмотрим, как подключать различные версии CRT. Это делается на страничке C/C++ => Code Generation свойств проекта с помощью списка Runtime Library. Обратите внимание, что кроме обычной или дебаговой версии можно ещё указать статическую или динамическую линковку. Используйте статическую линковку, если вам необходимо получить экзешник, "готовый к употреблению", т. е. не требующий никаких DLL. Замечу, что ранее (до 2005-ой студии) можно было также выбрать статическую линковку с однопоточной версией (Single-threaded) рантайма. Она отличалась неспособностью нормального функционирования в многопоточных приложениях. Например, в двух независимых потоках вы выполняете некоторые операции, могущие изменить errno, затем проверяете значение errno. В однопоточной версии на все потоки будет только одна переменная errno, что, естественно, не правильно. Поэтому и была придумана усовершенствованная версия рантайма (при использовании динамических библиотек проблема автоматически отпадает вследствие организации разделения памяти в современных ОСях). В 2005-ой студии старую версию рантайма выкинули совсем. Ну и правильно!

Наиболее сложно устранимы тип ошибок - ошибки логики программы. У вас всё скомпилировалось, скомпоновалось, но вываливается с рантаймовой ошибкой (обычно - неправильное управление памятью) или же выдаёт не те результаты, которые ожидали вы. Иногда, правда, такие ошибки замечает компилятор и выдаёт варнинги. Например, если вы в условии вместо сравнения написали присваивание. Поэтому следует просматривать варнинги - зачастую полезно. Но чаще причину ошибки не удаётся установить. Многие, не знакомые с отладчиком, начинают через каждую строку печатать значения переменных для контроля, сохранять в файлы различные логи, но зачастую это неудобно и излишне. Именно для этого и существует пункт меню Debug, открывающей вход к отладчику, называемому в народе дебаггером. Команды меню меняются при начале отладки. Далее, если не оговорено обратное, подразумевается что все команды находятся в меню Debug.

Несколько общих замечаний по отладке. Во-первых, не дебажьте релиз-версию. Это возможно, но при этом многие возможности дебаггера будут недоступны, вдобавок к тому порядок выполнения кода может оказаться неожиданным. У релиз-версии есть две важные особенности: меньше сохраняется дебаговой информации и код значительно оптимизируется. Во-вторых, помните, что запуск отладки применяется к тому проекту из солюшена, который помечен как запускаемый (StartUp Project), на всех "проектных" панелях (например, Solution Explorer и Class View). Переключить его можно из контекстного меню проекта (из любой "проектной" панели) пунктом Set as StartUp Project. Можно также начать отладку любого проекта через его контекстное меню (подменю Debug), но обычно этим не пользуются (по крайней мере я). Чтобы прервать отладку используется команда Stop Debugging (Shift + F5). Наконец-таки заметьте, что положение панелей для отладки сохраняется отдельно, т. к. в это время обычно используются другие панельки.

Первым способом начала отладки является пошаговая отладка, или трассировка. Для этой цели в меню Debug есть две команды: Step into (Шаг в - F11) и Step over (Шаг через - F10). Они построчно исполняют программу (т. е. после выполнения каждой строки происходит останов), заметьте не пооператорно, а построчно. Правда, если у вас один оператор будет разбит на несколько строк, то он будет выполнен за один шаг. Текущая строка помечается жёлтой стрелкой на поле слева. Аккуратно взяв эту стрелку мышкой, её можно переместить на любую строку, вплоть до другой функции. Правда, последствия при этом (незаконном переходе в другой блок) будут непредсказуемы. У Step into есть одно отличие - если на текущей строке есть вызовы функций, то вы войдёте в тело первой функции. Причём это может быть также функция рантайма или иной библиотеки, если VS знает, где находятся её исходники (это настраивается в папках VC) и отладочные базы данных (*.pdb). Для всех библиотек VS всё это входит в дистрибутив. Для быстрого выхода из выполняемой функции используется команда Step out (Shift + F11). После выхода из функции (неважно протрейсили ли мы её целиком или использовали быстрый выход) нажатие F11 (Step out) приведёт к входу в следующую (по очерёдности выполнения) функцию на данной строке. Если таковой нет, то выполнение просто перейдёт к следующей строке. Нередко, нажимая F11, попадаешь в какую-нибудь библиотечную неинтересную функцию (вместо своей), давишь Shift + F11, опять F11 и т. д., пока не доберёшься до того, что тебе нужно.

Во время паузы выполнения программы можно многое сделать кроме переноса жёлтой стрелочки (по сути - изменение значения регистра EIP - Extended Instruction Pointer, т. е. расширенный указатель инструкций). Во-первых, посмотреть значения переменных. Для этого есть много способов. Первый - тултипы, в 2005-ой студии этот способ стал очень удобным (насколько я знаю, такие навороченный тултипы есть только в VS). Если во время отладки вы подведёте мышку к некоторому имени, то вы увидите в тултипе:

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

Кроме того для массивов, структур, ссылок, любых строк (как вы увидите позже, в том числе для классов строк) и указателей вы увидите слева плюсик. Щелчок по нему приведёт к разворачиванию подтултипа (как это назвать иначе - не знаю), зачастую многострочного. В нём в аналогичном виде вы увидите все элементы массива, поля структуры, отдельный символ, ссылаемый или указываемый элемент. Если указатель ни на что не указывает, т. е. равен 0 (или, что то же самое - NULL), или указывает на недоступную область памяти (например, для него был вызван delete), то вы увидите значок ошибки и надпись о невозможности вычисления выражения. Если элементов много, то будет показана часть, но сверху и снизу будут стрелки, достаточно подвести к ним мышку и начнётся перемотка списка (можно также пользоваться колёсиком). Здесь в свою очередь могут быть новые плюсики (например, массив структур) и т. д. Как вы увидите через несколько уроков, для списков таких плюсиков можно сделать бесконечное количество. Самое рульное, что всё это убирается и появляется лишь перемещением мышки (даже без щелчков!), в том числе разворачивание и сворачивание (хотя можно и щёлкать, но обычно не нужно). Для всех неразворачиваемых полей (а иногда и для разворачиваемых) можно интерактивно поменять их значение непосредственно в тултипе. Достаточно щёлкнуть по значению и ввести произвольное выражение (можно использовать при этом любые переменные и функции), затем делаете щелчок в любом другом месте - происходит вычисление выражения. Особенно весело (правда, не знаю чем) издеваться над адресами указателей. Есть у этих тултипов (если их можно так назвать) ещё одна особенность - наличие контекстного меню. В нём вы найдёте:

  1. Copy - копирование в буфер данных, вроде "x = 15".
  2. Copy Expression - копирование только левой части.
  3. Copy Value - копирование только значения.
  4. Edit Value - переход в режим редактирования - ненужная команда.
  5. Add Watch - добавление элемента в окошко Watch (о нём - чуть ниже).
  6. Hexadecimal Display - включение/отключение шестнадцатеричного (вместо десятичного) показа значений. Адреса всегда показываются хексом.

Надо заметить, что тултипы появляются не только для имени, но и для более сложных вещей. Например, у вас в программе написано x[0]. Подведя мышку к x - получим тултип для x, подведя к квадратным скобкам или к нулю - тултип для x[0]. Ещё одна интересная фича - это визуализаторы. Вообще говоря, сама MS утверждает, что для C++ они работают только в управляемом (читай - дотнетовском) коде, но я вам расскажу о недокументированных возможностях. Работает это дело для строк (любых). Для них в тултипе появляется кнопочка в виде лупы. Щелчок по ней откроет последний использованный для этой строки (с этим именем) визуализатор. Сбоку на кнопке есть стрелка, которая открывает меню, из которого можно выбрать визуализатор. Их три. Во-первых - текстовый. При его выборе откроется окошко, в котором ваша строка будет показана как обычный текст (с учётом управляющих символов). Удобно для больших строк. Во-вторых - XML. При этом в окошке вы увидите XML-структуру вашей строки (парсер, естественно, иешный). Удобно при работе со строками, хранящими XML-данные (странно, почему это удобно именно для них?). Третий вариант - HTML. Ну, думаю вы поняли, что вы получите и для чего это нужно. Можно также писать свои визуализаторы. Надо проверить, будут ли они работать для нэтивного кода. Ещё одна очень полезная вещь для дебаговых тултипов (только что прочитал в MSDN, что они называются дататипам - DataTips) - это возможность сделать их прозрачными (я бы сказал точнее - плоховидимыми) с помощью зажатия Ctrl. Я надеялся, что при зажатии Shift-а можно заставить их мигать (естественно, где-то в настройках можно указать частоту), но, к сожалению, эту возможность мы увидим, наверно, лишь в следующей версии студии :(.

Очень часто возможностей дебаговых тултипов (ну, не нравится мне название "дататипы"!) хватает с головой. Более того для большинства случаев это самый удобный способ просмотра и изменения данных. Но есть и другие способы. Во-первых это панелька Watch. Вообще говоря, их может быть несколько (в Professional версии - 4), они абсолютно равнозначны и только следует принципу "хороших панелей должно быть много". Лично, я бы предпочёл если была бы одна маленькая панелька Watch, в ней был список подпанелей, желательно для профессиональной версии (хотя можно для любой) неограниченное количество, которые бы появлялись при наведении мышки (при желании на первые, скажем 10, можно назначить по хоткею) и представляли собой настраиваемые дататипы! Вот такие у меня мечты после того как я увидел дататипы в 2005-ой студии. Все панели Watch доступны из подменю Debug => Windows => Watch (это подменю появляется только во время отладки). Панелька представляет из себя простой список с тремя колонками: имя, значение, тип. В списке всегда есть по крайней мере один элемент - пустой, именно он используется для добавления новых элементов в список. Для редактирования любого элемента (в том числе пустого) щёлкните по нему (или нажмите Tab, когда он выделен). В качестве имени можно написать любое выражение, его значение будет вычисляться и показываться на каждом шаге, а также при ручном изменении других переменных, от которых зависит это выражение. Если имя представляет из себя переменную, поля структуры и т. д. (вообще говоря - lvalue, о том, что это такое вы узнаете через несколько уроков), то можно отредактировать также значение (щелчком или двойным табом). Окошку Watch присущи некоторые навороты дататипов: сворачивание/разворачивание и визуализаторы. Правда для массива из тысячи элементов будут показаны все элементы (прокрутка идёт для всего списка панели). Есть, правда, некоторые фичи, недоступные для дататипов (в их текущей реализации). Все они основаны на том, что в окне Watch можно редактировать поле для имени. Первая, как я сказал выше, это вычисление выражений. Вторая - настройка формата вывода. Для этого через запятую ставится спецификатор вывода (елсли его нет, то он определяется автоматически, обычно правильно). Вот наиболее интересные для нас (на текущий момент):

  1. d или i - десятичное целое.
  2. u - невыровненное целое.
  3. x - шестнадцатеричное целое.
  4. f - вещественное число.
  5. e - вещественное число в экспоненциальной форме (12.5 = 1.250000e+000)
  6. g - вещественное число в наиболее короткой форме.
  7. c - символ (показывается также код символа)
  8. s - строка (указатель на начало строки)
  9. su - юникодовская строка.
  10. ! - "сырой" формат.

Кроме того, если у вас есть динамический массив (указатель), то, чтобы просмотреть все его элементы можно указать число - количество элементов. Ещё одна особенность панели Watch - возможность использования псевдопеременных:

  1. $handles - количество хендлов, используемых вашим приложением.
  2. $vframe - текущий адрес начала стековой памяти *памяти, используемый для размещения нестатических переменных, объявленных в функциях).
  3. $TID - идентификатор текущего потока (о самих потоках мы поговорим позже).
  4. $ENV - размер блока, хранящего переменные среды. Попытка редактирования приведёт к отображению в панели Output (обычная панель для вывода информации, доступная из меню View) самих переменных среды с "отредактированным размером", само редактирование на самом деле выполнено не будет.
  5. $CMDLINE - аналогично $ENV показывает размер командной строки (с возможностью псевдоредактирования).
  6. $user - информация об акаунте пользователя, от имени которого запущено приложение.

Кроме Watch есть ещё подобные панельки. Во-первых, это панель Autos (Debug => Windows => Autos). Она похожа на панель Watch, но список в ней в каждый момент выбирается отладчиком автоматически в зависимости от текущего контекста. У этой панели есть одна особенность - после вызова функции в неё также записывается значение, которое вернула функция (такого в Watch сделать нельзя). Ещё одна панель - Locals (Debug => Windows => Locals), она показывает текущие локальные переменные.

Пошаговая отладка, безусловно, не самый удобный вариант отладки. Обычно, нам не надо просматривать всё выполнение программы, достаточно некоторых ключевых моментов. Естественным образом вводится понятие точек останова, иначе называемых брейкпоинтами, а в простонародье просто бряками. Для простой установки/снятия брейкпоинта на текущей строке используется команда Toggle Breakpoint (F9). При этом на поле слева появится красный кружок. Можно также просто щелкнуть по полю, но почему-то на практике этот способ используется даже реже (обычно используют хоткей). Для запуска обычной (не пошаговой) отладки используется команда Start Debugging (F5). После останова на брейкпоинте и просмотра/изменения переменных (при необходимости - перетаскивании стрелки, указывающей на текущую команду) можно:

  1. Продолжить отладку командой Continue (F5).
  2. Завершить отладку командой Stop Debugging (Shift + F5).
  3. Перезапустить отладку командой Restart (Ctrl + Shift + F5).
  4. Начать пошаговую отладку.

Кроме обычных можно создавать более навороченные брейкпоинты, а в 2005-ой студии ещё и трейспоинты. Для этого обычно используются две вещи: контекстное меню брейкпоинта (правый клик по символу на полях) и панель Breakpoints (Debug => Windows => Breakpoints). Панель Breakpoints содержит полный список брейкпоинтов, установленных в текущий момент, а также некоторые кнопки. Рассмотрим вначале подробно команды контекстного меню:

  1. Delete Breakpoint - бесполезная команда удаления брейкпоинта (то же делаем через Toggle Breakpoint).
  2. Disable Breakpoint - временный запрет брейкпоинта. При этом символ на полях изменит свой вид (см. рисунок ниже).
  3. Location - позволяет точно настроить положение брейкпоинта, вплоть до номера символа. Обычно не нужно (если вы, конечно, не пишите всю программу на одной строке).
  4. Condition - позволяет задать условие, при котором брейкпоинт будет срабатывать. В диалоге Breakpoint Condition есть переключатель, могущий находится в двух положениях: Is true (брейкпоинт сработает, если введённое выражение верно) или Has changed (брейкпоинт сработает, если значение введённого выражения изменилось после последнего останова). Надо заметить, что при этом символ брейкпоинта заменяется на символ расширенного брейкпоинта (см. ниже).
  5. Hit Count - позволяет установить цикличность срабатывания брейкпоинта. Доступные варианты:
    1. break always - без настроек цикличности.
    2. break when the hit count is equal to - позволяет ввести число, указывающая на каком повторении должна сработать точка останова.
    3. break when the hit count is multiple of - указывает множитель повторений (например, брейкпоинт будет срабатывать каждый третий раз).
    4. break when the hit count is greater than or equal to - аналогично второму, но здесь после первого срабатывания брейкпоинт будет срабатывать всегда (больше либо равно).
    6. В этом же окошке есть кнопка Reset, сбрасывающая счётчик проходов через брейкпоинт. Текущее значение счётчика также отображается в в этом окошке.
  6. Filter - установка фильтра на всякие гадости вроде потока, процесса, имени машины и др. Вряд ли нужно.
  7. When Hit - новинка 2005-ой студии для создания трейспоинтов. Позволяет выполнить некоторое действие в момент срабатывания брейкпоинта. Этим действием может быть вывод некоторого сообщения в окно Output (с возможностью использования значений переменных и специальных псевдопеременных) или запуск макроса (вопрос создания макросов на VB. NET для студии не входит в задачи этого учебника). При этом можно как остановить выполнение программы, так и просто продолжить его. В самом окошке When Breakpoint Is Hit всё довольно подробно описано.

Окошко Breakpoints обычно используется для быстрого нахождения забытых вами брейкпоинтов. Это окошко представляет собой список с уже знакомым вам интерфейсом (вспомните, например, букмарки). Галочки у каждого элемента позволяют временно запретить брейкпоинт. Также есть кнопочки для удаления и временного запрета для всех брейкпоинтов (текущего проекта). Действительно новой будет кнопка new, а точнее командочка New Data Breakpoint. Эта команда позволяет установить брейкпоинт на изменение адреса памяти (в любом месте программы). Для указания адреса можно использовать указатели, а также операцию взятия адреса (амперсанд).

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

#include <stdio.h>

int main()
{
    printf("");
    throw 5;
    return 0;
}

При запуске отладки первого теста (F5) вы тут же увидите сообщение о неперехваченном исключении типа int (тип пятёрки). В этом окошке практически всегда нажимаем кнопку Break (прервать). Но вы видите, что вы оказываетесь далеко не в своей программе, а в файле mlock.c (это один из файлов исходника сишного рантайма). Нам сейчас неважно, почему вы оказались именно там. Скажу лишь, что при прерывание из исключений очень часто оказываешься не там где надо. Вот здесь то и пригодится панелька Call Stack (стек вызовов), доступная из подменю Debug => Windows. Эта панелька представляет из себя таблицу из двух строк. В первой вы видите имя модуля (исполняемого файла), затем восклицательный знак (просто разделитель) и заголовок функции. Во второй - язык (для нас это либо C, либо C++). Часть строк этой таблицы показывается серым цветом. Это означает, что для этих модулей отсутствует отладочная информация, т. е. *.pdb файлы, в которых хранится сопоставление адресов и имён. Для таких модулей вместо заголовка функции показывается лишь её адрес. Текущая функция (самая верхняя) помечается слева жёлтой стрелкой. Двойным щелчком по любой функции мы можем открыть её код. При этом если это не текущая функция, то мы увидим в окне кода не жёлтую стрелку, а зелёную (несколько другой формы), указывающую на строку, которая будет выполнена после выхода из всех функций, находящихся выше выбранной. Обратите внимание, что в нашем случае текущей функцией на самом деле является функция из kernel32.dll, но её исходников у нас нет (жадный Microsoft), потому дебаггер автоматически выкинул нас на ближайшую функцию, от которой у него есть исходник. Эта функция расположена в msvcr80.dll (для 2005-ой студии). Проезжаем по таблице вниз, находим функцию main из нашего модуля (у меня - test.exe) и дважды кликаем по ней. Данный приём типичен при отслеживании фактических мест возникновения исключений. Напомню, что этим исключением может также являться неправомерный доступ к памяти (что бывает очень часто). Классическая методика ловли и исправления ошибок такова:

  1. Запускаем дебаггер, вываливаемся по экзепшену (исключению).
  2. Вылезаем с помощью стека вызовов на наш код.
  3. Просматриваем значения переменных (какие указатели у нас недействительны).
  4. Анализируем.
  5. Далее есть два пути:
    1. Ставим бряк на изменение проблемной переменной и перезапускаем отладку. Я так делаю редко (точнее - практически никогда).
    2. Ставим бряки на все строки где с нашими переменными (обычно с указателями) происходит что-то важное. Перезапускаем отладку. Находим ошибочную строку, думаем, исправляем ошибку. Билдим и переходим к первому пункту (я надеюсь, лишь к первой его части).

Есть другие панельки, так или иначе связанные со стеком вызовов. Все они доступны через одноимённый пункт в подменю Debug => Windows. Их три:

  1. Modules - список загруженных модулей (вашей программы). Для каждого указывается различная дополнительная информация (версия, язык, отладочная информация и др.).
  2. Processes - список процессов, созданных вашей программой. Двойным кликом открываем в отладчике текущую команду текущего потока выбранного процесса.
  3. Threads - список потоков вашей программы. Дабл-клик приведёт к открытию в дебаггере текущей команды данного потока.

Наверно, вы заметили, что при попытке открыть в отладчике функцию модуля без дебаговой информации выскакивает окошко, в котором вам напоминают, что символы для этого модуля не загружены и предлагают вам открыть окно Disassembly. Это окно также доступно одноимённой командой в подменю окон отладчика. В нём отображается полный дизассемблированный код памяти контекста ваших процессов. Если вы не знакомы с ассемблером, то вам достаточно знать только, что это символическая запись машинного кода (не совсем верно, но в мои задачи более подробные объяснения не входят). Сам ассемблерный код отображается серым цветом (по дефолту). Но здесь же вы можете найти строки вашего кода (или кода библиотек с дебаговой информацией), они отображаются чёрным цветом. Таким образом легко посмотреть, какой код для какой вашей команды сгенерировал компилятор. В частности можно посмотреть, что значит оптимизатор (в релиз-режиме), что мы и сделаем несколькими строками ниже. Для навигации по ассемблерному коду наиболее рационально использовать Call Stack. Если активно окошко Disassembly (по умолчанию оно встаёт в один ряд с окнами документов), то двойной щелчок по функции в стеке вызовов приведёт к открытию её ассемблерного кода (с параллельным обычным). Можно также пользоваться пунктами контекстного меню стека вызовов: Go To Source Code и Go To Disassembly. Надо заметить, что дизассемблер в VS достаточно умный: он подставит где надо имена ваших переменных и функций, правильно распределит соответствие исходного и ассемблерного кода. В окошке Disassembly можно использовать пошаговую отладку (по одной ассемблерной команде), брейкпоинты, дататипы. Кроме того у вас есть ещё окошко Registers (Debug => Windows => Registers), которое показывает значения регистров процессора. При этом регистры, изменённые после выполнения последней команды (как минимум это EIP) выделяются красным цветом. Есть также окошки Memmory (Debug => Windows => Memmory => Memmory X, где X - номер окошка). Они позволяют просмотреть значения памяти по некоторому адресу. По аналогии с Watch их тоже несколько (для профессиональной версии - 4). Хотя я сомневаюсь, что работая в VS вам придётся ручками просматривать память - несколько другой всё-таки профиль решаемых задач... |:0

Напоследок, попробуйте ввести такой код:

#include <stdio.h>

int Func(int param)
{
    int a = 0, b;

    for (int i = 0; i < 5; ++i)
    {
        a += 5;
        b = 12;
    }

    return a + b;
}

int main()
{
    printf("%i\n", Func(3));
    return 0;
}

Пробилдим релиз-версию. Запустим отладку (скажем по F10) и глянем в Disassembly. О, ужас! От всех наших стараний оставили на самом деле только вызов printf с двумя параметрами (printf("%i\n",37)), да зануление регистра EAX (возвращаемое значение). Мдя, а я так старался (запутать оптимизатор)...


lvalue и rvalue. Ссылки

Эти два понятия (lvalue и rvalue) были введены в своё время довольно естественным образом. lvalue означало некоторое выражение, могущее стоять в левой части оператора присваивания (left value), в противоположность ему rvalue (right value) - нет. Подумаем, что в принципе может стоять в левой части этого оператора? Очевидно, что, во-первых, это переменные. Но, как вы помните, не только переменные могут здесь оказаться. Здесь могут быть, скажем, элементы массивов. Далее вспоминается, что также можно засунуть разыменованные указатели. А раз уж мы дошли до указателей, то (статистика для любых вещей в C++) дальше можно придумывать бесконечно. Несколько примеров:

int * pi = new int[5];
*pi = 5; // то же, что pi[0]
pi[4] = 5;
*(pi+4) = 8; // уже красиво
int * pi2 = pi;
*pi2++ = 9; // элемент pi[1]
*((int*)0x00360000) = 10; // обошлись без переменных

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

Со временем первоначальное определение утеряло смысл. Зачастую lvalue не может использоваться в левой части оператора присваивания - вина этому явная или неявная константность.

const int x = 10;
x = 5; // !!! нельзя - явно задан описатель const
int * pi = new int;
int a[10]; // a - lvalue
a = pi; // !!! нельзя, статический массив - это константный указатель

Опять-таки наиболее важна последняя строка. Помните, что описатель const для массив делает указатель на константы (int const *), сам же указатель для статического массива всегда константен.

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

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

У новичков часто возникают разногласия насчёт того, как передавать параметры в функцию: по значению, по ссылке или по указателю, использовать ли const. Рассмотрим все варианты:

  1. Передача по значению (без const) - разумно использовать для небольших типов (для структур уже нежелательно), не требующих модификации в функции.
  2. Передача по значению с const - глупость, т. к. значение в любом случае копируется, следовательно любое его изменение ничему не повредит, разве что может испортить (иногда) алгоритм функции.
  3. Передача по указателю необходима для массивов (допустима также запись с помощью квадратных скобок, т. е. вместо int* x можно написать int x[]), в том числе и строк, а также для других данных, которые по смыслу являются указателями, в последнем случае часто возможны ссылки на указатели (заметьте ссылки на ссылки и уж тем более указатели на ссылки не возможны). В языке C не существовало ссылок, поэтому для всех модифицируемых значений использовались указатели (вспомните scanf), в C++ такой метод пожалуй нежелателен - используйте указатели там, где они естественны. Правда есть исключение - многие программисты считают передачу элементов массива по указателю (в стиле x+i) лучшей, чем по ссылке (x[i]) из-за соображений эффективности. Но если вам первый вариант кажется менее удобным для чтения - смело используйте ссылки и помните, что на вашей стороне мощь оптимизатора.
  4. Передача по константному указателю - это иллюзия. На самом деле для функций константным будет указываемый элемент (или элементы), т. к. использование const для самого указателя бессмысленно (см. пункт 2).
  5. Передача по ссылке наиболее желательна, если вы собираетесь модифицировать переменную в функции (с сохранением значения после выполнения функции), про одно исключение было сказано выше. Замечу, что при вызове в качестве параметра обязательно должно быть lvalue.
  6. Передача по константной ссылке - лучшее решение для не модифицируемых и достаточно больших данных. Замечу, что передаваемый аргумент не обязан быть lvalue. Откуда же тогда берётся ссылка? Дело в том, что при вычислении выражения всегда создаются временные объекты для промежуточных вычислений, ссылка именно на них и передаётся. Для неконстантной ссылки это не работает по вполне понятным причинам бессмысленности.

Замечу также, что можно по ссылке возвращать значение. При этом обязательно надо возвращать lvalue, которое затем может быть использовано как любое lvalue (присваивание, взятие адреса и др.). Можно даже написать что типа этого:

int& myFunc()
{
    int a;
    return a;
}

int main()
{
    myFunc() = 3; // !
    return 0;
}

Хороший компилятор на такое извращенье выдаст варнинг. Например, VC++ говорит, что возвращается адрес локальной или временной переменной.

Получился такой небольшой урочек, но я считаю, что тема достойна отдельного выделения. В последующих уроках концепцию lvalue и rvalue нам придётся использовать достаточно часто.


Типы, операции, выражения. Подробности

Как я и обещал, в этой части мы рассмотрим некоторые важные моменты нашего любимого языка поподробней. И начнём мы с выражений и всего, что с этим связано.

Типы данных - это концепция, типичная для всех нескриптовых языков, в том числе для всех языков, которых я отношу к "золотой пятёрке". Что дают нам типы данных? Достаточно много:

  1. Информацию о количестве памяти, необходимую для переменных этого типа. Как я уже говорил, для этого есть оператор времени компиляции sizeof.
  2. Информацию для программиста, читающего программу. Скажем, тяжело понять что означает просто s. Обычно это либо сумма, либо строка (я говорю про скриптовые языки). Также эта информация используется для автокомплита (IntelliSense в VS).
  3. Возможность перегрузки. То есть могут существовать функции с одинаковыми именами (обычно они при этом предназначены для сходной цели), но с разным алгоритмом.
  4. Типозащищённость, т. е. невозможность ошибочного присваивания разных типов данных. Хотя не надо забывать, что во многих естественных случаях возможно неявное преобразование.
  5. Допустимый набор операций. Например, глупо умножать строки. Правда в C++ умножать символы вы сможете - это наследие (на мой взгляд неудачное) языка C.

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

  1. sizeof (тип) - получение размера типа (в байтах).
  2. (тип) выражение - явное приведение к некоторому типу. Замечу, что часто выражение приходится брать в скобки (если оно не состоит из единственной константы, переменной или вызова функции).

Во-первых в C++ есть 5 типов для выровненных целых чисел. Термин выровненные означает равномерное распределение положительных и отрицательных чисел (при этом 0 относится к положительным числам). Это следующие типы: "signed char", "signed short int", "signed int", "signed long int" и "signed long long int". Замечу, что для всех типов ключевые слова int и signed можно опускать, правда для типа signed int хоть что-то оставить надо (либо signed, либо int). Более того ключевое слово signed ставить даже как-то неприлично (исключение - signed char). Конкретный объём памяти, а следовательно представляемый диапазон стандартом не декларируется, но подразумевается, что он при переходе к каждому следующему типу (в соответствии с приведённым списком) не уменьшается. Кроме того signed char это 1 байт, а остальные не меньше 2. Обычно short int - 2, long int - 4, long long int - 8. Обычный int чаще всего совпадает с long (т. е. long int), единственное исключение, известное мне - Borland C++, в нём int совпадает с short. Но вообще говоря, ориентироваться на конкретные диапазоны не следует. Типы для выровненных целых часто называют просто выровненными типами.

Соответственно выровненным существует 5 невыровненных типов. Они отличаются от соответствующих выровненных заменой ключевого слова signed на unsigned . Все невыровненные типы имеют тот же размер (в байтах), что и соответствующие выровненные. Для всех невыровненных типов ключевое слово int также можно опустить. Но для unsigned int я этого делать не советую (грубо говоря, ради симметрии).

Следующий тип - это символьный тип char (напомню, что символы записываются в апострофах). В стандарте подчёркивается, что это тип отличается от типов signed char и unsigned char, хотя в большинстве компиляторов они внутренне совпадают. Фактически, char представляет из себя целочисленный код символа, поэтому допустимо неявное преобразование из целочисленных типов в char и обратно. Можете даже написать что-то вроде 'z' * 5. На C++ возможно всё [image]! Для юникодовских символов (буковка L перед апостроофами) предназначен тип wchar_t.

Для логических значений отведён специальный тип - bool. Он может принимать всего два значения: true и false. Типы bool, char, wchar_t, а также все выровненные и невыровненные типы называют целыми типами. Между всеми целыми типами допустимо неявное приведение. Можете, например, поделить истину (true) на международный вопрос (L'?'). Поистине, возможности C++ безграничны (самое интересное, что результат имеет тип int)!

Есть три типа для вещественных чисел: float, double, long double. Про их диапазоны также нельзя сказать ничего конкретного. Лично я могу вам гарантировать, что float - это 4 байта, а double - 8. В Visual C++ long double совпадает с double (по крайней мере в восьмёрке). Также обстоит дело и в Open Watcom C++. В GNU C++ и Digital Mars C++ для long double выделено 12 байт. Вещественные и целые типы вместе называются арифметическими типами.

Особый тип - это тип void. Множество его значений пусто. И вообще void - это "недоделанный" тип (в стандарте читаем - Incomplete). Любое выражение может быть явно преобразовано к типу void. Этот тип используется для следующих вещей:

  • В качестве возвращаемого типа для многих функций.
  • В качестве второго или третьего операнда в операторе '? :'.
  • В качестве операнда операции запятая (подробно об этом ниже).
  • В качестве результата оператора (команды).

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

Особо так же надо сказать о такой константе, как нулевой указатель. Дело в том, что в C++ эта константа не имеет (по крайней мере по текущему стандарту) литерального представления (в новом стандарте предлагается специальный литерал - nullptr). Но допустимо неявное преобразование из целочисленного нуля к нулевому указателю, это ещё одно не очень удачное наследие от C, в итоге указателю можно присвоить выражение вроде 1-1. Довольно часто используется макроопределение NULL, в C++ оно определяется как целочисленный ноль. Расположено это макроопределение в заголовочном файле stddef.h (standard definitions - стандартные определения). В этом же файле расположены многие другие общие объявления рантайма (например, тип size_t). Этот файл включается, практически, через все остальные заголовки CRT (возможно, что и через все - зависит от реализации).

Вот и все базовые типы. Рассмотрим наиболее важные правила преобразований между базовыми и некоторыми пользовательскими типами:

  • Все целые типы кроме bool (для него действуют особые правила) могут быть неявно преобразованы друг в друга. Если итоговый диапазон меньше начального, то полученное значение неопределенно (зависит от компилера).
  • bool может быть преобразован в любой целый тип (true - 1, false - 0). При обратном преобразовании false получится только из нуля. Во всех остальных случаях - true.
  • Перечисления (enum) могут быть неявно преобразованы в int (а следовательно в любой целый тип - двойным неявным преобразованием). Обратное преобразование должно быть явным.
  • Между вещественными типами допустимо неявное преобразование. Если значение не входит в целевой диапазон, то результат не определён. Если не хватает точности итогового типа, то может быть выбрано любое значение (большее или меньшее) в зависимости от реализации компилятора (implementation-defined).
  • Допустимо неявное преобразование из целых в вещественные типы на основе правил, изложенных выше.
  • Любой указатель может быть неявно преобразован к указателю на void. Обратное преобразование должно быть явным.
  • Указатель (в том числе на void) может быть неявно приведён к логическому типу. Для нулевого указателя результат будет false, в остальных случаях - true.
  • Преобразование указателей к целым типам (кроме bool), а также обратное допустимо только явное.

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

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

1. Особые операторы

1. 1 Индексирование

Реализуется с помощью квадратных скобок. Применяется к указателям, в том числе и к массивам. a[i] аналогично *(a+i). В результате получаем lvalue типа указываемого элемента.

1.2 Вызов функции

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

1.3 Приведение типов

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

1.4 Операторы-селекторы

Знакомые вам точка (применяется к структурам) и стрелка (применяется к указателям на структуры). Итоговый тип зависит от типа поля структуры. Замечу, что если поле представляет из себя указатель, то допустимы также составные операторы: ".*" и "->*". Эти операторы новичкам не кажутся независимыми, но, с точки зрения компилятора, это совершенно новые операторы. Пока просто запомните это, позже эти знания вам пригодятся.

1.5 Оператор запятая

Применяется к нескольким выражениям произвольных типов. Результат всех выражений кроме последнего игнорируется, последнее выражение становится результатом выполнения оператора. Следует не путать эту запятую с запятой, разделяющей параметры в функции. Классический пример - вызов функции f с тремя аргументами: f(a, (t=3, t+2), c). При вычислении второго аргумента вначале будет выполнено присваивание, а затем результат выражения t+2 загнан в качестве аргумента. Вспомните, этим же мы как-то пользовались для for-а.

1.6 Условный оператор

Довольно часто используемый оператор. Первый операнд должен иметь тип bool (или неявно приводиться к bool). Второй и третий операнд могут быть любого типа, в том числе void. В последнем случае результат операции имеет тип void. Иначе - используется оптимальный выбор из типов второго и третьего операндов.

1.7 Операторы new и delete

Более подробно об этих операторах мы поговорим позже. Пока заметьте, что new возвращает указатель на подставленный тип, а delete возвращает void.

1.8 Оператор sizeof

Этот оператор нами уже был рассмотрен. Замечу лишь, что возвращаемое значение имеет тип unsigned int.

2. Унарные операторы

2.1 Инкремент и декремент

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

int x = 0;
printf("%i\n",++x); // напечатает 1, значение x теперь 1
printf("%i\n",x++); // напечатает 1, значение x теперь 2
printf("%i\n",++x); // напечатает 3, значение x   -    3

Применяется ко всем арифметическим типам, а также к указателям. Результат всегда имеет тот же тип, что и операнд. Для арифметических типов работает как увеличение/уменьшение на единицу, исключение - тип bool, для него инкрементация форсирует результат как true (сохранено для совместимости со старыми программами), а декремент запрещён. Для указателей операторы сдвигают его к следующему (инкремент) или предыдущему (декремент) элементу воображаемого массива. Если при этом происходит выход за границы массива (такое будет всегда для указателей, указывающих на одиночный элемент), то результат вообще говоря не определён (implementation-defined). Заметьте, что и операнд, и результат - это lvalue (поэтому записи, вроде (a*7)++ абсурдны).

2.2 Преобразование знака

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

2.3 Адресные операции

Операция взятия адреса, обозначаемая амперсандом, применяется к любым lvalue. Результат имеет тип указателя на тип операнда. Если переменная имела описатель const, то результат представляет собой указатель (неконстантный) на константное значение. Операция разыменования (обозначается звёздочкой) принимает указатель, в результате получается lvalue указываемого типа.

2.4 Побитовое отрицание

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

2.5 Логическое отрицание

Обозначается восклицательным знаком. Операнд должен иметь тип bool или неявно приводится к bool. Результат также имеет тип bool (true -> false, false -> true).

3. Бинарные операторы

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

3.1 Аддитивные операторы

Под аддитивными понимают операторы сложения и вычитания. Существует несколько их перегруженных форм:

  1. Сложение и вычитание арифметических типов. Результат определяется по вышеприведённому правилу.
  2. Сложение указателя и данных типа ptrdiff_t. Этот тип определяется (через typedef) в заголовочном файле stddef.h и означает целочисленное адресное смещение. Совпадает с адресной разрядностью платформы. Фактически представляет собой выровненное целое, поэтому допустимо неявное преобразование из целых типов в ptrdiff_t. В результате получаем указателей сдвинутый на некоторый элемент воображаемого массива согласно смещению. Так как указатель хранится в виде адреса, то следующие двсе строки эквивалентны:
        int *b, *a = new int[10];
    b = b + 2; // 1
    b = (int*)((unsigned long)b + 2 * sizeof(int)); // 2
    Правда, сомневаюсь, что второй вариант будет приятней (вдобавок, во втором варианте подразумевается 32-битность указателей, что в принципе неверно).
  3. Вычитание из указателя типа ptrdiff_t. Аналогично предыдущему варианту.
  4. Вычитание из указателя другого указателя. Оба указателя должны указывать на один и тот же тип (или наследуемый тип, но сейчас нас это не интересует), причём не void. Если указываемые находятся в разных воображаемых массивах, то результат операции не определён и в принципе компилятор вправе генерировать при этом ошибку (в том числе рантаймовую). На практике, правда эта операция выполняется простым вычитанием адресов с последующем деление на размер типа, поэтому получаем бред, но без ошибок. Если же оба указателя относятся к одному массиву, то получаем значение типа ptrdiff_t (можно неявно преобразовать к любому целому типу), представляющая разницу между индексами элементов.
3.2 Мультипликативные операторы

Их три. Все они выполняется над двумя арифметическими операндами с использованием вышеприведённого правила для результата:

  1. Умножение (*).
  2. Деление (/). Обратите внимание, при деление целого на целое будет получено целое (5 / 2 = 2). Для получения вещественного типа явно приводите один из операндов к нужному типу или используйте (если это возможно) вещественные литералы (это обсуждалось в первой части).
  3. Остаток от целочисленного деления (%). Применяется только к целым типам с использованием нашего правила.
3.4 Операторы побитового сдвига

Применяются к целым типам с использованием арифметического преобразования. Сдвиг вправо (>>) сдвигает все биты в первом операнде вправо на количество позиций, равное правому операнду (аналогично целочисленному делению на двойку в степени второго операнда). Сдвиг влево (<<) - сдвигает влево. Освободившиеся биты заполняются нулями.

3.5 Операторы побитовых преобразований

Также применяются только к целым типам. Вначале по нашему правилу операнды преобразуются к одному типу, а затем ко всем их битам (независимо) применяется операция. Их три:

  1. Побитовое "и" (&). В результате получается единичный бит только для двух единиц.
  2. Побитовое "или" (|). Здесь наоборот нулевой бит получается только для двух нулей.
  3. Побитовое "исключающее или" (^). Для двух одинаковых битов получается ноль, для разных - единица.
3.6 Логические операторы

Применяются к значениям типа bool или к значениям, неявно преобразуемым к bool. Операций две:

  1. Логическое "и", также называемое логическим умножением (&&). true получится только для двух true.
  2. Логическое "или", также называемое логическим сложением (||). false получится только для двух false.

Обратите внимание на следующий код:

bool b1 = 0x0f && 0xf0;
bool b2 = 0x0f & 0xf0;

b1 будет вычисляться так:

  1. 0x0f и 0xf0 будут преобразованы к типу bool. Так как они оба не равны нулю. следовательно мы получим две истины (true).
  2. Будет вычислено логическое умножение для двух истин, в результате получим истину (true).

b2 же будет вычислено так:

  1. Над двумя целыми операндами (типа int) будет выполнено побитовое "и". В результат получим 0, т. к. нет ни одной пары из соответственных битов, состоящей из одних единиц.
  2. 0 будет преобразован к типу bool, естественно, получим false.
3.7 Операторы сравнения

Применяются к арифметическим типам и к указателям. При работе с указателями они должны указывать на один и тот же тип (если не говорить о наследовании), либо же один из них должен указывать на void. Существуют 6 операторов:

  1. == - равно.
  2. != - не равно.
  3. > - больше.
  4. >= - больше или равно.
  5. < - меньше.
  6. <= - меньше или равно.

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

3.8 Оператор присваивания

Первый операнд оператора присваивания (левая часть) должен быть неконстантным lvalue. Второй операнд (правая часть) должен иметь тот же тип или приводится к тому же типу, что и левый операнд. Обычно результат выполнения оператора не используется (используется лишь совершаемое им действие), но в принципе он возвращает присвоенное rvalue. Кроме того, у этого оператора есть особенность: для множества присваиваний приоритет определяется справа налево. Это позволяет писать что-то вроде a = b = c или даже так: a = 5 * (b = c). Вторая запись, правда, нежелательна из-за малопонятности (по крайней мере, разбив это дело на два предложения, мы бы увеличили читабельность. Однако, часто можно встретить что-то вроде if (f = fopen("file.bin", "r")). Такая запись выполняет присваивание, а затем полученное значение приводит к bool.

3.9 Комбинированное присваивание

Существует множество операторов комбинированного присваивания (вместе с выполнением некоторой бинарной операции). Представьте увеличение переменной скажем на 5. Традиционной (для алгоритмических языков) формой было бы такое решение: a = a + 5. Как реализуется такая запись? Вначале во временные ячейки (регистры) копируются значения переменной a и константы 5. Затем они складываются. Полученное значение пересылается в ячейку, в которой хранится наша переменная. Получается двойное обращение к переменной. Хотя достаточно сразу сложить a с пятёркой, загнанной во временную ячейку. Поэтому придумали запись: a += 5. Эта запись не только эффективней, но и несколько иначе работает с точки зрения человеческого мышления (мы увеличиваем a на 5, а не присваиваем a значение a, увеличенное на 5). Данная концепция оказалось достаточно удобной, и потому нашла отражение даже в тех языках или конструкциях языков, где (вследствие реализации) это не влияет на эффективность или об эффективности просто не говорят. Позже вы ни раз в этом убедитесь и в C++. Вся логика операторов комбинированного присваивания для программиста, не являющегося разработчиком компилятора, аналогична соответствующему присваиванию с выполнением некоторой бинарной операцией. На этом соображении основываются все преобразования типов и ограничения на них. Вот список операторов комбинированного присваивания:

  • +=
  • -=
  • *=
  • /=
  • %=
  • <<=
  • >>=
  • &=
  • |=
  • ^=

Итак, мы познакомились подробно со всеми вещами, необходимыми для построения выражений. Здесь надо сказать о двузначным толковании термина "оператор". Во многих языках под этим термином понимают каждую команду языка. Чтобы не было противоречий, мы будем называть это командой. В оригинале стандарта C++ это названо словом statement. Подходящего перевода ("предложение"?) я не нахожу.

Чтобы быть уж совсем точным, скажу, что одним из видов команд являются команды-выражения (expression-statement). Эти команды представляют из себя выражение, заканчивающееся точкой с запятой. Не следует их путать с командами-объявлениями (declaration-statement). На всякий случай скажу, что объявление переменных имеет следующий вид:

список_описателей список_переменных ;

К описателям относится тип переменной, а также вспомагательные описатели (вроде const). Элементы списка переменных разделяются запятыми, каждый имеет вид:

[*] [&] имя [описатели массива] [ = выражение_инициализации]

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

// Файл helper.h
extern int a;
///

// Файл helper.cpp
#include "helper.h"

int a;
int c = func();

int func()
{
    a = 10;
    return a;
}
///

// Файл main.cpp
#include "helper.h"

int b = 2 * a;

int main()
{
    printf("b = %i\n", b);
    return 0;
}
///

А интересен нам здесь тот факт, что глобальные переменные инициализируются до вызова main, в пределах одного модуля (грубо говоря - файла) порядок инициализации соответствует порядку объявления, но угадать инициализация в каком модуле будет выполнена вперёд - невозможно (это зависит от компилятора). То есть как вызов func (для инициализации c) до инициализации b, так и после будет правильным. В итоге значение b может получиться и 20 (если вначале будет выполнена инициализация из helper.cpp), и любое число (значение a до выполнения инициализации в файле helper.cpp не определено). Выводов два: глобальные переменные должны использоваться как можно реже, а вся динамическая (с вызовом функций и рантаймовыми вычислениями) инициализация (выполняемая уже присваиванием) должна осуществляться в самой программе (обычно вызовом функции с именем, вроде init).

На закуску, напишите такой хейдер:

#include <stdio.h>

int myMain()
{
    printf("I'm new main\n");
    return 0;
}

int myVar = myMain();

Теперь подключите этот заголовок к файлу кода в первой строке, попробуйте, вывести в main что-то вперёд этого текста! Если получится, значит выбросьте ваш компилер. А теперь напечатайте из main какой-нибудь текст и попробуйте, не модифицируя (больше) основной текст программы, а модифицируя лишь этот заголовок, вывести на экран только фразу, написанную выше! Надеюсь, что у вас получится!


Указатели. C-строки. Подробности

Почему-то начинающие очень не любят указатели, поэтому, наверно, не помешает рассмотреть эту тему ещё раз и поподробней. Во-первых, важно понять типы памяти любого компьютера (в любой операционной системе). Самый простой и наглый способ хранения данных - регистры процессора. При их использовании непосредственно указывается имя регистра, к которому мы хотим обратится, понятно, что количество этих имён чрезвычайно мало, зато доступ к ним очень быстр. Об их использовании вам обычно заботится не надо, пускай работает компилятор со своим оптимизирующим кодогенератором. Об одном исключении мы поговорим позже. Второй тип памяти - статическая память. Она применяется (как ни странно) к статическим переменным. При этом переменные "зашиваются" в исполняемый файл и при его запуске загружаются в оперативную память, таким образом заранее точно известно, по какому смещению (относительно начала исполняемого файла) будет находится эта переменная. Замечу, что небольшие глобальные (но не статические) переменные компилятор может тоже разместить в статической памяти. Существует также стековая память. Стек принято представлять в виде стопки книг: мы можем положить и взять только сверху. Стековая память имеет более сложную структуру, поэтому можно добавлять/уничтожать данные только сверху, но прочитать можно с любой позиции (правда проще с верхней). Этот механизм идеально подходит для большинства переменных - память, занятая ими выделяется при входе, а освобождается при выходе из некоторого блока (области видимости). Также стековая память позволяет организовать рекурсию. В старых компиляторах (например, в первых компилятора языка FORTRAN) для всего, в том числе и для переменных функций (подпрограмм) использовалась статическая память, в итоге вызов подпрограммы из самой себя не был возможен - это переписало бы все данные подпрограммы по новому. Последней тип памяти - память произвольного доступа, называемая кучей (heap) или просто хипом (по английскому названию). Эта память выделяется операционной системой по соответствующему запросу со стороны нашей программы. В любой момент, зная адрес, мы можем прочитать/выделить/освободить память в хипе. Естественно, сам адрес где-то сохраняется, так как (в отличие от статической памяти) мы узнаём его от ОСи в рантайме.

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

static int a;
int b;

int *p1 = &a;
int *p2 = &b;

В то же время можно работать с хипом без указателей:

int& a = *(new int);
a = 5;
delete &a;

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

int a = *(new int);
a = 5;
delete &a;

В этом примере выделяется память для целого числа, полученный указатель разыменовывается, получаем lvalue. Затем это lvalue преобразуется к rvalue (т. е. используется в контексте rvalue). В итоге мы используем лишь значение, получившееся в нашей ячейке, её адрес безвозвратно теряется, т. е. память для a расположена в стеке, а не в куче. В последней строчке мы видим ещё большее извращение - попытка удалить объект из стека оператором delete! Мдя!

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

Правда работать с динамическими массивами без указателей не получится. Хотя для аргументов функций и здесь у вас есть альтернативный синтаксис - массив без указания размерности (int a[]). Правда, заметьте реализовать таким образом ссылку на динамический массив, т. е. указатель не получится. А это может понадобится, если значение указателя изменяется в функции (например, под него выделяется память).

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

char s1[] = "My String 1";
char * s2 = "My String 2";
char * s3 = new char[sizeof("My String 3")];
strcpy(s3,"My String 3");
char s4[sizeof("My String 4")];
strcpy(s4,"My String 4");

s1[0] = '!';
s2[0] = '!';
s3[0] = '!';
s4[0] = '!';

delete [] s3;

Чем плох первый вариант? Тем, что мы не сможем никак изменить размер s1. Это не всегда плохо, но очень часто. Чем же тогда плох второй вариант? Тем, что он незаконен, хотя все попавшиеся мне под руку компилеры успешно проглотили это. Их ошибки я связываю с тем, что большинство (по крайней мере все популярные) компиляторов создаются как компиляторы и C, и C++. Эту строку они разбирают по сишным правилам. Но с точки зрения плюсов любой строковый литерал - константный массив, присваивание указателю не на константы константного массива невозможно по правилам плюсов. Интересно, что при выполнении присваивания символу (нулевому) строки s2 восклицательного знака, все экзехи (от разных компиляторов) вылетали с ошибкой. Красота, да и только! Самое весёлое - релиз-версии в VC++. Например:

#include <stdio.h>

int main()
{
    char * s = "???";
    s[1] = '!';
    printf("%s\n",s);
    return 0;
}

Дебаговая, т. е. отладочная версия успешно вываливается с ошибкой (наверно, чтобы было, что отлаживать), но в релизе из-за оптимизатора (ох уж этот оптимизатор!) переменной s нету, есть лишь вывод трёх вопросительных знаков (о восклицательных - не звука).

Третий вариант достаточно хорош, правда красивей, наверно, размер выделяемой памяти задавать не для конкретной строки, а с запасом. Наилучшим для данного примера, наверно, будет последний вариант - нам не приходиться выделять/освобождать память. Недостаток последнего способа - необходимо знать предельный размер на этапе компиляции. Зачастую к тому же мы его знаем, но в то же время знаем, что вероятность, что нам понадобится именно предельный размер ничтожно мала (иногда этот предельный размер существует, но достаточно велик). Помните также, что для константных строк, разумно использовать первый вариант, также можно использовать несколько изменённый третий:

const char * const s = "Constant string";

Обратите внимание на "двойную" константность нашей строки. Если вы планируете использовать s в дальнейшем для других целей (кроме хранения этой строки), то:

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

Последнее замечание - насчёт оператора delete. При использовании различных библиотек структурного программирования (например, WinAPI) очень часто приходится сохранять (временно) указатели в эквивалентных числовых переменных (адресах) или в указателях на void (последнее делается и в других парадигмах программирования, хотя по возможности этим пользоваться не рекомендуют). При этом для корректного освобождения памяти придётся юзать явное приведение памяти к нужному типу:

MyType *p = new MyType;
unsigned long addr = (unsigned long)p;
// ...
delete (MyType*)addr;

Мы могли бы привести к любому другому указателю, в чём же тогда разница? А разница в том, что будет освобождён разный объём памяти, он определяется через sizeof (если мы не заставим это делать иначе - об этом мы поговорим в следующих частях). Для динамических массивов никаких принципиальных отличий нет (кроме, естественно использование других форм операторов new и delete). Возникает очень логичный вопрос - откуда же берётся размер массива? На самом деле оператор new в форме для массивов выделяет несколько больше памяти чем "надо" - он дополнительно сохраняет размер выделенной памяти в первых её байтах. Оператор delete использует эти данные. Так что следующие две строки выделяют разное количество памяти:

int *p1 = new int;
int *p2 = new int[1];


Динамические структуры данных. Односвязный список

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

  1. Создаём новый массив нужного размера.
  2. Копируем в него весь старый массив. Можно обойтись без циклов с помощью функции memcpy из memory.h (подробности смотрите в вашем справочнике).

Понятно, что такой алгоритм достаточно накладен. Принято говорить, что вставка элемента в массив имеет линейную сложность. Понятие сложности было придумано для сравнения эффективности различных алгоритмов. Различают постоянную сложность (например, обращение к произвольному элементу массива выполняется с одинаковой эффективностью независимо от его сложности), линейную (например, обычный поиск элемента, его сложность в среднем равна n/2, где n - общее количество элементов), квадратичную () Сам массив (указатель), текущий "рабочий" размер и текущий фактический размер можно объединить в структуру. Если интересно, можете написать собственную небольшую библиотеку для полноценной работы с динамическими массивами (ресайзинг, удаление и вставка элементов).

Существует множество других динамических структур данных (в частности, множества :)). Мы рассмотрим в этом уроке односвязные списки, чтобы понять основные идеи работы с динамическими структурами данных.

Односвязный список проще всего представить в виде цепочки данных (поезда). Есть некоторый начальный элемент, у каждого элемента кроме последнего есть следующий. Попробуем реализовать теперь это дело программно. Сразу скажу, что в файлах к уроку прилагается проект реализации односвязного списка. Проект разделён на несколько модулей (h и cpp файлов), здесь я привожу простое описание кода, не выделяя в каком именно файле он находится. Думаю, что разделение на файлы не входит в задачи этого урока.

Во-первых, выделим (для удобства модификации) функции и типы для работы с данными списка отдельно:

typedef int ListData; // данные элемента списка

void inputListData(ListData& data)
{
    scanf("%i\n", &data);
}

void printListData(const ListData& data)
{
    printf("%i\n", data);
}

Обратите внимание - для изменения типа данных, хранящихся в списке, достаточно вместе первой строки описать тип ListData так, как нам надо. Заметьте, что при выводе мы данные передаём по константной ссылке, сделано это из-за возможности использования более громоздких (чем int) типов. Например:

struct ListData
{
    char Name[20];
    unsigned char Age;
};

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

struct ListNode
{
    ListData data; // данные узла
    ListNode *next; // указатель на следующий узел
};

Обратите внимание на рекурсивность объявления. Весь список хранится на самом деле с помощью единственного указателя на ListNode - на начальный элемент. Его часто называют головным элементом, поэтому переменную часто называют head (head - голова). Изначально этот элемент должен быть нулевым (никуда не указывать). Обратите внимание, что надо явно инициализировать его нулём, так как понятия нулевой и недопустимый (указывающий на память, недоступную в данный момент нашей программе) указатель различны. Напишем для начала функцию ввода списка:

void inputList(ListNode* &head)
{
    // Указатель на предыдущий узел для обеспечения связывания
    ListNode* prev;
    head = 0;

    do
    {
        // Создаём новый узел
        ListNode* node = new ListNode;
        printf("Input Node Data\n");
        inputListData(node->data);

        if (head) // создаём не первый узел
            prev->next = node;
        else
            head = node;
        prev = node;

        printf("Next (+/-)?\n");
    } while (_getch() == '+');

    // Ставим признак последнего узла
    prev->next = 0;
}

Таким образом мы заполнили ряд элементов. Обратите внимание, что головой можно считать любой элемент списка, в данной функции, правда голова должна быть нулевым или недопустимым указателем (не проинициализированным). Таким образом в любой момент мы можем продолжить ввод списка, передав в функцию InputList указатель на последний элемент списка. Возникает потребность в поиске этого последнего элемента. Мы напишем более универсальную функцию - для поиска элемента, по заданному следющему элементу. Для последнего элемента следующего нету, т. е. next - нулевой указатель, это значение будет дефолтным в нашей функции:

ListNode* findNode(ListNode* head, const ListNode* next = 0)
{
    if (!head)
        return 0;

    while (head->next != next)
    {
        if (head->next)
            head = head->next;
        else
            return 0;
    }

    return head;
}

Обратите внимание, что head передаётся по значению (не по ссылке), поэтому можем смело изменять его в функции, на "настоящую" голову это никак не повлияет. Также заметьте, что второй указатель получает описатель const, напомню, что для параметров функций он означает константность указываемого значения, а не самого указателя. Голову законстантить нельзя, так как возвращаемое значение основывается на голове и с помощью него можно модифицировать данные списка (из вызывающей функции). Если head объявить с описателем const, то функция должна будет вернуть указатель на константу.

Если походящий элемент не найден (дошли до конца), то возвращается нулевой указатель. Раз уж мы занялись поиском, то сделаем поиск также по данным списка. Обратите внимание, что критерии поиска при этом могут быть разными (например, для целых чисел можно искать элементы в диапазоне от 5 до 15). Для обеспечения универсальности отбора мы используем указатель на логическую функцию, которая принимает две константных ссылки на ListData. Как всегда перегрузка рулит (функцию также называем findNode). Но всё-таки достаточно частым критерием отбора является простое сравнение элементов. Естественным решением будут аргументы по умолчанию и специальная функция сравнения. Но хотелось бы не включать эту функцию в хейдер (см. проект в файлах к уроку). Используем простую хитрость с нулевым указателем на функцию:

typedef bool (*CompareFunc) (const ListData&, const ListData&);

bool equalData(const ListData& data1, const ListData& data2)
{
    return data1 == data2;
}

ListNode* findNode(ListNode* head, const ListData& data, CompareFunc compareFunc)
{
    if (!head)
        return 0;

    if (!compareFunc)
        compareFunc = equalData;

    while (!compareFunc(data, head->data))
    {
        if (head->next)
            head = head->next;
        else
            return 0;
    }

    return head;
}

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

ListNode* getNodeByNumber(ListNode* head, int number)
{
    while (number-- && head)
        head = head->next;
    return head;
}

int getNumberByNode(const ListNode* head, const ListNode* node)
{
    int number = -1;
    while (head)
    {
        ++number;
        if (head == node)
            return number;
        head = head->next;
    }

    return - 1;
}

Есть такое золотое правило: каждому new по delete. Обратите внимание, что весь ряд данных находится в куче и держится всего на одном указателе! Сделав делит только головы мы просто потеряем "туловище". Необходимо написать функцию удаления всего списка. Первая идея - искать последний элемент и делитить его. Реализация этой идеи не очень сложна, хотя на придётся делать два поиска: последнего элемента и элемента перед последним (для зануления связывающего указателя). Но есть более эффективный и изящный способ - удаления с первого элемента с сохранением указателя на второй:

void deleteList(ListNode* head)
{
    while (head)
    {
        ListNode* node = head;
        head = head->next;
        delete node;
    }
}

Обратите внимание зануление головы не происходит (как и в обычном delete), это нужно сделать явно вне функции, чтобы в дальнейшем список корректно распознавался как пустой. Заметьте также, что из этой функции следует, что ListData не может быть указателем или структурой, содержащей указатели. Точнее может, но память мы должны освободить до вызова deleteList наглым пробегом по всем узлам. Напишем также функцию вывода всего списка:

void printList(const ListNode* head)
{
    for (int i = 0; head; ++i, head = head->next)
    {
        printf("Node #%i\n", i);
        printListData(head->data);
    }
}

А теперь получим удовольствие от списков - напишем функции, которые являются главными фичами списков, функции, с которыми так плохо справляются массивы. Во-первых, это вставка узлов или же целых других списков. Мы напишем для этого функцию insertItem, т. е. "вставка элемента", причём элементом, как я уже сказал, может быть целый список, передаём ссылку на указатель на узел, до которого должны вставляться элементы. Можно на первый взгляд передавать указатель на элемент, после которого должны вставляться данные, но это не будет работать с head. Данный же способ работает для всех случаев, в том числе если мы передадим ссылку на нулевой конечный указатель. Но помните, имея только сторонний указатель на элемент (не голову и не поле next), перед которым надо вставить что-то мы не сможем выполнить вставку. Можно воспользоваться первой версией findNext, если она вернёт нулевой указатель использовать голову, а иначе поле next структуры, указатель на которую вернёт функция. Есть другое решение - использование списков, данные первого узла которого просто игнорируются. Такие списки называют списками с заглавным звеном (или узлом). Если хотите можете реализовать это сами. Мы же воспользуемся первым способом:

ListNode* insertItem(ListNode* &before, const ListData& data)
{
    ListNode* node = new ListNode;
    node->data = data;
    node->next = before; // *1
    before = node; // *2
    return node;
}

ListNode* insertItem(ListNode* &before, ListNode* head)
{
    if (!head) // вставляемый список пуст
        return 0;

    ListNode* end = findNode(head); // последний узел вставляемого списка
    end->next = before;
    before = head;
    return end;
}

Обратите внимание на очерёдность перестановки указателей (строки *1 и *2). Вначале мы присоединяем к новому элементу правую часть списка, а затем отрываем её от самого списка и соединяем левую часть списка с вставляемым элементом. В обратном порядке мы просто потеряем правую часть списка. В результате имеем утечку памяти. Заметьте, что функции вставки возвращают указатель на последний вставленный элемент (на первый получить очень просто - before, т. е. передаваемый в функцию параметр).

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

ListNode* excludeItem(ListNode* &start, int count = 1)
{
    if (!start)
        return 0;

    ListNode* finish = getNodeByNumber(start, count);
    ListNode* node = start;
    start = finish;
    return node;
}

ListNode* excludeItem(ListNode* &start, ListNode* finish)
{
    if (!start)
        return 0;

    ListNode* node = start;
    start = finish;
    return node;
}

Для удаления части элементов из списка достаточно вызвать deleteList для указателя, который вернёт excludeItem.

Подробно использование многих из этих функций продемонстрировано в примере. Проект разделён на три модуля: ListData, list, main (последний без хейдера). Обратите внимание, что в main.cpp есть инклюд list.h, но нет инклюда ListData.h, так как последний подключается через list.h. При его двойном подключении (в пределах одного модуля) мы получили бы переопределение имён, т. е. ошибку компиляции. Замечу, что не все функции модуля List используются, но я думаю вы поймёте как ими пользоваться.

Наверно, вы заметили, что в наших функциях очень часто используются предыдущие узлы для некоторого другого узла. У нас для этого есть функция findNode, он работает с линейной сложностью. Естественно, хотелось бы повысить эффективность (ведь доступ к следующему элементу имеет постоянную сложность). Возникает идея двусвязных списков, каждый узел которых кроме указателя на следующий элемент хранит также указатель на предыдущий. Существуют также кольцевые списки, в которых голову замыкают с "хвостом". Таки списки удобны для обеспечения универсальности многих действий, в которых нам пришлось юзать ссылки на указатели. Очень разнообразны реализации деревьев, т. е. многоуровневых динамических структур данных. Узел простейшего бинарного дерева имеет такую структуру:

struct BinaryTreeNode
{
    BinaryTreeData data;
    BinaryTreeNode *left, *right;
};

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

Файлы

Проект


Рекурсия. Плюсы и минусы

Механизм рекурсии достаточно важен и достоин отдельного рассмотрения. Напомню, что рекурсия - это вызов функции из самой себя. Выделяют также косвенную (неявную) рекурсию, в этом случае функция A вызывает функцию B, а функция B вызывает функцию A. Последнее реализовать не так просто, как кажется на первый взгляд. Следующий код компилироваться не будет:

void A()
{
    B();
}

void B()
{
    A();
}

Причина очень проста - при разборе функции A компилятор не в состоянии определить, что такое B. Стандартным решением является использование предварительных объявлений:

void A();
void B();

void A() { B(); }
void B() { A(); }

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

struct ListNode;

typedef ListNode* ListPtr;

struct ListNode
{
    ListData data;
    ListPtr next;
}

Рекурсия появилась не сама по себе, хотя на первый взгляд никаких языковых средств для неё не нужно. Но, напомню, что осуществление рекурсии без использовании концепции стековой памяти невозможно. Напомню, что все переменные в рекурсивных функциях занимают отдельную память. Почему-то классическими примерами рекурсии стали вычисления факториала и чисел Фиобоначи. Напомню на всякий случай, что факториал неотрицательного числа n - это произведение чисел от 1 до n, а каждое число последовательности Фиобоначи - это сумма двух предыдущих, при этом первые два числа это 0 и 1 (далее 1, 2, 3, 5, 8 ...). Рекурсивно эти функции во многих учебниках записывают так (по крайней мере типа того):

unsigned long long Fact(unsgined int n)
{
    if (n)
        return Fact(n-1) * n;
    else
        return 1;
}

unsigned int Fiob(unsigned int n)
{
    if (n == 0)
        return 0;
    if (n == 1)
        return 1;
    return Fiob(n-1) * Fiob(n-2);
}

Меня лично эти примеры всегда очень раздражали. Во-первых, я бы не сказал, что они принципиально красивей нерекурсивных способов (особенно факториал). Во-вторых представьте вычисление по одной из этих функций. Чтобы вычислить15-е число Фиобоначи, у вас одновременно будут находиться в памяти 15 чисел. Кроме того для всех вызовов функции создаётся стек вызовов (для возврата управления в нужное место), передача параметров и т. д. И всё это 15 раз. Вдобавок вызовы рекурсивных функций хуже поддаются обработке оптимизатора. Какая уж тут производительность... И это не глупость, рекурсия действительно достаточно дорогая вещь, чтобы решиться на рекурсию надо быть уверенным, что иной алгоритм будет хуже, либо слишком уродливо смотреться. В частности, при обработке деревьев попытка обойтись без рекурсии приведёт лишь к её ручной имитации, причём смотрящийся достаточно некрасиво. Вспомните нашу структуру для узла бинарного дерева. Напишем его полную распечатку:

void printBinaryTree(BinaryTreeNode* head)
{
    if (head)
    {
        printData(head->data);
        printBinaryTree(head->left);
        printBinaryTree(head->right);
    }
}

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

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

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


Некоторые алгоритмы сортировки последовательных коллекций данных

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

Любая сортировка использует перестановки элементов. Выделим для начало два способа осуществления этого:

  1. Способ в лоб - перестановка в лоб с помощью промежуточной переменной и копирования (переприсваивания).
  2. Перестановка связывающих указателей для списков.

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

void swap(int &a, int &b)
{
    int temp = a;
    a = b;
    b = temp;
}

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

Алгоритмов сортировки сегодня не просто много, а очень много, если вам интересно почитать о различных алгоритмах, рекомендую посетить сайт Algolist. Мы рассмотрим лишь три наиболее популярных алгоритма. Замечу, что все они реализованы в проекте, прилагаемом к уроку. В этом проекте реализована также система тестирования, позволяющая сравнить эффективность алгоритмов в различных случаях. Интересно заметить (это правда к 8-му уроку), что массив из полумиллиона элементов в стеке мне создать не удалось (переполнение стека), потому юзаем хип.

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

  1. Просматриваются все пары соседних элементов (в последней паре первый элемент - n-1).
  2. Если следующий элемент меньше предыдущего, то они переставляются - таким образом самый больший элемент выталкивается в конец, а остальные попутно также в некоторой мере сортируются.
  3. Шаги 1-2 повторяются для всех элементов кроме последнего, затем кроме предпоследнего и т. д.

Реализация алгоритма предельно проста:

void BubbleSort(int* ar, size_t size)
{
    for (int i = n - 1; i; --i)
    {
        for (int j = 0; j < i; ++j)
        {
            if (ar[j+1] < ar[j])
            {
                int temp = ar[j];
                ar[j] = ar[j+1];
                ar[j+1] = temp;
            }
        }
    }
}

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

void BubbleSort(int* ar, size_t size)
{
    for (int i = n - 1; i; --i)
    {
        bool sorted = true; // предполагаем, что массив отсортирован
        for (int j = 0; j < i; ++j)
        {
            if (ar[j+1] < ar[j])
            {
                sorted = false; // перестановки были - считаем, что надо сортировать ещё
                int temp = ar[j];
                ar[j] = ar[j+1];
                ar[j+1] = temp;
            }
        }

        if (sorted)
            return;
    }
}

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

  1. Ищется минимальный элемент.
  2. Он переставляется с первым элементом.
  3. Считая теперь первым второй элемент, повторяют шаги 1 и 2. Процесс продолжается до n-1 элемента, так как последний автоматически становится куда надо.

Замечу, что под n здесь и ниже подразумевается общее количество элементов. Для списков используется проверка на нулевой указатель, но по-моему это не принципиально (легко изменяется). Такую сортировку называют также сортировкой выбором. Воспользуемся этим алгоритмом:

void ChoiseSort(int* ar, size_t size)
{
    for (int i = 0; i < (size - 1); ++i)
    {
        int i_min = i; // предполагаем, что текущий элемент - минимальный
        for (int j =  i + 1; j < size; ++j)
        {
            if (ar[j] < ar[i_min]) // нашли меньший элемент
                min = j;
        }

        if (i_min != i) // нет смысла переставлять самого себя
        {
            int temp = ar[i];
            ar[i] = ar[i_min];
            ar[i_min] = temp;
        }
    }
}

Эффективность этого алгоритма достаточна мала, но стабильна для любых типов массивов (при постоянном размере). Незначительные изменения связаны с изменением количества перестановок (кол-во проходов цикла постоянно и равно n*(n-1)/2).

Наиболее эффективным из общеизвестных и популярных алгоритмов сортировки является алгоритм, называемый "быстрой сортировкой" (quick sort). Правильней даже сказать, что это не отдельный алгоритм, а целый ряд алгоритмов. Наверно, если вы запускали проект, прилагаемый к уроку, то заметили, что производительность любого алгоритма значительно падает при увеличении размера массива, причём эта зависимость нелинейная. Возникает идея, на которой основан алгоритм быстрой сортировки. Заключается она в разбивке массива (или иной коллекции данных) на меньшие подмассивы, которые после сортировки объединяются. Рассмотрим достаточно понятный и популярный рекурсивный алгоритм, воплощающий идеи быстрой сортировки:

  1. Выбирается такой элемент массива, чтобы все элементы до него были меньше, а после - больше него. При этом возможно производится реструктурирование массива для обеспечения эффективного положения такого элемента (не самый крайний).
  2. Алгоритм независимо применяется к левому и правому подмассиву. Точка разбиения (третий подмассив) остаётся на месте.
  3. Получаем отсортированный массив, никаких алгоритмов для правильного объединения не требуется.

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

  1. За точку разбиения принимается первый элемент (можно любой другой, главное, чтобы он был в массиве): sp_point = 0.
  2. Ищется слева элемент, который не меньше точки разбиения: ar[i] >= ar[sp_point].
  3. Ищется справа элемент, который не больше точки разбиения: ar[j] <= ar[sp_point].
  4. Если i<j, то найденные элементы меняются местами: swap(ar[i], ar[j]).
  5. Пункты 2-4 повторяются до тех пор, пока второй найденный элемент не будет левее первого: i>j.
  6. Элемент принятый за точку разбиения ставится на его настоящее место: swap(ar[sp-point], ar[j]).
  7. За точку разбиения считают соответствующий элемент: sp_point = j.

Реализуем этот алгоритм:

void QuickSort(int ar[], int start, int finish)
{
    if (start > finish)
        return; // выход из рекурсии

    int i = start;
    int j = finish;

    for (;;)
    {
        while ((ar[i] <= ar[start]) && (i < finish))
            ++i;

        while ((ar[j] >= ar[start]) && (j > start))
            --j;

        if (i >= j)
            break;

        swap(ar[i], ar[j]);
    }

    // Меняем первый элемент и ar[i]
    swap(ar[j], ar[start]);

    RQSort(ar, start, j - 1); // левый подмассив
    RQSort(ar, j + 1, finish); // правый подмассив
}

Этот алгоритм в большинстве случае показал себя лучше всех. Наиболее сложным случаем для него стала сортировка массива данного в обратном порядке. Для массива из 100 тысяч элементов этот случай оказался не по зубам нашему алгоритму - стек вызова функция (Call Stack) при рекурсивном вызове переполнился и программа вылетела. Это в копилку предыдущего урока. В то же время для массива из 500 тысяч элементов, заполненного случайными числами этот алгоритм был на высоте: 0 (!) секунд против 260 (сортировка выбором) и 798 (пузырьковая сортировка). Для миллионного массива, заполненного случайными числами - 1 секунда. Своё название алгоритм оправдал!

Надо заметить, что есть библиотечная реализация быстрой сортировки (её визуаловский исходник находится в файле %VSINSTALLDIR%\VCx\crt\src\qsort.c). Она значительно более навороченная, а потому более эффективная. Функция называется qsort и объявлена в search.h и stdlib.h (инклюдьте любой). Принимает она четыре параметра:

  1. void* base - указатель на начало массива. Обратите внимание, что массив может включать любые элементы, поэтому используется указатель на void.
  2. size_t num - количество сортируемых элементов в массиве.
  3. size_t width - размер одного элемента массива. Определяется с помощью sizeof.
  4. int (*compare) (const void*, cons void*) - указатель на сравнивающую функцию. Функция должна возвращать положительное число, если первый элемент больше второго, отрицательно - если меньше и ноль - если они равны.

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

int compare(const void* a, const void* b)
{
    int x = *(int*)a;
    int y = *(int*)b;
    return (x > y) ? 1 : (x < y) ? -1 : 0;
}

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

Файлы

Проект


Алгоритмы и приёмы, связанные с сортировкой и поиском

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

struct BookInfo
{
    char author[15];
    char title[30];
    unsigned short year;
    unsigned char count;
}

Эта структура имеет размер 48 байт (возможно в каких-то компилерах будет несколько другое число, но достаточно близкое). Если нам достаточно часто приходится сортировать записи в нашей БД по разным критериям (автор, название, год издания, количество экземпляров), то придётся запасаться машиной с хорошим процессором и памятью (последнее - для квик сорта). Некоторые пошли по другому пути и решили завести дополнительный индексный массив, т. е. массив, в котором хранятся индексы (номера) элементов массива. Фактически сортируется лишь индексный массив. Например, так будет выглядеть пузырьковая сортировка по автору и дальнейшая печать массива

/*
    size - размер массив
    books - массив из BookInfo
    swap - функция перестановки эл-тов
*/

int indexes[size];
// инициализация индексов - прямой порядок (несортированный)
for (int i = 0; i < size; ++i)
    indexes[i] = i;

for (int i = size - 1; i; --i)
{
    bool sorted = true;
    for (int j = 0; j << i; ++j)
    {
        if (strcmp(books[indexes[j]].author, books[indexes[j+1]].author) < 0)
        {
            swap(indexes[j], indexes[j+1]);
            sorted = false;
        }
    }

    if (sorted)
        break;
}

for (int i = 0; i < size; ++i)
{
    BookInfo& bi = books[indexes[i]];
    printf("Book #%i\n", i);
    printf("Author:\t %s\n", bi.author);
    printf("Title:\t %s\n", bi.title);
    printf("Year:\t%u\n", bi.year);
    printf("Count:\t%u\n", bi.count);
}

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

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

Другой важнейшей алгоритмической задачей всегда был поиск. Наиболее универсальным является алгоритм линейного поиска, т. е. простого перебора и сравнения элементов. Реализуем его для целочисленного массива с учётом того, что искомый элемент может вовсе отсутствовать (тгда возвращается -1):

int SimpleSearch(int ar[], size_t size, int x)
{
    for (int i = 0; i < size; ++i)
    {
        if (ar[i] == x)
            return i;
    }

    return -1;
}

Иногда пытаются написать вместо этого что-то типа такого:

int SimpleSearch(int ar[], size_t size, int x)
{
    int i = 0;
    while (ar[i] != x)
        ++i;
    return i;
}

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

int SimpleSearch(int ar[], size_t size, int x)
{
    ar[size] = x; // "сторожевой" элемент
    int i = 0;
    while (ar[i] != x)
        ++i;
    return (i == size) ? -1 : i;
}

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

Надо заметить, что в любом случае линейный поиск работает со сложностью около n/2 (в среднем). Если же нам нужно найти элемент в уже отсортированном массиве, то есть гораздо более эффективный способ бинарного поиска. При этом с каждой проверкой отсекается половина массива (потому поиск и называется бинарным). Рассмотрим этот алгоритм (считаем, что массив упорядочен по возрастанию):

  1. Делим массив пополам.
  2. Сравниваем средний элемент с искомым. Если в массиве чётное число элементов, то берём любой элемент ((n+1)/2 или (n-1)/2).
  3. Если они равны, завершаем поиск.
  4. Если искомый элемент меньше среднего, то берём в качестве нового массива левую половину и возвращаемся к первому шагу, в противном случае берём правую половину и также переходим к первому шагу.

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

int BinarySearch(int ar[], size_t size, int x)
{
    // Левая и правая граница в текущей итерации
    int left = 0;
    int right = size - 1;

    while (right != left)
    {
        int middle = (left + right) / 2; // целочисленное деление, т. к. оба аргумента целые
        if (ar[middle] == x)
            return middle;

        if (ar[middle] > x)
            right = middle - 1;
        else
            left = middle + 1;
    }

    if (ar[left] == x)
        return left;

    return -1;
}

Обратите внимание, что мы имеем право говорить, что в конце концов left станет равным right (если элемент не будет найден раньше). Даже если у нас останется два элемента, то средним будет выбран левый элемент (из-за игнорирования остатка при целочисленном делении).

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

Для бинарного поиска в массивах есть специальная функция bsearch. Объявлена она в search.h и stdlib.h. Она принимает следующие параметры:

  1. const void* key - указатель на искомый объект.
  2. const void* base - начало массива.
  3. size_t num - количество элементов.
  4. size_t width - размер одного элемента.
  5. int (*compare) (const void*, const void*) - сравнивающая функция.

Возвращает функция указатель на найденный элемент (void*).


Разработка приложений в структурном программировании

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

Многие абсолютно правильно говорят, что нельзя научиться программированию без написания программ. Но при этом приходилось видеть многих людей, которые желая стать профессионалами, изучают справочники, документацию и начинают себе ставить различные задачи и программерить. При этом они руководствуются простым принципом - главное, чтобы работало. Некоторые заботятся ещё и о конечной "эффективности", т. е. скорости работы программы и размере исполняемого файла. Но при этом они думают только о жестокой технике программирования, но никак не об искусстве. Читать код, написанный ими невозможно, я уже молчу о модификации. Через месяц они и сами не в состоянии понять, как работает их программа, они лишь гордо отвечают: "Главное, что работает!" Ни о каком развитии их проекта говорить не приходится. Следует помнить, что эффективность для программы - это в первую очередь грамотность кода. На самом деле грамотность кода автоматически обеспечивает вам шустрость кода (благо, этим компиляторы C++ могут гордиться). А воспользовавшись некоторыми техническими приёмами вы без больших проблем можете уменьшить размер исполняемого файла и прочую эффективность в "новичковском" понимании. Иногда сам удивляешься, насколько это просто, если не ставить эту "эффективность" самоцелью!

В связи с этим, замечу, что я считаю наиболее полезным в учебном плане написание различных библиотек. В первую очередь оупенсорсных (пусть на правах какой-нибудь лицензии MFPL - My Friends Public License, т. е. только для своих друзей). Почему? Причин много:

  1. Код библиотеки надо читать. В идеале код должен быть самодокументированным (с помощью комментариев). На ранних этапах разработки другой документации у вас скорей не будет. Некоторые правда рекомендуют писать документацию вперёд проекта, но на самом деле это бред, так как в процессе разработки вы ещё сто раз всё поменяете, в итоге документация будет лишь лишней обузой. При этом, чтобы по вашим комментарием действительно можно было извлечь такую же информацию, как и из хорошей документации, необходимо руководствоваться некими формализованными правилами. В стандартный набор Java-разработчика (JDK) входит утилита javadoc, позволяющая из исходных файлов, прокомментированных по определённым правилам, генерировать файлы документации (в различных форматах). Для C++ существует множество различных утилит подобного рода, самая известная из них - Doxygen, нечто подобное ей (возможно, даже основанное на ней) должно войти в C++0x, про который я ни раз упоминал. Рекомендую начать юзать прямо сейчас. Doxygen научит вас писать комментарии по правилам!
  2. Архитектура библиотеки чрезвычайно важна. Любой изъян будет мешать вам при попытке использовать библиотеку, потому, если прикладным приложением, очень часто прощают те или иные ограничения, а зачастую и кривизну архитектуры, то для библиотеки - никогда!
  3. Ни одно приложение не тестируется так, как библиотека. Прикладное ПО можно тестировать, не думая о его коде, а затем сообщать в службу технической поддержки список багов. Для тестирования библиотеки обычно пишутся специальные приложения (различные системы тестирования) - приходится думать, что тестить. Если архитектура библиотеки или одного из её модулей слишком призрачна, то ответить на этот вопрос будет сложно.
  4. Наконец-таки оупен-сорсный проект увидите не только вы. Как минимум, стыдно просто говорить, что я написал библиотеку и она работает. То, что она работает с вашим конкретным демо-приложением (или двумя, тремя и т. д. вашими приложениями) ничего не означает. Ваши друзья захотят испробовать эту библиотеку. Если её юзанье не будет приятным, то...

Любой готовый ваш проект (если главной или одной из его целей была учебная) обязательно покажите более-менее опытному программисту, а лучше нескольким (всё-таки программисты тоже люди, а потому их мнение в каких-то вопросах может быть не объективным). Если таковых нету в зоне видимости, обращайтесь на форумы. Могу вам обещать (по крайней мере за Винград отвечаю), что вам всегда будут рады и что-нибудь да ответят. Вообще в учебном процессе программиста очень большое значение (я не преувеличиваю) имеет "програмитское" общение. Лучше, конечно, оффлайновое, просто по той причине, что оно быстрее и приятнее (обычно). Достоинство онлайна, в большой концентрации опытных программистов.

Кроме оценки готовых проектов не бойтесь обращаться к другим программистам с имеющимися проблемами. Лучше будет, если вы лишний раз спросите очевидное, чем научитесь (сами) программировать неграмотно. Хотя некоторые вещи всё-таки разумней решать ручками. Это я про чтение документации. Помните, что умение находить нужную информацию в хорошей документации не только большой плюс для программиста, но и одно из основных его качеств. Учитесь любить документацию - это важно. Заметьте, что сишный рантайм нами рассмотрен был далеко не весь. В том числе пропущены некоторые достаточно часто используемые вещи. Не были рассмотрены функции для классификации символов (буквы, цифры, регистр) из ctype.h, математические функции (math.h), функции для работы с блоками памяти (memory.h). Описать их несложно, но более полезно будет попрактиковаться в юзанье доков. Все эти функции не представляют новых идей - это лишь технический инструментарий. Ещё одна важная тема, которая не была нами рассмотрена - многопоточное программирование с использованием POSIX-функций. Оставим это сишникам. В плюсах создавать многопоточные приложения с использованием POSIX-функций бессмысленно. Разумней будет сразу изучать более высокоуровневые средства, например, объектно-ориентированную библиотеку Boost.Thread, нечто основанное на ней должно войти в C++0x.

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

  • src, sources - сорсы, т. е. исходники библиотеки.
  • inc, include - заголовки, которые вам придётся инклюдить в своих проектах.
  • bin, binaries - бинарники, т. е. собранная библиотека. Папка может быть пустой (собирайте библу мол сами).
  • lib - файлы для прилинковки DLL или другие статические lib-файлы, часто находятся в предыдущей папке.
  • doc, docs - документация. Часто в этой папке расположено несколько папок по форматам документации (html, pdf, man, chm и т. д.).
  • samples, demos - демонстрационные приложения.
  • tools - различные утилиты, связанные с библиотекой.

Кроме того в корне обычно расположен текстовый файл (не обязательно с расширением txt), с именем вроде README или BUILD. В нём описывается как построить (скомпилировать и собрать библиотеку). В некоторых библиотеках могут быть просто проекты для VS, но это бывает редко. Обычно вам придётся работать с командной строки (в VS для неё есть специальный ярлык, прописывающий все каталоги). Сгенерировав с помощью специальной утилиты (описанной в текстовике) мэйкфайл, пишем make (в VC++ - nmake). Ждём. Бинарники обычно ложатся в нужную папку, если есть DLL-ки, то рекомендую скопировать их в %SystemRoot%\Sustem32. Наконец-таки пропишите каталоги с либами, инклюд-файлами и сорсами (последнее - для дебагера). За подробностями обращайтесь к докам вашего компилера и IDE, как это сделать в MS-шной студии я уже говорил. Для большинства компилеров потребуется прописать переменные окружения INCLUDE и LIB. В винде это делается с помощью апплета панели управления System (Система). На вкладке Advansed (Дополнительно) щёлкните кнопку Environment Variables (Переменные среды). Дальше, думаю, разберётесь. Очень часто удобней сделать по другому - создать в соответствующих стандартных каталогах компилера подкаталоги с именем библиотеки, а туда копирнуть нужные файлы (заголовки, либы и др.). Тогда не забывайте включать имя библы в пути: #include <MyLib/MyHeader.h>.

Помните также, что в структуре библиотеки также могут быть сабфолдеры (MyLib/Part1/Header1.h). Ещё одно замечание - при проектировании библиотеки важно сразу определиться с именами заголовков. Почему? Очень просто: допустим вы (или кто-то ещё) заюзали библу в некоторой проге, затем переписали структуру хейдеров библы и решили компильнуть свою прогу... Весёлый финал. Для сорсов же, а также для небиблиотечных хейдеров, напротив, нужно не бояться изменить структуру каталогов и имена файлов. Довольно типична проблема двойного инклюда некоторого файла. Скажем, файлы x.h и y.h инклюдят a.h, в main.cpp мы заинклюдиваем оба хейдера (x и y), в итоге - двойной инклюд a.h, а значит двойное объявление имён из него, что недопустимо (по правилам C++). Возможно, в C++0x появится конструкция инклюд на уровне языка (сам Бьярни Строуструп, автор C++, предлагал однажды такое, но комитет не принял), способная проверять двойное включение. Ныне же это дело работает на уровне тупого, но мощного препроцессора. VC++ предлагает нам прагму once (#pragma once). Эта директива, помещённая вначале хейдера заставит компилер заботится о контроле двойного вкючения заголовка при компиляции одного модуля. Её также поддерживает виндовый интеловский компилер (линевый, воде, нет). Я бы рекомендовал отказаться от неё. Есть классический трюк с использованием препроцессора. Скажем MyHeader.h может выглядеть так:

#ifndef MY_HEADER_H_INCLUDED	// при первом инклюде это макро не определено, значит
                                // директива срабатывает (препроцессор не выкидывает текст ниже)
#define MY_HEADER_H_INCLUDED	// отныне (а значит и при втором инклюде) макро определено

// всякие объявления

#endif	// MY_HEADER_H_INCLUDED

Последний комментарий принято вставлять для понятности к чему относится endif. Не забывайте, что по стандарту, точка входа - функция main. Кстати для неё действуют несколько иные правила, чем для других функций - она может не возвращать значения (хотя и пишем int main). Чтобы ретурн не мозолил глаза (это на самом деле главная цель этого правила). Дефолтовый код возврата - 0. Так что если ваша программа завершилась нормально, можете ничего не возвращать, вам простят (хотя, если вернёте - хуже не будет). К слову, в неплюсанутом нашем любимом языке (чистый Си) такого нет, а жаль.

Помните, что надо уметь переписывать код, если вы видите, что это необходимо. Помните, что большее значение имеет общая модель вашего проекта, чем конкретная реализация незначительных подзадач. Есть два важных понятие, связанных с проектированием: восходящее и нисходящее проектирование. Оба они идеализированы и на практике в чистом виде не применимы, хотя некоторые теоретики любят усиленно пропагандировать тот или иной способ. При восходящем проектировании вначале решаются мелкие подзадачи, "кирпичики" большого проекта. Разработку компиляторов тоже можно отнести сюда, так как они тоже воплощают инструментарий для дальнейшей работы. С точки зрения структурного программирования, основанного на подпрограммах, восходящее проектирование предусматривает написание множества подпрограмм (возможно, ненужных), а потом - самой программы. Идеальное восходящее проектирование невозможно по простой причине - невозможности заранее предсказать какие именно кирпичики нам понадобятся без продумывания общего плана проекта. Многие библиотеки включают в себя средства, которые нужно чрезвычайно редко - это принцип восходящего проектирования. При использовании методов нисходящего проектирования вначале разрабатывают общий план проекта, не задумываясь о деталях. То есть вначале пишется простая программа, использующая ещё ненаписанные подпрограммы (или их заглушки), а затем реализуются "кирпичики". Очевидно, что данный метод также не воплотим в полной мере: невозможно сделать идеальные заглушки, позволяющие проверить всю логику программы, невозможно связать кирпичики воедино, не зная, что точно будут они из себя представлять. При нормальном подходе следует сочетать оба метода, причём именно перелетать их, а не пользоваться то одним, то другим. Вы продумываете модель проекта, ориентируюсь на некоторые предположения реализации. Но в любое время какие-то моменты вы можете реализовать по-новому. А может и наоборот - вы оставляете удачную реализацию какой-то детали, но используете её в новой модели. Проектирование - это метод проб и ошибок, точного подхода не может существовать в принципе. Именно поэтому надо быть готовым в любой момент переписать какую-то часть кода или использовать существующую в новом контексте. А для этого надо хорошо и грамотно всё комментировать и писать код как можно естественней.

В связи с этим важно обсудить различные трюки и хитрости в программировании. Они появились очень давно. Вначале они были вызваны лишь естественным желанием выжить в жестоких условиях нехватки памяти и производительности процессора. Вполне реальным тогда представлялось во имя оптимизации переписать кусок кода проекта на ассемблере. Сейчас это невозможно по двум причинам. Первая - это объём кода современных программ, даже читать, а тем более писать его весь вручную очень тяжело. Вторая причина важнее - это бесполезность оптимизации на уровне ассемблера. Современные компиляторы реализуют достаточно сложные механизмы по оптимизации кода. Поверьте, лучше нормального компилятора вы не сможете сделать более менее приличный (по размеру) код. В то же время автоматическая оптимизация ассемблерного кода хотя и возможна, но не продуктивна. Гораздо разумнее объяснить компилятору на высокоуровневом языке более абстрактно то, что вы хотите сделать, а затем довериться ему. Компиляторы хорошо знают процессоры, на которые они рассчитаны, его команды и двоичную арифметику. И если раньше трюки с внутренним представлением числа (вроде умножения/деления на числа, являющиеся степенями двойки, путём побитового сдвига или проверки чётности побитовым AND с единицей) были красивой нормой, то сейчас от этого разумно отказаться ради читабельности. Мои тесты различных компилеров показали, что все компилеры уже нетакие глупые как десять лет назад. К слову VC++ под виндой показал превосходную оптимизацию (интеловский компилер не считаем из-за игры на своём поле). Из этого не следует, что надо забить на оптимизацию. Просто оптимизация стала несколько другая, чем раньше. Оптимизируйте логику работы приложения в целом, а также технологию функционирования отдельных модулей. Но не беритесь за оптимизацию отдельных команд. Исключение составляют операции, связанные с взаимодействием со внешним миром. В первую очередь это вызов функций. Не допускайте очевидных глупостей вроде передачи больших блоков данных по значению вместо константной ссылки. Почему компилятор не сможет это оптимизировать? Очень просто - компилятор не способен предсказать всех мест откуда может быть вызвана ваша функция. В частности она может быть вызвана из другого модуля (файла cpp), напомню, что компиляция модулей идёт независимо друг от друга. Это возможно и в том случае, если в хейдере нету определения функции. Напомню, что его запросто можно написать в любом модуле, хотя такой стиль порождает ошибки. Раз компилятор не знает, где будет вызываться функция, он не может сказать и как она будет вызываться (как передавать и принимать параметры). Единственный выход отсюда - объявления функций. И загрузка, и приём параметров должны строго соответствовать этому объявлению.

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

  1. Повышение производительности кода. Про это в основном я уже сказал, но следует добавить, что существуют некоторые хитрости, связанные с общей логикой работы каких-либо модулей. Ими пренебрегать не стоит.
  2. Уменьшение размера кода, т. е. более короткая запись каких-либо вещей.
  3. Повышение модифицируемости кода, т. е. лёгкости дальнейшего добавления или изменения функциональности. Это одна из важнейших задач проектирования.

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

bool f; // флаг
// Здесь какие-то действия
if (f == false) // !!!
// ...

За такой код надо жестоко убивать! По крайней мере для людей, пишущих книги по C++, написание подобного явно не допустимо. Также многие забывают о возможности неявного приведения любого указателя к типу bool и начинают явно проверять его на 0. Правда, стоит заметить, что проверка на равенство 0 некоторым нравится больше, чем логическое отрицание указателя. Тут уж вам решать. Но что касается неравенства нулю (что встречается чаще) - для этого и сделано неявное приведение.

Также многие не любят присваивание внутри if-а. На самом деле зачастую это более удобно, хотя надо смотреть на конкретный код. Стоит заметить, что подобные строки до ужаса стандартны, и не зря:

if (f = fopen(name, "r"))
{
    // чтение данных
    fclose(f);
}
else
{
    // выдача сообщения об ошибке, возможно exit, сохранение лога и т. д.
}

Заметьте также, что в операторе if можно использовать объявление вместо выражения:

if (FILE* f = fopen(name, "r"))
{
    // ...
}
else
{
    // ...
}

Самое приятное при этом, что переменная f определена только внутри if-а (включая ветвь else).

Ещё одна типичная черта программистов, привыкших к C или Паскалю - это желание поместить все объявления переменных вначале функции. Ни в коем случае так не делайте! Хорошо выделять в логические блоки (отделяя их комментарием, говорящим о назначении блока, и пустой строкой) объявления переменных, но ни в коем случае не всей функции! Также следствием непонимания языка являются вопросы вроде: "Как мне получить код символа?". Вследствие этого некоторые долго думают над типичной строкой вроде c-'0', где c имеет тип char. Подобные "хитрости" хитростями на самом деле являются только при плохом знании языка.

Также часто не понимают сути операторов инкремента и декремента. В итоге записи воде *p++ многих пугают (p - указатель), особенно когда они используются как lvalue.

Иногда правда стремясь показать своё "превосходное" знание синтаксиса языка (но не его идеологии) пытаются вместить весь код чуть ли не в одну строку или же в один оператор (с девизом "запятые рулят!"). Наибольшие споры вызывает оператор for. Зная возможность записи в этом операторе всего цикла, а особенно после знакомства с оператором "запятая", многие пытаются в этот оператор писать всё. При этом на чтение столь "профессионального" кода уходит достаточно много времени. На самом деле синтаксис for предназначен, чтобы воплотить в себе именно типичные задачи: подготовка цикла, условие выхода и итерационный шаг. Это позволяет смотреть на цикл, как на единое целое. Новички думают, что в большинстве случаев инициализация - это задание нижней границы, условие - <= или >=, а шаг - инкремент или декремент. На самом деле это верно только для итераций по массивам. Скажем, итерация по списку может выглядеть так (я использую имена из урока про списки):

for (ListNode* p = head; p; p = p->next())
// ...

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

for (MyIterator it = getStart(collection); isFinish(it); moveNext(it))
// ...

Хотя обычно для реализации концепции итераторов используются классы. Мы ещё вернёмся к этой концепции при изучении ООП и стандартной библиотеки C++.

Почему начинающие не хотят использовать многие возможности языка? Потому что они не видя в них смысла. Легко понять технические вещи: математические операции, различные функции, ветвления, циклы и т. д. Гораздо сложнее перейти на новый уровень абстракции - понять вещи без которых можно обойтись. А ведь именно понимание более сложных уровней абстракции составляет сущность современного программирования. Многие думают, что надо программировать для компьютера, и начинают думать якобы как компьютер. Глупость! Языки программирования придуманы людями для этих самых людяй, то есть людей. Для связи с компьютером используется компилятор, но не язык программирования. Да и вообще, компьютер думает так, как вы ему скажите. Были люди, которые не могли понять что такое массивы. Почему? Потому что это уже небольшая абстракция. Почему-то указатели, адресация с использованием сдвига - это понимали, а вот массивы... А уж структуры и перечисления (енумы) новички вообще ненавидят. Вместо первых стремятся использовать просто независимые переменные, а вместо вторых - целые числа. Простой пример - хранение данных о состоянии клетки шахматной доски. Некоторые используют для этого unsigned char (0 - пустая клетка, положительные числа - белые, отрицательные - чёрные), аргументируя это малым объёмом памяти. Глупость! Излишняя компактность данных приводит к потерям на времени обработки, а это обычно более важно. А читабельность и грамотность кода ещё важнее! Если нет - асм вам в руки и поехали. Когда (надо сказать, что нескоро) доделаете свои сверхоптимизированные шахматы, вы вдруг увидите, что у процов появилась новая фича. Вы надолго засядете за компьютером, пытаясь осуществить новый виток оптимизации. Ваш приятель, пишущий на C++, достанет свежий компилер, пару минут и... да здравствуют плюсы! Пусть наигранно, но суть остаётся та же - модифицируемость, портируемость и адаптируемость важнее всего.

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

enum FigureType
{
    // ...
};

struct Figure
{
    bool ourFigure;
    FigureType type;
};

struct Field
{
    bool empty;
    Figure figure;
};

Можно сформулировать несколько правил, основанных на типичных ошибках:

  1. Целочисленные типы используются только для целых чисел в математическом смысле. Иначе - либо логические переменные, либо енумы, а иногда что-то более сложное (вроде предыдущего примера).
  2. Если несколько нелокальных переменных объединены логически, то следует юзать структуры.
  3. Больше 8 параметров у функции вызывают подозрение (из-за плохой читабельности её вызовов). Больше 12 - карается годом работы на Windows 95. Юзаем структуры: и красиво, и память экономится при вызове функции (если структуру передаём по ссылке).
  4. Больше 7 более-менее приличных кейсов в свитче заставляют задуматься. Для однострочных кейсов лимит расширяется до 16, в некоторых случаях и больше. Иногда даже есть смысл использовать if - else if (вместо свитча), хотя редко. Помните, что кейс всегда можно (хотя не всегда нужно) сделать однострочным, вынеся его тело в отдельную функцию.
  5. Пихать указатели в юнионы (объединения) небезопасно (из-за возможности случайного затирания с потерей указываемого блока памяти). Лучше уж структуры (вместо юнионов). В первую очередь это касается строк.

Все эти правила рекомендательны и выведены опытным путём. Что ж, успехов вам!


Практикум

Настала пора усесться за свою IDE или текстовый редактор + компилер. Вам предстоит решать задачи. Для удобства я их разбил на несколько разделов. Не обязательно решать всё, но... не помешает! Удачи вам!

Алгоритмы

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

  1. Реализуйте различные виды сортировок для односвязных и двусвязных списков. Попробуйте для перестановок использовать два способа: переприсваивание (через промежуточную переменную) и манипуляции со связывающими указателями (будьте внимательны!). Желательно всё реализовать в одной программе.
  2. Продемонстрируйте работу алгоритма, называемого "решетом Эратосфена". Это достаточно простой и наглый способ поиска простых чисел в диапазоне от 2 до заданного n. Необходимо выписать все числа от 2 до n. Вычёркиваем, начиная с 4, числа через 1 (2-1=1). Делаем такое же вычёркивание для тройки - после тройки через 2 (3-1=2). Для четвёрки ничего не делаем, так как она сам уже вычеркнута. Переходим к пятёрке и т. д. Невычеркнутые числа - простые. Алгоритм напоминает постепенное отсеивание числе через решето, потому так и назван (в название также попал его изобретатель).
  3. Реализуйте инструментарий для работы с бинарным деревом поиска. При вставке сразу определяйте нужную позицию. Позаботьтесь также о корректном удалении. Можете заняться оптимизацией (выравниванием) дерева. При это полезно будет хранить в узле некоторые дополнительные данные, а также, возможно, указатель на родительский узел. Не бойтесь перекидывать с помощью указателей целые ветви, но будьте аккуратны.
  4. Напишите программу, решающую системы линейных уравнений (количество уравнений совпадает с количеством неизвестных и определяется в рантайме) матричным методом. Напомню, что любая система линейных уравнений записывается как A*x = B. Где A - матрица коэффициентов при неизвестных, x - столбец из неизвестных, а B - столбец свободных членов. Результирующий столбец вычисляется так: x = A-1*B. Заодно вспомните матричную алгебру! Тоже полезно. На всякий случай в текстовике к уроку даю вам краткий справочник.

Библиотеки

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

  1. Для пары задач предыдущего раздела (желательно двух последних) напишите решение с использованием библиотек.
  2. Создайте усовершенствованную библиотеку консольного интерфейса. В неё могут войти: каскадные меню, хоткеи в меню, глобальные хоткеи, диалоги ввода стандартных типов данных (например, функция int InputInt(const char* message = 0)), диалоги алертов (мессаджбоксов) и т. д.
  3. Напишите библиотеку, обрабатывающую аргументы командной строки. Существуют различны стили их задания (например, UNIX-стиль или стиль MS-DOS), я рекомендую воспользоваться правилами, основанными на классическом стиле:
    1. Все ключи начинаются с тире и могут иметь два варианта: полный и сокращённый (например, -time и -t).
    2. Существуют ключи-флаги. Важно лишь их наличие или отсутствие.
    3. Опции могут иметь значения следующих типов: целый, вещественный, строковый, массив строк (несколько аргументов, например, список файлов).
    4. Опции могут иметь предустановленные значения, задаваемые специальным ключом (например, -now означает -time 0).
    5. Все новые значения переписывают предыдущие, но для массивов строк может действовать правило добавления.
    6. Может быть введён один безымянный параметр. Обычно это имя входного файла.
    Следует учитывать возможность некорректной командной строки - это не катастрофа, просто не принимайте во внимание всякий бред. Не забывайте проверять argc и помните, что argv[0] - это имя исполняемого файла. Очень тщательно в этой задаче следует отнестись к структурам данных. В частности, будут полезны отображения, т. е. коллекции, элементы которых представляют из себя пары "ключ - значение". При этом должен быть оптимизирован поиск по ключу.
  4. Реализуйте библиотеку для работы с регулярными выражениями, т. е. строками, являющимися шаблонами других строк (формируются с использованием управляющих символов). В народе они называются регэкспами от английского слова regexp (от regular expression). Для каждого шаблона можно указать с какого и до какого символа ему соответствует заданная строка (если соответствует вообще). Такую подстроку называют капчуренной строкой (от слова capture). В файлах к уроку приведен примеры шаблонов, их описаний и (иногда) капчуримых строк, примеры помогут вам понять точнее некоторые моменты. Существуют различные синтаксисы регэкспов. Я предлагаю вам усечённый вариант популярного синтаксиса, используемого в языке Perl:
    • Просто символ (неспециальный) означает самого себя.
    • Символ, экранированный бекслешем, означает самого себя, за исключением управляющих символов, указанных ниже, и специальных символов: \r, \n, \t и некоторых других (предусмотрите возможность их лёгкого расширения).
    • . - любой символ.
    • \d - цифра.
    • \D - не цифра.
    • \s - пробел.
    • \S - не пробел.
    • \w - буква или цифра.
    • \W - не буква и не цифра.
    • [xyz] 0 любое из a, b и c.
    • [^abc] - не a, не b и не c.
    • [a-b] - символ в диапазоне от a до b.
    • E? - одно или ноль вхождений E.
    • E+ - одно или больше вхождений E.
    • E* - 0 или больше вхождений E.
    • E{n} - ровно n вхождений E (по дефолту для всех символов используется {1}).
    • E{n,} - n или больше вхождений E.
    • E{,m} - m или меньше (до 0) вхождений E.
    • E{n,m} - от n до m вхождений E.
    • ^ - означает начало строки. Например, шаблону abc соответствуют и abc, и xxxabc, и abx (с - любой символ), а шаблону ^abc строка xxxabc не подходит.
    • $ - конец строки.
    • \b - граница слова (пробел, знак пунктуации, начало или конец строки).
    • \B - не граница слова.
    • Круглые скобки используются для группировки в выражениях (см. примеры).
    • | - варианты (или).
    • (?=E) - вхождение указанной подстроки E (эта подстрока не капчурется).
    • (?!E) - невхождение указанной подстроки E.
    Вам следует реализовать функции для:
    • Проверку строки на соответствие шаблону.
    • Поиск номеров начала и конца подстроки., соответствующей шаблону (нумерация - с 0). Удобно вернуть это дело структурой.
    • Подсчёт количества подстрок, соответствующих шаблону.
    • Замена первого, последнего или всех вхождений подстрок, соответствующих шаблону, на заданную строку.

Утилиты

Эти задачи представляют из себя утилиты, работающие с командной строки. Они получают на вход некоторые параметры (обычно среди них имя обрабатываемого файла), производят некоторые действия без пауз (ожидания ввода) и завершают свою работу, при ошибке код возврата (ретурн в мейн-функции) должен отличаться от 0. Можно дополнительно использовать диагностический вывод на stdout. Для приёма параметров командной строки желательно использовать библиотеку из третьего упражнения прошлого раздела. Каждой утилите я даю название. На основе него можете дать имя экзешнику. В VS это делается в свойствах проекта на закладке Linker => General (поле Output File). С тем же именем можете создать конфигурационный файл (с расширением config) хранящий некоторые настройки (если таковые нужны). Не забудьте дефолтные настройки при отсутствии конфиг-файла, а также возможность его игнорирования с помощью параметра командной строки (вроде -no-config). Помните, что в конфиг-файле хранятся довольно глобальные настройки, прочие передаются через параметры командной строки.

  1. File Splitter. Утилита позволяет разбить один файл на несколько частей, а также затем собрать его из этих частей. Имена этих частей удобно сделать автогенерируемыми. Например, файл f.bin будет разбит на f.bin.001, f.bin.002 и т. д. Или же на f.bin.part1, f.bin.part2 и т. д. При сборке исходного файла указывается только базовое имя и параметр, указывающий что нам надо объединять файлы (вроде -merge). Файлы ищутся автоматически. Неплохо бы в первом файле сохранить общее количество частей для простейшего контроля целостности (не забыли ли одну из частей). Следует предусмотреть возможность задания (через аргументы командной строки) размера одной части (для последней части он вероятно будет меньше), а также предустановленные значения для этого параметра (размеры типичных дискет, ZIP-дисков, лазеров и др.), их список можно хранить в конфиг-файле. Не забудьте ограничить размер буфера при чтении/записи файла, так как при разбивке для обычных сидюшников отнимать от оперативки 700 мегабайт - это жестоко.
  2. Text Merger. Утилита объединяет два текстовых файла в один, считая их оба преобразованными версиями некоторого исходного файла. Например, в файле main.cpp была большая программы. Один человек работал над функцией f1, а другой - над f2. В итоге надо получить файл, отражающий работу обоих программистов. Файлы должны обрабатываться построчно, максимальную длину строки можно передать в параметрах (по дефолту, скажем 80). Следует учитывать, что некоторые строки одного файла могут совсем отсутствовать в другом (даже модифицированные). Подумайте над алгоритмом определения "похожести" строк (идеальный алгоритм в данном случае, конечно, не возможен). При возникновении конфликтов (одно место модифицировано в обоих файлах) выберите один из шести вариантов: воспользоваться версией из первого/второго файла (2 варианта), записать данные в начале из одного затем из другого файла (2 варианта в зависимости от порядка файлов), не писать ничего, вывалиться с эррором. Вариант задаётся из командной строки (по дефолту, пожалуй, эррор). Для каждой конфликтной ситуации можно вывести лог в stdout.
  3. HTML Stripper. HTML - язык разметки, с помощью которого создаются веб-странички. Разметка задаётся с помощью тегов, т. е. конструкций, заключённых в угловые скобки (символы больше/меньше). Существуют непарные теги (например, <br>), открывающие (например <p>) и закрывающие (например, </p>). Последние, как вы, наверно, заметили, выделяются с помощью слеша. Незакрывающие теги могут иметь атрибуты (например, <p align="justify">). Иногда закрывающие теги можно опускать, если их наличие очевидно из других открывающих тегов. Знающие HTML могут учитывать эту возможность, остальные могут на это забить. Вообще в HTML можно многое опускать, но мы будем считать (для простоты), что страничка сделана без этих наворотов (хотя вы можете предусмотреть возможность их наличия). Далее, называя тег, я не пишу угловые скобки, это общепринятый синтаксис. При это, говоря о парных тегах, подразумевают открывающий и (позже) закрывающий теги. Помните также, что регистр тегов не имеет значения. Замечу также, что лишние пробелы (более одного), перевод строки и табуляция также игнорируются, если это дело не заключено в праный тег pre. Тело страницы заворачивается в парный тег body, а служебная информация - в head. Служебная информация обязательно должна быть перед телом. Из служебной информации нас интересует только заголовок, завёрнутый в парный тег title. Некоторые специальные символы (например, больше/меньше) в HTML записываются с использованием с помощью амперсанда. Признак конца специального символа 0 точка с запятой (например, &lt; - меньше). Неполный список специальных символов приведён в файлах к этому уроку. Предусмотрите возможность лёгкого его расширения за счёт конфиг-файла. Утилита, которую вам предстоит написать, должна из веб-страницы (HTML-файла) получать текстовый его вариант. Это включает в себя:
    • Удаление всех тегов.
    • Разметку параграфов, заключённых в парные теги p (с помощью перевода строки), и разрывов строки (тег br).
    • Запись заголовка в самом начале в различных форматах (с использованием звёздочек, тире и т. д.).
    • Запись таблиц с использованием указанных (с помощью параметров) разделителей между строками и ячейками. Таблица заключается в тег table, строки - tr, ячейки - td.
    • Запись специальных символов.
  4. UUE Encoder. Метод UUE-кодирования позволяет записать любые данные в формате, приемлемом для передачи по электронной почте. Этот формат позволяет наличие байт со значениями только в диапазоне от 32 до 127. Входной поток интерпретируется как последовательность битов, которая разбивается на группы по 6 битов. Десятичное значение каждой группы находится в диапазоне от 0 до 31. К нему прибавляется число 32, а нули заменяются на 96 (это связано с особенностями работы некоторых почтовых клиентов). Полученная цепочка чисел представляет собой символьный UUE-код (естественно, большой длины, чем двоичный код исходного файла). Существует также специальный общепринятый формат файлов - uue (расширение). В простейшем случае он состоит из трёх строк. В первой пишется begin 644 имя_исходного_файла (число 644 связано с никсовыми правами доступа, виндовые приложения его игнорируют). Во второй - UUE-код (заметьте, что в нём разрывов строк не может быть из-за диапазона символов). В третьей - end. После каждой строки обязательно ставится символ новой строки (\n). Интересно заметить, что полученный uue-файл WinZip воспринимает как архив и в состоянии "распаковать" его. Такой вот архив, который больше исходного файла! Что ж напишите такой "архиватор".

Тесты

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

  1. Найдите и объясните ошибки:
        typedef float* float_ptr;
    #define double_ptr double* ;

    float f;
    double d;

    float_ptr f1;
    float_ptr f2 = 0;
    float_ptr f3, f4;
    float_ptr f5, f6 = 0;
    float_ptr f7, f8 = &f;

    double_ptr d1;
    double_ptr d2 = 0;
    double_ptr d3, d4;
    double_ptr d5, d6 = 0;
    double_ptr d7, d8 = &d;
  2. Найдите и объясните ошибки:
        enum E1 {a1, b1, c1};
    enum {a2, b2, c2};
    enum E3 {a3 = 0, b3 = 2, c3 = 1};
    enum E4 {a4 = 0x01, b4, c4};
    enum E5 {a5 = 0x00C0, b5, c5 = 5};
    enum E6 {a6 = 0, b6, c6 = 1};
    enum E7 {a7 = 7, b6 = 6, c5 = 5};
    enum E8 {};
    enum {};
    enum;
  3. Найдите и объясните ошибки:
        #include <stdio.h>

    void f(int param)
    {
        if (param == 0)
            throw 5;
        else if (param > 0)
            throw "+++";
        else
            throw;
    }

    int main()
    {
        try
        {
            int a;
            scanf("%i", &a);
            f(a);
        }
        catch (int i)
        {
            printf("int = %i\n", i);
        }
        catch (const char*)
        {
            printf("string\n");
        }
        catch
        {
            printf("void\n");
        }
    }
  4. Что выведет следующая программа:
        #include <stdio.h>
    #include <string.h>

    int main()
    {
        char s[10];
        strcpy(s, "123456789");
        char* p = strchr(s, '5');
        *p++ = 0;
        *p = '0';

        int a = 5;
        printf("%s\n%s\n", (a = 1) ? s : p, (a != 1) ? p : s);

        return 0;
    }

Олимпиадные задачи

В этом разделе приводятся некоторые олимпиадные задачи по программированию. Решать их достаточно интересно, требуются какие-либо незаурядные идеи. Но, надо сказать, что ни технику, ни искусство программирования они не развивают. Потому решать их совсем не обязательно. Хотя, конечно, интересно. Не привести подобные задачи было бы с моей стороны просто нагло. Что ж дерзайте! Классика олимпиадных задач требуют, чтобы входные данные размещались в текстовый файл input.txt, а выходные - в output.txt. Код возврата программы - только 0. Все сообщения об ошибках пишутся в выходной файл. Обычно также даются конкретные критерии на формат файлов, но я оставляю это на ваше усмотрение.

  1. Упорядочить по невозрастанию пять целых чисел за семь операций сравнения.
  2. На входе программе даются два числа N и P. Программа на выходе должна дать такое максимальное число M, что N! делится на P в степени M, но не делится на P в степени M+1. Следует учитывать, что N и P так велики, что нет смысла считать значение N! (из-за возможности переполнения).
  3. На плоскости заданы по двум точкам N прямых (даются декартовы координаты точек). Определить насколько частей эти прямые разбивают плоскость. Например, две прямые могут разбить её на 4 части (пересекающиеся), на 3 (параллельные) или 2 (совпадающие).

Творческие проекты

Эти задачи предназначены для того, чтобы вы почувствовали прелесть настоящего девелопмента, коряво переводимого как "разработка". Конкретно детали задания вам предстоит подумать самим, могу лишь сказать, что чем больше сделаете, тем лучше. Хотя не переборщите, желательно чтобы этот проект не превратился в коммерческий пакет. А если так и станет, не забудьте дать мне лицензию со скидкой [image]! Тщательно отнеситесь к проектированию. Обязательно предусмотрите элементы, позволяющие легко развивать проект в дальнейшем. Не забудьте, что проект не может располагаться в одном файле :). Также призываю вас использовать библиотеку консольного интерфейса из упражнения выше. Неплохо бы, чтобы программы имели бы конфиг-файл и диалог для изменения настроек. Замечу, что реализовывать все проекты не обязательно - хватит только одного (хотя остальные лишними не будут).

  1. Файловый менеджер. Все подробности - придумывайте сами. Могу предложить для простоты показывать в одно время только одну панель (если вы будете использовать классическую двухпанельную модель), а при нажатии таба - показывать другую. Очень хорошо, если вы реализуете поиск и командную строку (про system не забыли ;)).
  2. СУБД. Может быть громко сказано, но всё же. Можете использовать предопределённую структуру. Варианты: "список книг в библиотеке", "список сотрудников крупной фирмы", "список товаров супермаркета" и т. д. Чем больше будет полей - тем лучше. Реализуйте поиск по каждому полю, а также поиск по регэкспу (для строк) или диапазону (для чисел). Не забудьте про вставку/удаление записей. Если сделаете динамическую структуру (с использованием универсальных типов) - большой плюс.
  3. Коллекция игр. Например, "Морской бой", "Сапёр", "Домино". Для указания выбранного элемента на клеточных досках можете указывать слева и вверху по специальному символу, для небольшого поля пересечение легко определить визуально.

Что ж мне остаётся только пожелать вам успехов и бессонных ночей!

Файлы

Инфа по матричной алгебре
Примеры регэкспов
Специальные символы HTML


Идеи программирования современного C++

Идеи структурного программирования были просты и естественны, они идеально подходили для построения и описания различных алгоритмов. Языки структурного программирования назвали в своё время языками высокого уровня, в противоположность языкам низкого уровня: ассемблеру и машинным кодам. Прошли года, задачи проектирования простыми алгоритмами стало описывать не только сложно, но и глупо. Стали появляться специфические языки: Lisp, Cobol... Наконец таки появился первый объектно-ориентированный язык - Simula. Этот язык ставил своей целью проектирование, а не алгоритмизацию. Современные языки эффективно сочетают в себе различные концепции программирования, особенно это относится к C++. Начальные идеи ООП будут рассмотрены в этой части

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

Мы также изучим в этой части некоторые другие идиомы программирования плюсов: перегрузку и шаблоны. Именно они позволяют C++ очень эффективно, без введения новых языковых средств (на уровне библиотек) поддерживать идиомы других языков программирования. В частности ФОП - функционально-ориентированное программирование. Никакие дженерики из явы или дотнэта не могут даже тягаться с шаблонами плюсов. Приплюснутые темплейты (template - шаблон) реализуют идиомы так называемого обобщённого программирование. Главной языковой линией развития C++ практически официально считается "Generic and generative programming". Ява и шарп курят в сторонке! В C++0x обобщённое программирование получает дальнейшее развитие - появляется термин "концепции", а также языковые средства для работы с ними. Эту тему мы, к сожалению, рассмотреть не можем в силу отсутствия окончательного официального решения о её реализации.

В общем после этой части вы должны понять в целом что такое современный язык C++. О его современных библиотеках мы поговорим позже.


Работа с препроцессором

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

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

Во-первых, препроцессор реализует работу с модулями с помощью хорошо знакомой вам директивы include. Как вы помните, инклюдимый файл ищется отсносительно каталогов, указанных в переменной окружения INCLUDE, если же он указан в кавычках (а не угловых скобках), то он дополнительно вначале ищется относительно текущего каталога. Файл не обязан находится в самом каталоге - можно использовать две точки для перехода в родительский каталог и слеш как разделитель между каталогами. В винде можно юзать бекслеш, но зачем? Ответ - нафиг надо. Директива инклюда коварна. Вспомните, например, двойные инклюды. Ещё одна загвоздка - даже если ошибка была в инклюдимом файле компилятор укажет на cpp-шник (или иной сорс-файл), т. к. обработку инклюда ведёт препроцессор, а компилер получает итоговый обработанный сорс-файл. Однажды Бьярни Строуструп предлагал сделать инструкцию языка (не директиву препроцессора) include, заботящуюся о дабл-инклюде, а также позволяющую просматривать истинный источник ошибки. К сожалению, тогда комитет стандартизации не принял этого предложения. Лично мне хотелось бы видеть инклюд конструкцией языка. Вдобавок я добавил к ней возможность автовключения в неймспейсы, о том что это такое вы узнаете в следующих уроках.

Следующая задача препроцессора - константы. В отличии от переменных с описателем const, задефайненные константы просто подставляются в нужные места ещё до компиляции, во время выполнения они в памяти не хранятся. Для больших констант это становится недостатком - каждый раз загружать в память огромную строку менее приятно, чем просто постоянно хранить её. Если у вас конечно объём памяти не равен 16 килобайтам... А теперь самое весёлое - консты в хороших компилерах умные. Если значение конст-переменной легко сгенерировать, то компилер не будет хранить его в памяти, т. е. сгенерирует код, который не хранит эту переменную постоянно, а просто синтезирует в нужный момент её значение. Есть правда ещё один тип констант - константы для условной компиляции. Например, VC++ определяет макро _MSC_VER со значением равным версии компилера. Она записывается в виде четырёхзначного числа, первые две - версия, оставшиеся - подверсия. Замечу, что версия отличается от версии VC++ на 6 (это связано со старыми MS-шными компилерами, не имеющими приставки Visual, например Quick C). Таким образом компилер 2005-ой студии (VC++ 8.0) определяет это макро как 1400. Подобные константы используются препроцессором и потому говорить об их замене на конст-переменные не приходится.

Самая естественная ниша для препроцессора - это условная компиляция. Посмотрите на пример (как всегда не забудьте прочитать коментарии):

#if _MSC_VER >= 1310
 // код только для VC++ 7.1 и выше
#else
 // код для остальных компилеров
#endif

#if defined(_UNICODE)
 // используем препроцессорную функцию-оператор defined для проверки объявления макро
 // здесь юникодовский код
#elseif _MSC_VER >= 1400
 // неюникодовский код для VC++ 8.0 и выше
#else
 // неюникодовский код для остальных компилеров
#endif

#if !defined(_UNICODE)
 // неюникодовский код
#endif

#ifdef _UNICODE // аналогично defined
 // ...
#endif

#ifndef _UNICODE // аналогично !defined
 // ...
#endif

Заметьте, что в связи с условной компиляцией часто дефайнят препроцессорные макро без значения. Важен лишь факт их объявления. Удобно при этом объявлять (или нет) их через командную строку компилера. В VS это можно сделать в свойствах проекта на закладке C/C++ => Preprocessor (поле Preprocessor Definitions).

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

#define MAX(a,b) ((a>b)?a:b)

Во-первых, обратите внимание на скобки. Помните, что препроцессорное выражение будет целиком подставлено в нужное место, допустим мы напишем такой код:

printf("max = %i", MAX(1|5, 6));

Без использование скобок прпроцессор сгенерирует из этого:

printf("max = %i", (1|5>6)?1|5:6);

Согласно порядку операций вначале будут сравнены 5 и 6, полученное bool-значение будет преобразовано в int, а затем будет выполнено побитовое или с единецей. Понятно, что мы хотели совсем другое. Поэтому, чтобы не думать о всех возможных ситуациях лучше ни играть с препроцессором, а всегда юзать скобки. Применение подобной штуки похоже на обычную функцию, но есть отличия. Во-первых, для такой маленькой операции (поиск максимума) не хотелось бы использовать вызов функции (с использованием стека вызовов и с передачей параметров), с препроцессором это удаётся. В каждом месте будет вставлен отдельный код. Во-вторых, универсализм типов аргументов (препроцессор не смотрит на них вообще). Как вы вскоре увидите, первое релизится через инлайн-функции, а второе через темплейтные функции. Так что, как говорится, "препроцессор в топку".

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

#pragma comment(lib, "my")

Подробней о прагмах смотрите в доках по вашему компилеру, для визуала наберите в сиче MSDN "Pragma Directives". Хотя я бы советовал по возможности воздержаться от юзанья прагм. Запрагмивание кода уменьшает читабельность. Лучше юзать командную строку компилера (или проперти проекта в IDE).


Переменный список аргументов в функциях

Вряд ли вы не заметили, что функции семейства printf могут вызываться с разным количеством и типами аргументов. Важно лишь, чтобы первый имел тип const char*. Вероятно, вам было интересно узнать, как реализуется такая штука. Что ж, давайте узнаем!

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

int printf(const char* format, ...);

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

Первое, что надо сделать в функции - объявить переменную типа va_list, она используются для идентификации списка параметров. Затем надо вызвать макрофункцию va_start, передав ей два параметра. Первый - наша va_list-переменная, а второй - последний обязательный параметр функции. Хоть один такой параметр обязан быть. С помощью него va_start получает адрес начала опциональных параметров (все параметры расположены подряд). Для получения каждого параметра используется макрофункция va_arg. Ей также передаём наш идентификатор списка, а также предполагаемый тип для очередного аргумента, возвращает она значение аргумента. Надо заметить, что в принципе данный нами тип может не совпадать с фактическим типом аргумента. Этого никто не заметит, будет использована наглая бинарная реинтерпретация. Можете поэкспериментировать с printf, посовав разные фактические параметры и буковки в форматной строке. Особенно рекомендую написать процентов больше, чем параметров [image]. Вёсёлые аксесс-виолэйшены вам гарантированы. Думаю вы поняли, что говорить о типосейфети в данном случае не приходится. Переменный список аргументов в функциях в текущей реализации - это чисто сишная вещь, живущая в плюсах ради совместимости. Возможно, в C++0x появится типобезопасная реализация этой вещи, по крайней мере запрос на это числится в списке задач комитета стандартизации.

Итак, об окончании списка параметров вам придётся узнавать по какой-то информации из обязательных параметров (например, printf узнаёт об этом по процентикам в форматной строке). После окончания перебора параметров надо вызвать va_end с одним параметром - идентификатором списка.

Рассмотрим простой пример - функция создания и заполнения целочисленного списка, первый параметр - количество элементов:

//
//	Файл list.h
//

#ifndef _LIST_H_
#define _LIST_H_

struct list_node_t
{
    int data;
    list_t* next;
};

ListNode* create_list(unsigned int count, ...);

#endif /* _LIST_H_ */

// Конец файла list.h

//
// Файл list.cpp
//

#include "list.h"

#include <stdarg.h>

ListNode* create_list(unsigned int count, ...)
{
    va_list arg_list;
    va_start(arg_list, count);

    list_node_t root = 0;
    list_node_t prev;

    while (count--) // уменьшение и проверка предыдущего значения на 0
    {
        list_node_t node = new list_node_t;
        node->data = va_arg(arg_list, int);

        if (root)
            prev->next = node;
        else
            root = node;
        prev = node;
    }

    prev->next = 0;
    va_end(arg_list);
    return root;
}


C++ Coding Standard

Last Modified: 2005-11-03

tmh@possibility.com / http://www.possibility.com/Tmh

Using this Standard. If you want to make a local copy of this standard and use it as your own you are perfectly free to do so. That's why I made it! If you find any errors or make any improvements please email me the changes so I can merge them in. I also have a programming blog at http://radio.weblogs.com/0103955/categories/stupidHumanProgramming/ that is accidently interesting at times, as is my wiki at http://www.possibility.com/epowiki/Wiki.jsp.

Contents

  1. Introduction
    • Standardization is Important
    • Standards Enforcement
    • Accepting an Idea
    • 6 Phases of a Project (joke)
    • Flow Chart of Project Decision Making (joke)
    • On Leadership
  2. Resources- Take a Look!
    • General
    • Book Recommendations
  3. Names
    • Make Names Fit
    • Include Units in Names
    • No All Upper Case Abbreviations
    • Class Names
    • Class Library Names
    • Method Names
    • Class Attribute Names
    • Method Argument Names
    • Variable Names on the Stack
    • Pointer Variables
    • Reference Variables and Functions Returning References
    • Global Variables
    • Global Constants
    • Static Variables
    • Type Names
    • Enum Names
    • #define and Macro Names
    • C Function Names
    • C++ File Extensions
  4. Documentation
    • Comments Should Tell a Story
    • Document Decisions
    • Use Extractable Headers
    • Comment All Questions a Programmer May Have When Looking at Your Code
    • Make Your Code Discoverable by Browsing
    • Write Comments as You Code
    • Make Gotchas Explicit
    • Interface and Implementation Documentation
    • Directory Documentation
    • Include Statement Documentation
    • Block Comments
  5. Complexity Management
    • Layering
    • Minimize Dependencies with Abstract Base Classes
    • Liskov's Substitution Prinicple
    • Open/Closed Principle
    • Register/Dispatch Idiom
    • Delegation
    • Follow the Law of Demeter
    • Design by Contract
  6. Classes
    • Naming Class Files
    • Class Layout
    • What should go in public/protected/private?
    • Prototype Source File
    • Use Header File Guards
    • Required Methods for a Class
    • Method Layout
    • Formating Methods with Multiple Arguments
    • Different Accessor Styles
    • Init Idiom for Initializing Objects
    • Initialize all Variables
    • Minimize Inlines
    • Think About What Work to do in Constructors
    • Don't Over Use Operators
    • Thin vs. Thick Class Interfaces
    • Short Methods
    • In a Source file Indicate if a Method is Static or Virtual
  7. Process
    • Use a Design Notation and Process
    • Using Use Cases
    • Using Stories
    • Unified Modeling Language
    • Code Reviews
    • Create a Source Code Control System Early and Not Often
    • Create a Bug Tracking System Early and Not Often
    • RCS Keyword, Change Log, and History Policy
    • Honor Responsibilities
    • Process Automation
    • Tools Agreement
    • Non-Blocking Scheduling
    • Using Personas
    • Use a Continuous Build System
    • Code in the Dominant Style
    • Run Unit Tests Before Every Check-in
  8. Formatting
    • Brace {} Policy
    • Indentation/Tabs/Space Policy
    • Parens () with Key Words and Functions Policy
    • A Line Should Not Exceed 78 Characters
    • If Then Else Formatting
    • switch Formatting
    • Use of goto,continue,break and ?:
    • One Statement Per Line
    • Alignment of Declaration Blocks
    • Document Null Statements
    • Include static and virtual Key Words in Source File
  9. Exceptions
    • Create One Exception for Each Library
    • Selecting Between Exceptions And Asserts
    • Be Careful Throwing Exceptions in Destructors
    • Working With Libaries Using All Sorts of Different Policies
  10. Templates
  11. Namespaces
    • Create Unique Name Space Names
    • Don't Globally Define using
    • Create Shortcut Names
  12. Miscellaneous
    • Be Const Correct
    • Use Streams
    • No Magic Numbers
    • Error Return Check Policy
    • To Use Enums or Not to Use Enums
    • Macros
    • Do Not Default If Test to Non-Zero
    • The Bull of Boolean Types
    • Usually Avoid Embedded Assignments
    • Reusing Your Hard Work and the Hard Work of Others
    • Commenting Out Large Code Blocks
    • Use #if Not #ifdef
    • Creating a C Function in C++
    • Mixing C and C++
    • No Data Definitions in Header Files
    • Make Functions Reentrant
    • Use the Resource Acquisition is Initialization (RAII) Idiom
  13. Portability
    • Use Typedefs for Types
    • Alignment of Class Members
    • Compiler Dependent Exceptions
    • Compiler Dependent RTTI
  14. Popular Myths
    • Promise of OO
    • You can't use OO and C++ on Embedded Systems

Introduction

Standardization is Important

It helps if the standard annoys everyone in some way so everyone feels they are on the same playing field. The proposal here has evolved over many projects, many companies, and literally a total of many weeks spent arguing. It is no particular person's style and is certainly open to local amendments.

Good Points

When a project tries to adhere to common standards a few good things happen:
  • Programmers can go into any code and figure out what's going on.
  • New people can get up to speed quickly.
  • People new to C++ are spared the need to develop a personal style and defend it to the death.
  • People new to C++ are spared making the same mistakes over and over again.
  • People make fewer mistakes in consistent environments.
  • Programmers have a common enemy :-)

Bad Points

Now the bad:
  • The standard is usually stupid because it was made by someone who doesn't understand C++.
  • The standard is usually stupid because it's not what I do.
  • Standards reduce creativity.
  • Standards are unnecessary as long as people are consistent.
  • Standards enforce too much structure.
  • People ignore standards anyway.
  • Standards can be used as a reason for NIH (not invented here) because the new/borrowed code won't follow the standard.

Discussion

The experience of many projects leads to the conclusion that using coding standards makes the project go smoother. Are standards necessary for success? Of course not. But they help, and we need all the help we can get! Be honest, most arguments against a particular standard come from the ego. Few decisions in a reasonable standard really can be said to be technically deficient, just matters of taste. So be flexible, control the ego a bit, and remember any project is fundamentally a team effort.

Standards Enforcement

First, any serious concerns about the standard should be brought up and worked out within the group. Maybe the standard is not quite appropriate for your situation. It may have overlooked important issues or maybe someone in power vehemently disagrees with certain issues :-)

In any case, once finalized hopefully people will play the adult and understand that this standard is reasonable, and has been found reasonable by many other programmers, and therefore is worthy of being followed even with personal reservations.

Failing willing cooperation it can be made a requirement that this standard must be followed to pass a code inspection.

Failing that the only solution is a massive tickling party on the offending party.

Accepting an Idea

  1. It's impossible.
  2. Maybe it's possible, but it's weak and uninteresting.
  3. It is true and I told you so.
  4. I thought of it first.
  5. How could it be otherwise.
If you come to objects with a negative preconception please keep an open mind. You may still conclude objects are bunk, but there's a road you must follow to accept something different. Allow yourself to travel it for a while.

6 Phases of a Project

  1. Enthusiasm
  2. Disillusionment
  3. Panic
  4. A Search for the Guilty
  5. The Punishment of the Innocent
  6. Praise and Honor for the Non-Participants

Flow Chart for Project Decision Making

                       +---------+
                       |  START  |
                       +---------+
                            |
                            V
            YES       +------------+      NO
      +---------------|  DOES THE  |---------------+
      |               | DAMN THING |               |
      V               |    WORK?   |               V
+------------+        +------------+        +--------------+  NO
| DON'T FUCK |                              | DID YOU FUCK |-----+
| WITH IT    |                              |   WITH IT?   |     |
+------------+                              +--------------+     |
      |                                            |             |
      |                                            | YES         |
      |                                            V             |
      |  +------+     +-------------+       +---------------+    |
      |  | HIDE |  NO | DOES ANYONE |<------| YOU DUMBSHIT! |    |
      |  |  IT  |<----|    KNOW?    |       +---------------+    |
      |  +------+     +-------------+                            |
      |      |               |                                   |
      |      |               V                                   |
      |      |        +-------------+       +-------------+      |
      |      |        |   YOU POOR  |  YES  |  WILL YOU   |      |
      |      |        |   BASTARD   |<------| CATCH HELL? |<-----+
      |      |        +-------------+       +-------------+
      |      |               |                     |
      |      |               |                     | NO
      |      |               V                     V
      |      V        +-------------+       +------------+
      +-------------->|    STOP     |<------| SHITCAN IT |
                      +-------------+       +------------+

Leadership

I wish i had said this, but it was said by asd@asd.com in comp.software-eng.

Leaders:

  1. lead by example
  2. don't ask anything of anyone they wouldn't do themselves
  3. are called on to make difficult and unpopular decisions
  4. keep the team focused
  5. reward/support their team in whatever they do
  6. keep/clear unnecessary crap out of the way of the team
Consensus is great. If it lasts for the project lifecycle, consider yourself blessed. I've been on a couple projects where two engineers just blantantly disagreed! They were always:

Programmer #1 says " x = 1"
Programmer #2 says " x != 1"

That's when a Project Leader is required. Unless you want to flip a coin.

Oh yah - one more thing. Project leaders: TAKE the blame when things go wrong and SHARE the credit when things go right.

Ain't easy - but it's the way I try to run my life.

Resources- Take a Look!

General

  • Code Reviews. If you are interested in coding standards you may also be interested in Code Review Standards I have created at http://www.possibility.com/epowiki/Wiki.jsp?page=CodeReviews.
  • Design Stories
  • OO Info Sources
  • Designing Qt-Style C++ APIs - A good paper on API I design for C++. I don't agree with everything, but it is good.
  • Unified Modeling Language (UML)
  • OO FAQ - All You Wanted to Know About OO
  • C++ FAQ LITE
  • C++ Source Libraries
  • C++ Tutorials
  • ACE C++ Library
  • Collection of Other Standards
  • Design by Contract from Eiffle
  • C++ isn't Perfect, Here are Some Reasons Why
  • Doxygen - is a 'javadoc' like utility that extracts comments and relevant information from your C++/C programs and generates HTML pages from it.
  • Const Correctness - A very nice article on const correctness by Chad Loder.
  • Abraxis Code Check - A program for checking code for coding standard violations and other problems.

Book Recommendations

What are some good C++ books you can buy for you and your team?
  1. Koenig/Moo's "Accelerated C++"
  2. Lippman/Moo's "C++ Primer" 4th Edition
  3. Bruce Eckel's "Thinking In C++"
  4. Scott Meyers "Effective C++"
  5. Dewhurst's "C++ Gotchas"
  6. Meyers' "Effective STL"
  7. Josuttis' "The C++ Standard Library"
  8. Vandevoorde/Josuttis' "C++ Templates"
  9. Langer/Kreft's "Standard C++ IOStreams and Locales"
  10. Sutter's "Exceptional C++"
  11. Sutter's "More Exceptional C++ and Exceptional C++ Style"
  12. Martin's "Agile Software Development: Principles, Patterns, and Practices"

Names

Make Names Fit

Names are the heart of programming. In the past people believed knowing someone's true name gave them magical power over that person. If you can think up the true name for something, you give yourself and the people coming after power over the code. Don't laugh!

A name is the result of a long deep thought process about the ecology it lives in. Only a programmer who understands the system as a whole can create a name that "fits" with the system. If the name is appropriate everything fits together naturally, relationships are clear, meaning is derivable, and reasoning from common human expectations works as expected.

If you find all your names could be Thing and DoIt then you should probably revisit your design.

Class Names

  • Name the class after what it is. If you can't think of what it is that is a clue you have not thought through the design well enough.
  • Compound names of over three words are a clue your design may be confusing various entities in your system. Revisit your design. Try a CRC card session to see if your objects have more responsibilities than they should.
  • Avoid the temptation of bringing the name of the class a class derives from into the derived class's name. A class should stand on its own. It doesn't matter what it derives from.
  • Suffixes are sometimes helpful. For example, if your system uses agents then naming something DownloadAgent conveys real information.

Method and Function Names

  • Usually every method and function performs an action, so the name should make clear what it does: CheckForErrors() instead of ErrorCheck(), DumpDataToFile() instead of DataFile(). This will also make functions and data objects more distinguishable.

    Classes are often nouns. By making function names verbs and following other naming conventions programs can be read more naturally.

  • Suffixes are sometimes useful:
    • Max - to mean the maximum value something can have.
    • Cnt - the current count of a running count variable.
    • Key - key value.

    For example: RetryMax to mean the maximum number of retries, RetryCnt to mean the current retry count.

  • Prefixes are sometimes useful:
    • Is - to ask a question about something. Whenever someone sees Is they will know it's a question.
    • Get - get a value.
    • Set - set a value.

    For example: IsHitRetryLimit.

Include Units in Names

If a variable represents time, weight, or some other unit then include the unit in the name so developers can more easily spot problems. For example:
uint32 mTimeoutMsecs;
uint32 mMyWeightLbs;
Better yet is to make a variable into a class so bad conversions can be caught.

No All Upper Case Abbreviations

  • When confronted with a situation where you could use an all upper case abbreviation instead use an initial upper case letter followed by all lower case letters. No matter what.

Justification

  • People seem to have very different intuitions when making names containing abbreviations. It's best to settle on one strategy so the names are absolutely predictable.

    Take for example NetworkABCKey. Notice how the C from ABC and K from key are confused. Some people don't mind this and others just hate it so you'll find different policies in different code so you never know what to call something.

Example

   class FluidOz             // NOT FluidOZ
   class NetworkAbcKey       // NOT NetworkABCKey

Class Names

  • Use upper case letters as word separators, lower case for the rest of a word
  • First character in a name is upper case
  • No underbars ('_')

Justification

  • Of all the different naming strategies many people found this one the best compromise.

Example

   class NameOneTwo

   class Name

Class Library Names

  • Now that name spaces are becoming more widely implemented, name spaces should be used to prevent class name conflicts among libraries from different vendors and groups.
  • When not using name spaces, it's common to prevent class name clashes by prefixing class names with a unique string. Two characters is sufficient, but a longer length is fine.

Example

John Johnson's complete data structure library could use JJ as a prefix, so classes would be:
   class JjLinkList
   {
   }

Method Names

  • Use the same rule as for class names.

Justification

  • Of all the different naming strategies many people found this one the best compromise.

Example

   class NameOneTwo
   {
   public:
      int                   DoIt();
      void                  HandleError();
   }

Class Attribute Names

  • Attribute names should be prepended with the character 'm'.
  • After the 'm' use the same rules as for class names.
  • 'm' always precedes other name modifiers like 'p' for pointer.

Justification

  • Prepending 'm' prevents any conflict with method names. Often your methods and attribute names will be similar, especially for accessors.

Example

   class NameOneTwo
   {
   public:
      int                   VarAbc();
      int                   ErrorNumber();
   private:
      int                   mVarAbc;
      int                   mErrorNumber;
      String*               mpName;
   }

Method Argument Names

  • The first character should be lower case.
  • All word beginnings after the first letter should be upper case as with class names.

Justification

  • You can always tell which variables are passed in variables.
  • You can use names similar to class names without conflicting with class names.

Example

   class NameOneTwo
   {
   public:
      int                   StartYourEngines(
                               Engine& rSomeEngine,
                               Engine& rAnotherEngine);
   }

Variable Names on the Stack

  • use all lower case letters
  • use '_' as the word separator.

Justification

  • With this approach the scope of the variable is clear in the code.
  • Now all variables look different and are identifiable in the code.

Example

   int
   NameOneTwo::HandleError(int errorNumber)
   {
      int            error= OsErr();
      Time           time_of_error;
      ErrorProcessor error_processor;
      Time*          p_out_of_time= 0;
   }

The standard pointer notation is not entirely satisfactory because it doesn't look quite right, but it is consistent.

How do you handle statics? There's never a reason to have a static local to a function so there's no reason to invent a syntax for it.

Pointer Variables

  • pointers should be prepended by a 'p' in most cases
  • place the * close to the pointer type not the variable name

Justification

  • The idea is that the difference between a pointer, object, and a reference to an object is important for understanding the code, especially in C++ where -> can be overloaded, and casting and copy semantics are important.
  • Pointers really are a change of type so the * belongs near the type. One reservation with this policy relates to declaring multiple variables with the same type on the same line. In C++ the pointer modifier only applies to the closest variable, not all of them, which can be very confusing, especially for newbies. You want to have one declaration per line anyway so you can document each variable.

Example

  String* pName= new String;

  String* pName, name, address; // note, only pName is a pointer.

Reference Variables and Functions Returning References

  • References should be prepended with 'r'.

Justification

  • The difference between variable types is clarified.
  • It establishes the difference between a method returning a modifiable object and the same method name returning a non-modifiable object.

Example

   class Test
   {
   public:
      void               DoSomething(StatusInfo& rStatus);

      StatusInfo&        rStatus();
      const StatusInfo&  Status() const;

   private:
      StatusInfo&        mrStatus;
   }

Global Variables

  • Global variables should be prepended with a 'g'.

Justification

  • It's important to know the scope of a variable.

Example

    Logger  gLog;
    Logger* gpLog;

Global Constants

  • Global constants should be all caps with '_' separators.

Justification

It's tradition for global constants to named this way. You must be careful to not conflict with other global #defines and enum labels.

Example

    const int A_GLOBAL_CONSTANT= 5;

Static Variables

  • Static variables may be prepended with 's'.

Justification

  • It's important to know the scope of a variable.

Example

   class Test
   {
   public:
   private:
      static StatusInfo msStatus;
   }

Type Names

  • When possible for types based on native types make a typedef.
  • Typedef names should use the same naming policy as for a class with the word Type appended.

Justification

  • Of all the different naming strategies many people found this one the best compromise.
  • Types are things so should use upper case letters. Type is appended to make it clear this is not a class.

Example

   typedef uint16  ModuleType;
   typedef uint32  SystemType;

Enum Names

Labels All Upper Case with '_' Word Separators

This is the standard rule for enum labels.

Example

   enum PinStateType
   {
      PIN_OFF,
      PIN_ON
   };

Enums as Constants without Class Scoping

Sometimes people use enums as constants. When an enum is not embedded in a class make sure you use some sort of differentiating name before the label so as to prevent name clashes.

Example

   enum PinStateType            If PIN was not prepended a conflict
   {                            would occur as OFF and ON are probably
      PIN_OFF,                  already defined.
      PIN_ON
   };

Enums with Class Scoping

Just name the enum items what you wish and always qualify with the class name: Aclass::PIN_OFF.

Make a Label for an Error State

It's often useful to be able to say an enum is not in any of its valid states. Make a label for an uninitialized or error state. Make it the first label if possible.

Example

enum { STATE_ERR,  STATE_OPEN, STATE_RUNNING, STATE_DYING};

#define and Macro Names

  • Put #defines and macros in all upper using '_' separators.

Justification

This makes it very clear that the value is not alterable and in the case of macros, makes it clear that you are using a construct that requires care.

Some subtle errors can occur when macro names and enum labels use the same name.

Example

#define MAX(a,b) blah
#define IS_ERR(err) blah

C Function Names

  • In a C++ project there should be very few C functions.
  • For C functions use the GNU convention of all lower case letters with '_' as the word delimiter.

Justification

  • It makes C functions very different from any C++ related names.

Example

   int
   some_bloody_function()
   {
   }

C++ File Extensions

In short: Use the .h extension for header files and .cc for source files.

For some reason an odd split occurred in early C++ compilers around what C++ source files should be called. C header files always use the .h and C source files always use the .c extension. What should we use for C++?

The short answer is as long as everyone on your project agrees it doesn't really matter. The build environment should be able to invoke the right compiler for any extension. Historically speaking here have been the options:

  • Header Files: .h, .hh, .hpp
  • Source Files: .C, .cpp, .cc

Header File Extension Discussion

Using .hh extension is not widely popular but makes a certain kind of sense. C header files use .h file extension and C++ based header files use .hh file extension. The problem is if we consider a header file an interface to a service then we can have a C interface to a service and C++ interface to the service in the same file. Using preprocessor directives this is possible and common. The recommendation is to stick with using the .h extension.

Source File Extension Discussion

The problem with the .C extension is that it is indistinguishable from the .c extensions in operating systems that aren't case sensitive. Yes, this is a UNIX vs. windows issue. Since it is a simple step aiding portability we won't use the .C extension. The .cpp extension is a little wordy. So the .cc extension wins by default.

Documentation

Comments Should Tell a Story

Consider your comments a story describing the system. Expect your comments to be extracted by a robot and formed into a man page. Class comments are one part of the story, method signature comments are another part of the story, method arguments another part, and method implementation yet another part. All these parts should weave together and inform someone else at another point of time just exactly what you did and why.

Document Decisions

Comments should document decisions. At every point where you had a choice of what to do place a comment describing which choice you made and why. Archeologists will find this the most useful information.

Use Extractable Headers

Use a document extraction system like Doxygen when documenting your code.

These headers are structured in such a way as they can be parsed and extracted. They are not useless like normal headers. So take time to fill them out. If you do it right once no more documentation may be necessary.

As part of your nighlty build system have a step the generates the documentation from the source. Then index the source using a tool like Lucene. Have a front end to the search so developers can do full text searches on nightly builds and for release builds. This is a wonderfully useful feature.

The next step in automation is to front the repository with a web server documentation can directly refer to a source file with a URL.

Comment All Questions a Programmer May Have When Looking at Your Code

At every point in your code think about what questions a programmer may have about the code. It's crucial you answer all those questions somehow, someway. If you don't, as the code writer, answer those questions, who will?

If you think your code is so clear and wonderful that nobody will have any questions then you are lying to yourself. I have never seen a large system with this wonderful self-documenting code feature. I've seen very few small libraries are even a single class that are so wonderfully self-documented.

You have a lot of tools at your disposal to answer questions:

  1. A brain to think up the questions you should be answering. Why? Who? When? How? What?
  2. Variable names.
  3. Class names.
  4. Class decomposition.
  5. Method decomposition.
  6. File names.
  7. Documentation at all levels: package, class, method, attribute, inline.
The better you are at orchestrating all these elements together the clearer your code will be to everyone else.

I don't really consider unit tests a question answering device because if you can't understand the code by reading it, reading something else about the code you don't understand won't help you understand it better.

Make Your Code Discoverable by Browsing

Programmers should be able to navigate your code by looking at markers in the code, namely the names and the directory structure. Nothing is more frustrating to than to have to look at pile of code and have no idea what it's organizing principles are.

Have a logicanl directory structure. Have directories called doc, lib, src, bin, test, pkg, install, etc and whatever, so I at least have some idea where stuff is. People use the weirdest names and lump everything together so that it it can be detangles. Clear thought is evidenced from the beginning by a directory stucture.

Don't put more than one class in a file. Otherwise, how will I know its there when I browse your code? Should I really need to use search to find every last thing? Can't I just poke around the code and find it? I can if you organize your code.

Name your files after your classes. I didn't believe this one until I saw it. Why you name a file different than the class? How I am possibly supposed to know what's in the file otherwise?

Write Comments as You Code

You won't every go back later and document your code. You just won't. Don't lie to yourself, the world, and your mother by saying that you will.

So when you do something document it right then and there. When you create a class- document it. When you create a method- document it. And so on. That way when you finish coding you will also be finished documenting.

I advocate simultaneously writing code, writing tests, and writing documentaiton. Which comes first depends on you and the problem. I don't think there is any rule that says which should come first. On the path to getting stuff done I'll take the entrance that seems easiest to me at the time. Once on the path it's easy to follow the entire trail.

Won't this break the flow? No, I think it improves flow because it keeps you mindful of what you are doing, why you are doing, and how it fits in the big picture. My take on TDD (test driven development) is that it's not the tests that are really important, it's that the tests keep you mindful while programming. A test means you are keeping everything in you mind at once you need to remember to successfully code something up. As you can't keep large chunks in your mind then smaller chunks are better. Writing a test forces you to remember what your code is supposed to accomplish. It's forcing you to also think about the use case/story/intent behind why you are writing the code.

The result is a pointed mind that has focussed all its powers on doing one thing. When you can bring that focus to you programming you can be successful. The tests are really secondary. If your system/acceptance tests can't find bugs you are screwed anyway. And I find code written mindfully, one step at a time, has very few bugs. Unit tests are just one definition of a "step." You can use the orignial story you are implementing as a step as well. I use unit tests more as a mental focussing device while developing, like Zen Archery, than for the actual tests. After development unit tests are very useful in making sure code doesn't break. So I am not saying unit tests aren't useful. I just don't think they are the real reason behind why TDD generates working code. With a clear well functioning focussed mind we generate working code. But getting into that state is hard.

Writing comments simultaneously with all other aspects of development deepens your mindfulness because you are thinking about everything at once. All the interconnections are present in your brain because you are explaining the intent behind what you are doing.

There's a saying that you don't know something until you teach it. Comments are teaching what you are doing to someone else. When you are writing comments you must generate the thoughts to teach, to explain to someone else the intent behing what you are doing. It's very difficult to make a coding error when all this context is hot in your mind.

I'll go back and forth between documenting, testing, and coding. I'll let the problem dictate what happens when as I am working my way through solving the problem. Saying testing should always come first is too simple a rule and I think misses the larger point about software development.

Software is ultimately mind stuff. Using our minds better is the real methodology.

Make Gotchas Explicit

Explicitly comment variables changed out of the normal control flow or other code likely to break during maintenance. Embedded keywords are used to point out issues and potential problems. Consider a robot will parse your comments looking for keywords, stripping them out, and making a report so people can make a special effort where needed.

Gotcha Keywords

  • :TODO: topic
    Means there's more to do here, don't forget.

  • :BUG: [bugid] topic
    means there's a Known bug here, explain it and optionally give a bug ID.

  • :KLUDGE:
    When you've done something ugly say so and explain how you would do it differently next time if you had more time.

  • :TRICKY:
    Tells somebody that the following code is very tricky so don't go changing it without thinking.

  • :WARNING:
    Beware of something.

  • :COMPILER:
    Sometimes you need to work around a compiler problem. Document it. The problem may go away eventually.

  • :ATTRIBUTE: value
    The general form of an attribute embedded in a comment. You can make up your own attributes and they'll be extracted.

Gotcha Formatting

  • Make the gotcha keyword the first symbol in the comment.
  • Comments may consist of multiple lines, but the first line should be a self-containing, meaningful summary.
  • The writer's name and the date of the remark should be part of the comment. This information is in the source repository, but it can take a quite a while to find out when and by whom it was added. Often gotchas stick around longer than they should. Embedding date information allows other programmer to make this decision. Embedding who information lets us know who to ask.

Example

   // :TODO: tmh 960810: possible performance problem
   // We should really use a hash table here but for now we'll
   // use a linear search.

   // :KLUDGE: tmh 960810: possible unsafe type cast
   // We need a cast here to recover the derived type. It should
   // probably use a virtual method or template.

See Also

See Interface and Implementation Documentation for more details on how documentation should be laid out.

Interface and Implementation Documentation

There are two main audiences for documentation:
  • Class Users
  • Class Implementors
With a little forethought we can extract both types of documentation directly from source code.

Class Users

Class users need class interface information which when structured correctly can be extracted directly from a header file. When filling out the header comment blocks for a class, only include information needed by programmers who use the class. Don't delve into algorithm implementation details unless the details are needed by a user of the class. Consider comments in a header file a man page in waiting.

Class Implementors

Class implementors require in-depth knowledge of how a class is implemented. This comment type is found in the source file(s) implementing a class. Don't worry about interface issues. Header comment blocks in a source file should cover algorithm issues and other design decisions. Comment blocks within a method's implementation should explain even more.

Directory Documentation

Every directory should have a README file that covers:
  • the purpose of the directory and what it contains
  • a one line comment on each file. A comment can usually be extracted from the NAME attribute of the file header.
  • cover build and install directions
  • direct people to related resources:
    • directories of source
    • online documentation
    • paper documentation
    • design documentation
  • anything else that might help someone
Consider a new person coming in 6 months after every original person on a project has gone. That lone scared explorer should be able to piece together a picture of the whole project by traversing a source directory tree and reading README files, Makefiles, and source file headers.

Include Statement Documentation

Include statements should be documented, telling the user why a particular file was included. If the file includes a class used by the class then it's useful to specify a class relationship:
  • ISA - this class inherits from the class in the include file.
  • HASA - this class contains, that is has as a member attribute, the class in the include file. This class owns the memory and is responsible for deleting it.
  • USES - this class uses something from the include file.
  • HASA-USES - this class keeps a pointer or reference to the class in the include file, but this class does not own the memory.

Example

#ifndef XX_h
#define XX_h

// SYSTEM INCLUDES
//
#include                              // standard IO interface
#include                             // HASA string interface
#include                               // USES auto_ptr
Notice how just by reading the include directives the code is starting to tell you a story of why and how it was built.

Block Comments

Use comments on starting and ending a Block:
{
   // Block1  (meaningful comment about Block1)
  ... some code

  {
     // Block2  (meaningful comment about Block2)
     ... some code
  }  // End Block2

}  // End Block1
This may make block matching much easier to spot when you don't have an intelligent editor.

Complexity Management

Layering

Layering is the primary technique for reducing complexity in a system. A system should be divided into layers. Layers should communicate between adjacent layers using well defined interfaces. When a layer uses a non-adjacent layer then a layering violation has occurred.

A layering violation simply means we have dependency between layers that is not controlled by a well defined interface. When one of the layers changes code could break. We don't want code to break so we want layers to work only with other adjacent layers.

Sometimes we need to jump layers for performance reasons. This is fine, but we should know we are doing it and document appropriately.

Minimize Dependencies with Abstract Base Classes

One of the most important strategies in C++ is to remove dependencies among different subsystems. Abstract base classes (ABCs) are a solid technique for dependency removal.

An ABC is an abstraction of a common form such that it can be used to build more specific forms. An ABC is a common interface that is reusable across a broad range of similar classes. By specifying a common interface as long as a class conforming to that interface is used it doesn't really matter what is the type of the derived type. This breaks code dependencies. New classes, conforming to the interface, can be substituted in at will without breaking code. In C++ interfaces are specified by using base classes with virtual methods.

The above is a bit rambling because it's a hard idea to convey. So let's use an example: We are doing a GUI where things jump around on the screen. One approach is to do something like:

   class Frog
   {
   public:
      void Jump();
   }
   class Bean
   {
   public:
      void Jump();
   }
The GUI folks could instantiate each object and call the Jump method of each object. The Jump method of each object contains the implementation of jumping behavior for that type of object. Obviously frogs and beans jump differently even though both can jump.

Unfortunately the owner of Bean didn't like the word Jump so they changed the method name to Leap. This broke the code in the GUI and one whole week was lost.

Then someone wanted to see a horse jump so a Horse class was added:

   class Horse
   {
   public:
      void Jump();
   }
The GUI people had to change their code again to add Horse.

Then someone updated Horse so that its Jump behavior was slightly different. Unfortunately this caused a total recompile of the GUI code and they were pissed.

Someone got the bright idea of trying to remove all the above dependencies using abstract base classes. They made one base class that specified an interface for jumping things:

   class Jumpable
   {
   public:
      virtual void Jump() = 0;
   }
Jumpable is a base class because other classes need to derive from it so they can get Jumpable's interface. It's an abstract base class because one or more of its methods has the = 0 notation which means the method is a pure virtual method. Pure virtual methods must be implemented by derived classes. The compiler checks.

Not all methods in an ABC must be pure virtual, some may have an implementation. This is especially true when creating a base class encapsulating a process common to a lot of objects. For example, devices that must be opened, diagnostics run, booted, executed, and then closed on a certain event may create an ABC called Device that has a method called LifeCycle which calls all other methods in turn thus running through all phases of a device's life. Each device phase would have a pure virtual method in the base class requiring implementation by more specific devices. This way the process of using a device is made common but the specifics of a device are hidden behind a common interface.

Back to Jumpable. All the classes were changed to derive from Jumpable:

   class Frog : public Jumpable
   {
   public:
      virtual void Jump() { ... }
   }

   etc ...
We see an immediate benefit: we know all classes derived from Jumpable must have a Jump method. No one can go changing the name to Leap without the compiler complaining. One dependency broken.

Another benefit is that we can pass Jumpable objects to the GUI, not specific objects like Horse or Frog:

   class Gui
   {
   public:
      void MakeJump(Jumpable*);
   }

   Gui gui;
   Frog* pFrog= new Frog;

   gui.MakeJump(pFrog);
Notice Gui doesn't even know it's making a frog jump, it just has a jumpable thing, that's all it cares about. When Gui calls the Jump method it will get the implementation for Frog's Jump method. Another dependency down. Gui doesn't have to know what kind of objects are jumping.

We also removed the recompile dependency. Because Gui doesn't contain any Frog objects it will not be recompiled when Frog changes.

Downside

Wow! Great stuff! Yes but there are a few downsides:

Overhead for Virtual Methods

Virtual methods have a space and time penalty. It's not huge, but should be considered in design.

Make Everything an ABC!

Sometimes people overdo it, making everything an ABC. The rule is make an ABC when you need one not when you might need one. It takes effort to design a good ABC, throwing in a virtual method doesn't an ABC make. Pick and choose your spots. When some process or some interface can be reused and people will actually make use of the reuse then make an ABC and don't look back.

Liskov's Substitution Principle (LSP)

This principle states:
   All classes derived from a base class should be interchangeable
   when used as a base class.
The idea is users of a class should be able to count on similar behavior from all classes that derive from a base class. No special code should be necessary to qualify an object before using it. If you think about it violating LSP is also violating the Open/Closed principle because the code would have to be modified every time a derived class was added. It's also related to dependency management using abstract base classes.

For example, if the Jump method of a Frog object implementing the Jumpable interface actually makes a call and orders pizza we can say its implementation is not in the spirit of Jump and probably all other objects implementing Jump. Before calling a Jump method a programmer would now have to check for the Frog type so it wouldn't screw up the system. We don't want this in programs. We want to use base classes and feel comfortable we will get consistent behaviour.

LSP is a very restrictive idea. It constrains implementors quite a bit. In general people support LSP and have LSP as a goal.

Open/Closed Principle

The Open/Closed principle states a class must be open and closed where:
  • open means a class has the ability to be extended.
  • closed means a class is closed for modifications other than extension. The idea is once a class has been approved for use having gone through code reviews, unit tests, and other qualifying procedures, you don't want to change the class very much, just extend it.
The Open/Closed principle is a pitch for stability. A system is extended by adding new code not by changing already working code. Programmers often don't feel comfortable changing old code because it works! This principle just gives you an academic sounding justification for your fears :-)

In practice the Open/Closed principle simply means making good use of our old friends abstraction and polymorphism. Abstraction to factor out common processes and ideas. Inheritance to create an interface that must be adhered to by derived classes. In C++ we are talking about using abstract base classes . A lot.

Register/Dispatch Idiom

Another strategy for reducing dependencies in a system is the Register/Dispatch Idiom (RDI). RDI treats large grained occurrences in a system as events. Events are identified by some unique identifier. Objects in the system register with a dispatch system for events or classes of events it is interested in. Objects that are event sources send events into the dispatch system so the dispatch system can route events to consumers.

RDI separates producers and consumers on a distributed scale. Event producers and consumers don't have to know about each other at all. Consumers can drop out of the event stream by deregistering for events. New consumers can register for events at anytime. Event producers can drop out with no ill effect to event consumers, the consumer just won't get any more events. It is a good idea for producers to have an "I'm going down event" so consumers can react intelligently.

Logically the dispatch system is a central entity. The implementation however can be quite different. For a highly distributed system a truly centralized event dispatcher would be a performance bottleneck and a single point of failure. Think of event dispatchers as being a lot of different processes cast about on various machines for redundancy purposes. Event processors communicate amongst each other to distribute knowledge about event consumers and producers. Much like a routing protocol distributes routing information to its peers.

RDI works equally well in the small, in processes and single workstations. Parts of the system can register as event consumers and event producers making for a very flexible system. Complex decisions in a system are expressed as event registrations and deregistrations. No further level of cooperation required.

More expressive event filters can also be used. The above proposal filters events on some unique ID. Often you want events filtered on more complex criteria, much like a database query. For this to work the system has to understand all data formats. This is easy if you use a common format like attribute value pairs. Otherwise each filter needs code understanding packet formats. Compiling in filter code to each dispatcher is one approach. Creating a downloadable generic stack based filter language has been used with success on other projects, being both simple and efficient.

Delegation

Delegation is the idea of a method using another object's method to do the real work. In some sense the top layer method is a front for the other method. Delegation is a form of dependency breaking. The top layer method never has to change while it's implementation can change at will.

Delegation is an alternative to using inheritance for implementation purposes. One can use inheritance to define an interface and delegation to implement the interface.

Some people feel delegation is a more robust form of OO than using implementation inheritance. Delegation encourages the formation of abstract class interfaces and HASA relationships. Both of which encourage reuse and dependency breaking.

Example

   class TestTaker
   {
   public:
      void WriteDownAnswer()   { mPaidTestTaker.WriteDownAnswer(); }
   private:
      PaidTestTaker  mPaidTestTaker;
   }
In this example a test taker delegates actually answering the question to a paid test taker. Not ethical but a definite example of delegation!

Follow the Law of Demeter

The Law of Demeter states that you shouldn't access a contained object directly from the containing object, you should use a method of the containing object that does what you want and accesses any of its objects as needed.

Justification

The purpose of this law is to break dependencies so implementations can change without breaking code. If an object wishes to remove one of its contained objects it won't be able to do so because some other object is using it. If instead the service was through an interface the object could change its implementation anytime without ill effect.

Caveat

As for most laws the Law of Demeter should be ignored in certain cases. If you have a really high level object that contains a lot of subobjects, like a car contains thousands of parts, it can get absurd to created a method in car for every access to a subobject.

Example

   class SunWorkstation
   {
   public:
      void          UpVolume(int amount) { mSound.Up(amount); }

      SoundCard     mSound;

   private:
      GraphicsCard  mGraphics;
   }

   SunWorksation sun;

   Do   : sun.UpVolume(1);
   Don't: sun.mSound.Up(1);

Design by Contract

The idea of design by contract is strongly related to LSP . A contract is a formal statement of what to expect from another party. In this case the contract is between pieces of code. An object and/or method states that it does X and you are supposed to believe it. For example, when you ask an object for its volume that's what you should get. And because volume is a verifiable attribute of a thing you could run a series of checks to verify volume is correct, that is, it satisfies its contract.

The contract is enforced in languages like Eiffel by pre and post condition statements that are actually part of the language. In other languages a bit of faith is needed.

Design by contract when coupled with language based verification mechanisms is a very powerful idea. It makes programming more like assembling spec'd parts.

Using Design by Contract

  1. DO NOT PUT "REAL" CODE IN DBC CALLS.. Dbc calls should only test conditions. No code that can't be compiled out should be included in Dbc calls.
  2. Every method should define its pre and post conditions.
  3. Every class should define its invariants.
  4. Callers are responsible for checking preconditions. An object may not and is not required to test for assertion violations.
  5. Method pre-conditions should be documented in a method's interface documentation.
  6. Pre-conditions can be weakened by derived classes.
  7. Post-conditions can be strengthened by derived classes.
  8. Every Class Should:
    1. Develop its class invariants.
    2. Code its invariants and call them in its operations.
    3. Document its invariants in the class documentation.
    Yes, this takes a lot of work, but high availibility is the system's __primary goal__, meeting this goal requires a lot of work and effort by each programmer.
  9. Every Method Should:
    1. Develop a list of exceptions.
    2. Develop operation pre-conditions.
    3. Develop operation post-conditions.
    4. Code the exceptions, pre, and post conditions.
    5. Document the exceptions, pre, and post conditions.
    Yes, this takes a lot of work, but high availibility is the system's __primary goal__, meeting this goal requires a lot of work and effort by each programmer.

Classes

Naming Class Files

Class Definition in One File

Each class definition should be in its own file where each file is named directly after the class's name:
   ClassName.h

Implementation in One File

In general each class should be implemented in one source file:
   ClassName.cc   // or whatever the extension is: cpp, c++

But When it Gets Really Big...

If the source file gets too large or you want to avoid compiling templates all the time then add additional files named according to the following rule:
   ClassName_section.C
section is some name that identifies why the code is chunked together. The class name and section name are separated by '_'.

Class Layout

A common class layout is critical from a code comprehension point of view and for automatically generating documentation. C++ programmers, through a new set of tools, can enjoy the same level generated documentation Java programmers take for granted.

Class and Method Documentation

It is recommended a program like Doxygen be used to document C++ classes, method, variables, functions, and macros. The documentation can be extracted and put in places in a common area for all programmers to access. This saves programmers having to read through class headers. Documentation generation should be integrated with the build system where possible.

Template

Please use the following template when creating a new class.

/**  A one line description of the class.
 *
 * #include "XX.h" <BR>
 * -llib
 *
 * A longer description.
 *
 * @see something
 */

#ifndef XX_h
#define XX_h

// SYSTEM INCLUDES
//

// PROJECT INCLUDES
//

// LOCAL INCLUDES
//

// FORWARD REFERENCES
//

class XX
{
public:
// LIFECYCLE

   /** Default constructor.
    */
   XX(void);

   /** Copy constructor.
    *
    * @param from The value to copy to this object.
    */
   XX(const XX& from);

   /** Destructor.
    */
   ~XX(void);

// OPERATORS

   /** Assignment operator.
    *
    * @param from THe value to assign to this object.
    *
    * @return A reference to this object.
    */
   XX&                     operator=(XX& from);

// OPERATIONS
// ACCESS
// INQUIRY

protected:
private:
};

// INLINE METHODS
//

// EXTERNAL REFERENCES
//

#endif  // _XX_h_

Required Methods Placeholders

The template has placeholders for required methods . You can delete them or implement them.

Ordering is: public, protected, private

Notice that the public interface is placed first in the class, protected next, and private last. The reasons are:
  • programmers should care about a class's interface more than implementation
  • when programmers need to use a class they need the interface not the implementation
It makes sense then to have the interface first. Placing implementation, the private section, first is a historical accident as the first examples used the private first layout. Over time emphasis has switched deemphasizing a class's interface over implementation details.

LIFECYCLE

The life cycle section is for methods that control the life cycle of an object. Typically these methods include constructors, destructors, and state machine methods.

OPERATORS

Place all operators in this section.

OPERATIONS

Place the bulk of a class's non access and inquiry method methods here. A programmer will look here for the meat of a class's interface.

ACCESS

Place attribute accessors here.

INQUIRY

These are the Is* methods. Whenever you have a question to ask about an object it can be asked via in Is method. For example: IsOpen() will indicate if the object is open. A good strategy is instead of making a lot of access methods you can turn them around to be questions about the object thus reducing the exposure of internal structure. Without the IsOpen() method we might have had to do: if (STATE_OPEN == State()) which is much uglier.

What should go in public/protected/private?

Public Section

Only put an object's interface in the public section. DO NOT expose any private data items in the public section. At least encapsulate access via access methods. Ideally your method interface should make most access methods unnecessary. Do not put data in the public interface.

Protected and Private Section

What should go into the protected section versus the private section is always a matter of debate.

All Protected

Some say there should be no private section and everything not in the public section should go in the protected section. After all, we should allow all our children to change anything they wish.

All Private

Another camp says by making the public interface virtual any derived class can change behavior without mucking with internals.

Wishy Washy

Rationally decide where elements should go and put them there. Not very helpful.

And the Winner Is...

Keeping everything all private seems the easiest approach. By making the public methods virtual flexibility is preserved.

Prototype Source File

#include "XX.h"  // class implemented

/////////////////////////////// PUBLIC ///////////////////////////////////////

//============================= LIFECYCLE ====================================

XX::XX()
{
}// XX

XX::XX(const XX&)
{
}// XX

XX::~XX()
{
}// ~XX

//============================= OPERATORS ====================================

XX&
XX::operator=(XX&);
{
   return *this;

}// =

//============================= OPERATIONS ===================================
//============================= ACESS      ===================================
//============================= INQUIRY    ===================================
/////////////////////////////// PROTECTED  ///////////////////////////////////

/////////////////////////////// PRIVATE    ///////////////////////////////////

Use Header File Guards

Include files should protect against multiple inclusion through the use of macros that "guard" the files.

When Not Using Namespces

#ifndef filename_h
#define filename_h

#endif
The new line after the endif if is required by some compilers.

When Using Namespaces

If namespaces are used then to be completely safe:
#ifndef namespace_filename_h
#define namespace_filename_h

#endif
  1. Replace filename with the name of the file being guarded. This should usually be the name of class contained in the file. Use the exact class name. Some standards say use all upper case. This is a mistake because someone could actually name a class the same as yours but using all upper letters. If the files end up be included together one file will prevent the other from being included and you will be one very confused puppy. It has happened!

  2. Most standards put a leading _ and trailing _. This is no longer valid as the C++ standard reserves leading _ to compiler writers.

  3. When the include file is not for a class then the file name should be used as the guard name.

  4. Compilers differ on how comments are handled on preprocessor directives. Historically many compilers have not accepted comments on preprocessor directives.

  5. Historically many compilers require a new line after last endif.

Required Methods for a Class

To be good citizens almost all classes should implement the following methods. If you don't have to define and implement any of the "required" methods they should still be represented in your class definition as comments.
  • Default Constructor

    If your class needs a constructor, make sure to provide one. You need one if during the operation of the class it creates something or does something that needs to be undone when the object dies. This includes creating memory, opening file descriptors, opening transactions etc.

    If the default constructor is sufficient add a comment indicating that the compiler-generated version will be used.

    If your default constructor has one or more optional arguments, add a comment indicating that it still functions as the default constructor.

  • Virtual Destructor

    If your class is intended to be derived from by other classes then make the destructor virtual.

  • Copy Constructor

    If your class is copyable, either define a copy constructor and assignment operator or add a comment indicating that the compiler-generated versions will be used.

    If your class objects should not be copied, make the copy constructor and assignment operator private and don't define bodies for them. If you don't know whether the class objects should be copyable, then assume not unless and until the copy operations are needed.

  • Assignment Operator

    If your class is assignable, either define a assignment operator or add a comment indicating that the compiler-generated versions will be used.

    If your objects should not be assigned, make the assignment operator private and don't define bodies for them. If you don't know whether the class objects should be assignable, then assume not.

Justification

  • Virtual destructors ensure objects will be completely destructed regardless of inheritance depth. You don't have to use a virtual destructor when:
    • You don't expect a class to have descendants.
    • The overhead of virtualness would be too much.
    • An object must have a certain data layout and size.
  • A default constructor allows an object to be used in an array.
  • The copy constructor and assignment operator ensure an object is always properly constructed.

The Law of The Big Three

A class with any of (destructor, assignment operator, copy constructor) generally needs all 3. For more information see http://www.parashift.com/c++-faq-lite/coding-standards.html#[25.9].

Example

The default class template with all required methods. An example using default values:
class Planet
{
public:
  /** The following is the default constructor if no arguments are supplied.
   */
  Planet(int radius= 5);

  // Use compiler-generated copy constructor, assignment, and destructor.
  // Planet(const Planet&);
  // Planet& operator=(const Planet&);
  // ~Planet();
};

Method Layout

The approach used is to place a comment block before each method that can be extracted by a tool and be made part of the class documentation. Here we'll use Doxygen which supports the Javadoc format. See the Doxygen documentation for a list of attributes supported by the document generator.

Method Header

Every parameter should be documented. Every return code should be documented. All exceptions should be documented. Use complete sentences when describing attributes. Make sure to think about what other resources developers may need and encode them in with the @see attributes.
  /** Assignment operator.
   *
   *
   * @param val The value to assign to this object.
   * @exception LibaryException The explanation for the exception.
   * @return A reference to this object.
   */
   XX&                     operator=(XX& val);

Additional Sections

In addition to the standard attribute set, the following sections can be included in the documentation:
  1. PRECONDITION
    Document what must have happened for the object to be in a state where the method can be called.

  2. WARNING
    Document anything unusual users should know about this method.

  3. LOCK REQUIRED
    Some methods require a semaphore be acquired before using the method. When this is the case use lock required and specify the name of the lock.

  4. EXAMPLES
    Include exampes of how to use a method. A picture says a 1000 words, a good example answers a 1000 questions.

For example:
  /** Copy one string to another.
   *
   * PRECONDITION

   * REQUIRE(from != 0)
   * REQUIRE(to != 0)
   *
   * WARNING

   * The to buffer must be long enough to hold
   * the entire from buffer.
   *
   * EXAMPLES

   *
   * strcpy(somebuf, "test")
   *

   *
   * @param from The string to copy.
   * @param to The buffer to copy the string to.
   *
   * @return void
   */
   void  strcpy(const char* from, char* to);

Common Exception Sections

If the same exceptions are being used in a number of methods, then the exceptions can be documented once in the class header and referred to from the method documentation.

Formatting Methods with Multiple Arguments

We should try and make methods have as few parameters as possible. If you find yourself passing the same variables to every method then that variable should probably be part of the class. When a method does have a lot of parameters format it like this:
   int                     AnyMethod(
                              int   arg1,
                              int   arg2,
                              int   arg3,
                              int   arg4);

Different Accessor Styles

Why Accessors?

Access methods provide access to the physical or logical attributes of an object. Accessing an object's attributes directly as we do for C structures is greatly discouraged in C++. We disallow direct access to attributes to break dependencies, the reason we do most things. Directly accessing an attribute exposes implementation details about the object.

To see why ask yourself:

  • What if the object decided to provide the attribute in a way other than physical containment?
  • What if it had to do a database lookup for the attribute?
  • What if a different object now contained the attribute?
If any of the above changed code would break. An object makes a contract with the user to provide access to a particular attribute; it should not promise how it gets those attributes. Accessing a physical attribute makes such a promise.

Accessors Considered Somewhat Harmful

At least in the public interface having accessors many times is an admission of failure, a failure to make an object's interface complete. At the protected or private level accessors are fine as these are the implementation levels of a class.

Implementing Accessors

There are three major idioms for creating accessors.

Get/Set

   class X
   {
   public:
      int    GetAge() const     { return mAge; }
      void   SetAge(int age)    { mAge= age; }
   private:
      int mAge;
   }
The problem with Get/Set is twofold:
  • It's ugly. Get and Set are strewn throughout the code cluttering it up.
  • It doesn't treat attributes as objects in their own right. An object will have an assignment operator. Why shouldn't age be an object and have its own assignment operator?
One benefit, that it shares with the One Method Name, is when used with messages the set method can transparently transform from native machine representations to network byte order.

One Method Name

   class X
   {
   public:
      int    Age() const     { return mAge; }
      void   Age(int age)    { mAge= age; }
   private:
      int mAge;
   }
Similar to Get/Set but cleaner. Use this approach when not using the Attributes as Objects approach.

Attributes as Objects

   class X
   {
   public:
      int              Age() const     { return mAge; }
      int&             rAge()          { return mAge; }

      const String&    Name() const    { return mName; }
      String&          rName()         { return mName; }
   private:
      int              mAge;
      String           mName;
   }
The above two attribute examples shows the strength and weakness of the Attributes as Objects approach.

When using an int type, which is not a real object, the int is set directly because rAge() returns a reference. The object can do no checking of the value or do any representation reformatting. For many simple attributes, however, these are not horrible restrictions. A way around this problem is to use a class wrapper around base types like int.

When an object is returned as reference its = operator is invoked to complete the assignment. For example:

   X x;
   x.rName()= "test";
This approach is also more consistent with the object philosophy: the object should do it. An object's = operator can do all the checks for the assignment and it's done once in one place, in the object, where it belongs. It's also clean from a name perspective.

When possible use this approach to attribute access.

Init Idiom for Initializing Objects

  • Objects with multiple constructors and/or multiple attributes should define a private Init() method to initialize all attributes. If the number of different member variables is small then this idiom may not be a big win and C++'s constructor initialization syntax can/should be used.

Justification

  • When using C++'s ability to initialize variables in the constructor it's difficult with multiple constructors and/or multiple attributes to make sure all attributes are initialized. When an attribute is added or changed almost invariably we'll miss changing a constructor.

    It's better to define one method, Init(), that initializes all possible attributes. Init() should be called first from every constructor.

  • The Init() idiom cannot be used in two cases where initialization from a constructor is required:
    • constructing a member object
    • initializing a member attribute that is a reference

Example

   class Test
   {
   public:
      Test()
      {
         Init();   // Call to common object initializer
      }

      Test(int val)
      {
         Init();   // Call to common object initializer
         mVal= val;
      }

   private:
      int      mVal;
      String*  mpName;

      void Init()
      {
         mVal  = 0;
         mpName= 0;
      }
   }

Since the number of member variables is small, this might be better
written as:

   class Test
   {
   public:
     Test(int val = 0, String* name = 0)
       : mVal(val), mpName(name) {}
   private:
     int         mVal;
     String*     mpName;
   };

Initialize all Variables

  • You shall always initialize variables. Always. Every time.

Justification

  • More problems than you can believe are eventually traced back to a pointer or variable left uninitialized. C++ tends to encourage this by spreading initialization to each constructor. See Init Idiom for Initializing Objects .

Minimize Inlines

Minimize inlining in declarations or inlining in general. As soon as you put your C++ code in a shared library which you want to maintain compatibility with in the future, inlined code is a major pain in the butt. It's not worth it, for most cases.

Think About What Work to do in Constructors

Should you do work that can fail in constructors? If you have a compiler that does not support exceptions (or thread safe exceptions if it matters to you) then the answer is definitely no. Go directly to Do Work in Open. If your compiler supports exception then go to Do Work in Constructor. There are still reasons to use an Open method even with exceptions.

Use Open Reasons

  1. It is difficult to write exception safe code in constructor. It's possible to throw an exception and not destruct objects allocated in the constructor. Use of auto_ptr can help prevent this problem.
  2. Some compilers do not support thread safe exceptions on all platforms.
  3. Virtual methods are not available in base classes. If the base class is expecting a virtual method implemented by derived classes to be available during construction then initialization must follow construction. This is common in frameworks.
  4. Larger scale state machines may dictate when initialization should occur. An object may contain numerous other objects that may have complex initialization conditions. In this case we could wait to construct objects but then we always have to worry about null pointers.
  5. If deletion is needed to free resources we still may want to keep the state around for debugging or statistics or as a supplier of information for other objects.

Do Work in Constructor

With exceptions work done in the constructor can signal failure so it is fine to perform real work in the constructor. This is the guru endorced approach as a matter of fact. But there are reasons to still use an open style approach.

The constructor code must still be very careful not to leak resources in the constructor. It's possible to throw an exception and not destruct objects allocated in the constructor.

There is a pattern called Resource Acquisition as Initialization that says all initialization is performed in the constructor and released in the destructor. The idea is that this is a safer approach because it should reduce resource leaks.

Do Work in Open

Do not do any real work in an object's constructor. Inside a constructor initialize variables only and/or do only actions that can't fail.

Create an Open() method for an object which completes construction. Open() should be called after object instantiation.

Example

   class Device
   {
   public:
      Device()    { /* initialize and other stuff */ }
      int Open()  { return FAIL; }
   };

   Device dev;
   if (FAIL == dev.Open()) exit(1);

Don't Over Use Operators

C++ allows the overloading of all kinds of weird operators. Unless you are building a class directly related to math there are very few operators you should override. Only override an operator when the semantics will be clear to users.

Justification

  • Very few people will have the same intuition as you about what a particular operator will do.

Thin vs. Thick Class Interfaces

How many methods should an object have? The right answer of course is just the right amount, we'll call this the Goldilocks level. But what is the Goldilocks level? It doesn't exist. You need to make the right judgment for your situation, which is really what programmers are for :-)

The two extremes are thin classes versus thick classes. Thin classes are minimalist classes. Thin classes have as few methods as possible. The expectation is users will derive their own class from the thin class adding any needed methods.

While thin classes may seem "clean" they really aren't. You can't do much with a thin class. Its main purpose is setting up a type. Since thin classes have so little functionality many programmers in a project will create derived classes with everyone adding basically the same methods. This leads to code duplication and maintenance problems which is part of the reason we use objects in the first place. The obvious solution is to push methods up to the base class. Push enough methods up to the base class and you get thick classes.

Thick classes have a lot of methods. If you can think of it a thick class will have it. Why is this a problem? It may not be. If the methods are directly related to the class then there's no real problem with the class containing them. The problem is people get lazy and start adding methods to a class that are related to the class in some willow wispy way, but would be better factored out into another class. Judgment comes into play again.

Thick classes have other problems. As classes get larger they may become harder to understand. They also become harder to debug as interactions become less predictable. And when a method is changed that you don't use or care about your code will still have to be recompiled, possibly retested, and rereleased.

Short Methods

  • Methods should limit themselves to a single page of code.

Justification

  • The idea is that the each method represents a technique for achieving a single objective.
  • Most arguments of inefficiency turn out to be false in the long run.
  • True function calls are slower than not, but there needs to a thought out decision (see premature optimization).

In a Source file Indicate if a Method is Static or Virtual

In a source file you can't tell a method is static or virtual because this information is in the header file. Knowing this information in a source file is useful and can be communicated using comments:
/*virtual*/ void
Class::method()
{
}

/*static*/ void
Class::method()
{
}

I've only seen this format once in source code, but it is interesting enough that I thought I would include here for your consideration. Notice how the method name sits alone on its own line. This looks more like C code and looks very clean for some reason.

Process

Use a Design Notation and Process

Programmers need to have a common language for talking about coding, designs, and the software process in general. This is critical to project success.

Any project brings together people of widely varying skills, knowledge, and experience. Even if everyone on a project is a genius you will still fail because people will endlessly talk past each other because there is no common language and processes binding the project together. All you'll get is massive fights, burnout, and little progress. If you send your group to training they may not come back seasoned experts but at least your group will all be on the same page; a team.

There are many popular methodologies out there. The point is to do some research, pick a method, train your people on it, and use it. Take a look at the top of this page for links to various methodologies.

You may find an Agile methodology to your liking. For more information see http://www.possibility.com/epowiki/Wiki.jsp?page=agile.

You may find the CRC (class responsibility cards) approach to teasing out a design useful. Many others have. It is an informal approach encouraging team cooperation and focusing on objects doing things rather than objects having attributes. There's even a whole book on it: Using CRC Cards by Nancy M. Wilkinson.

Using Use Cases

A use case is a generic description of an entire transaction involving several objects. A use case can also describe the behaviour of a set of objects, such as an organization. A use case model thus presents a collection of use cases and is typically used to specify the behavior of a whole application system together with one or more external actors that interact with the system.

An individual use case may have a name (although it is typically not a simple name). Its meaning is often written as an informal text description of the external actors and the sequences of events between objects that make up the transaction. Use cases can include other use cases as part of their behaviour.

Requirements Capture

Use cases attempt to capture the requirements for a system in an understandable form. The idea is by running through a set of use case we can verify that the system is doing what it should be doing.

Have as many use cases as needed to describe what a system needs to accomplish.

The Process

  • Start by understanding the system you are trying to build.
  • Create a set of use cases describing how the system is to be used by all its different audiences.
  • Create a class and object model for the system.
  • Run through all the use cases to make sure your model can handle all the cases. Update your model and create new use cases as necessary.

Using Stories

A user story consists of just a phrase or a few sentences written down, announcing something the user wants to do. The user story is not used as a requirements specification, but as a marker for a future conversation.

  • User stories are used as tokens in the planning process after assessment of business value and risk. The customer prioritizes the stories and schedules them for implementation.
  • User Story is a story about how the system is supposed to solve a problem. Each UserStory represents a chunk of functionality that is coherent in some way to the customer.
  • User Story is a token that stands in for a conversation whose purpose is roughly equivalent to that of a UseCase. The content of a UserStory is often an abbreviated version of the main success scenario of a UseCase, but it doesn't have to be.
  • User Story is a marker that reminds the customer and developers what to talk about, and also inspires them in exploring some particular area of business value. A UserStory, then, contains whatever the customer thinks is necessary to jog the memories and inspirations of those who will later explore the story. Expertise in the business area will help the customer decide what are the essential "jogs" to record in the initial UserStory marker.
  • Developers should be able to implement a story in 2-4 weeks. The iteration length depends on you project.

A Story Is

  • Testable -You can write automatic tests to detect the presence of the story.
  • Progress - The customers side of the team is willing to accept the story as a sign of progress toward their larger goal.
  • Bite-sized - The story should be completable within the iteration.
  • Estimatable - The technical side of the team must be able to guess how much of the team's time the story will require to get working.

How It All Fits Together

There are a lot of entrants in this part of the development process. We have user stories, use cases, task analysis, task cases, usage scenarios, etc and etc.

It seems we need something more detailed than requirements as requirements tend to be fairly high level and are usually not directly useful for development. A requirement will cut across many subsystems and each of the cross-cuts must be identified. Thus requirements need to go through refinement and elaboration to be used in development.

If we get too detailed then we get bogged down in detailed design before we are ready. If we aren't detailed enough then we can't know if our requirements are met. Nor can we convince ourselves that we know how to solve the problem.

User Stories Need to be Fleshed Out

User Stories are symbolic of the stuff that needs to get done. They are like the reminder notes you make to yourself. In the detailed design these can be expanded out to uses cases, to more detailed notes, state machines, etc. Or TestDrivenDesign practices could be used to flesh out the detailed implementation.

For more information see http://www.possibility.com/epowiki/Edit.jsp?page=UserStory.

Unified Modeling Language

The Unified Modeling Language is too large to present here. Fortunately you can see it at Rational's web site. Since you do need a modeling language UML is a safe choice. It combines features from several methods into one unified language. Remember all languages and methods are open to local customization. If their language is too complex then use the parts you and your project feel they need and junk the rest.

Code Reviews

If you can make a formal code review work then my hat is off to you. Code reviews can be very useful. Unfortunately they often degrade into nit picking sessions and endless arguments about silly things. They also tend to take a lot of people's time for a questionable payback.

My god he's questioning code reviews, he's not an engineer!

Not really, it's the form of code reviews and how they fit into normally late chaotic projects is what is being questioned.

First, code reviews are way too late to do much of anything useful. What needs reviewing are requirements and design. This is where you will get more bang for the buck.

Get all relevant people in a room. Lock them in. Go over the class design and requirements until the former is good and the latter is being met. Having all the relevant people in the room makes this process a deep fruitful one as questions can be immediately answered and issues immediately explored. Usually only a couple of such meetings are necessary.

If the above process is done well coding will take care of itself. If you find problems in the code review the best you can usually do is a rewrite after someone has sunk a ton of time and effort into making the code "work."

You will still want to do a code review, just do it offline. Have a couple people you trust read the code in question and simply make comments to the programmer. Then the programmer and reviewers can discuss issues and work them out. Email and quick pointed discussions work well. This approach meets the goals and doesn't take the time of 6 people to do it.

Create a Source Code Control System Early and Not Often

A common build system and source code control system should be put in place as early as possible in a project's lifecycle, preferably before anyone starts coding. Source code control is the structural glue binding a project together. If programmers can't easily use each other's products then you'll never be able to make a good reproducible build and people will piss away a lot of time. It's also hell converting rogue build environments to a standard system. But it seems the right of passage for every project to build their own custom environment that never quite works right.

Some issues to keep in mind:

  • Appoint a buildmaster who is in charge of the build environment and making builds. If you have a tools group so much the better as build environments generally generate a lot of tools.
  • Programmers should generally leave tagging and other advanced operations to the buildmaster.
  • Shared source environments like CVS usually work best in largish projects.
  • Makefiles should be completely templated so developers need no makefile expertise.
  • You will need multiple builds of the same code meaning programmers will want to have the option of building against optimized code versions, debugger code versions, code with the -DDEBUG macro defined and versions without, etc. One way to do this is have everyone check out all code and do a total rebuild. The other better way is for the buildmaster to build all necessary versions of code so programmers can check out only the code which they are modifying. The idea is that each combination of compile flags and macros defines a build.
  • Dependency checking should work!
  • Maintain a cycle of builds. Create a nightly build so problems can be detected early. Create a valid build every week or for every feature set that is guaranteed to be good. Every release to QA or to customers should be tagged and available in the tree. Official builds should be official because they contain a certain set of features that assumably meet a schedule, not because it's a certain date.
  • If you use CVS use a reference tree approach. With this approach a master build tree is kept of various builds. Programmers checkout source against the build they are working on. They only checkout what they need because the make system uses the build for anything not found locally. Using the -I and -L flags makes this system easy to setup. Search locally for any files and libraries then search in the reference build. This approach saves on disk space and build time.
  • Get a lot of disk space. With disk space as cheap it is there is no reason not to keep plenty of builds around and have enough space for programmers to compile.
  • Make simple things simple. It should be dead simple and well documented on how to:
    • check out modules to build
    • how to change files
    • how to setup makefiles
    • how to add new modules into the system
    • how to delete modules and files
    • how to check in changes
    • what are the available libraries and include files
    • how to get the build environment including all compilers and other tools

    Make a web page or document or whatever. New programmers shouldn't have to go around begging for build secrets from the old timers.

  • Use the idea of services. If using a feature requires linking to 3 libraries think about making a service specifiable in the makefile programmers can use instead of having to know the exact libraries needed. With this approach the libraries making up a service can be changed in a based makefile without requiring changes in all makefiles.
  • On checkins log comments should be useful. These comments should be collected every night and sent to interested parties.

Sources

If you have the money many projects have found Clear Case a good system. Perfectly workable systems have been build on top of GNU make and CVS. CVS is a freeware build environment built on top of RCS. Its main difference from RCS is that is supports a shared file model to building software.

Create a Bug Tracking System Early and Not Often

The earlier people get used to using a bug tracking system the better. If you are 3/4 through a project and then install a bug tracking system it won't be used. You need to install a bug tracking system early so people will use it.

Programmers generally resist bug tracking, yet when used correctly it can really help a project:

  • Problems aren't dropped on the floor.
  • Problems are automatically routed to responsible individuals.
  • The lifecycle of a problem is tracked so people can argue back and forth with good information.
  • Managers can make the big schedule and staffing decisions based on the number of and types of bugs in the system.
  • Configuration management has a hope of matching patches back to the problems they fix.
  • QA and technical support have a communication medium with developers.
Not sexy things, just good solid project improvements.

FYI, it's not a good idea to reward people by the number of bugs they fix :-)

Source code control should be linked to the bug tracking system. During the part of a project where source is frozen before a release only checkins accompanied by a valid bug ID should be accepted. And when code is changed to fix a bug the bug ID should be included in the checkin comments.

Sources

Several projects have found DDTS a workable system. There is also a GNU bug tracking system available. Roll your own is a popular option but using an existing system seems more cost efficient.

RCS Keywords, Change Log, and History Policy

When using RCS directly this policy must change, but when using other source code control systems like CVS that support RCS style keywords:
  • Do not use RCS keywords within files.
  • Do not keep a change history in files.
  • Do not keep author information in files.

Justification

  • The reasoning is your source control system already keeps all this information. There is no reason to clutter up source files with duplicate information that:
    • makes the files larger
    • makes doing diffs difficult as non source code lines change
    • makes the entry into the file dozens of lines lower in the file which makes a search or jump necessary for each file
    • is easily available from the source code control system and does not need embedding in the file
  • When files must be sent to other organizations the comments may contain internal details that should not be exposed to outsiders.

Honor Responsibilities

Responsibility for software modules is scoped. Modules are either the responsibility of a particular person or are common. Honor this division of responsibility. Don't go changing things that aren't your responsibility to change. Only mistakes and hard feelings will result.

Face it, if you don't own a piece of code you can't possibly be in a position to change it. There's too much context. Assumptions seemingly reasonable to you may be totally wrong. If you need a change simply ask the responsible person to change it. Or ask them if it is OK to make such-n-such a change. If they say OK then go ahead, otherwise holster your editor.

Every rule has exceptions. If it's 3 in the morning and you need to make a change to make a deliverable then you have to do it. If someone is on vacation and no one has been assigned their module then you have to do it. If you make changes in other people's code try and use the same style they have adopted.

Programmers need to mark with comments code that is particularly sensitive to change. If code in one area requires changes to code in an another area then say so. If changing data formats will cause conflicts with persistent stores or remote message sending then say so. If you are trying to minimize memory usage or achieve some other end then say so. Not everyone is as brilliant as you.

The worst sin is to flit through the system changing bits of code to match your coding style. If someone isn't coding to the standards then ask them or ask your manager to ask them to code to the standards. Use common courtesy.

Code with common responsibility should be treated with care. Resist making radical changes as the conflicts will be hard to resolve. Put comments in the file on how the file should be extended so everyone will follow the same rules. Try and use a common structure in all common files so people don't have to guess on where to find things and how to make changes. Checkin changes as soon as possible so conflicts don't build up.

As an aside, module responsibilities must also be assigned for bug tracking purposes.

Process Automation

It's a sad fact of human nature that if you don't measure it or check for it: it won't happen. The implication is you must automate as much of the development process as possible and provide direct feedback to developers on specific issues that they can fix.

Process automation also frees up developers to do real work because they don't have to babysit builds and other project time sinks.

Automated Builds and Error Assignment

Create an automated build system that can create nightly builds, parse the build errors, assign the errors to developers, and email developers their particular errors so they can fix them.

This is the best way to maintain a clean build. Make sure the list of all errors for a build is available for everyone to see so everyone can see everyone elses errors. The goal is replace a blaim culture with a culture that tries to get things right and fixes them when they are wrong. Immediate feedback makes this possible.

Automated Code Checking

As part of the automated build process you can check for coding standard violations and for other problems. If you don't check for it people will naturally do their own thing. Code reviews aren't good enough to keep the code correct. With a tool like Abraxis Code Check you can check the code for a lot of potential problems.

This feature like the automated error assignment makes problems immediately visible and immediately correctable, all without a lot of blame and shame.

Documentation Extraction

Related to this principle is the need to automatically extract documentation from the source code and make it available on line for everyone to use. If you don't do this documentation will be seen as generally useless and developers won't put as much effort into it. Making the documentation visible encourages people to do a better job.

Connect Source Code Control System and Bug Tracking System

  1. When a check-in of source code fixes a bug then have the check-in automatically tell the bug tracking system that the bug was fixed.
  2. When a bug fix is built in a build change the state of the bug to BUILT.
  3. Have a submit tool where people ask judges of they can submit a bug fix on the branch.
There are lots of things you can do depending on how complicated your environment. The more complicated the environment the more you should think about connecting all systems together.

Tools Agreement

The reality of different tool preferences is something to deal with explicitly and openly. Tools include IDEs, languages, editors, make program, source code control, bug system, debuggers, test framework, etc. Some tool decisions by their nature must be project wide, other decisions can be customized per developer.

A split might also be done by who is performing the build. For example, an IDE should be able to used in local builds, but the make program would be used for nightly and release builds.

Certain things are easy/trivial/useful with one tool, but hard/complicated/stupid with another tool. Unstated tool assumptions can be the source of a lot of confusion. "Get a better editor" is not always a workable response, though sometimes that's all there is to it!

Non-Blocking Scheduling

Schedules are lies. Schedules suck. Yes, yes, yes. But we still need them.

The most effective scheduling rule i've used is to schedule so as to unblock others. The idea is to complete the portions of a feature that will unblock those dependent on you. This way development moves along smoothly because more lines of development can be active at a time. For example, instead of implementing the entire database, implement the simple interface and stub it out. People can work for a very long time this way using that portion of the feature that caused others not to block. Plus it's a form of rapid prototyping because you get immediate feedback on these parts. Don't worry about the quality of implementation because it doesn't matter yet.

Using Personas

Personas are a powerful design tool, especially when combined with responsibility driven design. Cooper's personas are:.
simply pretend users of the system you're building. You describe
them, in a surprising amount of detail, and then design your
system for them.

I have a standard set of personas that i consider when creating a design/architecture that don't seem to be common. When you write code their are a lot of personas looking over your shoulder:

  1. other programmers using the code
  2. maintenance
  3. extension
  4. documentation group
  5. training group
  6. code review
  7. test and validation
  8. manufacturing
  9. field support
  10. first and second line technical support
  11. live debugging
  12. post crash debugging
  13. build system (documentation generation and automatic testing)
  14. unit testing
  15. system testing
  16. source code control
  17. code readers
  18. legal

You are much more careful and more thorough when you really thing about all the personas, all the different people and all their different roles and purposes.

Use a Continuous Build System

Setup a continuous build system for your group. It should build all new check-ins and run the unit tests for each change. If you have the resources run a smoke test for each build as well.

If there's a failure email should go out to the developer's who made the check-ins.

This policy will allow you to catch errors as early as possible which will make your system more stable over time.

Code in the Dominant Style

Let's say someone invites you into your home as a guest. How do you act? Do you act as you do at home or do you try and fit in with the household in which you are staying? You try to fit in of course.

So when you have to write code for a new codebase do you 1) write code however you want or 2) write code like the code that already exists?

When writing code in a new codebase write code how the code is already written. Don't use your own style. Take some time, look around, and see how the code is written. To come in like a bull in a china shop is the mark of someone who is not a professional developer.

Run Unit Tests Before Every Check-in

Before every check-in developers must run unit tests to verify all unit tests should pass. This prevents letting any errors creep in.

Finding, debugging, and handling errors is a tremendous drain on the development process so we want have a solid working system from the start. The start means your code that you check-in should always work and we know it works by running unit tests and smoke tests if your organization has them.

Formatting

Braces {} Policy

Brace Placement

Of the three major brace placement strategies two are acceptable, with the first one listed being preferable:
  • Place brace under and inline with keywords:
       if (condition)        while (condition)
   {                     {
      ...                   ...
   }                     }
  • Traditional Unix policy of placing the initial brace on the same line as the keyword and the trailing brace inline on its own line with the keyword:
       if (condition) {      while (condition) {
      ...                   ...
   }                     }

Justification

  • Another religious issue of great debate solved by compromise. Either form is acceptable, many people, however, find the first form more pleasant. Why is the topic of many psychological studies.

    There are more reasons than psychological for preferring the first style. If you use an editor (such as vi) that supports brace matching, the first is a much better style. Why? Let's say you have a large block of code and want to know where the block ends. You move to the first brace hit a key and the editor finds the matching brace. Example:

         if (very_long_condition && second_very_long_condition)
     {
        ...
     }
     else if (...)
     {
    ..
     }
    To move from block to block you just need to use cursor down and your brace matching key. No need to move to the end of the line to match a brace then jerk back and forth.

When Braces are Needed

All if, while and do statements must either have braces or be on a single line.

Always Uses Braces Form

All if, while and do statements require braces even if there is only a single statement within the braces. For example:
if (1 == somevalue)
{
   somevalue = 2;
}

Justification

It ensures that when someone adds a line of code later there are already braces and they don't forget. It provides a more consistent look. This doesn't affect execution speed. It's easy to do.

One Line Form

if (1 == somevalue) somevalue = 2;

Justification

It provides safety when adding new lines while maintainng a compact readable form.

Add Comments to Closing Braces

Adding a comment to closing braces can help when you are reading code because you don't have to find the begin brace to know what is going on.
while(1)
{
   if (valid)
   {

   } // if valid
   else
   {
   } // not valid

} // end forever

Consider Screen Size Limits

Some people like blocks to fit within a common screen size so scrolling is not necessary when reading code.

Indentation/Tabs/Space Policy

  • Indent using 3, 4, or 8 spaces for each level.
  • Do not use tabs, use spaces. Most editors can substitute spaces for tabs.
  • Tabs should be fixed at 8 spaces. Don't set tabs to a different spacing, uses spaces instead.
  • Indent as much as needed, but no more. There are no arbitrary rules as to the maximum indenting level. If the indenting level is more than 4 or 5 levels you may think about factoring out code.

Justification

  • Tabs aren't used because 8 space indentation severely limits the number of indentation levels one can have. The argument that if this is a problem you have too many indentation levels has some force, but real code can often be three or more levels deep. Changing a tab to be less than 8 spaces is a problem because that setting is usually local. When someone prints the source tabs will be 8 characters and the code will look horrible. Same for people using other editors. Which is why we use spaces...
  • When people using different tab settings the code is impossible to read or print, which is why spaces are preferable to tabs.
  • Nobody can ever agree on the correct number of spaces, just be consistent. In general people have found 3 or 4 spaces per indentation level workable.
  • As much as people would like to limit the maximum indentation levels it never seems to work in general. We'll trust that programmers will choose wisely how deep to nest code.

Example

   void
   func()
   {
      if (something bad)
      {
         if (another thing bad)
         {
            while (more input)
            {
            }
         }
      }
   }

Parens () with Key Words and Functions Policy

  • Do not put parens next to keywords. Put a space between.
  • Do put parens next to function names.
  • Do not use parens in return statements when it's not necessary.

Justification

  • Keywords are not functions. By putting parens next to keywords keywords and function names are made to look alike.

Example

    if (condition)
    {
    }

    while (condition)
    {
    }

    strcpy(s, s1);

    return 1;

A Line Should Not Exceed 78 Characters

  • Lines should not exceed 78 characters.

Justification

  • Even though with big monitors we stretch windows wide our printers can only print so wide. And we still need to print code.
  • The wider the window the fewer windows we can have on a screen. More windows is better than wider windows.
  • We even view and print diff output correctly on all terminals and printers.

If Then Else Formatting

Layout

It's up to the programmer. Different bracing styles will yield slightly different looks. One common approach is:
   if (condition)                 // Comment
   {
   }
   else if (condition)            // Comment
   {
   }
   else                           // Comment
   {
   }
If you have else if statements then it is usually a good idea to always have an else block for finding unhandled cases. Maybe put a log message in the else even if there is no corrective action taken.

Condition Format

Always put the constant on the left hand side of an equality/inequality comparison. For example:

if ( 6 == errorNum ) ...

One reason is that if you leave out one of the = signs, the compiler will find the error for you. A second reason is that it puts the value you are looking for right up front where you can find it instead of buried at the end of your expression. It takes a little time to get used to this format, but then it really gets useful.

switch Formatting

  • Falling through a case statement into the next case statement shall be permitted as long as a comment is included.
  • The default case should always be present and trigger an error if it should not be reached, yet is reached.
  • If you need to create variables put all the code in a block.

Example

   switch (...)
   {
      case 1:
         ...
      // FALL THROUGH

      case 2:
      {
         int v;
         ...
      }
      break;

      default:
   }

Use of goto,continue,break and ?:

Goto

Goto statements should be used sparingly, as in any well-structured code. The goto debates are boring so we won't go into them here. The main place where they can be usefully employed is to break out of several levels of switch, for, and while nesting, although the need to do such a thing may indicate that the inner constructs should be broken out into a separate function, with a success/failure return code.

   for (...)
   {
      while (...)
      {
         ...
         if (disaster)
            goto error;
      }
   }
   ...
error:
   clean up the mess

When a goto is necessary the accompanying label should be alone on a line and to the left of the code that follows. The goto should be commented (possibly in the block header) as to its utility and purpose.

Continue and Break

Continue and break are really disguised gotos so they are covered here.

Continue and break like goto should be used sparingly as they are magic in code. With a simple spell the reader is beamed to god knows where for some usually undocumented reason.

The two main problems with continue are:

  • It may bypass the test condition
  • It may bypass the increment/decrement expression

Consider the following example where both problems occur:

while (TRUE)
{
   ...
   // A lot of code
   ...
   if (/* some condition */) {
      continue;
   }
   ...
   // A lot of code
   ...
   if ( i++ > STOP_VALUE) break;
}
Note: "A lot of code" is necessary in order that the problem cannot be caught easily by the programmer.

From the above example, a further rule may be given: Mixing continue with break in the same loop is a sure way to disaster.

?:

The trouble is people usually try and stuff too much code in between the ? and :. Here are a couple of clarity rules to follow:
  • Put the condition in parens so as to set it off from other code
  • If possible, the actions for the test should be simple functions.
  • Put the action for the then and else statement on a separate line unless it can be clearly put on one line.

Example

   (condition) ? funct1() : func2();

   or

   (condition)
      ? long statement
      : another long statement;

One Statement Per Line

There should be only one statement per line unless the statements are very closely related.

The reasons are:

  1. The code is easier to read. Use some white space too. Nothing better than to read code that is one line after another with no white space or comments.

One Variable Per Line

Related to this is always define one variable per line:
Not:
char **a, *x;

Do:
char** a= 0;  // add doc
char*  x= 0;  // add doc
The reasons are:
  1. Documentation can be added for the variable on the line.
  2. It's clear that the variables are initialized.
  3. Declarations are clear which reduces the probablity of declaring a pointer when you meant to declare just a char.

Alignment of Declaration Blocks

  • Block of declarations should be aligned.

Justification

  • Clarity.
  • Similarly blocks of initialization of variables should be tabulated.
  • The ‘&’ and ‘*’ tokens should be adjacent to the type, not the name.

Example

   DWORD       mDword
   DWORD*      mpDword
   char*       mpChar
   char        mChar

   mDword   = 0;
   mpDword  = NULL;
   mpChar   = NULL;
   mChar    = 0;

Include static and virtual Key Words in Source File

If a method is made static or virtual in the header file then also include this information as a short comment in the source file. For example:
In .h file:

class PopcornPopper
{
public:
   virtual void Pop(void);
   static PopcornPopper* Singleton();
};

In .cpp file:

/*virtual*/ void
PopcornPopper::Pop(void)
{
}// Pop

/*static*/ PopcornPopper*
PopcornPopper::Singleton()
{
}// Singleton

A source file doesn't contain as much information as the header file because the virtual and static keywords are dropped from method implementations. This is very useful information to have when reading code, so we want to include it in the source file.

Document Null Statements

Always document a null body for a for or while statement so that it is clear that the null body is intentional and not missing code.

   while (*dest++ = *src++)
      ;         // VOID

Exceptions

Create One Exception for Each Library

Creating very elaborate exception hierarchies is a waste of time. Nobody ends of caring and all the effort goes to waste. Instead, create one exception per library or namespace and have an exception reason within that exception to indicate the type of the exception.

For example, for your OS encapsulation libary, make an exception called OsencapException.

By using just one exception you make it easy for people using your code to catch your exception. If you have thousand exceptions it is impossible for anyone to handle those. And as exceptions are added you will break existing code that thinks they are handling all your exceptions.

If you think you need to create derived exceptions the derive them from your libraries' base exception class. That allows your library users to still catch the base class exception if they wish.

Most exceptions are things the code can't do anything about anyway, so creating lots of exceptions to express every little thing that can go wrong with a separate class and is time confusing and confusing for the user. It's not necessary.

Make sure to document that an exception is being thrown in you method comment header.

Create a macro for throwing the exception so that you can transparently include __FILE__ and __LINE__ in the exception. This information is usefull when debugging. You can also automatically take a time stamp and get the thread ID.

The exception should take a one or two string arguments so developers can include all the information they need in the exception without having to create a derived exception to add a few more pieces of data.

Include a stack trace of where the exception happened so you can log the stack trace.

Throwing an exception should take only one line of code. Don't uglify your code with tons of exception handling. Make a macro that looks like:

THROW_NAMESPACE_EX_IF(cond, msg1, msg2, reason);
The condition causes the exception to be thrown when true. Msg1 and msg2 are local context. Reason is the specific error code for the exception, if you think you need it.

Don't worry about running out of memory errors. Go ahead and allocate memory in your exception code. If you are running out of memory that should be taken care of in the bad_alloc exception handler.

Selecting Between Exceptions And Asserts

If there is reason to believe that an important test could fail in the released product, it should not be tested with an assert, use an exception instead.

Two useful questions to ask yourself are:

  1. Should this error ever happen?
  2. Is the error so unexpected or potentially damaging that the system should failover if it is found?

When at the boundry of user input we should expect invalid parameters and thus should use an exception instead of DBC.

Exception Definition

There are as many definitions of exception as there are programmers:

The Clean Code Definition

Write a function as if everything worked.
Any error handling that prevents the code from looking
like this is an exception.

The Design by Contract Definition

When an operation is invoked with it's pre-condition
satisfied, yet it cannot return with its post-condition
satisfied.

There are those who say that exceptions should NOT be used to catch things like range errors. Here's why:

Range errors fall into a class of problem that are called Programmer Errors. Programmer errors are things that should NOT occur in a Bug Free Program. Ideally, range type problems are one of those things that should be caught during development. It is for these problems that we have Design By Contract.

You program defensively by putting lots of asserts in to make sure that your program is functioning as expected. Then when you've committed a bug, like not checking user input properly, allowing an invalid index into an array to be derived, the assert macro catches it, and the program does a gracful crash.

Exceptions, on the other hand, should be used to catch problems that would arise even in a Bug Free Program, i.e. Exceptional Circumstances. The most perfect program can still be afflicted by things like shortage of memory and other resources, communications errors, and file problems. When one of these things occurs, an exception should be thrown, and caught at a point where the program can either deal with the problem, or close gracefully.

The real theory behind exceptions is to force the programmer to anticipate things the programmer has no control over. Exceptions support the following kind of scenarios:

  1. Logic error in the middle of a database transaction. An exception would allow the program a chance to leave the database in a consistent state. A trashed database can be very expensive.
  2. Logic error in the middle of a program that is using a resource ike a modem and the program runs unattended. Exception handling would give the program a chance to hang up the modem connection, rather than sit there with an connection until it's discovered the next day, running up the phone bill.
  3. Logic error in the middle of something like a word processor. Consider the user that has been working for a couple of hours with unsaved edits, and your assert message pops up, and the last two hours of work are pretty much lost - causing the user some misery. If you used exception handling, you could at least give the user a chance to salvage the unsaved document in a different file as part of the cleanup.

Design-by-Contract (DBC)

A design technique developed by Bertrand Meyer for producing "bug free" systems.

Design by Contract (DBC) views the relationship between a class and its clients as formal agreement, expressing each party's rights and obligation. Rights and obligations are determined by a class' specification. Correctness can only be determined in reference to an object's specification. Specifications, in DBC, are expressed through assertions.

Design by Contract is a development approach where a specification is associated with every software element. These specifications (or contracts) govern the interaction of the element with the rest of the world. A contract takes form as a set of preconditions, postconditions, and invariants that are run as the system executes. The contract is published in the comment block of each method.

Assertion

A boolean statement that should never be false and, therefore, will only be false because of a bug. Typically assertions are checked only during debug and are not checked during production execution, although they can be left in when performance and memory size are not issues.

Design by Contract uses three kinds of assertions: post-conditions, pre-conditions, and invariants. Pre-conditions and post-conditions apply to operations. Invariants apply to the class.

Pre-condition

A statement of how we expect the world to be before we execute an operation. We might design a pre-condition of the "square" operation as this >= 0. Such a pre-condition says that it is an error to invoke "square" on a negative number.

Post-condition

A statement of what the world should look like after execution of an operation. For example, if we define the operation "square" on a number, the post-condition would take the form result = this *this. The post-condition is a useful way of saying what we do without saying how we do it- in other words, of separating interface from implementation.

Invariant

An invariant is an assertion about a class. For instance, an Account class may have an invariant that says that balance == sum(entries.amount()). The invariant is always true for all instances of the class. "Always" means whenever the object is available for an operation to be invoked. During an operation invariants may not be satisfied, but invariants must hold when an operation has finished.

Is pop'ing an empty stack an exception?

When using a stack data structure is calling pop on an empty stack an exception? This is the standard use case for the endless discussions on what is and what isn't an exception. The upshot is there is not a "right" answer. In practice, as long as the library designer completely documents their choice the library user shouldn't make a mistake.

Let's apply our definition of exception.

The Clean Code View

If you write your application assuming everything works then is pop returning null something you wouldn't expect? It seems reasonable that if you ask for something it may not be there.

Before calling pop the client can call isEmpty to check if there's anything in the list. It's still possible in a multi-threaded environment for the stack to return null even if isEmpty returns true.

So don't use an exception. Pop returns data so it should return null when empty.

The Design by Contract View

Should the library designer have as a pre-condition that pop shouldn't be called when the stack is empty? From an implementor point of view this could seem reasonable. After all, why should an operation be called when there's nothing there? So, pop could assert.

But, library designers should alway see their clients point-of-view. It's a bit extreme to cause a system crash because pop has an empty stack. There's nothing fundamentally or systemically wrong in this scenario that would justify an assert.

It appears the pre-conditions are satisfied for pop. Would any post-conditions not be satisfied? (this defines an exception in DBC). The stack should still be empty after the operation so no post-conditions would be violated. Thus, by definition an exception should not be thrown.

Be Careful Throwing Exceptions in Destructors

An object is presumably created to do something. Some of the changes made by an object should persist after an object dies (is destructed) and some changes should not. Take an object implementing a SQL query. If a database field is updated via the SQL object then that change should persist after the SQL objects dies. To do its work the SQL object probably created a database connection and allocated a bunch of memory. When the SQL object dies we want to close the database connection and deallocate the memory, otherwise if a lot of SQL objects are created we will run out of database connections and/or memory.

The logic might look like:

Sql::~Sql()
{
   delete connection;
   delete buffer;
}

Let's say an exception is thrown while deleting the database connection. Will the buffer be deleted? No. Exceptions are basically non-local gotos with stack cleanup. The code for deleting the buffer will never be executed creating a gaping resource leak.

Special care must be taken to catch exceptions which may occur during object destruction. Special care must also be taken to fully destruct an object when it throws an exception.

Usign RAII can help prevent many of not most of these type of errors.

Working With Libaries Using All Sorts of Different Policies

On many projects we have to work with C libraries that use error return codes, old C++ libraries that use error return codes, and newer C++ libraries that use exceptions. How should all these different approaches work together?

Ideally exceptions should be used where possible because that's the where all new code is going. But I also see new libraries being forced to use error return codes because the old libraries use them and application maintainers don't want you to use exceptions.

In C++ any method can throw an exception and you can't tell. In Java you know when you are not catching an exception. Not in C++. So what happens is that if a new library comes in and throws an exception, a crticial program may start failing because an exception is not caught. This may make everyone use error codes instead of exceptions.

Don't give into this old timer bias. Error return codes are virtually useless because they are competely ingorable. Instead, an application should just wrap their code in a big try catch block that catches all exceptions. That way an application won't die with prejudice and C++ libarary writers can make use of exceptions in their design.

Templates

Templates have never come up as an issue on project I've worked on, so my advice on templates isn't very useful.

Keeping templates simple has worked best. Just use templates for what they are good at, making a class work over different types. Not everything needs to be a template or needs to be perfectly generic. Because templates are so complex and so difficult to debug, it's best to use them only when necessary.

Template meta programming is too elite for me so I'll leave that to other people.

Namespaces

Create Unique Name Space Names

A namespace isn't of much use if it's not globally unique.

On way to make name spaces unique is to root tham at some naming

There are two basic strategies for naming: root that name at some naming authority, like the company name and division name. The Java convention is to use the inverse of the companys domain name for package names.

Personally that always seemed overkill to me, but does work in the large.

For internal packages just pick a short simple descriptive namespace that will likely prevent conflict within a project.

For externally visible code you'll probably need to the full domain style namespace just to be safe.

Don't Globally Define using

Don't place using namespace directive at global scope in a header file. This can cause lots of magic invisible conflicts that are hard to track. Keep using statements to implementation files.

For example, let's say I want to use boost's fabulous gregorian date library. In my class I want to return dates and use dates in methods. So my code ends up looking like:

#include "boost/date_time/gregorian/gregorian.hpp" //include all types plus i/o

class Calendar
{
public:
   boost::gregorian::date GetEventDate(void) const;
   void                   SetEventDate(boost::gregorian::date dateOfTheEvent);
};
Clearly these are long names and are noisy and are pain to use. So you are tempted to put at the top of you class file:
using namespace boost::gregorian;  // make namespace global for this file
This would mean you could just use "date" instead of "boost::gregorian::date". That's nice. But you can't do that. If you do you are making the decision for everyone who uses your class as well. They may have a conflict, "date" is a very common name afterall.

So, don't use using in you header file, but you can use it in your source file. Because it's your source file you can make the decision to use using to shorten up the names. This strategy preserves most of the convenience while being a good citizen.

Create Shortcut Names

You can use using to create aliases or shortcuts, that is names that are more convenient to access than long namespace names.

For example:

   using alias = a::very::long::namespace;
   class YourClass: public alias::TheirClass
   { ... }
Clearly this makes code both harder to read and easier to read. The code is harder to read because you as a programmer have to compile the using directive in your head and know what it means whenever it is used in the code.

The code is easier to read because it is less complex. Complex namespaces are distracting and confusing. Even with the additional cognitive load, using namespace alias make code a lot easier to read.

Miscellaneous

This section contains some miscellaneous do's and don'ts.

  • Don't use floating-point variables where discrete values are needed. Using a float for a loop counter is a great way to shoot yourself in the foot. Always test floating-point numbers as <= or >=, never use an exact comparison (== or !=).

  • Compilers have bugs. Common trouble spots include structure assignment and bit fields. You cannot generally predict which bugs a compiler has. You could write a program that avoids all constructs that are known broken on all compilers. You won't be able to write anything useful, you might still encounter bugs, and the compiler might get fixed in the meanwhile. Thus, you should write ``around'' compiler bugs only when you are forced to use a particular buggy compiler.

  • Do not rely on automatic beautifiers. The main person who benefits from good program style is the programmer him/herself, and especially in the early design of handwritten algorithms or pseudo-code. Automatic beautifiers can only be applied to complete, syntactically correct programs and hence are not available when the need for attention to white space and indentation is greatest. Programmers can do a better job of making clear the complete visual layout of a function or file, with the normal attention to detail of a careful programmer (in other words, some of the visual layout is dictated by intent rather than syntax and beautifiers cannot read minds). Sloppy programmers should learn to be careful programmers instead of relying on a beautifier to make their code readable. Finally, since beautifiers are non-trivial programs that must parse the source, a sophisticated beautifier is not worth the benefits gained by such a program. Beautifiers are best for gross formatting of machine-generated code.

  • Accidental omission of the second ``='' of the logical compare is a problem. The following is confusing and prone to error.
            if (abool= bbool) { ... }
    Does the programmer really mean assignment here? Often yes, but usually no. The solution is to just not do it, an inverse Nike philosophy. Instead use explicit tests and avoid assignment with an implicit test. The recommended form is to do the assignment before doing the test:

       abool= bbool;
       if (abool) { ... }

  • Modern compilers will put variables in registers automatically. Use the register sparingly to indicate the variables that you think are most critical. In extreme cases, mark the 2-4 most critical values as register and mark the rest as REGISTER. The latter can be #defined to register on those machines with many registers.

Be Const Correct

C++ provides the const key word to allow passing as parameters objects that cannot change to indicate when a method doesn't modify its object. Using const in all the right places is called "const correctness."

It's hard at first, but using const really tightens up your coding style. Const correctness grows on you.

If you don't use const correctness from the start it can be nightmare to add it in later because it causes a chain reaction of needing const everywhere. It's better to start being const correct from the start or you probably won't be.

You can always cast aways constness when necessary, but it's better not to.

For more information see Const Correctness in the C++ FAQ.

Use Streams

Programmers transitioning from C to C++ find stream IO strange preferring the familiarity of good old stdio. Printf and gang seem to be more convenient and are well understood.

Type Safety

Stdio is not type safe, which is one of the reasons you are using C++, right? Stream IO is type safe. That's one good reason to use streams.

Standard Interface

When you want to dump an object to a stream there is a standard way of doing it: with the << operator. This is not true of objects and stdio.

Interchangeablity of Streams

One of the more advanced reasons for using streams is that once an object can dump itself to a stream it can dump itself to any stream. One stream may go to the screen, but another stream may be a serial port or network connection. Good stuff.

Streams Got Better

Stream IO is not perfect. It is however a lot better than it used to be. Streams are now standardized, acceptably efficient, more reliable, and now there's lots of documentation on how to use streams.

Check Thread Safety

Some stream implementations are not yet thread safe. Make sure that yours is.

But Not Perfect

For an embedded target tight on memory streams do not make sense. Streams inline a lot of code so you might find the image larger than you wish. Experiment a little. Streams might work on your target.

No Magic Numbers

A magic number is a bare naked number used in source code. It's magic because no-one has a clue what it means including the author inside 3 months. For example:

if      (22 == foo) { start_thermo_nuclear_war(); }
else if (19 == foo) { refund_lotso_money(); }
else if (16 == foo) { infinite_loop(); }
else                { cry_cause_im_lost(); }
In the above example what do 22 and 19 mean? If there was a number change or the numbers were just plain wrong how would you know? Instead of magic numbers use a real name that means something. You can use #define or constants or enums as names. Which one is a design choice. For example:
#define   PRESIDENT_WENT_CRAZY  (22)
const int WE_GOOFED= 19;
enum
{
   THEY_DIDNT_PAY= 16
};

if      (PRESIDENT_WENT_CRAZY == foo) { start_thermo_nuclear_war(); }
else if (WE_GOOFED            == foo) { refund_lotso_money(); }
else if (THEY_DIDNT_PAY       == foo) { infinite_loop(); }
else                                  { happy_days_i_know_why_im_here(); }
Now isn't that better? The const and enum options are preferable because when debugging the debugger has enough information to display both the value and the label. The #define option just shows up as a number in the debugger which is very inconvenient. The const option has the downside of allocating memory. Only you know if this matters for your application.

Error Return Check Policy

  • Check every system call for an error return, unless you know you wish to ignore errors. For example, printf returns an error code but rarely would you check for its return code. In which case you can cast the return to (void) if you really care.
  • Include the system error text for every system error message.
  • Check every call to malloc or realloc unless you know your versions of these calls do the right thing. You might want to have your own wrapper for these calls, including new, so you can do the right thing always and developers don't have to make memory checks everywhere.

To Use Enums or Not to Use Enums

C++ allows constant variables, which should deprecate the use of enums as constants. Unfortunately, in most compilers constants take space. Some compilers will remove constants, but not all. Constants taking space precludes them from being used in tight memory environments like embedded systems. Workstation users should use constants and ignore the rest of this discussion.

In general enums are preferred to #define as enums are understood by the debugger.

Be aware enums are not of a guaranteed size. So if you have a type that can take a known range of values and it is transported in a message you can't use an enum as the type. Use the correct integer size and use constants or #define. Casting between integers and enums is very error prone as you could cast a value not in the enum.

A C++ Workaround

C++ allows static class variables. These variables are available anywhere and only the expected amount of space is taken.

Example

class Variables
{
public:
   static const int   A_VARIABLE;
   static const int   B_VARIABLE;
   static const int   C_VARIABLE;
}

Macros

Don't Turn C++ into Pascal

Don't change syntax via macro substitution. It makes the program unintelligible to all but the perpetrator.

Replace Macros with Inline Functions

In C++ macros are not needed for code efficiency. Use inlines.

Example

#define  MAX(x,y)	(((x) > (y) ? (x) : (y))	// Get the maximum

The macro above can be replaced for integers with the following inline function with no loss of efficiency:

   inline int
   max(int x, int y)
   {
      return (x > y ? x : y);
   }

Be Careful of Side Effects

Macros should be used with caution because of the potential for error when invoked with an expression that has side effects.

Example

   MAX(f(x),z++);

Always Wrap the Expression in Parenthesis

When putting expressions in macros always wrap the expression in parenthesis to avoid potential communitive operation ambiguity.

Example

#define ADD(x,y) x + y

must be written as

#define ADD(x,y) ((x) + (y))

Make Macro Names Unique

Like global variables macros can conflict with macros from other packages.
  1. Prepend macro names with package names.
  2. Avoid simple and common names like MAX and MIN.

Do Not Default If Test to Non-Zero

Do not default the test for non-zero, i.e.

   if (FAIL != f())
is better than

   if (f())
even though FAIL may have the value 0 which C considers to be false. An explicit test will help you out later when somebody decides that a failure return should be -1 instead of 0. Explicit comparison should be used even if the comparison value will never change; e.g., if (!(bufsize % sizeof(int))) should be written instead as if ((bufsize % sizeof(int)) == 0) to reflect the numeric (not boolean) nature of the test. A frequent trouble spot is using strcmp to test for string equality, where the result should never ever be defaulted. The preferred approach is to define a macro STREQ.

   #define STREQ(a, b) (strcmp((a), (b)) == 0)

Or better yet use an inline method:

   inline bool
   StringEqual(char* a, char* b)
   {
      return strcmp(a, b) == 0;
   }

Note, this is just an example, you should really use the standard library string type for doing the comparison.

The non-zero test is often defaulted for predicates and other functions or expressions which meet the following restrictions:

  • Returns 0 for false, nothing else.
  • Is named so that the meaning of (say) a true return is absolutely obvious. Call a predicate IsValid(), not CheckValid().

The Bull of Boolean Types

Any project using source code from many sources knows the pain of multiple conflicting boolean types. The new C++ standard defines a native boolean type. Until all compilers support bool, and existing code is changed to use it, we must still deal with the cruel world.

The form of boolean most accurately matching the new standard is:

   typedef int     bool;
   #define TRUE    1
   #define FALSE   0

or

   const int TRUE  = 1;
   const int FALSE = 0;
Note, the standard defines the names true and false not TRUE and FALSE. The all caps versions are used to not clash if the standard versions are available.

Even with these declarations, do not check a boolean value for equality with 1 (TRUE, YES, etc.); instead test for inequality with 0 (FALSE, NO, etc.). Most functions are guaranteed to return 0 if false, but only non-zero if true. Thus,

   if (TRUE == func()) { ...
must be written

   if (FALSE != func()) { ...

Usually Avoid Embedded Assignments

There is a time and a place for embedded assignment statements. In some constructs there is no better way to accomplish the results without making the code bulkier and less readable.

   while (EOF != (c = getchar()))
   {
      process the character
   }

The ++ and -- operators count as assignment statements. So, for many purposes, do functions with side effects. Using embedded assignment statements to improve run-time performance is also possible. However, one should consider the tradeoff between increased speed and decreased maintainability that results when embedded assignments are used in artificial places. For example,

   a = b + c;
   d = a + r;
should not be replaced by

   d = (a = b + c) + r;
even though the latter may save one cycle. In the long run the time difference between the two will decrease as the optimizer gains maturity, while the difference in ease of maintenance will increase as the human memory of what's going on in the latter piece of code begins to fade.

Reusing Your Hard Work and the Hard Work of Others

Reuse across projects is almost impossible without a common framework in place. Objects conform to the services available to them. Different projects have different service environments making object reuse difficult.

Developing a common framework takes a lot of up front design effort. When this effort is not made, for whatever reasons, there are several techniques one can use to encourage reuse:

Ask! Email a Broadcast Request to the Group

This simple technique is rarely done. For some reason programmers feel it makes them seem less capable if they ask others for help. This is silly! Do new interesting work. Don't reinvent the same stuff over and over again.

If you need a piece of code email to the group asking if someone has already done it. The results can be surprising.

In most large groups individuals have no idea what other people are doing. You may even find someone is looking for something to do and will volunteer to do the code for you. There's always a gold mine out there if people work together.

Tell! When You do Something Tell Everyone

Let other people know if you have done something reusable. Don't be shy. And don't hide your work to protect your pride. Once people get in the habit of sharing work everyone gets better.

Don't be Afraid of Small Libraries

One common enemy of reuse is people not making libraries out of their code. A reusable class may be hiding in a program directory and will never have the thrill of being shared because the programmer won't factor the class or classes into a library.

One reason for this is because people don't like making small libraries. There's something about small libraries that doesn't feel right. Get over it. The computer doesn't care how many libraries you have.

If you have code that can be reused and can't be placed in an existing library then make a new library. Libraries don't stay small for long if people are really thinking about reuse.

If you are afraid of having to update makefiles when libraries are recomposed or added then don't include libraries in your makefiles, include the idea of services. Base level makefiles define services that are each composed of a set of libraries. Higher level makefiles specify the services they want. When the libraries for a service change only the lower level makefiles will have to change.

Keep a Repository

Most companies have no idea what code they have. And most programmers still don't communicate what they have done or ask for what currently exists. The solution is to keep a repository of what's available.

In an ideal world a programmer could go to a web page, browse or search a list of packaged libraries, taking what they need. If you can set up such a system where programmers voluntarily maintain such a system, great. If you have a librarian in charge of detecting reusability, even better.

Another approach is to automatically generate a repository from the source code. This is done by using common class, method, library, and subsystem headers that can double as man pages and repository entries.

Commenting Out Large Code Blocks

Sometimes large blocks of code need to be commented out for testing.

Using #if 0

The easiest way to do this is with an #if 0 block:
   void
   example()
   {
      great looking code

      #if 0
      lots of code
      #endif

      more code
    }

You can't use /**/ style comments because comments can't contain comments and surely a large block of your code will contain a comment, won't it?

Don't use #ifdef as someone can unknowingly trigger ifdefs from the compiler command line.

Use Descriptive Macro Names Instead of 0

The problem with #if 0is that even day later you or anyone else has know idea why this code is commented out. Is it because a feature has been dropped? Is it because it was buggy? It didn't compile? Can it be added back? It's a mystery.

Use Descriptive Macro Names Instead of #if 0

#if NOT_YET_IMPLEMENTED

#if OBSOLETE

#if TEMP_DISABLED

Add a Comment to Document Why

Add a short comment explaining why it is not implemented, obsolete or temporarily disabled.

Use #if Not #ifdef

Use #if MACRO not #ifdef MACRO. Someone might write code like:
#ifdef DEBUG
        temporary_debugger_break();
#endif
Someone else might compile the code with turned-of debug info like:
cc -c lurker.cpp -DDEBUG=0
Alway use #if, if you have to use the preprocessor. This works fine, and does the right thing, even if DEBUG is not defined at all (!)
#if DEBUG
        temporary_debugger_break();
#endif
If you really need to test whether a symbol is defined or not, test it with the defined() construct, which allows you to add more things later to the conditional without editing text that's already in the program:
#if !defined(USER_NAME)
 #define USER_NAME "john smith"
#endif

Creating a C Function in C++

extern "C" void
a_c_function_in_cplusplus(int a)
{
}

__cplusplus Preprocessor Directive

If you have code that must compile in a C and C++ environment then you must use the __cplusplus preprocessor directive. For example:

#ifdef __cplusplus

extern "C" some_function();

#else

extern some_function();

#endif

Mixing C and C++

In order to be backward compatible with dumb linkers C++'s link time type safety is implemented by encoding type information in link symbols, a process called name mangling. This creates a problem when linking to C code as C function names are not mangled. When calling a C function from C++ the function name will be mangled unless you turn it off. Name mangling is turned off with the extern "C" syntax. If you want to create a C function in C++ you must wrap it with the above syntax. If you want to call a C function in a C library from C++ you must wrap in the above syntax. Here are some examples:

Calling C Functions from C++

extern "C" int strncpy(...);
extern "C" int my_great_function();
extern "C"
{
   int strncpy(...);
   int my_great_function();
};

No Data Definitions in Header Files

Do not put data definitions in header files. for example:

/*
 * aheader.h
 */
int x = 0;

  1. It's bad magic to have space consuming code silently inserted through the innocent use of header files.
  2. It's not common practice to define variables in the header file so it will not occur to devellopers to look for this when there are problems.
  3. Consider defining the variable once in a .cpp file and use an extern statement to reference it.
  4. Consider using a singleton for access to the data.

Make Functions Reentrant

Functions should not keep static variables that prevent a function from being reentrant. Functions can declare variables static. Some C library functions in the past, for example, kept a static buffer to use a temporary work area. Problems happen when the function is called one or more times at the same time. This can happen when multiple tasks are used or say from a signal handler. Using the static buffer caused results to overlap and become corrupted.

The moral is make your functions reentrant by not using static variables in a function. Besides, every machine has 128MB of RAM now so we don't worry about buffer space any more :-)

Use the Resource Acquisition is Initialization (RAII) Idiom

RAII is a pattern useful in making sure allocated resources get freed. What kind of resources? Memory usually. Could be file descriptors or locks too. Anything that is allocated at one point and needs to be deleted at another point in your code.

C++ can help make this painless because of a feature I miss a lot in Java, that your destructor is always called when an object leaves scope. This feature has caused the creation of the Guard idiom. A Guard can be wrapped around any resource so has to free the resource when the guard goes out of scope.

If you think you are always so careful to pair new and deletes that you don't need to use one of those pattern thingies then I am sure I'll be sitting beside you someday, like I have dozens of times before, searching for resource leaks in your code.

The acquisition phase happens at construction. You are supposed to do all the initialization in the constructor so you can do all the cleaning up in the destructor. And if you can guarantee through some different means that the destructor is always called then you can be pretty sure you won't leak.

Some intialization sequences can't be done in the constructor, particularly those using message driven state machines, but pretty much everything else can. And for those things use RAII. The justification is that it will save your butt from the many memory links that haunt large software products written in C++.

The key is to make sure the destructor is always called. The easiest way is to use a stack variable. That way you know the destructor will be called when leaving scope.

For pointers you can use one of the many smart pointer variants like auto_ptr. These will make sure your pointers are deleted when memory ownership is transferred or the scope is exited. You can even use them as member variables so you don't have to write code in the destructor.

If you can't use exceptions then this idiom isn't for you. As you are doing a lot of work in the constructor you can have errors and you'll want throw an exception.

See http://www.hackcraft.net/raii/ for a better explanation of RAII.

Portability

Use Typedefs for Types

It's a good idea to typedef int8, int16, int32, int64, float32, float64, uint8, uint16, uint32, uint64, etc., instead of assuming it'll be done with int, long, float, and short.

Alignment of Class Members

There seems to be disagreement on how to align class data members. Be aware that different platforms have different alignment rules and it can be an issue. Alignment may also be an issue when using shared memory and shared libraries.

The real thing to remember when it comes to alignment is to put the biggest data members first, and smaller members later, and to pad with char[] so that the same structure would be used no matter whether the compiler was in "naturally aligned" or "packed" mode.

For the Mac there's no blanket "always on four byte boundaries" rule -- rather, the rule is "alignment is natural, but never bigger than 4 bytes, unless the member is a double and first in the struct in which case it is 8". And that rule was inherited from PowerOpen/AIX.

Compiler Dependent Exceptions

Using exceptions across the shared library boundary could cause some problems if the shared library and the client module are compiled by different compiler vendors.

Compiler Dependent RTTI

Different compilers are not guaranteed to name types the same.

Popular Myths

Promise of OO

OO has been hyped to the extent you'd figure it would solve world hunger and usher in a new era of world peace. Not! OO is an approach, a philosophy, it's not a recipe which blindly followed yields quality.

Robert Martin put OO in perspective:

  • OO, when properly employed, does enhance the reusability of software. But it does so at the cost of complexity and design time. Reusable code is more complex and takes longer to design and implement. Furthermore, it often takes two or more tries to create something that is even marginally reusable.
  • OO, when properly employed, does enhance the software's resilience to change. But it does so at the cost of complexity and design time. This trade off is almost always a win, but it is hard to swallow sometimes.
  • OO does not necessarily make anything easier to understand. There is no magical mapping between the software concepts and every human's map of the real world. Every person is different. What one person percieves to be a simple and elegant design, another will perceive as convoluted and opaque.
  • If a team has been able, by applying point 1 above, to create a repository of reusable items, then development times can begin to shrink significantly due to reuse.
  • If a team has been able, by applying point 2 above, to create software that is resilient to change, then maintenance of that software will be much simpler and much less error prone.

You can't use OO and C++ on Embedded Systems

Oh yes you can. I've used C++ on several embedded systems as have many others. And if you can't why not? Please don't give in to vague feelings and prejudice. An attitude best shown with a short exchange:
Rube: Our packet driver is slow. We're only getting 100 packets per second.
Me  : Good thing you didn't do it in C++ huh?
Rube: Oh yah, it would have been really slow then!
Me  : (smiled secretly to myself)
My initial response was prompted by a general unacceptance of C++ in the project and blaming C++ for all problems. Of course all the parts written in C and assembly had no problems :-) Embedded systems shops tend to be hardware driven companies and tend not to know much about software development, thus any new fangled concepts like OO and C++ are ridiculed without verbally accessible reasons. Counter arguments like code that is fast and small and reusable don't make a dent. Examples like improving the speed of a driver by inlining certain methods and not hacking the code to death gently roll into the bit bucket.

Techniques

Of course C++ can be a disaster for an embedded system when used incorrectly, which of course is true of any tool. Here's some ideas to use C++ safely in an embedded system:
  • Get Some Training!

    If people don't know C++ and OO then they will likely fail and blame their tools. A good craftsperson doesn't blame their tools. Get training. Hire at least one experienced person as guide/mentor.

  • Be Careful Using Streams

    The streams library is large and slow. You are better off making a "fake" streams library by overloading the << operator. If you have a lot of memory then use streams, they are convenient and useful.

  • Be Careful Using Templates

    Code using templates can suffer from extreme code bloat. This is pretty much a function of your compiler as templates can be efficiently used when done correctly. Test your compiler for it how handles templates. If it doesn't make a copy per file for each template then you are in business. Templates have good time efficiency so they would be nice to use.

    You can fix the template code bloat problem by using explicit instantiation. Actually, even if the compiler generates one copy per source file. This, however, is often too much programmer work to expect on a large project, so be careful. Many linkers are smart enough to strip away all but one of the copies.

    Another issue to consider is template complexity. Templates can be complex for those new to C++. Bugs in templates are very hard to find and may overwhelm the patience of users.

  • Exceptions Beware

    Embedded applications are usually interrupt driven and multi-threaded. Test that exceptions are thread safe. Many compilers support exceptions, but not thread safe exceptions. And you probably don't want to call code in an interrupt that throws exceptions.

  • Use Polymorphic Interfaces to Make Frameworks

    When you think through your design and come up with good abstractions you will be shocked at how little code and how little time it takes to implement new features.

  • Make an OS Encapsulation Library

    Don't use your embedded OSs features directly. Create a layer that encapsulates OS functions and use those encapsulations. Most feature like tasks, interrupts, semaphores, message queues, messages, etc. are common to all systems. With good encapsulations it's quite possible to have the same code compile for Solaris, VxWorks, Windows, and other systems. It just takes a little thought.

  • ROM Beware

    A lot of systems create a ROM and download code later over the network that is linked against the ROM. Something to remember is linkers will try and include only code that is used. So your ROM may not contain code that loaded code expects to be there. You need to include all functions in your ROM.

  • Multiple Interface Levels

    Most embedded systems have a command line interface which usually requires C linkage, then they may have an SNMP interface, and they may have some sort of other friendly interface. Design this up front to be common across all code. It will make your life much easier. C functions require access to global pointers so they can use objects. The singleton pattern makes this easier.

    Come up with common naming conventions. A decent one is:

    • Make up a module abbreviation that can be prefixed to all calls. For example: log for the logging module.
    • Encode an action after the prefix. For example: logHelp which prints help for the logging module.
    • Require a certain set of functions for each sub system: For example:
      • moduleHelp - prints help for the module
      • modulePrint - prints the current state of the module
      • moduleStart - start a module
      • moduleStop - stop a module
      • moduleSetDebug - set the debug level for a module. It's very nice to set debug levels on a module by module basis.

  • Debug and Error System First

    Make your debug and error system first so everyone writing code will use it. It's very hard to retrofit code with debug output and intelligent use of error codes. If you have some way to write system assert errors to NVRAM, disk, or some other form of persistent storage so you can recover it on the next reboot.

  • Think About Memory

    Think how you'll share memory buffers between ISR code and task level code. Think how fast your default memory allocator is, it is probably slow. Think if your processor supports purify! Think how you'll track memory corruption and leakage.

  • Think About System Integrity

    You need to design up front how you are going to handle watchdog functions and test that the system is still running and not corrupted.

  • Remember to Use Volatile

    When using memory mapped I/O make sure that you declare the input port variables as volatile, (some compilers do this automatically), since the value can change without notice, and the optimizer could eliminate what looks like a redundant access to that variable. Not using volatile leads to some very obscure bugs. If you suspect problems in this area take a look at the generated code to make sure read-only assumptions are being made.

    Sometimes the keyword volatile is ifdef'd out for portability reasons. Check that what you think is volatile is really declared as volatile.

Home

© Copyright 1995-2006. Todd Hoff. All rights reserved.


C++ Programming Style Guidelines

Version 4.1, April 2006
Geotechnical Software Services
Copyright © 1996 - 2006

This document is available at http://geosoft.no/development/cppstyle.html

Table of Content

  • 1 Introduction
    • 1.1 Layout of the Recommendations
    • 1.2 Recommendations Importance
  • 2 General Recommendations
  • 3 Naming Conventions
    • 3.1 General
    • 3.2 Specific
  • 4 Files
    • 4.1 Source Files
    • 4.2 Include Files and Include Statements
  • 5 Statements
    • 5.1 Types
    • 5.2 Variables
    • 5.3 Loops
    • 5.4 Conditionals
    • 5.5 Miscellaneous
  • 6 Layout and Comments
    • 6.1 Layout
    • 6.2 White space
    • 6.3 Comments
  • 7 References

1 Introduction

This document lists C++ coding recommendations common in the C++ development community.

The recommendations are based on established standards collected from a number of sources, individual experience, local requirements/needs, as well as suggestions given in [1] - [4].

There are several reasons for introducing a new guideline rather than just referring to the ones above. Main reason is that these guides are far too general in their scope and that more specific rules (especially naming rules) need to be established. Also, the present guide has an annotated form that makes it far easier to use during project code reviews than most other existing guidelines. In addition, programming recommendations generally tend to mix style issues with language technical issues in a somewhat confusing manner. The present document does not contain any C++ technical recommendations at all, but focuses mainly on programming style. For guidelines on C++ programming style refer to the C++ Programming Practice Guidelines.

While a given development environment (IDE) can improve the readability of code by access visibility, color coding, automatic formatting and so on, the programmer should never rely on such features. Source code should always be considered larger than the IDE it is developed within and should be written in a way that maximize its readability independent of any IDE.

1.1 Layout of the Recommendations.

The recommendations are grouped by topic and each recommendation is numbered to make it easier to refer to during reviews.

Layout of the recommendations is as follows:

n. Guideline short description
Example if applicable
Motivation, background and additional information.

The motivation section is important. Coding standards and guidelines tend to start "religious wars", and it is important to state the background for the recommendation.

1.2 Recommendation Importance

In the guideline sections the terms must, should and can have special meaning. A must requirement must be followed, a should is a strong recommendation, and a can is a general guideline.

2 General Recommendations

1. Any violation to the guide is allowed if it enhances readability.
The main goal of the recommendation is to improve readability and thereby the understanding and the maintainability and general quality of the code. It is impossible to cover all the specific cases in a general guide and the programmer should be flexible.

2. The rules can be violated if there are strong personal objections against them.
The attempt is to make a guideline, not to force a particular coding style onto individuals. Experienced programmers normally want adopt a style like this anyway, but having one, and at least requiring everyone to get familiar with it, usually makes people start thinking about programming style and evaluate their own habits in this area.

On the other hand, new and inexperienced programmers normally use a style guide as a convenience of getting into the programming jargon more easily.

3 Naming Conventions

3.1 General Naming Conventions

3. Names representing types must be in mixed case starting with upper case.
Line, SavingsAccount
Common practice in the C++ development community.

4. Variable names must be in mixed case starting with lower case.
line, savingsAccount
Common practice in the C++ development community. Makes variables easy to distinguish from types, and effectively resolves potential naming collision as in the declaration Line line;

5. Named constants (including enumeration values) must be all uppercase using underscore to separate words.
MAX_ITERATIONS, COLOR_RED, PI
Common practice in the C++ development community. In general, the use of such constants should be minimized. In many cases implementing the value as a method is a better choice:

  int getMaxIterations() // NOT: MAX_ITERATIONS = 25
  {
    return 25;
  }

This form is both easier to read, and it ensures a unified interface towards class values.

6. Names representing methods or functions must be verbs and written in mixed case starting with lower case.
getName(), computeTotalWidth()
Common practice in the C++ development community. This is identical to variable names, but functions in C++ are already distingushable from variables by their specific form.

7. Names representing namespaces should be all lowercase.
model::analyzer, io::iomanager, common::math::geometry
Common practice in the C++ development community.

8. Names representing template types should be a single uppercase letter.
template<class T> ... template<class C, class D> ...
Common practice in the C++ development community. This makes template names stand out relative to all other names used.

9. Abbreviations and acronyms must not be uppercase when used as name [4].
exportHtmlSource(); // NOT: exportHTMLSource(); openDvdPlayer(); // NOT: openDVDPlayer();
Using all uppercase for the base name will give conflicts with the naming conventions given above. A variable of this type whould have to be named dVD, hTML etc. which obviously is not very readable. Another problem is illustrated in the examples above; When the name is connected to another, the readbility is seriously reduced; the word following the abbreviation does not stand out as it should.

10. Global variables should always be referred to using the :: operator.
::mainWindow.open(), ::applicationContext.getName()
In general, the use of global variables should be avoided. Consider using singleton objects instead.

11. Private class variables should have underscore suffix.
class SomeClass { private: int length_; }
Apart from its name and its type, the scope of a variable is its most important feature. Indicating class scope by using underscore makes it easy to distinguish class variables from local scratch variables. This is important because class variables are considered to have higher significance than method variables, and should be treated with special care by the programmer.

A side effect of the underscore naming convention is that it nicely resolves the problem of finding reasonable variable names for setter methods and constructors:

  void setDepth (int depth)
  {
    depth_ = depth;
  }

An issue is whether the underscore should be added as a prefix or as a suffix. Both practices are commonly used, but the latter is recommended because it seem to best preserve the readability of the name.

It should be noted that scope identification in variables has been a controversial issue for quite some time. It seems, though, that this practice now is gaining acceptance and that it is becoming more and more common as a convention in the professional development community.

12. Generic variables should have the same name as their type.
void setTopic(Topic* topic) // NOT: void setTopic(Topic* value) // NOT: void setTopic(Topic* aTopic) // NOT: void setTopic(Topic* t) void connect(Database* database) // NOT: void connect(Database* db) // NOT: void connect (Database* oracleDB)
Reduce complexity by reducing the number of terms and names used. Also makes it easy to deduce the type given a variable name only.

If for some reason this convention doesn't seem to fit it is a strong indication that the type name is badly chosen.

Non-generic variables have a role. These variables can often be named by combining role and type:

  Point  startingPoint, centerPoint;
  Name   loginName;

13. All names should be written in English.
fileName; // NOT: filNavn
English is the prefered language for international development.

14. Variables with a large scope should have long names, variables with a small scope can have short names [1].
Scratch variables used for temporary storage or indices are best kept short. A programmer reading such variables should be able to assume that its value is not used outside a few lines of code. Common scratch variables for integers are i, j, k, m, n and for characters c and d.

15. The name of the object is implicit, and should be avoided in a method name.
line.getLength(); // NOT: line.getLineLength();
The latter seems natural in the class declaration, but proves superfluous in use, as shown in the example.

3.2 Specific Naming Conventions

17. The terms get/set must be used where an attribute is accessed directly.
employee.getName(); employee.setName(name); matrix.getElement(2, 4); matrix.setElement(2, 4, value);
Common practice in the C++ development community. In Java this convention has become more or less standard.

18. The term compute can be used in methods where something is computed.
valueSet->computeAverage(); matrix->computeInverse()
Give the reader the immediate clue that this is a potential time consuming operation, and if used repeatedly, he might consider caching the result. Consistent use of the term enhances readability.

19. The term find can be used in methods where something is looked up.
vertex.findNearestVertex(); matrix.findMinElement();
Give the reader the immediate clue that this is a simple look up method with a minimum of computations involved. Consistent use of the term enhances readability.

20. The term initialize can be used where an object or a concept is established.
printer.initializeFontSet();
The american initialize should be preferred over the English initialise. Abbreviation init should be avoided.

21. Variables representing GUI components should be suffixed by the component type name.
mainWindow, propertiesDialog, widthScale, loginText, leftScrollbar, mainForm, fileMenu, minLabel, exitButton, yesToggle etc.
Enhances readability since the name gives the user an immediate clue of the type of the variable and thereby the objects resources.

22. Plural form should be used on names representing a collection of objects.
vector<Point> points; int values[];
Enhances readability since the name gives the user an immediate clue of the type of the variable and the operations that can be performed on its elements.

23. The prefix n should be used for variables representing a number of objects.
nPoints, nLines
The notation is taken from mathematics where it is an established convention for indicating a number of objects.

24.The suffix No should be used for variables representing an entity number.
tableNo, employeeNo
The notation is taken from mathematics where it is an established convention for indicating an entity number.

An elegant alternative is to prefix such variables with an i: iTable, iEmployee. This effectively makes them named iterators.

25. Iterator variables should be called i, j, k etc.
for (int i = 0; i < nTables); i++) { : } for (vector<MyClass>::iterator i = list.begin(); i != list.end(); i++) { Element element = *i; ... }
The notation is taken from mathematics where it is an established convention for indicating iterators.

Variables named j, k etc. should be used for nested loops only.

26. The prefix is should be used for boolean variables and methods.
isSet, isVisible, isFinished, isFound, isOpen
Common practice in the C++ development community and partially enforced in Java.

Using the is prefix solves a common problem of choosing bad boolean names like status or flag. isStatus or isFlag simply doesn't fit, and the programmer is forced to choose more meaningful names.

There are a few alternatives to the is prefix that fits better in some situations. These are the has, can and should prefixes:

  bool hasLicense();
  bool canEvaluate();
  bool shouldSort();

27. Complement names must be used for complement operations [1].
get/set, add/remove, create/destroy, start/stop, insert/delete, increment/decrement, old/new, begin/end, first/last, up/down, min/max, next/previous, old/new, open/close, show/hide, suspend/resume, etc.
Reduce complexity by symmetry.

28. Abbreviations in names should be avoided.
computeAverage(); // NOT: compAvg();
There are two types of words to consider. First are the common words listed in a language dictionary. These must never be abbreviated. Never write:

cmd   instead of   command
cp    instead of   copy
pt    instead of   point
comp  instead of   compute
init  instead of   initialize
etc.

Then there are domain specific phrases that are more naturally known through their abbreviations/acronym. These phrases should be kept abbreviated. Never write:

HypertextMarkupLanguage  instead of   html
CentralProcessingUnit    instead of   cpu
PriceEarningRatio        instead of   pe
etc.

29. Naming pointers specifically should be avoided.
Line* line; // NOT: Line* pLine; // NOT: LIne* linePtr;
Many variables in a C/C++ environment are pointers, so a convention like this is almost impossible to follow. Also objects in C++ are often oblique types where the specific implementation should be ignored by the programmer. Only when the actual type of an object is of special significance, the name should empahsize the type.

30. Negated boolean variable names must be avoided.
bool isError; // NOT: isNoError bool isFound; // NOT: isNotFound
The problem arises when such a name is used in conjunction with the logical negation operator as this results in a double negative. It is not immediately apparent what !isNotFound means.

31. Enumeration constants can be prefixed by a common type name.
enum Color { COLOR_RED, COLOR_GREEN, COLOR_BLUE };
This gives additional information of where the declaration can be found, which constants belongs together, and what concept the constants represent.

An alternative approach is to always refer to the constants through their common type: Color::RED, Airline::AIR_FRANCE etc.

32. Exception classes should be suffixed with Exception.
class AccessException { : }
Exception classes are really not part of the main design of the program, and naming them like this makes them stand out relative to the other classes.

33. Functions (methods returning something) should be named after what they return and procedures (void methods) after what they do.
Increase readability. Makes it clear what the unit should do and especially all the things it is not supposed to do. This again makes it easier to keep the code clean of side effects.

4 Files

4.1 Source Files

34. C++ header files should have the extension .h. Source files can have the extension .c++ (recommended), .C, .cc or .cpp.
MyClass.c++, MyClass.h
These are all accepted C++ standards for file extension.

35. A class should be declared in a header file and defined in a source file where the name of the files match the name of the class.
MyClass.h, MyClass.c++
Makes it easy to find the associated files of a given class. An obvious exception is template classes that must be both declared and defined inside a .h file.

36. All definitions should reside in source files.
class MyClass { public: int getValue () {return value_;} // NO! ... private: int value_; }
The header files should declare an interface, the source file should implement it. When looking for an implementation, the programmer should always know that it is found in the source file. The obvious exception to this rule is of course inline functions that must be defined in the header file.

37. File content must be kept within 80 columns.
80 columns is a common dimension for editors, terminal emulators, printers and debuggers, and files that are shared between several people should keep within these constraints. It improves readability when unintentional line breaks are avoided when passing a file between programmers.

38. Special characters like TAB and page break must be avoided.
These characters are bound to cause problem for editors, printers, terminal emulators or debuggers when used in a multi-programmer, multi-platform environment.

39. The incompleteness of split lines must be made obvious [1].
totalSum = a + b + c + d + e; function (param1, param2, param3); setText ("Long line split" "into two parts."); for (int tableNo = 0; tableNo < nTables; tableNo += tableStep) { ... }
Split lines occurs when a statement exceed the 80 column limit given above. It is difficult to give rigid rules for how lines should be split, but the examples above should give a general hint.

In general:

  • Break after a comma.
  • Break after an operator.
  • Align the new line with the beginning of the expression on the previous line.

4.2 Include Files and Include Statements

40. Header files must contain an include guard.
#ifndef COM_COMPANY_MODULE_CLASSNAME_H #define COM_COMPANY_MODULE_CLASSNAME_H : #endif
The construction is to avoid compilation errors. The name convention resembles the location of the file inside the source tree and prevents naming conflicts.

41. Include statements should be sorted and grouped. Sorted by their hierarchical position in the system with low level files included first. Leave an empty line between groups of include statements.
#include <fstream> #include <iomanip> #include <qt/qbutton.h> #include <qt/qtextfield.h> #include "com/company/ui/PropertiesDialog.h" #include "com/company/ui/MainWindow.h"
In addition to show the reader the individual include files, it also give an immediate clue about the modules that are involved.

Include file paths must never be absolute. Compiler directives should instead be used to indicate root directories for includes.

42. Include statements must be located at the top of a file only.
Common practice. Avoid unwanted compilation side effects by "hidden" include statements deep into a source file.

5 Statements

5.1 Types

43. Types that are local to one file only can be declared inside that file.
Enforces information hiding.

44. The parts of a class must be sorted public, protected and private [2][3]. All sections must be identified explicitly. Not applicable sections should be left out.
The ordering is "most public first" so people who only wish to use the class can stop reading when they reach the protected/private sections.

45. Type conversions must always be done explicitly. Never rely on implicit type conversion.
floatValue = static_cast<float>(intValue); // NOT: floatValue = intValue;
By this, the programmer indicates that he is aware of the different types involved and that the mix is intentional.

5.2 Variables

46. Variables should be initialized where they are declared.
This ensures that variables are valid at any time. Sometimes it is impossible to initialize a variable to a valid value where it is declared:

  int x, y, z;
  getCenter(&x, &y, &z);

In these cases it should be left uninitialized rather than initialized to some phony value.

47. Variables must never have dual meaning.
Enhance readability by ensuring all concepts are represented uniquely. Reduce chance of error by side effects.

48. Use of global variables should be minimized.
In C++ there is no reason global variables need to be used at all. The same is true for global functions or file scope (static) variables.

49. Class variables should never be declared public.
The concept of C++ information hiding and encapsulation is violated by public variables. Use private variables and access functions instead. One exception to this rule is when the class is essentially a data structure, with no behavior (equivalent to a C struct). In this case it is appropriate to make the class' instance variables public [2].

Note that structs are kept in C++ for compatibility with C only, and avoiding them increases the readability of the code by reducing the number of constructs used. Use a class instead.

51. C++ pointers and references should have their reference symbol next to the type rather than to the name.
float* x; // NOT: float *x; int& y; // NOT: int &y;
The pointer-ness or reference-ness of a variable is a property of the type rather than the name. C-programmers often use the alternative approach, while in C++ it has become more common to follow this recommendation.

53. Implicit test for 0 should not be used other than for boolean variables and pointers.
if (nLines != 0) // NOT: if (nLines) if (value != 0.0) // NOT: if (value)
It is not necessarily defined by the C++ standard that ints and floats 0 are implemented as binary 0. Also, by using explicit test the statement give immediate clue of the type being tested.

It is common also to suggest that pointers shouldn't test implicit for 0 either, i.e. if (line == 0) instead of if (line). The latter is regarded so common in C/C++ however that it can be used.

54. Variables should be declared in the smallest scope possible.
Keeping the operations on a variable within a small scope, it is easier to control the effects and side effects of the variable.

5.3 Loops

55. Only loop control statements must be included in the for() construction.
sum = 0; // NOT: for (i = 0, sum = 0; i < 100; i++) for (i = 0; i < 100; i++) sum += value[i]; sum += value[i];
Increase maintainability and readability. Make a clear distinction of what controls and what is contained in the loop.

56. Loop variables should be initialized immediately before the loop.
isDone = false; // NOT: bool isDone = false; while (!isDone) { // : : // while (!isDone) { } // : // }

57. do-while loops can be avoided.
do-while loops are less readable than ordinary while loops and for loops since the conditional is at the bottom of the loop. The reader must scan the entire loop in order to understand the scope of the loop.

In addition, do-while loops are not needed. Any do-while loop can easily be rewritten into a while loop or a for loop. Reducing the number of constructs used enhance readbility.

58. The use of break and continue in loops should be avoided.
These statements should only be used if they prove to give higher readability than their structured counterparts.

60. The form while(true) should be used for infinite loops.
while (true) { : } for (;;) { // NO! : } while (1) { // NO! : }
Testing against 1 is neither necessary nor meaningful. The form for (;;) is not very readable, and it is not apparent that this actually is an infinite loop.

5.4 Conditionals

61. Complex conditional expressions must be avoided. Introduce temporary boolean variables instead [1].
bool isFinished = (elementNo < 0) || (elementNo > maxElement); bool isRepeatedEntry = elementNo == lastElement; if (isFinished || isRepeatedEntry) { : } // NOT: if ((elementNo < 0) || (elementNo > maxElement)|| elementNo == lastElement) { : }
By assigning boolean variables to expressions, the program gets automatic documentation. The construction will be easier to read, debug and maintain.

62. The nominal case should be put in the if-part and the exception in the else-part of an if statement [1].
bool isOk = readFile (fileName); if (isOk) { : } else { : }
Makes sure that the exceptions don't obscure the normal path of execution. This is important for both the readability and performance.

63. The conditional should be put on a separate line.
if (isDone) // NOT: if (isDone) doCleanup(); doCleanup();
This is for debugging purposes. When writing on a single line, it is not apparent whether the test is really true or not.

64. Executable statements in conditionals must be avoided.
File* fileHandle = open(fileName, "w"); if (!fileHandle) { : } // NOT: if (!(fileHandle = open(fileName, "w"))) { : }
Conditionals with executable statements are just very difficult to read. This is especially true for programmers new to C/C++.

5.5 Miscellaneous

65. The use of magic numbers in the code should be avoided. Numbers other than 0 and 1 should be considered declared as named constants instead.
If the number does not have an obvious meaning by itself, the readability is enhanced by introducing a named constant instead. A different approach is to introduce a method from which the constant can be accessed.

66. Floating point constants should always be written with decimal point and at least one decimal.
double total = 0.0; // NOT: double total = 0; double speed = 3.0e8; // NOT: double speed = 3e8; double sum; : sum = (a + b) * 10.0;
This emphasize the different nature of integer and floating point numbers. Mathematically the two model completely different and non-compatible concepts.

Also, as in the last example above, it emphasize the type of the assigned variable (sum) at a point in the code where this might not be evident.

67. Floating point constants should always be written with a digit before the decimal point.
double total = 0.5; // NOT: double total = .5;
The number and expression system in C++ is borrowed from mathematics and one should adhere to mathematical conventions for syntax wherever possible. Also, 0.5 is a lot more readable than .5; There is no way it can be mixed with the integer 5.

68. Functions must always have the return value explicitly listed.
int getValue() // NOT: getValue() { : }
If not exlicitly listed, C++ implies int return value for functions. A programmer must never rely on this feature, since this might be confusing for programmers not aware of this artifact.

69. goto should not be used.
Goto statements violates the idea of structured code. Only in some very few cases (for instance breaking out of deeply nested structures) should goto be considered, and only if the alternative structured counterpart is proven to be less readable.

70. "0" should be used instead of "NULL".
NULL is part of the standard C library, but is made obsolete in C++.

6 Layout and Comments

6.1 Layout

71. Basic indentation should be 2.
for (i = 0; i < nElements; i++) a[i] = 0;
Indentation of 1 is to small to emphasize the logical layout of the code. Indentation larger than 4 makes deeply nested code difficult to read and increase the chance that the lines must be split. Choosing between indentation of 2, 3 and 4, 2 and 4 are the more common, and 2 chosen to reduce the chance of splitting code lines.

72. Block layout should be as illustrated in example 1 below (recommended) or example 2, and must not be as shown in example 3 [4]. Function and class blocks must use the block layout of example 2.
while (!done) { doSomething(); done = moreToDo(); }
 while (!done) { doSomething(); done = moreToDo(); } while (!done) { doSomething(); done = moreToDo(); }
Example 3 introduce an extra indentation level which doesn't emphasize the logical structure of the code as clearly as example 1 and 2.

73. The class declarations should have the following form:
class SomeClass : public BaseClass { public: ... protected: ... private: ... }
This follows partly from the general block rule above.

74. Method definitions should have the following form:
void someMethod() { ... }
This follows from the general block rule above.

75. The if-else class of statements should have the following form:
if (condition) { statements; } if (condition) { statements; } else { statements; } if (condition) { statements; } else if (condition) { statements; } else { statements; }
This follows partly from the general block rule above. However, it might be discussed if an else clause should be on the same line as the closing bracket of the previous if or else clause:
  if (condition) {
    statements;
  } else {
    statements;
  }
The chosen approach is considered better in the way that each part of the if-else statement is written on separate lines of the file. This should make it easier to manipulate the statement, for instance when moving else clauses around.

76. A for statement should have the following form:
for (initialization; condition; update) { statements; }
This follows from the general block rule above.

77. An empty for statement should have the following form:
for (initialization; condition; update) ;
This emphasize the fact that the for statement is empty and it makes it obvious for the reader that this is intentional. Empty loops should be avoided however.

78. A while statement should have the following form:
while (condition) { statements; }
This follows from the general block rule above.

79. A do-while statement should have the following form:
do { statements; } while (condition);
This follows from the general block rule above.

80. A switch statement should have the following form:
switch (condition) { case ABC : statements; // Fallthrough case DEF : statements; break; case XYZ : statements; break; default : statements; break; }
Note that each case keyword is indented relative to the switch statement as a whole. This makes the entire switch statement stand out. Note also the extra space before the : character. The explicit Fallthrough comment should be included whenever there is a case statement without a break statement. Leaving the break out is a common error, and it must be made clear that it is intentional when it is not there.

81. A try-catch statement should have the following form:
try { statements; } catch (Exception& exception) { statements; }
This follows partly from the general block rule above. The discussion about closing brackets for if-else statements apply to the try-catch statments.

82. Single statement if-else, for or while statements can be written without brackets.
if (condition) statement; while (condition) statement; for (initialization; condition; update) statement;
It is a common recommendation that brackets should always be used in all these cases. However, brackets are in general a language construct that groups several statements. Brackets are per definition superfluous on a single statement. A common argument against this syntax is that the code will break if an additional statement is added without also adding the brackets. In general however, code should never be written to accommodate for changes that might arise.

83. The function return type can be put in the left column immediately above the function name.
void MyClass::myMethod(void) { : }
This makes it easier to spot function names within a file since they all start in the first column.

6.2 White Space

84.
- Conventional operators should be surrounded by a space character.
- C++ reserved words should be followed by a white space.
- Commas should be followed by a white space.
- Colons should be surrounded by white space.
- Semicolons in for statments should be followed by a space character.
a = (b + c) * d; // NOT: a=(b+c)*d while (true) // NOT: while(true) { ... doSomething(a, b, c, d); // NOT: doSomething(a,b,c,d); case 100 : // NOT: case 100: for (i = 0; i < 10; i++) { // NOT: for(i=0;i<10;i++){ ...
Makes the individual components of the statements stand out. Enhances readability. It is difficult to give a complete list of the suggested use of whitespace in C++ code. The examples above however should give a general idea of the intentions.

85. Method names can be followed by a white space when it is followed by another name.
doSomething (currentFile);
Makes the individual names stand out. Enhances readability. When no name follows, the space can be omitted (doSomething()) since there is no doubt about the name in this case.

An alternative to this approach is to require a space after the opening parenthesis. Those that adhere to this standard usually also leave a space before the closing parentheses: doSomething( currentFile );. This do make the individual names stand out as is the intention, but the space before the closing parenthesis is rather artificial, and without this space the statement looks rather asymmetrical (doSomething( currentFile);).

86. Logical units within a block should be separated by one blank line.
Matrix4x4 matrix = new Matrix4x4(); double cosAngle = Math.cos(angle); double sinAngle = Math.sin(angle); matrix.setElement(1, 1, cosAngle); matrix.setElement(1, 2, sinAngle); matrix.setElement(2, 1, -sinAngle); matrix.setElement(2, 2, cosAngle); multiply(matrix);
Enhance readability by introducing white space between logical units of a block.

87. Methods should be separated by three blank lines.
By making the space larger than space within a method, the methods will stand out within the class.

88. Variables in declarations can be left aligned.
AsciiFile* file; int nPoints; float x, y;
Enhance readability. The variables are easier to spot from the types by alignment.

89. Use alignment wherever it enhanbces readability.
if (a == lowValue) compueSomething(); else if (a == mediumValue) computeSomethingElse(); else if (a == highValue) computeSomethingElseYet(); value = (potential * oilDensity) / constant1 + (depth * waterDensity) / constant2 + (zCoordinateValue * gasDensity) / constant3; minPosition = computeDistance(min, x, y, z); averagePosition = computeDistance(average, x, y, z); switch (value) { case PHASE_OIL : strcpy(phase, "Oil"); break; case PHASE_WATER : strcpy(phase, "Water"); break; case PHASE_GAS : strcpy(phase, "Gas"); break; }
There are a number of places in the code where white space can be included to enhance readability even if this violates common guidelines. Many of these cases have to do with code alignment. General guidelines on code alignment are difficult to give, but the examples above should give a general clue.

6.3 Comments

90. Tricky code should not be commented but rewritten! [1]
In general, the use of comments should be minimized by making the code self-documenting by appropriate name choices and an explicit logical structure.

91. All comments should be written in English [2].
In an international environment English is the preferred language.

92. Use // for all comments, including multi-line comments.
// Comment spanning // more than one line.
Since multilevel C-commenting is not supported, using // comments ensure that it is always possible to comment out entire sections of a file using /* */ for debugging purposes etc.

There should be a space between the "//" and the actual comment, and comments should always start with an upper case letter and end with a period.

93. Comments should be included relative to their position in the code. [1]
while (true) { // NOT: while (true) { // Do something // Do something something(); something(); } }
This is to avoid that the comments break the logical structure of the program.

94. Class and method header comments should follow the JavaDoc conventions.
Regarding standardized class and method documentation the Java development community is more mature than the C/C++ one. This is due to the standard automatic Javadoc tool that is part of the development kit and that help producing high quality hypertext documentation from these comments.

There are Javadoc-like tools available also for C++. These follows the same tagging syntax as Javadoc. See for instance Doc++ or Doxygen.

7 References

[1] Code Complete, Steve McConnel - Microsoft Press

[2] Programming in C++, Rules and Recommendations, M Henricson, e. Nyquist, Ellemtel (Swedish telecom)
http://www.doc.ic.ac.uk/lab/cplus/c%2b%2b.rules/

[3] Wildfire C++ Programming Style, Keith Gabryelski, Wildfire Communications Inc.
http://www.wildfire.com/~ag/Engineering/Development/C++Style/

[4] C++ Coding Standard, Todd Hoff
http://www.possibility.com/Cpp/CppCodingStandard.htm

[5] Doxygen documentation system
http://www.stack.nl/~dimitri/doxygen/index.html

© 1998 - 2006 Geotechnical Software Services. All rights reserved.
This page is maintained by webwizard@geosoft.no


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

Единичной матрицой порядка n называют квадратную матрицу n*n, у которой
элементы главной диагонали (для которых номера строки и столбца совпадают)
равны 1, а остальные - 0. Обозначим её как E.

1. Сложение матриц: A+B=C.
    Матрицы одного размера.
    C[i,j] = A[i,j] + b[i,j]

2. Умножение матрицы на число: A*x=x*A=B.
    B[i,j]=x*A[i,j]

3. Умножение матриц: A*B=C.
    A - размера m*n, B - n*p. Значит C - m*p.
    C[i,j] = сумме произведений элементов i-той строки A на j-тый столбец B.
    Элементы берутся последовательным перебором, проще понять на примере.
    Например: A - размера 2*3 (2 строки, 3 столбца), B - 3*2.
    C[1,1] = A[1,1]*B[1,1] + A[1,2]*B[2,1] + A[1,3]*B[3,1]
    C[1,2] = A[2,1]*B[1,1] + A[2,2]*B[2,1] + A[2,3]*B[3,1]
    C[2,1] = A[1,1]*B[1,2] + A[1,2]*B[2,2] + A[1,3]*B[3,2]
    C[2,2] = A[2,1]*B[1,2] + A[2,2]*B[2,2] + A[2,3]*B[3,2]
    Заметьте, что A*E=A.

4. Транспонирование матриц: мы обозначим как A#=B
    Размеры одинаковы.
    B[i,j] = A[j,i]

5. Определитель матрицы: запишем как Determinator(A) = x (x - число).
    A - квадратная матрица n*n.
    Для матрицы из одного элемента Determinator равен её элементу.
    Для остальных - выясним чуть позже.

6. Алгебраическое дополнение элемента матрицы: AlgApp[i,j] от A.
    A - квадратная матрица.
    AlgApp = -1 в степени i+j умноженной на определитель
    матрицы полученной из A путём убирания i-той строки
    и j-того столбца.

7. Определитель произвольной квадратной матрицы.
    Равен сумме произведение всех элементов любого (одного)
    столбца (или строки) матрицы на соответствующие алгебраические
    дополнения.

8. Обратная матрица: A$=B (вообще обозначается степенью -1).
    Такая матрица, что A*B=E.
    Её можно получить за несколько действий.
    1. Пусть P - матрица, полученная из A заменой её элементов на их
       лгебраические дополнения.
    2. B = P# * (1 / Determinator(A)).
    Обратите внимание, что обратная матрица не всегда существует из-за
    невозможности деления на 0.


^\d\d>$
Десятичное число от 0 до 99, записанное строкой. Возможны варианты вроде "01".

^\S+$
Любые строки без пробелов.

\b(дом|house|хаус)\b
Строки (возможно предложения, содержащие одно из трёх слов: "дом", "house" или "хаус".
Строки с производными словами (вроде "роддом") не прокатят.

\b[Сс]обака$
Строки, в конце которых стоит слово "собака", записанное с большой или маленькой буквы.

\.$
Строка, заканчивающаяся точкой.

^\t([^\t]+)
Слово, в котором только одна табуляция - в самом начале.

(!|?){3,}
Строка, включающая не меньше трёх восклицательных или вопросительных знаков подряд
(могут быть два одних и один - другой).

((!|?).*){3,}
Строка, включающая не меньше трёх восклицательных или вопросительных знаков вообще.

&(?!amp;)
Можно использовать для поиска амперсандов, после которых не идёт "amp;".

(?=const\s+)char\s*\*
Капчурит подстроки вида "char *" (перед звёздочкой могут быть или нет пробелы. табы и др.).
Обязательное условие - константность (отделяется одним или более прбелами, табами, переводами строки и т. д.


Записаны лишь названия символов (без амперсанда и точки с запятой).
Многие символы относятся к юникода, я пишу их упрощённый вариант.
Приведены лишь несколько наиболее часто используемых символов.

amp		амперсанд (&)
lt		меньше (<)
gt		больше (>)
quote		кавычка (")
nbsp		пробел ( )
copy		копирайт ((c))
laquo		левая угловая кавычка (")
raquo		правая угловая кавычка (")
reg		зарегистрированная торговая марка ((r))
frac12		одна вторая (1/2)
frac14		одна четвёртая (1/4)
frac34		три четвёртых (3/4)