Комментарии и расположение текста




Программу гораздо легче читать, и она становится намного понятнее, если разумно использовать комментарии и систематически выделять текст программы пробелами. Есть несколько способов расположения текста программы, но нет причин считать, что один из них - наилучший. Хотя у каждого свой вкус. То же можно сказать и о комментариях. Однако можно заполнить программу такими комментариями, что читать и понимать ее будет только труднее. Транслятор не в силах понять комментарий, поэтому он не может убедиться в том, что комментарий: [1] осмысленный, [2] действительно описывает программу, [3] не устарел. Во многих программах попадаются непостижимые, двусмысленные и просто неверные комментарии. Лучше вообще обходиться без них, чем давать такие комментарии. Если некий факт можно прямо выразить в языке, то так и следует делать, и не надо считать, что достаточно упомянуть его в комментарии. Последнее замечание относится к комментариям, подобным приведенным ниже: // переменную "v" необходимо инициализировать. // переменная "v" может использоваться только в функции "f()". // до вызова любой функции из этого файла // необходимо вызвать функцию "init()". // в конце своей программы вызовите функцию "cleanup()". // не используйте функцию "weird()". // функция "f()" имеет два параметра. При правильном программировании на С++ такие комментарии обычно оказываются излишними. Чтобы именно эти комментарии стали ненужными, можно воспользоваться правилами связывания ($$4.2) и областей видимости, а также правилами инициализации и уничтожения объектов класса ($$5.5). Если некоторое утверждение выражается самой программой, не нужно повторять его в комментарии. Например: a = b + c; // a принимает значение b+c count++; // увеличим счетчик count Такие комментарии хуже, чем избыточные. Они раздувают объем текста, затуманивают программу и могут быть даже ложными. В то же время комментарии именно такого рода используют для примеров в учебниках по языкам программирования, подобных этой книге. Это одна из многих причин, по которой учебная программа отличается от настоящей. Можно рекомендовать такой стиль введения комментариев в программу: [1] начинать с комментария каждый файл программы: указать в общих чертах, что в ней определяется, дать ссылки на справочные руководства, общие идеи по сопровождению программы и т.д.; [2] снабжать комментарием каждое определение класса или шаблона типа; [3] комментировать каждую нетривиальную функцию, указав: ее назначение, используемый алгоритм (если только он неочевиден) и, возможно, предположения об окружении, в котором работает функция; [4] комментировать определение каждой глобальной переменной; [5] давать некоторое число комментариев в тех местах, где алгоритм неочевиден или непереносим; [6] больше практически ничего. Приведем пример: // tbl.c: Реализация таблицы имен. /* Использован метод Гаусса см. Ральстон "Начальный курс по..." стр. 411. */ // в swap() предполагается, что стек AT&T начинается с 3B20. /************************************ Авторские права (c) 1991 AT&T, Inc Все права сохранены **************************************/ Правильно подобранные и хорошо составленные комментарии играют в программе важную роль. Написать хорошие комментарии не менее трудно, чем саму программу, и это - искусство, в котором стоит совершенствоваться. Заметим, что если в функции используются только комментарии вида //, то любую ее часть можно сделать комментарием с помощью /* */, и наоборот.

Упражнения

