Смекни!
smekni.com

Правила правой руки 17 Замечания для программистов на c 17 Глава 1 (стр. 14 из 43)

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

эквивалентно записи:

const ASM = 0;

const AUTO = 1;

const BREAK = 2;

Перечисление может быть именованным. Например:

enum keyword { ASM, AUTO, BREAK };

Имя перечисления становится синонимом int, а не новым типом.

Описание переменной keyword, а не просто int, может дать как

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

преднамеренное. Например:

keyword key;

switch (key) {

case ASM:

// что-то делает

break;

case BREAK:

// что-то делает

break;

}

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

значения keyword из трех используются.

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

enum int16 {

sign=0100000, // знак

most_significant=040000, // самый значимый

least_significant=1 // наименее значимый

};

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

или положительными.

2.5 Экономия Пространства

В ходе программирования нетривиальных разработок неизбежно

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

чем имеется или отпущено. Есть два способа выжать побольше

пространства из того, что доступно:

- стр 73 -

[1] Помещение в байт более одного небольшого объекта; и

[2] Использование одного и того же пространства для хранения

разных объектов в разное время.

Первого можно достичь с помощью использования полей, второго -

через использование объединений. Эти конструкции описываются в

следующих разделах. Поскольку обычное их применение состоит чисто в

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

программисту следует дважды подумать, прежде чем использовать их.

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

полагаться на динамически выделяемую память (#3.2.6) и меньше на

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

2.5.1 Поля

Использование char для представления двоичной переменной,

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

экстравагантным, но char является наименьшим объектом, который в

C++ может выделяться независимо. Можно, однако, сгруппировать

несколько таких крошечных переменных вместе в виде полей struct.

Член определяется как поле путем указания после его имени числа

битов, которые он занимает. Допустимы неименованные поля; они не

влияют на смысл именованных полей, но неким машинно-зависимым

образом могут улучшить размещение:

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;

}

Получилось размещение регистра 0 сосояния DEC PDP11/45 (в

предположении, что поля в слове размещаются слева направо). Этот

пример также иллюстрирует другое основное применение полей:

именовать части внешне предписанного размещения. Поле должно быть

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

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

отладчике тип sreg можно было бы использовать так:

sreg* sr0 = (sreg*)0777572;

//...

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

// чистит массив

sr->access = 0;

}

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

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

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

манипуляции этими переменными, на большинстве машин возрастает.

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

- стр 74 -

переменные преобразовывались из полей бит в символы! Кроме того,

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

Поля - это просто удобная и краткая запись для применения

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

или введения информации в нее.

2.5.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&bsol;n";

break;

}

}

Поскольку string_value и int_value никогда не могут

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

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

членами union (объединения); например, так:

struct entry {

char* name;

char type;

union {

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

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

};

};

Это оставляет всю часть программы, использующую entry, без

изменений, но обеспечивает, что при размещении entry string_value и

int_value имеют один и тот же адрес. Отсюда следует, что все члены

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

наибольший член.

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

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

- стр 75 -

записывалось, совершенно оптимально. Но в больших программах

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

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

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

образом, чтобы соответствие между полем типа и типами членов было

гарантированно правильным (#5.4.6).

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

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

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

является необходимым). Например, это "преобразует" на VAX'е int в

int*, просто предполагая побитовую эквивалентность:

struct fudge {

union {

int i;

int* p;

};

};

fudge a;

a.i = 4096;

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.6 Упражнения

1. (*1) Заставьте работать программу с "Hello, world" (1.1.1).

2. (*1) Для каждого описания в #2.1 сделайте следующее: Если

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

определение. Если описание является определением, напишите для

него описание, которое при этом не является определением.

3. (*1) Напишите описания для: указателя на символ; вектора из 10

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

- стр 76 -

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

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

константного указателя на целое. Каждый из них

инициализируйте.

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

указательных типов. Используйте операцию sizeof.

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

цифры '0'...'9' и их числовые значения. Сделайте то же для

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

шестнадцатиричную запись.

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

0 на вашей системе. Подсказка: #2.5.2.

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

параметра типа double.

8. (*2) Каковы наибольшие и наименьшие значения, на вашей

системе, следующих типов: char, short, int, long, float,

double, unsigned, char*, int* и void*? Имеются ли

дополнительные ограничения на принимаемые ими значения? Может

ли, например, int* принимать нечетное значение? Как

выравниваются в памяти объекты этих типов? Может ли, например,

int иметь нечетный адрес?

9. (*1) Какое самое длинное локальное имя можно использовать в

C++ программе в вашей системе? Какое самое длинное внешнее имя

можно использовать в C++ программе в вашей системе? Есть ли

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

в имени?

10. (*2) Определите one следующим образом:

const one = 1;

Попытайтесь поменять значение one на 2. Определите num

следующим образом:

const num[] = { 1, 2 };

Попытайтесь поменять значение num[1] на 2.

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

значения). Используйте в качесте типа параметра int*. Напишите

другую переставляющую функцию, использующую в качесте типа

параметра int&.

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

char str[] = "a short string";

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

13. (*1.5) Определите таблицу названий месяцев года и числа дней

в них. Выведите ее. Сделайте это два раза: один раз используя

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

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

название месяца и число дней в нем.

14. (*1) С помощью typedef определите типы: беззнаковый char;

константный беззнаковый char; указатель на целое; указатель на

указатель на char; указатель на вектора символов; вектор из 7

целых указателей; указатель на вектор из 7 целых указателей;

и вектор из 8 векторов из 7 целых указателей.

* Глава 3 *

Выражения и операторы

С другой стороны,

мы не можем игнорировать эффективность

- Джон Бентли