Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Алгоритмы и структуры данных. Учебное пособие по направлению подготовки 09.03.02 «Информационные системы и технологии»

.pdf
Скачиваний:
0
Добавлен:
06.09.2026
Размер:
1 Мб
Скачать
затель). В описаниях и определениях звездочка означает, что описывается (определяется) указатель на значение использованного в объявлении типа.
Обозначение присваивания. Для обозначения операции присваивания
используется символ '='. Кроме того, в определении объекта он исполь­зуется при его инициализации:
/* инициализация структуры */ struct {char x, int у} А={ 'z', 1918 }; /* инициализация переменной */ int F = 66;
Признак препроцессорных средств. Символ '#' используется для
обозначения директив препроцессора. Например:
#include <stdio.h>
#define
9.6. Выражения и приведение арифметических типов
Каждое выражение состоит из одного или нескольких операндов, сим­волов операций и ограничителей, в качестве которых чаще всего высту­пают круглые скобки (). В зависимости от типа формируемых значений определяются типы выражений. Если значениями выражения являются целые и вещественные числа, то говорят об арифметических выражениях.
В арифметических выражениях допустимы следующие операции:
+ – сложение;
- – вычитание;
* – умножение;
/ - деление;
% – деление по модулю (т.е. получение остатка от целочисленного
деления первого операнда на второй).
Операндами для перечисленных операций служат константы и пере­менные арифметических типов, а также выражения, заключенные в круг­лые скобки.
Примеры выражений с двумя операндами:
а+b 12.3-х 3.14159*Z k/3 16%i
Нужно быть аккуратным, применяя операцию деления ‘/’ к цело­численным операндам. Например, как мы уже упоминали выше, за счет округления результата значением выражения 5/3 будет 1, а соответствует ли это замыслам программиста, зависит от смысла той конкретной конструкции, в которой это выражение используется. Чтобы результат выполнения арифметической операции был вещественным, необходимо, чтобы вещественным был хотя один из операндов. Например, значением выражения 5.0/2 будет 2.5, что соответствует смыслу обычного деления.
81
Операции *, /, % имеют один ранг (3), операции +, - также один ранг
(4), но более низкий. Арифметические операции одного ранга выпол-
няются слева направо. Для изменения порядка выполнения операций обычным образом используются скобки. Например, выражение (d+b)/2.0 позволяет получить среднее арифметическое операндов d и b.
Введены специфические унарные операции ++ (инкремент) и -­(декремент) для изменения на 1 операнда, который в простейшем случае должен быть переменной (леводопустимым значением). Каждая из этих операций может быть префиксной и постфиксной:
• выражение ++m увеличивает на 1 значение m, и это полученное
значение используется как значение выражения ++m (префиксная форма);
• выражение --к уменьшает на 1 значение к, и это новое значение
используется как значение выражения --к (префиксная форма);
• выражение i++ (постфиксная форма) увеличивает на 1 значение i, однако значением выражения i++ является предыдущее значение i (до его увеличения);
• выражение j-- (постфиксная форма) уменьшает на 1 значение j, одна­ко значением выражения j-- является предыдущее значение j (до его умень­шения).
Например, если n равно 4, то при вычислении выражения n++*2 результат равен 8, а n примет значение 5. При n, равном 4, значением выражения ++n*2 будет 10, а n станет равно 5.
Операнд для операции ++ (и для операции - -) не может быть константой либо произвольным выражением. Записи ++5 или 84++ будут неверными. ++(j+k) также неверная запись.
Внешнюю неоднозначность имеют выражения, в которых знак унарной операции ++ (или --) записан непосредственно рядом со знаком бинарной операции +:
х+++b или z--d
В этих случаях трактовка выражений однозначна и полностью определ­яется рангами операций (бинарные аддитивные + и - имеют ранг 4; унар­ные ++ и – имеют ранг 2). Таким образом:
х+++b эквивалентно (х++)+b
z---d эквивалентно (z--)-d
Отношения и логические выражения. Отношение определяется как
пара арифметических выражений, соединенных (разделенных) знаком опе­рации отношения. Знаки операций отношения (уже были введены выше):
< меньше, чем; > больше, чем;
<= меньше или равно; >= больше или равно;
== равно; != не равно.
a-b>6.3
(x-4)*3==12
82
б<=44
Логический тип в языке Си отсутствует, поэтому принято, что отноше­ние имеет ненулевое значение (обычно 1), если оно истинно, и равно 0, если оно ложно. Таким образом, значением отношения 6<=44 будет 1.
Операции >, >=, <, <= имеют один ранг 6. Операции сравнения на равенство == и ! = также имеют одинаковый, но более низкий ранг 7, чем остальные операции отношений. Арифметические операции имеют более высокий ранг, чем операции отношений, поэтому в первом примере для выражения а-b не нужны скобки.
Логических операций в языке Си три:
! – отрицание, т.е. логическое НЕ {ранг 2);
&& – конъюнкция, т.е. логическое И {ранг 11);
|| – дизъюнкция, т.е. логическое ИЛИ {ранг 12).
Они перечислены по убыванию старшинства (ранга). Как правило, логические операции применяются к отношениям. До выполнения логи­ческих операций вычисляются значения отношений, входящих в логичес­кое выражение. Например, если а, b, с – переменные, соответствующие длинам сторон треугольника, то для них должно быть истинно, т.е. не равно 0, следующее логическое выражение:
а+b>с && а+с>b && b+с>а
Несколько операций одного ранга выполняются слева направо, причем вычисления прерываются, как только будет определена истинность (или ложность) результата, т.е. если в рассмотренном примере а+b окажется не больше с, то остальные отношения не рассматриваются - результат ложен.
Так как значением отношения является целое (0 или 1), то ничто не противоречит применению логических операций к целочисленным зна­чениям. При этом принято, что любое ненулевое положительное значение воспринимается как истинное, а ложной считается только величина, равная нулю. Значением !5 будет 0, значением 4 && 2 будет 1 и т.д.
Присваивание (выражение и оператор). Символ "=" в языке Си обо-
значает бинарную операцию, у которой в выражении должно быть два операнда – левый (обычно переменная) и правый (обычно выражение). Если z – имя переменной, то
z = 2.3 + 5.1
есть выражение со значением 7.4. Одновременно это значение присваи­вается и переменной z. Только в том случае, когда в конце выражения с операцией присваивания помещен символ ";", это выражение становится оператором присваивания. Таким образом,
z = 2.3 + 5.1;
есть оператор простого присваивания переменной z значения, равного 7.4.
Тип и значение выражения с операцией присваивания определяются значением выражения, помещенного справа от знака '='. Однако этот тип
83
может не совпадать с типом переменной из левой части выражения. В этом случае при определении значения переменной выполняется преобразо­вание (приведение).
Так как выражение справа от знака '=' может содержать, в свою очередь, операцию присваивания, то в одном операторе присваивания можно присвоить значения нескольким переменным, т.е. организовать
"множественное" присваивание, например:
c=x=d=4.0+2.4;
Здесь значение 6.4 присваивается переменной d, затем 6.4 как значение выражения с операцией присваивания "d=4.0+2.4" присваивается х и, наконец, 6.4 как значение выражения "x=d" присваивается с. Естественное ограничение - слева от знака '=' в каждой из операций присваивания может быть только леводопустимое выражение (имя переменной).
В языке Си существует целый набор "составных операций присваи­вания" {ранг 14). Каждая из составных операций присваивания объединяет некоторую бинарную логическую или арифметическую операцию и собственно присваивание. Операция составного присваивания является основой оператора составного присваивания:
имя переменной ор=выражение;
где ор – одна из операций *, /, %, +, -, &, , |, «, ». Если рассматривать конструкцию "ор=" как две операции, то вначале выполняется ор, а затем '='. Например,
х*=2; z+=4; i/=x+4*z;
При выполнении каждого из этих операторов операндами для операции ор служат переменная из левой части и выражение из правой. Результат присваивается переменной из левой части.
Таким образом, первый пример можно рассматривать как обозначение требования "удвоить значение переменной х"; второй пример – "увеличить на 4 значение переменной z"; третий пример – "уменьшить значение переменной i в (x+4*z) раз". Этим операторам эквивалентны такие операторы простого присваивания:
х=х*2; z=z+4; i=i/(x+4*z) ;
В последнем из них пришлось ввести скобки для получения правиль­ного результата. Перейти от простого оператора присваивания к состав­ному можно только в тех случаях, когда одна переменная используется в обеих частях. Более того, для некоторых операций эта переменная должна быть обязательно первым (левым) операндом. Например, не удастся заменить составными следующие простые операторы присваивания:
a=b/a; x=z%x.
Приведение типов. Рассматривая операцию деления, мы отметили,
что при делении двух целых операндов результат получается целым. На­пример, значением выражения 5/2 будет 2, а не 2.5. Для получения веще-
84
ственного результата нужно выполнять деление не целых, а вещественных операндов, например, записав 5.0/2.0, получим значение 2.5.
Если операндами являются безымянные константы, то заменить целую константу (как мы только что сделали) на вещественную совсем не трудно. В том случае, когда операндом является именованная константа, перемен­ная или выражение в скобках, необходимо для решения той же задачи использовать операцию явного приведения (преобразования) типа. Напр­имер, рассмотрим такой набор определений и операторов присваивания:
int n=5, k=2 ;
double d;
int m;
d=(double) n/ (double) k;
m=n/k;
В этом фрагменте значением d станет величина 2.5 типа double, а зна­чением переменной m станет целое значение 2.
Операция деления является только одной из бинарных операций. Почти для каждой из них операнды могут иметь разные типы. Однако не всегда программист должен в явном виде указывать преобразования типов. Если у бинарной операции операнды имеют разные типы (а должны в соответствии с синтаксисом выражения иметь один
тип), то компилятор выполняет преобразование типов автоматически, т.е. приводит оба операнда к одному типу. Например, для тех же переменных значение выра­жения d+k будет иметь тип double за счет неявного преобразования, выполняемого автоматически без указания программиста. Рассмотрим правила, по которым такие приведения выполняются.
Правила преобразования типов. При вычислении выражений
некоторые операции требуют, чтобы операнды имели соответствующий тип, а если требования к типу не выполнены, принудительно вызывают вы­полнение нужных преобразований. Та же ситуация возникает при ини­циализации, когда тип инициализирующего выражения приводится к типу определяемого объекта. Напомним, что в языке Си присваивание является бинарной операцией, поэтому сказанное относительно преобразования типов относится и ко всем формам присваивания, однако при присваи­ваниях значение выражения из правой части всегда приводится к типу переменной из левой части, независимо от соотношения этих типов.
Правила преобразования в языке Си для основных типов определены стандартом языка. Эти стандартные преобразования включают перевод "низших" типов в "высшие".
Среди преобразований типов выделяют:
преобразования в арифметических выражениях;
преобразования при присваиваниях;
преобразования указателей.
85
Преобразование типов указателей будет рассмотрено в позже. Здесь рассмотрим преобразования типов при арифметических операциях и особенности преобразований типов при присваиваниях.
При преобразовании типов нужно различать преобразования, изменя­ющие внутреннее представление данных, и преобразования, изменяющие только интерпретацию внутреннего представления. Например, когда данные типа unsigned
int переводятся в тип int, менять их внутреннее
представление не требуется – изменяется только интерпретация. При пре­образовании значений типа float в значение типа int недостаточно изменить только интерпретацию, необходимо изменить длину участка памяти для внутреннего представления и кодировку. При таком преобразовании из float в int возможен выход за диапазон допустимых значений типа int, и реакция на эту ситуацию существенно зависит
от конкретной реализации. Именно поэтому для сохранения мобильности программ в них рекомен­дуется с осторожностью применять преобразование типов.
Рассмотрим последовательность выполнения преобразования операн-
дов в арифметических выражениях.
1. Все короткие целые типы преобразуются в типы не меньшей длины. Затем оба значения, участвующие в операции, принимают одинаковый тип в соответствии со следующими ниже правилами.
2. Если один из операндов имеет тип long double, то второй тоже будет преобразован в long double.
3. Если п. 2 не выполняется и один из операндов есть double, другой приводится к типу double.
4. Если п. 2 – 3 не выполняются и один из операндов имеет тип float, то второй приводится к типу float.
5. Если п. 2 – 4 не выполняются (оба операнда целые) и один операнд unsigned long int, то оба операнда преобразуются к типу unsigned long int.
6. Если п. 2 – 5 не выполняются и один операнд есть long, другой преобразуется к типу long.
7. Если п. 2 – 6 не выполняются и один операнд unsigned, то другой преобразуется к типу unsigned.
8. Если п. 2 – 7 не выполнены, то оба операнда принадлежат типу int.
Используя арифметические выражения, следует учитывать приведен­ные правила и не попадать в "ловушки" преобразования типов, так как некоторые из них приводят к потерям информации, а другие изменяют интерпретацию битового (внутреннего) представления данных.
86
Правила стандартных арифметических преобразований
Исходный тип Преобразованный тип Правила преобразований
char int
Расширение нулем или знаком в зависимости от умолчания для
char unsigned char int signed char int short int unsigned short unsigned int enum int
битовое поле
int
Старший байт заполняется нулем
Расширение знаком
Сохраняется то же значение
Сохраняется то же значение
Сохраняется то же значение
Сохраняется то же значение
На рисунке стрелками отмечены "безопасные" арифметические преобразования, гарантирующие сохранение точности и неизменность численного значения.
При преобразованиях, которые не отнесены схемой к безопасным, возможны существенные информационные потери. Преобразование цело­численных значений в вещественные осуществляется настолько точно, насколько это предусмотрено аппаратурой. Если конкретное целочислен­ное значение не может быть точно представлено как целочисленное, то младшие значащие цифры теряются и обратимость невозможна.
Приведение вещественного значения к целому
типу выполняется за счет отбрасывания дробной части. Преобразование целой величины в вещественную также может привести к потери точности.
87
10. ВВЕДЕНИЕ В ПРОГРАММИРОВАНИЕ НА ЯЗЫКЕ СИ
10.1. Структура и компоненты простой программы
Текст программы и препроцессор. Каждая программа на языке Си
есть последовательность препроцессорных директив, описаний и опреде­лений глобальных объектов и функций. Препроцессорные директивы управляют преобразованием текста программы до ее компиляции. Опреде­ления вводят функции и объекты. Объекты необходимы для представления в программе обрабатываемых данных. Функции определяют потенциально возможные действия программы. Описания уведомляют компилятор о свойствах и именах тех объектов и функций, которые определены в других частях программы (например, ниже по ее тексту или в другом файле).
Программа на языке Си должна быть оформлена в виде одного или нескольких текстовых файлов. Текстовый файл разбит на строки. В конце каждой строки есть признак ее окончания (плюс управляющий символ перехода к началу новой строки). Просматривая текстовый файл на экране дисплея, мы видим последовательность строк, причем признаки окончания строк невидимы, но по ним производится разбивка текста редактором.
Определения и описания программы на языке Си могут размещаться в строках текстового файла достаточно произвольно (в свободном формате.) Для препроцессорных директив существуют ограничения. Во-первых, препроцессорная директива обычно размещается в одной строке, т.е. признаком ее окончания является признак конца строки текста программы. Во-вторых, символ '#', вводящий каждую директиву препроцессора, дол­жен быть первым отличным от пробела символом в строке с препроцес­сорной директивой.
Исходная программа, подготовленная на языке Си в виде текстового файла, проходит три обязательных этапа обработки:
препроцессорное преобразование текста;
компиляция;
компоновка (редактирование связей или сборка).
Только после успешного завершения всех перечисленных этапов формируется исполняемый машинный код программы (рис.50).
88
Рис.50. Этапы обработки
Задача препроцессора – преобразование текста программы до ее ком­пиляции. Правила препроцессорной обработки определяет программист с помощью директив препроцессора. Каждая препроцессорная директива начинается с символа '#'. Пока нам будет достаточно двух директив:
#include и #define.
Препроцессор "сканирует" исходный текст программы в поиске строк, начинающихся с символа '#'. Такие строки воспринимаются препро­цессором как команды (директивы), которые определяют действия по преобразованию текста. Директива
#include определяет, какие текстовые файлы нужно включить в
#define указывает правила замены в тексте.
этом месте текста программы.
Директива #include <...> предназначена для включения в текст про­граммы текста файла из каталога "заголовочных файлов", поставляемых вместе со стандартными библиотеками компилятора. Каждая библиотечная функция, определенная стандартом языка Си, имеет соответствующее описание (прототип библиотечной функции плюс определения типов, переменных, макроопределений и констант) в одном из заголовочных файлов. Список заголовочных файлов для стандартных библиотек опреде­лен стандартом языка.
Важно понимать, что употребление в программе препроцессорной директивы
#include < имя_заголовочного_файла >
не подключает к программе соответствующую стандартную библиотеку. Препроцессорная обработка выполняется на уровне исходного текста
89
программы. Директива #include только позволяет вставить в текст программы описания из указанного заголовочного файла. Подключение к программе кодов библиотечных функций осуществляется только на этапе редактирования связей (этап компоновки), т.е. после компиляции, когда уже получен машинный код программы. Доступ к кодам библиотечных функций нужен только на этапе компоновки. Именно поэтому компили­ровать программу и устранять синтаксические ошибки в ее тексте можно без стандартной библиотеки, но обязательно с заголовочными файлами.
Здесь следует отметить еще одну важную особенность. Хотя в заголовочных файлах содержатся описания всех стандартных функций, в код программы включаются только те функции, которые используются в программе. Выбор нужных функций выполняет компоновщик на этапе, называемом "редактирование связей".
Термин "заголовочный файл" {header file} в применении к файлам, со­держащим описания библиотечных функций стандартных библиотек, не случаен. Он предполагает включение этих файлов именно в начало про­граммы. Мы настоятельно рекомендуем, чтобы до обращения к любой функции она была определена или описана в том же файле, где помещен текст программы. Описание или определения функций должны быть "выше" по тексту, чем вызовы функций. Именно поэтому заголовочные файлы нужно помещать в начале текста программы, т.е. заведомо раньше обращений к соответствующим библиотечным функциям.
Хотя заголовочный файл может быть включен в программу не в ее начале, а непосредственно перед обращением к нужной библиотечной функции, такое размещение директив #include <...> не рекомендуется.
Структура программы. После выполнения препроцессорной обработ-
ки в тексте программы не остается ни одной препроцессорной директивы. Теперь программа представляет собой набор описаний и определений. Если не рассматривать определений глобальных объектов и описаний, то программа будет набором определений функций.
Среди этих функций всегда должна присутствовать функция с фикси­рованным именем main. Именно эта функция является главной функцией программы, без которой программа не может быть выполнена. Имя этой главной функции для всех программ одинаково (всегда main) и не может выбираться произвольно. Таким образом, исходный текст программы в простом случае (когда программа состоит только из одной функции) имеет такой вид:
директивы препроцессора
void main()
{
определения объектов;
исполняемые_операторы; }
90
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]