1. (*1) Следующий цикл for перепишите с помощью оператора while: for (i=0; i<max_length; i++) if (input_line[i] == '?') quest_count++; Запишите цикл, используя в качестве его управляющей переменной указатель так, чтобы условие имело вид *p=='?'. 2. (*1) Укажите порядок вычисления следующих выражений, задав полную скобочную структуру: a = b + c * d << 2 & 8 a & 077!= 3 a == b || a == c && c < 5 c = x!= 0 0 <= i < 7 f(1,2) + 3 a = - 1 + + b -- - 5 a = b == c ++ a = b = c = 0 a[4][2] *= * b? c: * d * 2 a-b, c=d 3. (*2) Укажите 5 различных конструкций на С++, значение которых неопределено. 4. (*2) Приведите 10 разных примеров непереносимых конструкций на С++. 5. (*1) Что произойдет при делении на нуль в вашей программе на С++? Что будет в случае переполнения или потери значимости? 6. (*1) Укажите порядок вычисления следующих выражений, задав их полную скобочную структуру: *p++ *--p ++a-- (int*)p->m *p.m *a[i] 7. (*2) Напишите такие функции: strlen() - подсчет длины строки, strcpy() - копирование строк и strcmp() - сравнение строк. Какими должны быть типы параметров и результатов функций? Сравните их со стандартными версиями, имеющимися в <string.h> и в вашем руководстве. 8. (*1) Выясните, как ваш транслятор отреагирует на такие ошибки: void f(int a, int b) { if (a = 3) //... if (a&077 == 0) //... a:= b+1; } Посмотрите, какова будет реакция на более простые ошибки. 9. (*2) Напишите функцию cat(), которая получает два параметра-строки и возвращает строку, являющуюся их конкатенацией. Для результирующей строки используйте память, отведенную с помощью new. Напишите функцию rev() для перевертывания строки, переданной ей в качестве параметра. Это означает, что после вызова rev(p) последний символ p станет первым и т.д.10. (*2) Что делает следующая функция? void send(register* to, register* from, register count) // Псевдоустройство. Все комментарии сознательно удалены { register n=(count+7)/8; switch (count%8) { case 0: do { *to++ = *from++; case 7: *to++ = *from++; case 6: *to++ = *from++; case 5: *to++ = *from++; case 4: *to++ = *from++; case 3: *to++ = *from++; case 2: *to++ = *from++; case 1: *to++ = *from++; } while (--n>0); } } Каков может быть смысл этой функции? 11. (*2) Напишите функцию atoi(), которая имеет параметр - строку цифр и возвращает соответствующее ей целое. Например, atoi("123") равно 123. Измените функцию atoi() так, чтобы она могла переводить в число последовательность цифр не только в десятичной, но и в восьмеричной и шестнадцатеричной записи, принятой в С++. Добавьте возможность перевода символьных констант С++. Напишите функцию itoa() для перевода целого значения в строковое представление. 12. (*2) Перепишите функцию get_token() ($$3.12) так, чтобы она читала целую строку в буфер, а затем выдавала лексемы, читая по символу из буфера. 13. (*2) Введите в программу калькулятора из $$3.1 такие функции, как sqrt(), log() и sin(). Подсказка: задайте предопределенные имена и вызывайте функции с помощью массива указателей на них. Не забывайте проверять параметры, передаваемые этим функциям. 14. (*3) Введите в калькулятор возможность определять пользовательские функции. Подсказка: определите функцию как последовательность операторов, будто бы заданную самим пользователем. Эту последовательность можно хранить или как строку символов, или как список лексем. Когда вызывается функция, надо выбирать и выполнять операции. Если пользовательские функции могут иметь параметры, то придется придумать форму записи и для них. 15. (*1.5) Переделайте программу калькулятора, используя структуру symbol вместо статических переменных name_string и number_value: struct symbol { token_value tok; union { double number_value; char* name_string; }; }; 16.(*2.5) Напишите программу, которая удаляет все комментарии из программы на С++. Это значит, надо читать символы из cin и удалять комментарии двух видов: // и /* */. Получившийся текст запишите в cout. Не заботьтесь о красивом виде получившегося текста (это уже другая, более сложная задача). Корректность программ неважна. Нужно учитывать возможность появления символов //, /* и */ в комментариях, строках и символьных константах. 17. (*2) Исследуйте различные программы и выясните, какие способы выделения текста пробелами и какие комментарии используются.

* ГЛАВА 4

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

Введение

Роль файла в языке С++ сводится к тому, что он определяет файловуюобласть видимости ($$R.3.2). Это область видимости глобальныхфункций (как статических, так и подстановок), а также глобальныхпеременных (как статических, так и со спецификацией const). Крометого, файл является традиционной единицей хранения в системе, атакже единицей трансляции. Обычно системы хранят, транслируют ипредставляют пользователю программу на С++ как множество файлов,хотя существуют системы, устроенные иначе. В этой главе будетобсуждаться в основном традиционное использование файлов. Всю программу поместить в один файл, как правило, невозможно,поскольку программы стандартных функций и программы операционнойсистемы нельзя включить в текстовом виде в программу пользователя.Вообще, помещать всю программу пользователя в один файл обычнонеудобно и непрактично. Разбиения программы на файлы можетоблегчить понимание общей структуры программы и дает трансляторувозможность поддерживать эту структуру. Если единицей трансляцииявляется файл, то даже при небольшом изменении в нем следуетего перетранслировать. Даже для программ не слишком большогоразмера время на перетрансляцию можно значительно сократить, еслиее разбить на файлы подходящего размера. Вернемся к примеру с калькулятором. Решение было дано в видеодного файла. Когда вы попытаетесь его транслировать, неизбежновозникнут некоторые проблемы с порядком описаний. По крайней мереодно "ненастоящее" описание придется добавить к тексту, чтобытранслятор мог разобраться в использующих друг друга функцияхexpr(), term() и prim(). По тексту программы видно, что онасостоит из четырех частей: лексический анализатор (сканер),собственно анализатор, таблица имен и драйвер. Однако, этот фактникак не отражен в самой программе. На самом деле калькуляторне был запрограммирован именно так. Так не следует писатьпрограмму. Даже если не учитывать все рекомендации попрограммированию, сопровождению и оптимизации для такой "зряшной"программы, все равно ее следует создавать из нескольких файловхотя бы для удобства. Чтобы раздельная трансляция стала возможной, программистдолжен предусмотреть описания, из которых транслятор получитдостаточно сведений о типах для трансляции файла, составляющеготолько часть программы. Требование непротиворечивости использованиявсех имен и типов для программы, состоящей из нескольких раздельнотранслируемых частей, так же справедливо, как и для программы,состоящей из одного файла. Это возможно только в том случае, когдаописания, находящиеся в разных единицах трансляции, будутсогласованы. В вашей системе программирования имеются средства,которые способны установить, выполняется ли это. В частности, многиепротиворечия обнаруживает редактор связей. Редактор связей - это программа,которая связывает по именам раздельно транслируемые части программы.Иногда его по ошибке называют загрузчиком.

Связывание

Если явно не определено иначе, то имя, не являющееся локальным длянекоторой функции или класса, должно обозначать один и тот же тип,значение, функцию или объект во всех единицах трансляции даннойпрограммы. Иными словами, в программе может быть только одиннелокальный тип, значение, функция или объект с данным именем.Рассмотрим для примера два файла: // file1.c int a = 1; int f() { /* какие-то операторы */ } // file2.c extern int a; int f(); void g() { a = f(); } В функции g() используются те самые a и f(), которые определены вфайле file1.c. Служебное слово extern показывает, что описаниеa в файле file2.c является только описанием, но не определением.Если бы присутствовала инициализация a, то extern простопроигнорировалось бы, поскольку описание с инициализацией всегдасчитается определением. Любой объект в программе может определятьсятолько один раз. Описываться же он может неоднократно, но всеописания должны быть согласованы по типу. Например: // file1.c: int a = 1; int b = 1; extern int c; // file2.c: int a; extern double b; extern int c; Здесь содержится три ошибки: переменная a определена дважды ("int a;"- это определение, означающее "int a=0;"); b описано дважды, причемс разными типами; c описано дважды, но неопределено. Такие ошибки(ошибки связывания) транслятор, который обрабатывает файлыпо отдельности, обнаружить не может, но большая их частьобнаруживается редактором связей. Следующая программа допустима в С, но не в С++: // file1.c: int a; int f() { return a; } // file2.c: int a; int g() { return f(); } Во-первых, ошибкой является вызов f() в file2.c, поскольку в этомфайле f() не описана. Во-вторых, файлы программы не могут бытьправильно связаны, поскольку a определено дважды. Если имя описано как static, оно становится локальном в этомфайле. Например: // file1.c: static int a = 6; static int f() { /*... */ } // file2.c: static int a = 7; static int f() { /*... */ } Приведенная программа правильна, поскольку a и f определены какстатические. В каждом файле своя переменная a и функция f(). Если переменные и функции в данной части программы описаны какstatic, то в этой части программы проще разобраться, поскольку не нужнозаглядывать в другие части. Описывать функции как статическиеполезно еще и по той причине, что транслятору предоставляетсявозможность создать более простой вариант операции вызова функции.Если имя объекта или функции локально в данном файле, то говорят,что объект подлежит внутреннему связыванию. Обратно, если имяобъекта или функции нелокально в данном файле, то он подлежитвнешнему связыванию. Обычно говорят, что имена типов, т.е. классов и перечислений,не подлежат связыванию. Имена глобальных классов и перечисленийдолжны быть уникальными во всей программе и иметь единственноеопределение. Поэтому, если есть два даже идентичных определенияодного класса, это - все равно ошибка: // file1.c: struct S { int a; char b; }; extern void f(S*); // file2.c: struct S { int a; char b; }; void f(S* p) { /*... */ } Но будьте осторожны: опознать идентичность двух описаний классане в состоянии большинство систем программирования С++. Такоедублирование может вызвать довольно тонкие ошибки (ведь классыв разных файлах будут считаться различными). Глобальные функции-подстановки подлежат внутреннему связыванию,и то же по умолчанию справедливо для констант. Синонимы типов,т.е. имена typedef, локальны в своем файле, поэтому описанияв двух данных ниже файлах не противоречат друг другу: // file1.c: typedef int T; const int a = 7; inline T f(int i) { return i+a; } // file2.c: typedef void T; const int a = 8; inline T f(double d) { cout<<d; } Константа может получить внешнее связывание только с помощью явногоописания: // file3.c: extern const int a; const int a = 77; // file4.c: extern const int a; void g() { cout<<a; } В этом примере g() напечатает 77.

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

Типы одного объекта или функции должны быть согласованы во всех ихописаниях. Должен быть согласован по типам и входной текст,обрабатываемый транслятором, и связываемые части программы. Естьпростой, хотя и несовершенный, способ добиться согласованностиописаний в различных файлах. Это: включить во входные файлы,содержащие операторы и определения данных, заголовочные файлы,которые содержат интерфейсную информацию. Средством включения текстов служит макрокоманда #include,которая позволяет собрать в один файл (единицу трансляции)несколько исходных файлов программы. Команда #include "включаемый-файл" заменяет строку, в которой она была задана, на содержимое файлавключаемый-файл. Естественно, это содержимое должно быть текстомна С++, поскольку его будет читать транслятор. Как правило, операциявключения реализуется отдельной программой, называемой препроцессоромС++. Она вызывается системой программирования перед собственнотрансляцией для обработки таких команд во входном тексте. Возможнои другое решение: часть транслятора, непосредственно работающаяс входным текстом, обрабатывает команды включения файлов по мере ихпоявления в тексте. В той системе программирования, в которойработает автор, чтобы увидеть результат команд включения файлов,нужно задать команду: CC -E file.c Эта команда для обработки файла file.c запускает препроцессор(и только!), подобно тому, как команда CC без флага -E запускает самтранслятор. Для включения файлов из стандартных каталогов (обычно каталогис именем INCLUDE) надо вместо кавычек использовать угловые скобки< и >. Например: #include <stream.h> // включение из стандартного каталога #include "myheader.h" // включение из текущего каталога Включение из стандартных каталогов имеет то преимущество, что именаэтих каталогов никак не связаны с конкретной программой (обычновначале включаемые файлы ищутся в каталоге /usr/include/CC, азатем в /usr/include). К сожалению, в этой команде пробелы существенны: #include < stream.h> // <stream.h> не будет найден Было бы нелепо, если бы каждый раз перед включением файлатребовалась его перетрансляция. Обычно включаемые файлы содержаттолько описания, а не операторы и определения, требующие существеннойтрансляторной обработки. Кроме того, система программированияможет предварительно оттранслировать заголовочныефайлы, если, конечно, она настолько развита, что способна сделатьэто, не изменяя семантики программы. Укажем, что может содержать заголовочный файл: Определения типов struct point { int x, y; }; Шаблоны типов template<class T> class V { /*... */ } Описания функций extern int strlen(const char*); Определения inline char get() { return *p++; } функций-подстановок Описания данных extern int a; Определения констант const float pi = 3.141593; Перечисления enum bool { false, true }; Описания имен class Matrix; Команды включения файлов #include <signal.h> Макроопределения #define Case break;case Комментарии /* проверка на конец файла */ Перечисление того, что стоит помещать в заголовочный файл, не является требованием языка, это просто совет по разумному использованию включения файлов. С другой стороны, в заголовочном файле никогда не должно быть: Определений обычных функций char get() { return *p++; } Определений данных int a; Определений составных const tb[i] = { /*... */ }; констант По традиции заголовочные файлы имеют расширение.h, а файлы, содержащие определения функций или данных, расширение.c. Иногда их называют "h-файлы" или "с-файлы" соответственно. Используют и другие расширения для этих файлов:.C, cxx,.cpp и.cc. Принятое расширение вы найдете в своем справочном руководстве. Макросредства описываются в $$4.7. Отметим только, что в С++ они используются не столь широко, как в С, поскольку С++ имеет определенные возможности в самом языке: определения констант (const), функций-подстановок (inline), дающие возможность более простой операции вызова, и шаблонов типа, позволяющие порождать семейство типов и функций ($$8). Совет помещать в заголовочный файл определения только простых, но не составных, констант объясняется вполне прагматической причиной. Просто большинство трансляторов не настолько разумно, чтобы предотвратить создание ненужных копий составной константы. Вообще говоря, более простой вариант всегда является более общим, а значит транслятор должен учитывать его в первую очередь, чтобы создать хорошую программу.


Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2016-02-16 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: