Синтаксис си/с++

Типы данных: целые, вещественные, void, размеры и платформенная зависимость

Введение

Ранее рассматривали вызов square(int value) без подробного разбора, что именно представляет собой тип int — в этой статье разберём базовые типы данных C/C++ систематически, включая важный, часто упускаемый аспект — зависимость точного размера многих из этих типов от конкретной платформы и компилятора.

Концепция

C/C++ предоставляет несколько целочисленных типов разного размера (char, short, int, long, long long), каждый со знаковой (signed, по умолчанию) и беззнаковой (unsigned) вариацией, и стандарт языка гарантирует лишь МИНИМАЛЬНЫЙ размер каждого типа (например, int не менее 16 бит), но не точный, фиксированный размер — реальный размер на конкретной платформе и при конкретном компиляторе может отличаться (наиболее часто int занимает 32 бита на современных desktop-платформах, но это не гарантировано универсально для абсолютно любой платформы). Вещественные типы (float, double, long double) представляют числа с плавающей точкой согласно стандарту IEEE 754 (для float/double практически универсально на современных платформах), с float обычно 32 бита и double обычно 64 бита точности. void — особый тип, означающий «отсутствие типа», используемый для функций без возвращаемого значения и для нетипизированных указателей (void*).

Пример кода

#include <iostream>

int main()
{
    // sizeof — оператор, возвращающий размер типа в БАЙТАХ на ТЕКУЩЕЙ платформе/компиляторе
    std::cout << "sizeof(char): " << sizeof(char) << std::endl;       // гарантировано 1 байт стандартом
    std::cout << "sizeof(int): " << sizeof(int) << std::endl;          // ТИПИЧНО 4, но НЕ гарантировано
    std::cout << "sizeof(long): " << sizeof(long) << std::endl;        // часто отличается между платформами!
    std::cout << "sizeof(long long): " << sizeof(long long) << std::endl; // минимум 64 бита по стандарту
    std::cout << "sizeof(float): " << sizeof(float) << std::endl;      // типично 4
    std::cout << "sizeof(double): " << sizeof(double) << std::endl;    // типично 8

    return 0;
}
// Знаковые и беззнаковые типы — критичное различие в интерпретации одинаковых битов
#include <cstdint> // фиксированные по размеру типы — статья ниже о подводных камнях

int main()
{
    signed char signedValue = -1;
    unsigned char unsignedValue = static_cast<unsigned char>(signedValue);
    std::cout << static_cast<int>(unsignedValue) << std::endl; // 255 — те же биты, иная интерпретация!

    // Фиксированные по размеру типы — гарантированно одинаковый размер на ЛЮБОЙ платформе
    int32_t guaranteed32Bit = 100000;
    int64_t guaranteed64Bit = 10000000000LL;
    return 0;
}
// void как отсутствие типа возвращаемого значения и как нетипизированный указатель
void printMessage() // void — функция НЕ возвращает значение вовсе
{
    std::cout << "Сообщение" << std::endl;
}

void processRawData(void *data, size_t size) // void* — указатель без известного компилятору типа
{
    // Перед реальным использованием данных НЕОБХОДИМО явное приведение к конкретному типу
    unsigned char *bytes = static_cast<unsigned char *>(data);
    for (size_t i = 0; i < size; ++i) {
        std::cout << static_cast<int>(bytes[i]) << " ";
    }
}

Пояснения к коду

Вывод sizeof() для разных типов наглядно демонстрирует, что реальный размер большинства целочисленных типов не зафиксирован стандартом жёстко — sizeof(long) особенно показателен, поскольку на разных платформах (32 бита на Windows даже в 64-битных программах, но 64 бита на большинстве 64-битных Linux/macOS систем) может дать разный результат, что является источником реальных, практических ошибок переносимости кода между платформами. Пример со знаковым/беззнаковым char показывает фундаментальный принцип — те же самые биты в памяти интерпретируются совершенно по-разному в зависимости от того, считается ли тип знаковым или беззнаковым, и явное приведение типа (static_cast) делает эту переинтерпретацию видимой и осознанной, а не случайной. void* в processRawData показывает типичное использование нетипизированного указателя для функций, работающих с «сырыми» байтами данных независимо от их реальной структуры — компилятор не может выполнить никаких проверок типов для содержимого, на которое указывает void*, что требует особой аккуратности при последующем приведении к конкретному типу.

Подводные камни

  • Предположение о фиксированном, универсальном размере int/long без учёта платформенных различий — код, написанный с жёстким предположением «int всегда 4 байта» или «long всегда 8 байт», может работать некорректно при компиляции для иной платформы/компилятора, где эти предположения не верны, и для случаев, где точный размер действительно важен (сериализация данных, побитовые операции с фиксированной разрядностью), стоит использовать фиксированные по размеру типы из <cstdint> (int32_t, uint64_t и аналогичные), гарантированно одинаковые на любой платформе.
  • Сравнение знаковых и беззнаковых типов в одном выражении без понимания правил неявного преобразования — при сравнении signed-значения с unsigned-значением компилятор обычно неявно преобразует знаковое значение к беззнаковому типу перед сравнением, что для отрицательных значений (превращающихся в очень большое беззнаковое число после такого преобразования) может привести к результату сравнения, прямо противоречащему интуитивному, математическому ожиданию, и многие компиляторы выдают предупреждение именно для таких потенциально проблемных сравнений, которое не стоит игнорировать.
  • Переполнение целочисленного типа без явного контроля диапазона значений — арифметическая операция, результат которой выходит за пределы диапазона, представимого данным типом, для знаковых типов формально является неопределённым поведением согласно стандарту C++ (хотя на практике на большинстве платформ происходит «обёртывание» по модулю), и код, рассчитывающий на конкретное, предсказуемое поведение при переполнении, не является строго корректным согласно стандарту, даже если «работает» на конкретной, протестированной платформе.
  • Использование void* без крайней необходимости вместо типобезопасных альтернатив современного C++ (шаблоны; std::variant; полиморфизм через виртуальные функции,) — void* лишает компилятор возможности проверки типов на этапе компиляции, перекладывая всю ответственность за корректность приведения типа на разработчика, и в современном C++ коде явное использование void* обычно ограничено действительно низкоуровневыми, специфичными случаями (взаимодействие с C-API, работа с действительно «сырой» памятью), а не используется как общий, универсальный механизм передачи данных произвольного типа.