Типы данных в любом языке программирования являются одним из его краеугольных камней на котором все держится. И тема типизации данных в C обычно гораздо сложнее и комплекснее чем в других языках программирования и все по той же причине, системно-ориентированности (о чем говорилось в предыдущей статье) самого языка.
Типы данных и типизация данных это буквально основа любой программы, особенно написанной на C. Поэтому разобраться с темой типов данных (и системы типизации данных) достаточно важно. Так как это позволит лучше понимать, что происходит на самом деле, по каким причинам все устроено так как устроено, и как следствие, экономить свое время и силы при разработке ПО делая только нужные вещи вместо перебора методом тыка пока не найдется рабочий способ.
К тому же создавая ПО методом перебора любых возможно рабочих способов позволит вам не создать надежное, отказоустойчивое программное обеспечение, а скорее ПО наполненное багами и ошибками, что вряд ли приведет вас к успеху.

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

Предисловие

Итак, поехали.

Как было сказано в предыдущей статье язык: C - это язык программирования со статической слабой типизацией данных. Что это значит? Это значит что все типы должны быть известны заранее и явно указаны программистом. В самом языке даже нет оператора auto для явного выведения типа данных, в отличие от C++, хотя на самом деле он есть, но он предназначен для явного указания класса хранения переменной.

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

Историческая справка

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

В чем преимущества и недостатки статической типизации?

Статическая типизация появилась раньше динамической. Статическая типизация является полной противоположностью динамической, она более производительна, более простая для реализации создателями компиляторов, однако статическая типизация более сложная для программистов, использующих языках. Статическая типизация данных не позволяет динамически менять тип данных для уже созданных переменных и требует явного указания типов данных для компилятора на этапе написания программы.
Статическая типизация, фактически, всегда более быстрая и производительная чем динамическая. Большая производительностью берется из того факта что сами процессоры фактически всегда имеют исключительно статические переменные, а при динамической удаляется старая переменная и создается новая, с новым типом данных, то есть минимум 2 процессорные операции, что никогда не будет более производительным нежели 1 операция создания переменной и удаления и при её ненадобности.

В чем преимущества и недостатки слабой типизации?

Слабая типизация, на самом деле, крайне спорная тема, так как с одной стороны явное приведение практически любого типа данных к практически любому другому типу данных, особенно это касается массивов в языке C, это минус для новичков так как дополнительно создает возможность выстрелить себе в ногу и разработке ПО. С другой же стороны это дает более опытным программистам, например Джону Кармаку с проектом DOOM, производить очень сложные оптимизации, фактически невозможные в языках со строгой типизацией.
Слабая типизация дает большой спектр возможностей для произведения оптимизаций, но требует большой осторожности со стороны программистов. И стоит понимать что хоть это и усложняет создание и поддержку программ одновременно с этим позволяет повышать производительность выполнения программ.
Как например это описывалось самим Джоном Кармаком, когда он создавал несколько массивов подряд, что давало гарантию со стороны компилятора, что эти массивы будут расположены друг за другом в памяти. И через один массив перебирать несколько массивов одновременно специально выхода за пределы массива. Это давало возможность не создавать по одному циклу на каждый массив экономя процессорное время и размер программного кода. И хотя это не сильно сказалось на итоговой производительности, все это лишь микрооптимизации, но ПК в те времена были гораздо менее производительными чем текущие и поэтому имело смысл экономить каждую лишнюю процессорную инструкцию.

Немного про оптимизацию

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

Также есть несколько лично моих советов:

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

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

Да и оптимизация на самом деле большинству программного обеспечения вовсе и не требуется скорее требуется просто правильная архитектура, использование чуть более простых языков для повседневных задач. Например использовать для написания скриптов язык C, мягко говоря, не совсем правильно, так как потребует слишком много кода да и написанный код не будет кроссплатформенным.

Итог: не вставляйте себе палки в колеса и используйте сначала более простые технологии, а затем идите в более низкий уровень, но только при реальной необходимости. Не имеет смысла использовать язык C для оптимизации, если у вас нет с ней проблем или вам не требуется прямой доступ к аппаратному обеспечению ПК. И особенно не стоит сосредотачиваться на оптимизации преждевременно, когда никаких проблем с оной у вас нет. Да и как говорится: "Преждевременная оптимизация — корень всех зол" - цитата Дональда Кнута, из книги "Искусство программирования".

Краткое описание типов данных в языке

Итак в C существуют следующие базовые, целочисленные типы данных:

  • char - самый минимальный тип данных из существующих, по умолчанию, на большинстве самых популярных операционных систем, имеет знаковую форму (об этом позже). В компиляторах обычно реализуется как знаковый тип данных, занимающий в памяти ровно 1 байт или 8 бит.

  • short - редко используемый тип данных, так как служит по сути только в качестве промежуточного значения между char и int. В компиляторах обычно реализуется как знаковый тип данных, занимающий в памяти ровно 2 байта или 16 бит.

  • int - целочисленный тип данных, однако на практике самый часто используемый тип данных в C, особенно в старых кодовых базах, так как используется как отражение машинного слова. В компиляторах обычно реализуется как знаковый тип данных, занимающий в памяти ровно 4 байта или 32 бита.

  • long - чуть более интересный тип данных, так как конкретный размер может варьироваться в зависимости от компилятора и платформы на которой потом будет запущена программа. В компиляторах обычно реализуется как знаковый тип данных, занимающий в памяти ровно 8 байт или 64 бита. Но в windows равен типу int по своей размерности. Цель введения данного типа данных в язык, это варьирование размера типа данных в соответствии с разрядностью самого процессора. То есть на 32-битных процессорах это альтернатива int, на 64-битных это альтернатива long long

  • long long - самый большой тип данных данных из доступных. В компиляторах обычно реализуется как знаковый тип данных, занимающий в памяти ровно 8 байт или 64 бита.

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

  • float - минимальный тип данных для чисел с плавающей точкой с запятой. На всех платформах равен минимум 32 битам или 4 байтам.

  • double - расширенный или удвоенный тип данных, который равен 8 байтам или 64 битам.

  • long double - часть платформ не поддерживается, например Windows, на остальных равен 80 битам или 10 байтам и обычно является самым большим типом данных на платформе, который поддерживается стандартом языке и не является его расширением.

Также есть несколько примечаний касаемо типов данных:

  • Более подробную информацию вы можете найти в документации под ваш компилятор или на специализированных сайтах наподобие cppreference

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

Целочисленные типы данных без явного указания знака

Все целочисленные типы данных, которые могут быть как знаковыми так и беззнаковыми, имеют всегда сокращенную вариацию. Разберем на примере int:

  • signed int - знаковый целочисленный тип данных, который в любой системе будет знаковым, если это поддерживается платформой. Диапазон представляемых значений: от −2 147 483 648 до 2 147 483 647

  • unsigned int - беззнаковый целочисленный тип данных, который в любой системе будет беззнаковым, то есть не иметь отрицательного значения. Диапазон представляемых значений: от 0 до 4 294 967 295

  • int - на самом деле интересный тип данных так как на большинстве платформ будет альтернативой signed int, то есть будет иметь знак, хотя на некоторых платформах может и не иметь знака, то есть быть альтернативой unsigned int.

На самом деле вы можете заметить не совсем ровное соответствие диапазонов значений и особенно это может вызвать вопросы если вы станете работать с этими диапазонами значений через побитовые сдвиги. Все процессоры, на данный момент существующие, бинарные. Это значит что все что есть в процессоре кодируется только 2 вариациями бит, либо 0, либо 1 и так каждый бит. Следовательно из-за особенностей работы бинарной математики диапазон значений, например, у беззнаковых чисел должен быть равен числу 1 умноженному на 2 столько раз сколько бит число занимает в памяти. Проще говоря тип данных unsigned char (8 бит) будет равен 1 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 = 256 Следовательно получаем число 256, однако реальный максимальный диапазон значений будет 255, почему так? Потому что отсчет начинается не как нам людям привычнее с 1, а с 0 так как машине нет никакой разнице между 0 1 в бинарном виде и 0 имеет такой же вес как и 1 в битах.

