Найти в Дзене
ZDG

Типы данных: Зачем они нужны

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

Поэтому я решил написать о них с самого начала. Предупреждаю – длинно и очень издалека.

Каков самый низкий уровень программирования? Это машинные инструкции, которые хранятся в памяти. Процессор читает эти инструкции и делает то, что они требуют. А требовать им особо и нечего. Они говорят процессору – возьми число по такому-то адресу, сложи с числом по такому-то адресу, помести результат в такой-то адрес, и так далее. На этом уровне нет никаких типов. Есть память, в памяти есть адреса и числа, и всё. И с появлением языков программирования ничего не меняется. Память у компьютера остаётся та же самая, числа в ней те же самые, процессор вообще ничего не знает о типах.

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

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

Как известно, память компьютера – это последовательность байтов. Если в компьютере 4 гигабайта памяти, значит это такая длинная-предлинная цепочка из более чем 4 миллиардов байт, и у каждого байта есть свой адрес. Указывая процессу нужный адрес, мы делаем запись или чтение в нужный байт.

Хотя каждый байт состоит ещё из 8 бит, процессор может прочитать или записать только байт целиком. Но кроме того, если процессор 16-битный, он может за один раз прочитать 2 байта, а если 32-битный, то 4 байта, ну дальше вы поняли.

Когда процессор читает сразу 2 байта, они не являются двумя отдельными числами. Эти 2 байта представляют собой одно сплошное число длиной 16 бит. Также и читая сразу 4 байта, процессор читает не 4 отдельных числа, а одно число длиной 32 бита.

Чем больше бит в числе, тем больше оно может быть. Когда мы работаем с данными, мы обращаем внимание на то, какого они размера. Чтобы хранить номер месяца, нам не нужны числа больше чем 12. Значит, хватит одного байта. Можно хранить месяц и в 2-х байтах и в 4-х, но лишние байты будут всегда пустые.

А чтобы хранить население Земли, нам не хватит даже 4-х байт. Значит, нужно уже 8 байт. Почему 8, а не 5 или 6? Потому что процессор может лишь удваивать размер читаемой порции, а не выбирать её произвольно. Ну и да, процессор должен быть теперь уже 64-битный.

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

Например, одна инструкция: "прочитай по адресу 100 один байт" и другая инструкция: "прочитай по адресу 100 два байта". Несмотря на то, что чтение делается из одного и того же адреса, где лежат одни и те же данные, результат получится разный. Потому что в первом случае полученное число – это один байт по адресу 100, а во втором случае – это два байта по адресам 100 и 101. Как видим, адрес указывает только на первый байт, а остальные байты идут "прицепом" по порядку.

Заметьте, что процессору абсолютно всё равно. Мы можем сначала по адресу 100 записать 16-битное число. Оно займет 2 байта по адресам 100 и 101. Затем из адреса 100 мы можем прочитать число длиной всего 1 байт, наплевав на второй байт. Или прочитать число длиной 4 байта. Но мы же записали только 2 байта в адреса 100 и 101, а чтобы прочитать 4 байта, нужны еще адреса 102 и 103? Ну да, нужны. А что в них – вообще неважно. Сказано прочитать – значит прочитаем. Как видим, то, что мы записали, и то, что мы можем прочитать, никак вообще не связано и тем более не связано с нашими какими-то там переменными, типами и прочим, что существует в нашей голове или программе. В памяти лежат голые числа, которые можно читать и писать с любой позиции и любыми доступными порциями.

Собственно говоря, первые типы появились в языках программирования именно как средство автоматического распределения памяти. Самым важным их свойством была только их длина и ничего более. Давайте посмотрим на типы языка C:

  • char - от слова character, или "символ", длина 1 байт
  • int - от слова integer, или "целый", длина 2 байта*
  • long - от слова long, или "длинный", длина 4 байта*

(* на самом деле длина типа в C зависит от архитектуры компьютера и компилятора. Тип int может быть и 4-байтным. Но в стандарте он определен как тип, который имеет длину не менее 2-х байт)

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

char a = 'A';

то при операциях с такой переменной транслятор будет генерировать инструкции для процессора "запиши 1 байт" или "прочитай 1 байт". Если написать:

int a = 100;

то при операциях с такой переменной транслятор будет генерировать инструкции для процессора "запиши 2 байта" или "прочитай 2 байта".

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

Но погодите, а вот же переменная char a = 'A', она же символьного типа, и присвоили мы ей не число, а букву, разве это не имеет значения?

В языке C – нет, не имеет. Для него символ – это просто один байт. И все символы – это числа. Можете присвоить символьной переменной число 65, или символ 'A' – получится одно и то же (язык C сам переведет букву 'A' в число 65). Более того, если ваша переменная объявлена как int, вы можете присвоить ей два символа, например 'AB', потому что два символа – это два байта, и ваша int-переменная тоже два байта, так что пофиг вообще, что вы там присваиваете. Если немного напрячься, можно посчитать, что 'AB' это 65*256+66 = 16706, вот это число и будет присвоено вашей переменной в действительности.

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

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

Остальные свойства, применения, интерпретации и трансформации типов я разберу в следующей части.