Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Бьерн Страуструп.doc
Скачиваний:
1
Добавлен:
27.12.2019
Размер:
3.81 Mб
Скачать

2.6. Экономия памяти

В процессе создания нетривиальной программы рано или поздно наступает

момент, когда требуется больше памяти, чем можно выделить или

запросить. Есть два способа выжать еще некоторое количество памяти:

[1] паковать в байты переменные с малыми значениями;

[2] использовать одну и ту же память для хранения разных объектов

в разное время.

Первый способ реализуется с помощью полей, а второй - с помощью

объединений. И те, и другие описываются ниже. Поскольку назначение

этих конструкций связано в основном с оптимизацией программы, и

поскольку, как правило, они непереносимы, программисту следует

хорошенько подумать, прежде чем использовать их. Часто лучше изменить

алгоритм работы с данными, например, больше использовать динамически

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

2.6.1 Поля

Кажется расточительным использовать для признака, принимающего

только два значения ( например: да, нет) тип char, но объект типа

char является в С++ наименьшим объектом, который может независимо

размещаться в памяти. Однако, есть возможность собрать переменные

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

Член структуры является полем, если в его определении после имени

указано число разрядов, которое он должен занимать. Допустимы

безымянные поля. Они не влияют на работу с поименованными полями,

но могут улучшить размещение полей в памяти для конкретной машины:

struct sreg {

unsigned enable : 1;

unsigned page : 3;

unsigned : 1; // не используется

unsigned mode : 2;

unsigned : 4; // не используется

unsigned access : 1;

unsigned length : 1;

unsigned non_resident : 1;

};

Приведенная структура описывает разряды нулевого

регистра состояния DEC PDP11/45 (предполагается, что поля в слове

размещаются слева направо). Этот пример показывает также другое

возможное применение полей: давать имена тем частям

объекта, размещение которых определено извне. Поле должно иметь

целый тип ($$R.3.6.1 и $$R.9.6), и оно используется аналогично другим

объектам целого типа. Но есть исключение: нельзя брать адрес поля.

В ядре операционной системы или в отладчике тип sreg мог бы

использоваться следующим образом:

sreg* sr0 = (sreg*)0777572;

//...

if (sr0->access) { // нарушение прав доступа

// разобраться в ситуации

sr0->access = 0;

}

Тем не менее,

применяя поля для упаковки нескольких переменных в один байт, мы

необязательно сэкономим память. Экономится память для данных, но

на большинстве машин одновременно возрастает объем команд,

нужных для работы с упакованными данными.

Известны даже такие программы, которые значительно сокращались в объеме,

если двоичные переменные, задаваемые полями, преобразовывались в

переменные типа char! Кроме того, доступ к char или int обычно

происходит намного быстрее, чем доступ к полю. Поля - это просто

удобная краткая форма задания логических операций для извлечения

или занесения информации в части слова.

2.6.2. Объединения

Рассмотрим таблицу имен, в которой каждый элемент содержит имя и

его значение. Значение может задаваться либо строкой, либо целым числом:

struct entry {

char* name;

char type;

char* string_value; // используется если type == 's'

int int_value; // используется если type == 'i'

};

void print_entry(entry* p)

{

switch(p->type) {

case 's':

cout << p->string_value;

break;

case 'i':

cout << p->int_value;

break;

default:

cerr << "type corrupted\n";

break;

}

}

Поскольку переменные

string_value и int_value никогда не могут использоваться одновременно,

очевидно, что часть памяти пропадает впустую. Это можно легко исправить,

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

struct entry {

char* name;

char type;

union {

char* string_value; // используется если type == 's'

int int_value; // используется если type == 'i'

};

};

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

string_value и int_value будут размещаться с одного адреса, и

при этом не нужно менять все части программы, работающие с entry.

Из этого следует, что все члены объединения вместе занимают такой же

объем памяти, какой занимает наибольший член объединения.

Надежный способ работы с объединением заключается в том, чтобы

выбирать значение с помощью того же самого члена, который его записывал.

Однако, в больших программах трудно гарантировать, что объединение

используется только таким способом, а в результате использования

не того члена обЪединения могут возникать трудно обнаруживаемые ошибки.

Но можно встроить объединение в такую структуру, которая обеспечит

правильную связь между значением поля типа и текущим типом члена

объединения ($$5.4.6).

Иногда объединения используют для "псевдопреобразований" типа

(в основном на это идут программисты, привыкшие к языкам, в которых

нет средств преобразования типов, и в результате приходится обманывать

транслятор). Приведем пример такого "преобразования" int в int*

на машине VAX, которое достигается простым совпадением разрядов:

struct fudge {

union {

int i;

int* p;

};

};

fudge a;

a.i = 4095;

int* p = a.p; // некорректное использование

В действительности это вовсе не преобразование типа, т.к. на одних

машинах int и int* занимают разный объем памяти, а на других целое

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

использование объединений не является переносимым, тогда как

существует переносимый способ задания явного преобразования

типа ($$3.2.5).

Иногда объединения используют специально, чтобы избежать

преобразования типов. Например, можно использовать fudge, чтобы