Также есть еще одна интересная особенность в диапазонах но уже с числами знаковыми. Типичный диапазон на примере signed char это от -127 до 128. Однако почему именно такой неровный диапазон, почему не от -128 до 128 или от -127 до 127. В первом случае проблема со все тем же нулем, который также является значением и в таком случае диапазон выходил бы за 256 возможных значений. А во втором случае остается очень интересный случай с отрицательным нулем.

Вообще с точки зрения математики отрицательного нуля не существует, да и вообще ноль сам по себе крайне странная вещь с большим рядом особенностей в математике. И вообще-то говоря ноль не может иметь знака, так как пустота или отсутствие чего бы то ни было не может иметь знак. Однако в программировании из-за того как располагаются биты в памяти и как вообще представлены числа подобная ситуация возможна. И если положительный ноль это просто ноль, то вот отрицательный ноль существует но именно он и воспринимается компьютером как число 128. Это нужно чтобы при повышении разрядности типа данных (например из char в short) биты логично транслировались в памяти и не возникало ошибок. Поэтому реальный диапазон равно от -127 до 127, а отрицательный ноль автоматически воспринимается как число 128. И именно поэтому итоговый диапазон становится от -127 до 128.

Рекомендации по типизации данных

Я лично рекомендую использовать альтернативы в виде алиасов (псевдонимов), из стандартной библиотеки (stdint.h). Так как это сокращает название типа данных и делает его более читабельным и одновременно с этим есть дополнительные гарантии, что например тип данных uint_fast8_t будет самым быстрым из возможных на платформе, но который занимает минимум 8 бит в памяти. А тип данных uint_least8_t будет типом данных который имеет минимум 8 бит хотя может иметь и более.

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

Заключение.

На этом с базовой информацией по поводу типов данных и типизацией в языке C все. Также подписывайтесь на мой канал на хабре. Еще планируется куча лично мне достаточно интересных статей в первую очередь по C/C++. А потом через время возможно и моя основная сфера деятельности вас привлечет, а именно FrontEnd/Web разработка и можно будет начать цикл статей про данную сферу

Комментарии (3)


  1. randomsimplenumber
    19.07.2025 20:21

    отрицательный ноль автоматически воспринимается как число 128

    wat?


  1. Abstraction
    19.07.2025 20:21

    Пожалуйста, не надо так. Попытка указать все фактические ошибки этой статьи, кажется, будет длиннее самой статьи. Оставляя предисловие в стороне (не потому что в нём что-то хорошо), уже описание типов содержит серьёзные ошибки.

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

    Все целочисленные типы данных могут иметь padding bits. Ни для какого целочисленного типа данных стандарт не устанавливает численной константы количества бит, устанавливается только нижняя граница числа значащих бит и необходимость определить символическую константу CHAR_BITS. Для int эта граница равна 16, для long int 32, для long long int 64. Иными словами, утверждение что переменная типа int способна представить значение 2147483647 - в общем случае ложно.

    Про "отрицательный ноль" уже возмутились, кажется кто-то перепутал числа с плавающей точкой (у которых знаковый бит "живёт" отдельно, и потому можно получить "отрицательный ноль") и представление отрицательных чисел (которое обычно - но не всегда! - реализовано как арифметика по модулю 2^N, где таких фокусов нет).


  1. APh
    19.07.2025 20:21

    Дальше первых абзацев читать не смог. Ну, можно же нажать F7 в текстовом процессоре или поручить нейросетям работу. Зачем эти пытки?!!

    "О, маи глоза!"