Найти тему
ZDG

Куда программисты прячут мясо: шашлычники в шоке

Оглавление

Предыдущая часть:

Каждый настоящий программист знает, что такое DEADBEEF. Это число 3735928559, записанное в 16-ричной системе.

Используя цифры A, B, C, D, E, F, можно писать с их помощью примитивные слова, вроде CAFE, FACE, BAD. Но у DEADBEEF особая история.

Есть такое выражение "dead meat", которое переводится буквально как "мёртвое мясо", но на деле означает другое – какую-то сильную опасность, а по-русски просто

Но его записать в 16-ричном виде нельзя, поэтому кто-то выкрутился и вместо MEAT написал BEEF (говядина), что по смыслу подходит, потому что тоже мясо.

Это сочетание стали использовать для того, чтобы пометить определённые участки памяти. Отладку программ делали, просто просматривая содержимое памяти в 16-ричном виде, и поэтому если там появлялось сочетание байтов DE AD BE EF, его было легко заметить.

-2

Подробности этой истории вы легко нагуглите, а здесь речь пойдёт про

Магические константы

Магической константой становится любое число, в которое вкладывается особый смысл. Например, если в программе написано

a = 365;

То скорее всего 365 обозначает количество дней в году (а может и нет). Так или иначе, какой-то смысл в него заложен.

Проблема таких констант в том, что зачастую трудно сказать, что они означают. Если количество дней в году довольно узнаваемо, то догадаться о назначении числа 86400 будет уже труднее.

Поэтому такие значения должны быть понятны либо из локального контекста, либо из комментария:

a = 86400; // количество секунд в сутках

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

const SECONDS_IN_DAY = 86400;
a = SECONDS_IN_DAY;

Символьные константы являются лучшим вариантом. Во-первых, они имеют понятное обозначение, во-вторых, они задаются только в одном месте, и если поменять значение константы, не нужно потом менять это значение по всей программе.

Перечисляемые типы

Бывает так, что нужно много констант, объединённых какой-то общей идеей. Например, дни недели можно обозначить так:

const SUNDAY = 0;
const MONDAY= 1;
const TUESDAY = 2;
const WEDNESDAY = 3;
const THURSDAY = 4;
const FRIDAY = 5;
const SATURDAY = 6;

Такая куча констант выглядит неприкаянной, потому что это именно куча никак не связанных констант (они связаны только в нашем понимании).

Кроме того, для каждой подобной константы нужно придумывать своё значение. И здесь появляется другая проблема: мы можем случайно пропустить или задублировать какое-то значение, если будем задавать их руками. Особенно если это будут не дни недели, а какие-то наши собственные сущности, которые мы можем добавлять или убирать.

Иначе говоря, мы хотим задавать символьные имена констант, но не их численные значения. Нас бы устроило, чтобы эти значения просто шли по порядку.

Эта проблема решается с помощью перечисляемых типов. Посмотрим на язык C:

enum Weekday {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
};

Мы объявили константы для дней недели и одновременно с этим объединили их в один тип Weekday. Теперь они не разобщены.

Также нам не пришлось задавать значение для каждой константы, но эти значения задались автоматически и по порядку. То есть сейчас SUNDAY = 0, MONDAY = 1 и т.д. И естественно, что компилятор тут не ошибётся. Если понадобится переставить константы в другом порядке, например, начать неделю с MONDAY, мы просто это сделаем, а значения переназначатся автоматически и по порядку.

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

-3

Это и хорошо, так как предотвращает ошибки. Но есть и другой нюанс, во всяком случае в языке C :) Так как по факту это целочисленные константы, то их всё-таки можно присвоить любой int-переменной через приведение типа к целому:

-4

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

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

enum Weekday { SUNDAY = 0, MONDAY = 1, TUESDAY = 2, ... };

или полу-вручную, чтобы например задать стартовое значение:

enum Weekday { SUNDAY = 10, MONDAY, TUESDAY, ... };

Классы

