Введение
Ранее рассматривали вызов 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, работа с действительно «сырой» памятью), а не используется как общий, универсальный механизм передачи данных произвольного типа.