узнать, как представляется указатель 0:

fudge.p = 0;

int i = fudge.i; // i необязательно должно быть 0

Объединению можно дать имя, то есть можно сделать его

полноправным типом. Например, fudge можно описать так:

union fudge {

int i;

int* p;

};

и использовать (некорректно) точно так же, как и раньше. Вместе с тем,

поименованные объединения можно использовать и вполне корректным

и оправданным способом (см. $$5.4.6).

2.7 Упражнения

1. (*1) Запустить программу "Hello, world" (см. $$1.3.1).

2. (*1) Для каждого описания из $$2.1 сделать следующее: если описание

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

если же описание является определением, написать для него описание,

которое не являлось бы одновременно и определением.

3. (*1) Напишите описания следующих объектов: указателя на символ;

массива из 10 целых; ссылки на массив из 10 целых; указателя

на массив символьных строк; указателя на указатель на символ;

целого-константы; указателя на целое-константу; константного

указателя на целое. Описания снабдить инициализацией.

4. (*1.5) Напишите программу, которая печатает размеры основных типов

и типа указателя. Используйте операцию sizeof.

5. (*1.5) Напишите программу, которая печатает буквы от 'a' до 'z' и цифры

от '0' до '9' и их целые значения. Проделайте то же самое для других

видимых символов. Проделайте это, используя шестнадцатеричную

запись.

6. (*1) Напечатайте последовательность разрядов представления указателя

0 на вашей машине. Подсказка: см.$$2.6.2.

7. (*1.5) Напишите функцию, печатающую порядок и мантиссу параметра типа

double.

8. (*2) Каковы на используемой вами машине наибольшие и наименьшие

значения следующих типов: char, short,int,long, float, double,

long double, unsigned, char*, int* и void*? Есть ли какие-то

особые ограничения на эти значения? Например, может ли int* быть

нечетным целым? Как выравниваются в памяти объекты этих типов?

Например, может ли целое иметь нечетный адрес?

9. (*1) Какова максимальная длина локального имени, которое

можно использовать в вашей реализации С++ ? Какова максимальная

длина внешнего имени? Есть ли какие-нибудь ограничения на символы,

которые можно использовать в имени?

10. (*1) Напишите функцию, которая меняет местами значения двух целых.

В качестве типа параметров используйте int*. Напишите другую функцию

с тем же назначением, используя в качестве типа параметров int&.

11. (*1) Каков размер массива str в следующем примере:

char str[] = "a short string";

Какова длина строки "a short string"?

12. (*1.5) Составьте таблицу из названий месяцев года и числа дней

в каждом из них. Напишите программу, печатающую ее. Проделайте

это дважды: один раз - используя массивы для названий месяцев

и количества дней, а другой раз - используя массив структур,

каждая из которых содержит название месяца и количество дней в нем.

13. (*1) С помощью typedef определите типы: unsigned char, константный

unsigned char, указатель на целое, указатель на указатель на

символ, указатель на массив символов, массив из 7 указателей

на целое, указатель на массив из 7 указателей на целое и массив из

8 массивов из 7 указателей на целое.

14. (*1) Определить функции f(char), g(char&) и h(const char&) и

вызвать их, используя в качестве параметров 'a', 49, 3300, c, uc, и

sc, где c - char, uc - unsigned char и sc - signed char. Какой

вызов является законным? При каком вызове транслятору придется

завести временную переменную?

* ГЛАВА 3. ВЫРАЖЕНИЯ И ОПЕРАТОРЫ

"Но с другой стороны не следует

забывать про эффективность"

(Джон Бентли)

С++ имеет сравнительно небольшой набор операторов, который позволяет

создавать гибкие структуры управления, и богатый набор операций для

работы с данными. Основные их возможности показаны в этой главе на одном

завершенном примере. Затем приводится сводка выражений, и подробно

обсуждаются операции преобразования типа и размещение в свободной памяти.

Далее дана сводка операторов, а в конце главы обсуждается выделение

текста пробелами и использование комментариев.

3.1 Калькулятор

Мы познакомимся с выражениями и операторами на примере программы

калькулятора. Калькулятор реализует четыре основных арифметических

действия в виде инфиксных операций над числами с плавающей точкой.

В качестве упражнения предлагается добавить к калькулятору

переменные. Допустим, входной поток имеет вид:

r=2.5

area=pi*r*r

(здесь pi имеет предопределенное значение). Тогда программа калькулятора

выдаст:

2.5

19.635

Результат вычислений для первой входной строки равен 2.5, а результат

для второй строки - это 19.635.

Программа калькулятора состоит из четырех основных частей:

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

транслятор в миниатюре, в котором анализатор проводит синтаксический

анализ, функция ввода обрабатывает входные данные и проводит

лексический анализ, таблица имен хранит постоянную информацию, нужную

для работы, а драйвер выполняет инициализацию,

вывод результатов и обработку ошибок. К такому калькулятору можно

добавить много других полезных возможностей, но программа его и так

достаточно велика (200 строк), а введение новых возможностей

только увеличит ее объем, не давая дополнительной

информации для изучения С++.