М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 12
Текст из файла (страница 12)
Операции с присваиванием особенно важны в случае сложной переменной, включающей индексацию массива и т.д. Комбинированная операция не только экономит время набора на клавиатуре, но и позволяет избежать ошибки, если v написано не одинаково с обеих сторон от знака «=». И все же комбинированные присваивания — всего лишь стилистический прием, так как оптимизирующий компилятор может удалить второе вычисление адреса v.
Можно предотвратить присваивание значения объекту, объявляя его как константу.
const int N = 8; /* Константа в языке С */
N: constant Integer := 8; — Константа в языке Ada
Очевидно, константе должно быть присвоено начальное значение.
Есть различие между константой и статическим значением (static value), которое известно на этапе компиляции:
procedure P(C: Character) is
С1 : constant Character := С;
Ada |
Begin
…
case C is
when C1 => -- Ошибка, не статическое значение
when C2 => -- Правильно, статическое значение
…
end case;
…
end P;
Локальная переменная С1 — это постоянный объект, в том смысле что значение не может быть изменено внутри процедуры, даже если ее значение будет разным при каждом вызове процедуры. С другой стороны, варианты выбора в case должны быть известны во время компиляции. В отличие от языка С язык C++ рассматривает константы как статические:
C++ |
int a[N]; //Правильно в C++, но не в С
Реализация
После того как вычислено выражение в правой части присваивания, чтобы сохранить его значение в памяти, нужна как минимум одна команда. Если выражение в левой части сложное (индексация массива и т.д.), то понадобятся дополнительные команды для вычисления нужного адреса памяти.
Если длина значения правой части превышает одно слово, потребуется несколько команд, чтобы сохранить значение в случае, когда компьютер не поддерживает операцию блочного копирования, которая позволяет копировать последовательность заданных слов памяти, указав начальный адрес источника, начальный адрес приемника и число копируемых слов.
4.9. Упражнения
1. Прочитайте документацию вашего компилятора и выпишите, какая точность используется для разных целочисленных типов.
2. Запишите 200 + 55 = 255 и 100-150 = -50 в дополнительном коде.
3. Пусть а принимает все значения в диапазонах 50 .. 56 и -56 .. -50, и пусть b равно 7 или -7. Каковы возможные частные q и остатки г при делении а на b? Используйте оба определения остатка (обозначенные rem и mod в Ada) и отобразите результаты в графической форме. Подсказка: если используется rem, r будет иметь знак а; если используется mod, r будет иметь тот же знак, что и b.
4. Что происходит, когда вы выполняете следующую С-программу на компьютере, который сохраняет значения short int в 8 битах, а значения int в 16 битах?
short int i: [с]
int j = 280;
for (i = 0; i <j; i++) printf("Hello world");
5. Как бы вы реализовали атрибут T'Succ (V) языка Ada, если используется нестандартное представление перечисляемого типа?
6. Что будет печатать следующая программа? Почему?
C |
int j = 5;
if (i&j)printf("Hello world");
if (i.&&j) printf("Goodbye world");
7. Каково значение i после выполнения следующих операторов?
C |
int a[2] = { 10,11};
i=a[i++];
8. Языки С и C++ не имеют операции возведения в степень; почему?
9. Покажите, как могут использоваться модульные типы в Ada 95 и типы целого без знака в С для представления множеств. Насколько переносимым является ваше решение? Сравните с типом множества (set) в языке Pascal.
Глава 5
Составные типы данных
Языки программирования, включая самые первые, поддерживают составные типы данных. С помощью массивов представляются вектора и матрицы, используемые в математических моделях реального мира. Записи используются при обработке коммерческих данных для представления документов различного формата и хранения разнородных данных.
Как и для любого другого типа, для составного типа необходимо описать наборы значений и операций над этими значениями. Кроме того, необходимо решить: как они строятся из элементарных значений, и какие операции можно использовать, чтобы получить доступ к компонентам составного значения? Число встроенных операций над составными типами обычно невелико, поэтому большинство операций нужно явно программировать из операций, допустимых для компонентов составного типа.
Поскольку массивы являются разновидностью записей, мы начнем обсуждение с записей (в языке С они называются структурами).
5.1. Записи
Значение типа запись (record) состоит из набора значений других типов, называемых компонентами (components — Ada), членами (members — С) или полями (fields —Pascal). При объявлении типа каждое поле получает имя и тип. Следующее объявление в языке С описывает структуру с четырьмя компонентами: одним — типа строка, другим — заданным пользователем перечислением и двумя компонентами целого типа:
typedef enum {Black, Blue, Green, Red, White} Colors;
C |
char model[20];
Colors color;
int speed;
int fuel;
} Car_Data;
Аналогичное объявление в языке Ada таково:
type Colors is (Black, Blue, Green, Red, White);
Ada |
record
Model: String(1..20);
Color: Colors:
Speed: Integer;
Fuel: Integer;
end record;
После того как определен тип записи, могут быть объявлены объекты (переменные и константы) этого типа. Между записями одного и того же типа допустимо присваивание:
C |
с1 =с2;
а в Ada (но не в С) также можно проверить равенство значений этого типа:
С1, С2, СЗ: Car_Data;
Ada |
С1 =СЗ;
end if;
Поскольку тип — это набор значений, можно было бы подумать, что всегда можно обозначить* значение записи. Удивительно, но этого вообще нельзя сделать; например, язык С допускает значения записи только при инициализации. В Ada, однако, можно сконструировать значение типа запись, называемое агрегатом (aggregate), просто задавая значение правильного типа для каждого поля. Связь значения с полем может осуществляться по позиции внутри записи или по имени поля:
Ada |
С1 := (-Peugeot-, Red, C2.Speed, CS.Fuel);
C2 := (Model=>-Peugeot", Speed=>76,
Fuel=>46, Color=>White);
Это чрезвычайно важно, потому что компилятор выдаст сообщение об ошибке, если вы забудете включить значение для поля; а при использовании отдельных присваиваний легко просто забыть одно из полей:
Ada |
Ada С1.Model :=-Peugeot-;
--Забыли С1.Color
С1.Speed := C2.Speed;
С1.Fuel := CS.Fuel;
Можно выбрать отдельные поля записи, используя точку и имя поля:
C |
Будучи выбранным, поле записи становится обычной переменной или значением типа поля, и к нему применимы все операции, соответствующие этому типу.
Имена полей записи локализованы внутри определения типа и могут повторно использоваться в других определениях:
typedef struct {
float speed; /* Повторно используемое имя поля */
C |
Performance p;
Car_Data с;
p.speed = (float) с.speed; /* To же самое имя, другое поле*/
Отдельные записи сами по себе не очень полезны; их значение становится очевидным, только когда они являются частью более сложных структур, таких как массивы записей или динамические структуры, создаваемые с помощью указателей (см. раздел 8.2).
Реализация
Значение записи представляется некоторым числом слов в памяти, достаточным для того, чтобы вместить все поля. На рисунке 5.1 показано размещение записи Car_Data. Поля обычно располагаются в порядке их появления в определении типа записи.
Доступ к отдельному полю очень эффективен, потому что величина смещения каждого поля от начала записи постоянна и известна во время компиляции. Большинство компьютеров имеет способы адресации, которые позволяют добавлять константу к адресному регистру при декодировании команды. После того как начальный адрес записи загружен в регистр, для доступа к полям лишние команды уже не нужны:
load R1.&C1 Адрес записи
load R2,20(R1) Загрузить второе поле
load R3,24(R1) Загрузить третье поле
Так как для поля иногда нужен объем памяти, не кратный размеру слова, компилятор может
«раздуть» запись так, чтобы каждое поле заведомо находилось на границе слова, поскольку доступ к не выровненному на границу слову гораздо менее эффективен. На 16-разрядном компьютере такое определение типа, как:
typedef struct {
C |
int f2; /* 2 байта*/
char f3; /* 1 байт, пропустить 1 байт */
int f4; • /* 2 байта*/
};
может привести к выделению четырех слов для каждой записи таким образом, чтобы поля типа int были выровнены на границу слова, в то время как следующие определения:
typedef struct { [с]
C |
int f4; /* 2 байта*/
charfl ; /Мбайт*/
char f3; /* 1 байт */
потребовали бы только трех слов. При использовании компилятора, который плотно упаковывает поля, можно улучшить эффективность, добавляя фиктивные поля для выхода на границы слова. В разделе 5.8 описаны способы явного распределения полей. В любом случае, никогда не привязывайте программу к конкретному формату записи, поскольку это сделает ее непереносимой.
5.2. Массивы
Массив — это запись, все поля которой имеют один и тот же тип. Кроме того, поля (называемые элементами или компонентами) задаются не именами, а позицией внутри массива. Преимуществом этого типа данных является возможность эффективного доступа к элементу по индексу. Поскольку все элементы имеют один и тот же тип, можно вычислить положение отдельного элемента, умножая индекс на размер элемента. Используя индексы, легко найти отдельный элемент массива, отсортировать или как-то иначе реорганизовать элементы.
Индекс в языке Ada может иметь произвольный дискретный тип, т.е. любой тип, на котором допустим «счет». Таковыми являются целочисленные типы и типы перечисления (включая Character и Boolean):
Ada |
type Temperatures is array(Heat) of Float;
Temp: Temperatures;
Язык С ограничивает индексный тип целыми числами; вы указываете, сколько компонентов вам необходимо:
C |
float temp[Max];
а индексы неявно изменяются от 0 до числа компонентов без единицы, в данном случае от 0 до 3. Язык C++ разрешает использовать любое константное выражение для задания числа элементов массива, что улучшает читаемость программы:
C++ |
const int last = 3;
float temp [last+ 1];
Компоненты массива могут быть любого типа:
C |
typedef struct {... } Car_Data;
Car_Data database [100];
В языке Ada (но не в С) на массивах можно выполнять операции присваивания и проверки на равенство:
type A_Type is array(0..9) of Integer;
Ada |