Это ваша первая программа на C (или C++) — она не такая уж большая, и вы собираетесь скомпилировать ее. Вы нажимаете на compile
(или вводите команду компиляции) и ждете. Ваш компилятор выдает пятьдесят строк текста. Вы выбираете слова warning
и error
. Задумываетесь, значит ли это, что все в порядке. Вы ищите полученный исполняемый файл. Ничего. Черт возьми, думаете вы, я должен выяснить, что все это значит …
Типы ошибок компиляции
Во-первых, давайте различать типы ошибок. Большинство компиляторов покажет три типа предупреждений во время компиляции:
- предупреждения компилятора;
- ошибки компилятора;
- ошибки компоновщика.
Хоть вы и не хотите игнорировать их, предупреждения компилятора не являются чем-то достаточно серьезным, чтобы не скомпилировать вашу программу. Прочитайте следующую статью, которая расскажет вам, почему стоит дружить с компилятором и его предупреждениями. Как правило, предупреждения компилятора — это признак того, что что-то может пойти не так во время выполнения. Как компилятор узнает об этом? Вы, должно быть делали типичные ошибки, о которых компилятор знает. Типичный пример — использование оператора присваивания
=
вместо оператора равенства ==
внутри выражения. Ваш компилятор также может предупредить вас об использовании переменных, которые не были инициализированы и других подобных ошибках. Как правило, вы можете установить уровень предупреждений вашего компилятора — я устанавливаю его на самый высокий уровень, так что предупреждения компилятора не превращаются в ошибки в выполняемой программе (“ошибки выполнения”).
Тем не менее, предупреждения компилятора не должны останавливать работу вашей программы (если только вы не укажете компилятору рассматривать предупреждения как ошибки), так что они, вероятно, не так серьезны как ошибки.
Ошибки — это условия, которые препятствуют завершению компиляции ваших файлов.
Ошибки компилятора ограничены отдельными файлами исходного кода и являются результатом “синтаксических ошибок”. На самом деле, это означает, что вы сделали что-то, что компилятор не может понять. Например, выражение for(;)
синтаксически не правильно, потому что цикл всегда должен иметь три части. Хотя компилятор ожидал точку с запятой, он мог также ожидать условное выражение, поэтому сообщение об ошибке, которое вы получите может быть что-то вроде:
line 13, unexpected parenthesis ‘)’
Заметьте, что ошибки компилятора всегда будут включать номер строки, в которой была обнаружена ошибка.
Даже если вы прошли процесс компиляции успешно, вы можете столкнуться с ошибками компоновщика. Ошибки компоновщика, в отличие от ошибок компилятора, не имеют ничего общего с неправильным синтаксисом. Вместо этого, ошибки компоновщика — это, как правило, проблемы с поиском определения функций, структур, классов или глобальных переменных, которые были объявлены, но не определены, в файле исходного кода. Как правило, эти ошибки будут иметь вид:
could not find definition for X
Как правило, процесс компиляции начинается с серии ошибок компиляции и предупреждений и, исправив их, вы столкнетесь с ошибками компоновщика. В свою очередь, я бы сначала исправлял ошибки компиляции, а затем ошибки компоновщика.
Ошибки компилятора — с чего начать?
Если вы столкнулись с перечнем пятидесяти или шестидесяти ошибок и предупреждений, то будет сложно определить с чего начать. Самое лучшее место, тем не менее, в начале списка. В самом деле, вы почти никогда не начинаете исправлять ошибки от конца файла до его начала по одной простой причине: вы не знаете ошибки ли они на самом деле!
Одна ошибка в верхней части вашей программы может вызвать целый ряд других ошибок компилятора, потому что эти строки могут рассчитывать на что-то в начале программы, что компилятор не смог понять. Например, если вы объявляете переменную с неправильным синтаксисом, компилятор сообщит о синтаксических ошибках, и что он не может найти объявление для переменной. Точка с запятой, поставленные не в том месте, могут привести к огромному количеству ошибок. Это происходит, потому что синтаксис C и C++ синтаксис позволяет объявить тип сразу же после его определения:
struct { int x; int y; } myStruct;
код создаст переменную, MyStruct
, с местом для хранения структуры, содержащей два целых числа. К сожалению, это означает, что если вы опустите точку с запятой, компилятор будет интерпретировать это так, как будто следующая вещь в программе будет структурой (или возвращает структуру).
Что-то вроде этого:
struct MyStructType { int x; int y; } int foo() {}
может привести к огромному количеству ошибок, возможно, включая сообщения:
extraneous ‘int’ ignored
Все это из-за одного символа! Лучше всего начать с самого верха.
Анализ сообщения об ошибке
Большинство сообщений от компилятора будет состоять как минимум из четырех вещей:
- тип сообщения — предупреждение или ошибка;
- исходный файл, в котором появилась ошибка;
- строка ошибки;
- краткое описание того, что работает неправильно.
Вывод g++ для указанной выше программы может выглядеть следующим образом (ваши результаты могут отличаться, если вы используете другой компилятор):
foo.cc:7: error: semicolon missing after struct declaration
foo.cc это имя файла. 7 — номер строки, и ясно, что это ошибка. Короткое сообщение здесь весьма полезно, поскольку оно показывает именно то, что не правильно. Заметим, однако, что сообщение имеет смысл только в контексте программы. Оно не сообщает, в какой структуре не хватает запятой.
Более непонятным является другое сообщение об ошибке из той же попытки компиляции:
extraneous ‘int’ ignored
Программист должен выяснить, почему это произошло. Обратите внимание еще раз, что эта ошибка была вызвана проблемой в начале программы, не в строке 8, а раньше, когда в структуре не хватает точки с запятой. К счастью, понятно, что определение функции для foo было в порядке, это говорит нам о том, что ошибка должна быть где-то в другом месте программы. На самом деле, она должна быть в программе раньше — вы не будете получать сообщение об ошибке, которое указывает на синтаксическую ошибку до строки, на которой ошибка на самом деле произошла.
Это руководящий принцип вычисления ошибок компилятора: если сомневаетесь, посмотрите в программе раньше. Так как синтаксические ошибки могут позже иметь серьезные последствия, вполне возможно, что компилятор указывал номер строки, в которой на самом деле не было синтаксической ошибки!
Будет гораздо хуже, если компилятор не будет сообщать вам, что произошло ранее в программе. Даже первая ошибка компилятора, которую вы получите, может быть связана с несколькими строками до указанного предупреждения.
Обработка непонятных или странных сообщений
Есть несколько особенно сложных типов ошибок компилятора. Первый — это необъявленная переменная, которую, как вам кажется, вы объявили. Часто, вы можете указать, где именно переменная была объявлена! Проблема в том, что часто переменная просто написана с ошибкой. К сожалению, это довольно трудно увидеть, так как обычно мы читаем то, что ожидаем, а не то, что есть на самом деле. Кроме того, есть и другие причины, почему это может быть проблемой — например, проблемы с видимостью!
Чтобы разобраться в возможных проблемах, я делаю так: в строке, где находится якобы необъявленная переменная, надо выполнить поиск текстовым редактором слова под курсором (в качестве альтернативы можно скопировать имя переменной и выполнить поиск), и если я записал его неправильно, оно не найдется. Также не надо вводить имя переменной вручную, так как вы случайно можете ввести его правильно.
Второе непонятное сообщение:
unexpected end of file
Что происходит? Почему конец файла будет «неожиданным» ? Ну, здесь главное думать как компилятор; если конец файла является неожиданным, то он, должно быть, чего-то ждет. Что бы это могло быть? Ответ, как правило, «завершение». Например, закрывающие фигурные скобки или закрывающие кавычки. Хороший текстовый редактор, который выполняет подсветку синтаксиса и автоматический отступ, должен помочь исправить некоторые из этих ошибок, что позволяет легче обнаружить проблемы при написании кода.
В конечном счете, если сообщение непонятное, то подходите к проблеме, думая, как компилятор пытается интерпретировать файл. Это может быть трудно, когда вы только начинаете, но если вы обращаете внимание на сообщения и попробуете понять, что они могли бы означать, вы быстро привыкнете к общим закономерностям.
Наконец, если ничего не работает, вы всегда можете просто переписать несколько строк кода, чтобы убрать любые скрытые синтаксические ошибки, которые вы могли не увидеть. Это может быть опасно, так как вы можете переписать не ту секцию, но это может помочь.
Ошибки компоновщика
После того как вы окончательно исправили все ошибки синтаксиса, вздремнули, перекусили пару раз и морально подготовили себя к правильной компиляции программы, вы все равно можете столкнуться с ошибками компоновщика. Их часто довольно сложно исправить, потому что они не обязательно являются результатом того, что написано в вашей программе. Я вкратце опишу типичные видов ошибок компоновщика, которые можно ожидать, и некоторые пути их решения.
У вас могут возникнуть проблемы с тем, как вы настроили свой компилятор. Например, даже если включить нужные заголовочные файлы для всех ваших функций, вы все равно должны предоставить вашему компоновщику правильный путь в библиотеку, которая имеет фактическую реализацию. В противном случае, вы получите сообщение об ошибке:
undefined function
Обратите внимание на поддержку этих функций компилятором (это может произойти, если вы включите собственное объявление функции, чтобы обойти ошибку во время компиляции). Если ваш компилятор поддерживает эту функцию, то для решения проблемы обычно требуются конкретные настройки компилятора. Вам следует сообщить компилятору, где искать библиотеки и убедиться, что библиотеки были установлены правильно.
Ошибки компоновщика могут произойти в функциях, которые вы объявили и определили, если вы не включили все необходимые объектные файлы в процесс связывания. Например, если вы пишете определение класса в
myClass.cpp
, а ваша основная функция в myMain.cpp
, компилятор создаст два объектных файла, myClass.o и myMain.o, а компоновщику будут нужны оба из них для завершения создания новой программы. Если оставить myClass.o
, то у него не будет определения класса, даже если вы правильно включите myClass.h
!
Иногда появляются незначительные ошибки, когда компоновщик сообщает о более чем одном определении для класса, функции или переменной. Эта проблема может появиться по нескольким причинам: во-первых, у объекта может быть два определения — например, две глобальные переменные объявлены как внешние переменные, чтобы быть доступными за пределами файла исходного кода. Это относится как к функциям, так и к переменным, и это, на самом деле, нередко случается. С другой стороны, иногда это проблема с директивами компоновщика; несколько раз я видел, как люди включают несколько копий одного и того же объектного файла в процесс связывания. И бинго, у вас есть несколько определений. Типичным проявлением этой проблемы является то, что у целого ряда функций есть несколько определений.
Последний странный тип ошибки компоновщика — сообщение
undefined reference to main
Данная ошибка компоновщика отличается от других тем, что она может не иметь ничего общего с объектом, включая файлы или правильные пути к вашей библиотеке. Напротив, это означает, что компоновщик пытался создать исполняемый файл и не смог понять, где расположена функция main()
. Это может случиться, если вы забыли включить основную функцию, или, если вы попытаетесь скомпилировать код, который никогда не был отдельным исполняемым файлом (например, если вы попытались скомпилировать библиотеку).
0 / 0 / 1 Регистрация: 04.12.2012 Сообщений: 18 |
|
1 |
|
Игнорирование ошибок при компилировании10.09.2014, 04:18. Показов 9082. Ответов 16
Всем привет. Проблема такая: Есть код с ошибками. Необходимо его так и скомпилировать.
0 |
1449 / 1121 / 346 Регистрация: 11.04.2011 Сообщений: 2,621 |
|
10.09.2014, 05:30 |
2 |
Есть код с ошибками. Необходимо его так и скомпилировать.
0 |
4607 / 2028 / 359 Регистрация: 17.03.2012 Сообщений: 10,086 Записей в блоге: 6 |
|
10.09.2014, 10:44 |
3 |
Может, ему (компилятору) денег дать? Чтобы не кочевряжился.
6 |
Почетный модератор 16842 / 6720 / 880 Регистрация: 12.06.2012 Сообщений: 19,967 |
|
10.09.2014, 11:52 |
4 |
вы попробуйте системнику пистолетом погрозить.. Только надо уверенно, чтобы он поверил и испугался. Тогда он компилятору скажет, что там серьезно все и надо собрать несмотря на ошибки….
2 |
41 / 33 / 24 Регистрация: 09.06.2012 Сообщений: 144 |
|
10.09.2014, 13:11 |
5 |
populois, ну ответ такой — никак.
0 |
kolorotur 17215 / 12669 / 3321 Регистрация: 17.09.2011 Сообщений: 20,941 |
||||
10.09.2014, 13:48 |
6 |
|||
Есть код с ошибками. Необходимо его так и скомпилировать. Встречный вопрос: что должна выполнять программа, скомпилированная из нижеприведенного кода?
1 |
65 / 65 / 16 Регистрация: 07.04.2014 Сообщений: 332 |
|
10.09.2014, 16:02 |
7 |
Спасибо, поржал
0 |
484 / 439 / 123 Регистрация: 05.01.2010 Сообщений: 1,848 |
|
10.09.2014, 16:59 |
8 |
kolorotur, а что тут непонятно-то? написано же — хочу мир захватить. сделай
1 |
307 / 284 / 102 Регистрация: 06.05.2014 Сообщений: 861 |
|
10.09.2014, 17:02 |
9 |
Позвольте, в качестве нездорового любопытства, поинтересоваться — а с каких побуждений энто потребовалось?
0 |
1 / 1 / 1 Регистрация: 22.08.2014 Сообщений: 9 |
|
10.09.2014, 17:23 |
10 |
1. Заходишь в диск с виндой. По теме: каким образом должен комплиятор скомплириовать код с ошибкой?
0 |
65 / 65 / 16 Регистрация: 07.04.2014 Сообщений: 332 |
|
11.09.2014, 09:59 |
11 |
Позвольте, в качестве нездорового любопытства, поинтересоваться — а с каких побуждений энто потребовалось? А что тут непонятного? У человека есть код, но он вот не компилится. Пишет что-то вроде «NullReferenceException». Ну так пусть компилится и работает хоть как то.
0 |
4607 / 2028 / 359 Регистрация: 17.03.2012 Сообщений: 10,086 Записей в блоге: 6 |
|
11.09.2014, 10:13 |
12 |
Пишет что-то вроде «NullReferenceException». Ну так пусть компилится и работает хоть как то. NullReferenceException — это run-time ошибка.
0 |
65 / 65 / 16 Регистрация: 07.04.2014 Сообщений: 332 |
|
11.09.2014, 10:29 |
13 |
NullReferenceException — это run-time ошибка. И не при каких условиях во время компиляции вылететь не может? Даже если и так, то сразу после может.
0 |
4607 / 2028 / 359 Регистрация: 17.03.2012 Сообщений: 10,086 Записей в блоге: 6 |
|
11.09.2014, 10:39 |
14 |
И не при каких условиях во время компиляции вылететь не может? Даже если и так, то сразу после может. Не может. Разве что это будет ошибка в самом компиляторе, но я очень сильно в этом сомневаюсь
0 |
1216 / 1004 / 259 Регистрация: 15.06.2012 Сообщений: 3,888 |
|
11.09.2014, 13:09 |
15 |
Есть код с ошибками. Не проще исправить эти ошибки? Например можно привести этот код сюда, люди скажут, чего не собирается.
0 |
0 / 0 / 1 Регистрация: 04.12.2012 Сообщений: 18 |
|
05.08.2017, 16:16 [ТС] |
16 |
Эту тему действительно я создавал?)
0 |
17215 / 12669 / 3321 Регистрация: 17.09.2011 Сообщений: 20,941 |
|
05.08.2017, 16:29 |
17 |
Эту тему действительно я создавал? Самый простой способ проверить прирост своих знаний и опыта — это открыть старую тему или код и обратить внимание на свою реакцию. Так что мои поздравления!
3 |
Is this legal in Groovy?
class RequestContext {
def static requestContext
def static setRequestContext(rc) {
requestContext = rc
}
}
Given the above I expect the following to fail at compile time using the groovy-eclipse-compiler:
RequestContext.setRequestContext()
Yet this passes and I’m trying to get this to fail at mvn compile
time.
asked Mar 26, 2020 at 22:28
It can’t fail at compile time, because you might add that method dynamically at runtime via the metaclass, ie:
class Test {
}
Test.metaClass.static.woo = { -> println "yay" }
Test.woo() // prints 'yay'
To fail at compile time, you’d need to annotate the calling class with @CompileStatic
or @TypeChecked
answered Mar 26, 2020 at 22:36
tim_yatestim_yates
166k26 gold badges341 silver badges338 bronze badges
Вопрос:
Извините, если это такой простой вопрос, должно быть что-то, что я не понимаю о наследовании, virtual
и override
в c++. В следующем примере я получаю ошибку времени компиляции относительно виртуального метода, который я специально переопределяю, чтобы избежать такой ошибки в дочернем классе. Я делаю что-то неправильно?
#include <array>
#include <deque>
template <class T, class C>
struct foo
{
virtual const C& data() const =0;
inline virtual T& operator[] ( unsigned n ) const
{ return const_cast<T&>( data()[n] ); }
};
/**
* The implementation of foo::operator[] is useful for classes inheriting
* with simple sequence containers like:
* foo<T,std::deque<T>>, foo<T,std::vector<T>>, ..
*
* But the following requires operator[] to be redefined:
*/
template <class T, unsigned N>
struct baz
: public foo<T, std::deque<std::array<T,N>> >
{
typedef std::deque<std::array<T,N>> data_type;
data_type m_data;
inline const data_type& data() const
{ return m_data; }
inline virtual T& operator[] ( unsigned n ) const override
{ return const_cast<T&>( data()[n/N][n%N] ); }
};
int main()
{
baz<double,3> b; // throws an error relative to foo::operator[] depsite override
}
EDIT 1 Ошибка:
clang++ -std=c++0x -Wall virtual_operator.cpp -o virtual_operator.o
virtual_operator.cpp:11:12: error: const_cast from 'const value_type' (aka 'const std::__1::array<double, 3>') to 'double &' is not allowed
{ return const_cast<T&>( data()[n] ); }
^~~~~~~~~~~~~~~~~~~~~~~~~~~
virtual_operator.cpp:26:8: note: in instantiation of member function 'foo<double, std::__1::deque<std::__1::array<double, 3>, std::__1::allocator<std::__1::array<double, 3> > > >::operator[]'
requested here
struct baz
^
1 error generated.
EDIT 2 Я считаю это частью вопроса; если компиляция завершается с ошибкой, потому что foo::operator[]
по-прежнему может быть вызвана в baz
, то почему она компилируется отлично, если я не объявляю foo::operator[]
как виртуальную (т.е. скрываясь вместо переопределения)?
Лучший ответ:
Проблема в том, что, хотя вы только намереваетесь вызвать функцию производного operator[]
на экземплярах baz
, компилятору все равно необходимо сгенерировать код для базового класса, потому что эта функция по-прежнему может быть вызвана в случаях baz
. В этом случае генерация этого кода приводит к ошибке типа, потому что вы пытаетесь использовать const std::array<double,3>
в double&
.
Чтобы исправить это, вы должны иметь разные части иерархии, которые определяют оператора, который будет работать для всех его детей, например (с удалением непереносимого материала):
template <class T, class C>
struct foo
{
inline virtual T& operator[] ( unsigned n ) const = 0;
};
template <class T>
struct bar
: public foo<T,std::deque<T>>
{
inline virtual T& operator[] ( unsigned n ) const override
{ return const_cast<T&>( data()[n] ); }
};
template <class T, unsigned N>
struct baz
: public foo<T, std::deque<std::array<T,N>> >
{
inline virtual T& operator[] ( unsigned n ) const override
{ return const_cast<T&>( data()[n/N][n%N] ); }
};
Таким образом, если у вас есть какие-либо другие версии, которые вы хотите добавить позже, вы можете получить из бара или база и не обязательно определять оператора для каждого ребенка.
Прежде чем идти дальше, попытаемся завершить обсуждение арифметических выражений; оно осталось бы неполным без побитовых операций, записи целых чисел в системах счисления, отличных от десятичной, и именованных констант.
2.5.1. Побитовые операции
Побитовые операции выполняются над целыми числами одинакового типа, но при этом числа рассматриваются не как собственно числа, а как строки отдельных битов (т. е. двоичных цифр), составляющих машинное представление. Например, если в побитовой операции участвует число 75 типа integer, то имеется в виду битовая строка 000000001001011.
Побитовые операции можно разделить на два вида: логические операции, выполняемые над отдельными битами (причём над всеми одновременно) и сдвиги. Так, операция not, применённая к целому числу (в отличие от уже знакомой нам операции not в применении к значению типа boolean) даёт в результате число, все биты которого противоположны исходному. Например, если переменные х и у имеют тип integer, после выполнения операторов
в переменной у окажется число -76, машинное представление которого 1111111110110100 представляет собой побитовую инверсию приведённого выше представления числа 75. Отметим, что если бы х и у имели тип word, т. е. беззнаковый тип той же разрядности, то результат в переменной у составил бы 65460; машинное представление этого числа в виде 16-битного беззнакового такое же, как у числа -76 в виде 16-битного знакового.
Аналогичным образом над целыми числами работают уже знакомые нам по § 2.3.4 операции and, or и xor. Все эти операции являются бинарными, то есть требуют двух операндов; когда мы применяем их к целым числам, соответствующие логические операции (“и”, “или”, “исключающее или”) выполняются одновременно над первыми битами операндов, над их вторыми битами и так далее; результаты (тоже отдельные биты) соединяются в целое число того же типа и, как следствие, той же разрядности, которое и становится результатом всей операции. Например, восьмибитное беззнаковое представление (то есть представление типа byte) для чисел 42 и 166 будет соответственно 00101010 и 10100110; если у нас есть переменные х, у, р, q и r, имеющие тип byte, то после присваиваний
переменные р, q и r получат соответственно значения 34 (00100010), 174 (10101110) и 140 (10001100).
Операции побитового сдвига, как следует из названия, сдвигают битовое представление на определённое число позиций влево (shl, от слов shift left) или вправо (shr, shift right). Обе операции бинарные, то есть предусматривают два операнда; слева от названия операции ставится исходное целое число, справа — количество позиций, на которое нужно сдвинуть его побитовое машинное представление. При сдвиге влево на k позиций старшие k разрядов машинного представления числа пропадают, а справа (то есть в качестве младших разрядов) дописывается kнулевых битов. Сдвиг влево на k позиций эквивалентен домножению числа на 2k. Например, результатом выражения 1 shl 5 будет число 32, а результатом 21 shl 3 будет 168.
При сдвиге вправо пропадают, наоборот, k младших разрядов, что касается старших битов, то здесь операция работает по-разному для знаковых чисел и для беззнаковых. При сдвиге вправо беззнакового числа слева дописываются нулевые биты; при сдвиге знакового слева дописываются нули или единицы в зависимости от исходного значения самого старшего (“знакового”) бита числа, так что знак числа при сдвиге сохраняется: результатом сдвига вправо положительного числа будет всегда положительное, отрицательного — отрицательное. В частности, 21 shr 3 даст значение 2 (из представления 00010101 получится 00000010), (-64) shr 3 даст -8. Сдвиг вправо числа -1 всегда даёт -1, на сколько бы позиций мы его ни сдвигали.
2.5.2. Именованные константы
Исходно словом “константа” обозначается такое выражение, значение которого всегда одно и то же. Тривиальным примером константы может послужить литерал — например, просто число, написанное в явном виде. К примеру, “37.0” — это литерал, который представляет собой выражение типа real; очевидно, что значение этого выражения всегда будет одно и то же, а именно — 37.0; следовательно, это константа. Можно привести более сложный пример константы: выражение “6*7”. Это уже не литерал, это арифметическое выражение, а литералов тут два — это числа 6 и 7; тем не менее, значение этого выражения тоже всегда одно и то же, так что и это — пример константы.
Среди всех констант выделяют константы времени компиляции — это такие константы, значение которых компилятор определяет во время обработки нашей программы. К таким константам относятся все литералы, что вполне естественно; кроме того, во время компиляции компилятор может вычислять арифметические выражения, не содержащие обращений к переменным и функциям. Поэтому “6*7” — это тоже константа времени компиляции, компилятор сам вычислит, что значение здесь всегда 42, и именно число 42 поместит в машинный код; ни шестёрка, ни семёрка, ни операция умножения в коде фигурировать не будут.
Кроме констант времени компиляции, встречаются также константы времени исполнения — это выражения, которые по идее всегда имеют одно и то же значение, но компилятор это значение во время компиляции вычислить по каким-то причинам не может, так что оно становится известно только во время выполнения программы. Точная граница между этими видами констант зависит от реализации компилятора; например, Free Pascal умеет во время компиляции вычислять синусы, косинусы, квадратные корни, логарифмы и экспоненты, хотя делать всё это он совершенно не обязан, и другие версии Паскаля такого не делают.
Впрочем, можно найти ограничения к для Free Pascal: например, функции обработки строк, даже если их вызвать с константными литералами в качестве параметров, во время компиляции не вычисляются. Так, следующий фрагмент приведёт к ошибке во время компиляции, несмотря на то, что функция сору точно так же встроена в компилятор Паскаля, как и упоминавшиеся выше математические функции вроде синуса и логарифма:
Механизм именованных констант позволяет связать с неким постоянным значением (константой времени компиляции) некое имя, т. е. идентификатор, и во всём тексте программы вместо значения, записанного в явном виде, использовать этот идентификатор. Делается это в секции описания констант, которую можно расположить в любом месте между заголовком и началом главной части программы, но обычно секцию констант программисты располагают как можно ближе к началу файла — например, сразу после заголовка программы. Дело тут в том, что значения некоторых констант могут оказаться (и оказываются) самой часто изменяемой частью программы, и расположение констант в самом начале программы позволяет сэкономить время и интеллектуальные усилия при их редактировании.
Для примера рассмотрим программу hello20_for; она выдаёт “на экран” (в стандартный поток вывода) сообщение “Hello, world!”, причём делает это 20 раз. Эту задачу можно очевидным образом обобщить: программа выдаёт заданное сообщение заданное число раз. Из школьного курса физики нам известно, что практически любую задачу лучше всего решать в общем виде, а конкретные значения подставлять в самом конце, когда уже получено общее решение. Аналогичным образом можно поступать и в программировании. В самом деле, что изменится в программе, если мы захотим изменить выдаваемое сообщение? А если мы захотим выдавать сообщение не 20 раз, а 27? Ответ очевиден: изменятся только соответствующие константы-литералы. В такой короткой программе, как hello_20, конечно, найти эти литералы несложно; а если программа состоит хотя бы из пятисот строк? Из пяти тысяч? И ведь это далеко не предел: в наиболее крупных и сложных компьютерных программах счёт строк идёт на десятки миллионов.
При этом заданные в коде константы, от которых зависит выполнение программы, в достаточной степени произвольны: на это однозначно указывает то обстоятельство, что задача очевидным образом обобщается на произвольные значения. Логично при этом будет ожидать, что нам, возможно, захочется изменить значения констант, не меняя больше ничего в программе; в этом смысле константы подобны настроечным ручкам разнообразных технических устройств. Именованные константы позволяют облегчить такую “настройку”: если без их применения литералы рассыпаны по всему коду, то давая каждой константе собственное имя, мы можем собрать все “параметры настройки” в начале текста программы, при необходимости снабдив их комментариями. Например, вместо программы hello20_for мы можем написать следующую программу:
Как видим, секция описаний констант состоит из ключевого слова const, за которым следует одно или несколько описаний константы; каждое такое описание состоит из имени (идентификатора) новой константы, знака равенства, выражения, задающего значение константы (это выражение само должно быть константой времени компиляции) и точки с запятой. С того момента, как компилятор обработает такое описание, в дальнейшем тексте программы введённый этим описанием идентификатор будет заменяться на связанное с ним константное значение. Само имя константы, что вполне естественно, тоже считается константой времени компиляции; как мы увидим позже (например, когда будем изучать массивы), это обстоятельство достаточно важно.
Облегчением “настройки” программы полезность именованных констант не ограничивается. Например, часто бывает так, что одно и то же константное значение встречается в нескольких разных местах программы, причём по смыслу при изменении его в одном из мест нужно также (синхронно) изменить и все остальные места, где встречается та же самая константа. Например, если мы пишем программу, управляющую камерой хранения из отдельных автоматизированных ячеек, то нам наверняка потребуется знать, сколько ячеек у нас есть. От этого будет зависеть, например, подсчёт числа свободных ячеек, всяческие элементы пользовательского интерфейса, где необходимо выбрать одну ячейку из всех имеющихся, и многое другое. Ясно, что число, означающее общее количество ячеек, будет то и дело встречаться в разных частях программы. Если теперь инженеры вдруг решат спроектировать такую же камеру хранения, но на несколько ячеек больше, нам придётся внимательно просмотреть всю нашу программу в поисках проклятого числа, которое теперь надо везде поменять. Несложно догадаться, что такие вещи представляют собой неиссякаемый источник ошибок: если, скажем, в программе одно и то же число встречается тридцать раз, то можно быть уверенным, что с первого просмотра мы “выловим” от силы двадцать таких вхождений, а остальные упустим.
Ситуация резко осложняется, если в программе есть два разных, не зависящих друг от друга параметра, которые волей случая оказались равны одному и тому же числу; например, у нас имеется 26 ячеек камеры хранения, а ещё у нас есть чековый принтер, в строке которого умещается 26 символов, и оба числа встречаются прямо в тексте программы. Если один из этих параметров придётся изменить, то можно не сомневаться, что мы не только упустим часть вхождений нужного параметра, но разок-другой изменим тот параметр, который менять не требовалось.
Совсем другое дело, если в явном виде количество ячеек нашей камеры хранения встречается в программе лишь один раз — в самом её начале, а далее по тексту везде используется имя константы, например, LockerBoxCount или что-нибудь в этом духе. Изменить значение такого параметра очень просто, поскольку само это значение в программе написано ровно в одном месте; риск изменить что-нибудь не то при этом также исчезает.
Необходимо отметить ещё одно очень важное достоинство именованных констант: в программе, созданной с их использованием, гораздо легче разобраться. Поставьте себя, например, на место человека, который где-нибудь в дебрях длинной (скажем, в несколько тысяч строк) программы натыкается на число 80, написанное вот прямо так, цифрами в явном виде — и, разумеется, без комментариев. Что это за “80”, чему оно соответствует, откуда взялось? Может быть, это возраст дедушки автора программы? Или количество этажей в небоскрёбе на нью-йоркском Бродвее? Или максимально допустимое количество символов в строке текста, выводимой на экран? Или комнатная температура в градусах Фаренгейта?
Потратив изрядное количество времени, читатель такой программы может заметить, что 80 составляет часть сетевого адреса, так называемый порт, при установлении соединения с каким-то удалённым сервером; припомнив, что порт с этим номером обычно используется для веб-серверов, можно будет догадаться, что программа что-то откуда-то пытается получить по протоколу HTTP (и, кстати, ещё не факт, что догадка окажется верна). Сколько времени уйдёт на такой анализ? Минута? Десять минут? Час? Зависит, конечно, от сложности конкретной программы; но если бы вместо числа 80 в программе стоял идентификатор DefaultHttpPortNumber, тратить время не пришлось бы вовсе.
В большинстве случаев используемые правила оформления программного кода попросту запрещают появление в программе (вне секции описания констант) чисел, написанных в явном виде, за исключением чисел 0, 1 и (иногда) -1; всем остальным числам предписывается обязательно давать имена. В некоторых организациях программистам запрещают использовать в глубинах программного кода не только числа, но и строки, то есть все строковые литералы, нужные в программе, требуется вынести в начало и снабдить именами, а в дальнейшем тексте использовать эти имена.
Рассмотренные нами константы называются в Паскале нетипизированными, поскольку при их описании не указывается их тип, он выводится уже при их использовании. Кроме них, Паскаль (во всяком случае, его диалекты, родственные знаменитому Turbo Pascal, в том числе к наш Free Pascal) предусматривает также типизированные константы, для которых тип указывается в явном виде при описании. В отличие от нетипизированных констант, типизированные константы не являются константами времени компиляции; более того, в режиме, в котором компилятор работает по умолчанию, значения таких констант разрешается изменять во время выполнения, что вообще делает сомнительным применение названия “константы”. Историю возникновения этой странной сущности мы оставляем за рамками нашей книги; заинтересованный читатель легко найдёт соответствующие материалы самостоятельно. В нашем учебном курсе типизированные константы не нужны, и рассматривать их мы не будем. Так или иначе, на случай, если вам попадутся примеры программ, использующие типизированные константы, что-то вроде
помните, что это совсем не то же самое, что константы без указания типа, и по своему поведению похоже скорее на инициализированную переменную, чем на константу.
2.5.3. Разные способы записи чисел
До сих пор мы имели дело преимущественно с целыми числами, записанными в десятичной системе счисления, а когда нам приходилось работать с дробными числами, записывали их в простейшей форме — в виде обычной десятичной дроби, в которой роль десятичной запятой играет символ точки.
Современные версии языка Паскаль позволяет записывать целые числа в шестнадцатеричной системе счисления; для этого используется символ “4” и последовательность шестнадцатеричных цифр, причём для обозначения цифр, превосходящих девять, можно использовать как заглавные, так и строчные латинские буквы. Например, $1А7 или $1а7 — это то же самое, что и 423.
Free Pascal поддерживает, кроме этого, ещё литералы в двоичной к восьмеричной системах. Восьмеричные константы начинаются с символа “&”, двоичные — с символа “%”. Например, число 423 можно записать также и в виде 5110100111, и в виде &647. Другие версии Паскаля не поддерживают такую запись чисел; не было её и в Turbo Pascal.
Что касается чисел с плавающей точкой, то они всегда записываются в десятичной системе, но и здесь есть форма записи, отличающаяся от привычной. Мы уже сталкивались с так называемой научной нотацией, когда выводили числа с плавающей точкой на печать; напомним, что при этом печаталась мантисса, то есть число, удовлетворяющее условию 1 ≤ m < 10, затем буква “Е” и целое число, обозначающее порядок (степень 10, на которую нужно умножить мантиссу). Аналогичным образом можно записывать числа с плавающей точкой в тексте программы. Например, 7E3 — это то же самое, что 700.0, а 2.5Е-5 — то же, что 0.000025.