Если перечисляемых типов в языке нет, или мы по каким-то причинам не хотим их использовать, могут оказаться полезны классы с полями-константами. Например, рассмотрим PHP:

-5

От ручного нумерования опций это не спасает, но сохраняет организационное преимущество: константы крышуются одним классом, который, к примеру, можно импортировать в другие проекты одной строчкой.

Самодостаточные константы

Константы можно задавать и в виде строк. Например,

const SUNDAY = "SUNDAY";
const MONDAY = "MONDAY";
...

Если имя константы и её значение совпадают, то значит константа как таковая нам не особо и нужна. Сама строка становится (магической) константой и может использоваться в явном виде:

day = "MONDAY";

Разве что кавычки немного мешают.

Однако использовать строки это не то же самое, что использовать числа. Если вы сравните переменную со строкой, то во многих языках не увидите ничего особенного:

if (day == "MONDAY") ...

Но по факту сравнение строк это гораздо более тяжёлая операция, чем сравнение чисел. Особенно это заметно в C, где сравнение строк не спрятано под удобным синтаксисом, а требует вызова отдельной функции:

if (strcmp(day, "MONDAY) == 0) ...

Мультибайтные символы

Далее речь пойдёт только про специфику языка C. Как мы поняли, сравнивать строковые константы нецелесообразно, но если вспомнить про DEADBEEF, то мы можем трактовать это значение и как число, и как строку, верно? Потому что оно читается как строка.

-6

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

Однако у C есть интересная особенность. Как мы знаем, символы (тип char) задаются так:

char а = 'A';

Символ это число, то есть в нашем распоряжении есть символьные константы, состоящие из одной буквы, которые обрабатываются как числа. Одной буквы конечно мало, но...

int status = 'STOP';

Здесь переменная status имеет размер 4 байта, и ей присвоено число также размером 4 байта, но только это число записано не цифрами, а символами, где каждый символ это один байт – 'S', 'T', 'O', 'P'. Таким образом, мы можем записывать любые 4-символьные константы, которые будут выглядеть как строки, но по факту являться числами.

Эта техника, однако, является зависимой как от компилятора, так и от платформы, поэтому применять её надо локально для своих частных случаев в своей программе и ни в коем случае не для взаимодействия с другими сервисами.

Указатели

Наконец, в C мы можем сравнивать не строки, а указатели на строки, ведь указатели это числа.

-7

В данном примере переменная status указывает на адрес начала строки "STATUS_1". Затем она сравнивается не со строкой "STATUS_1", а с её адресом, и очевидно, происходит совпадение.

Таким образом мы сравнили переменную со строкой, не сравнивая её со строкой. Но здесь есть нюансы.

Во-первых, вы можете заметить, что строка "STATUS_1" фигурирует в программе дважды, но адрес у неё один. Разве это не должны быть две одинаковые строки, но с разными адресами? Нет. Одинаковые строковые константы, записанные в программе, являются указателями на один и тот же адрес памяти. То есть это реально настоящие константы.

Отсюда и вытекает второй нюанс. Если одной переменной мы присвоили адрес константы, а во вторую скопировали её содержимое:

-8

то несмотря на то, что содержимое совпадает, его копия находится уже по другому адресу, так что сравнение адресов тут не сработает. Требуется сравнивать именно содержимое строк с помощью strcmp().

Специальная ремарка

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

-9

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

Кроме того, есть флаг компилятора Visual C /GF, который делает именно то, что надо – объединяет одинаковые строки в одну. И у компилятора gcc тоже есть флаг, который делает то же самое: -fmerge-all-constants. Правда, что с ним, что без него результат всегда одинаков: строка только одна, и адрес у неё только один. Аналогично обстоят дела и у других компиляторов, проверить которые можно на сайте Compiler Explorer.

И тем не менее,

Данные примеры – не руководство к действию. Задачи лучше решать штатными средствами, вроде перечисляемых типов, но иногда вам может пригодиться что-то нестандартное. Плюс нужно понимать, как всё устроено.