Загадка размеров: почему int и long ведут себя по-разному на разных компиляторах?
Каждый, кто серьёзно изучал C++, рано или поздно сталкивался с этой головоломкой. Вы пишете простой код:
#include <iostream>
int main() {
std::cout << "Размер int: " << sizeof(int) << " байт\n";
std::cout << "Размер long: " << sizeof(long) << " байт\n";
std::cout << "Размер long long: " << sizeof(long long) << " байт\n";
return 0;
}
Запускаете на своём ноутбуке — получаете 4, 4, 8. Запускаете на сервере Linux — 4, 8, 8. А на старой 16-битной системе (да, такие ещё живы!) — 2, 4, 8. В чём подвох? Почему C++, язык, славящийся своей мощью и контролем, допускает такую неопределённость?
Ответ прост и сложен одновременно: стандарт C++ сознательно оставляет размеры целочисленных типов на усмотрение реализации. Это не баг, а фича, которая позволяет языку работать на любом железе — от микроконтроллеров до суперкомпьютеров. Но для разработчика это означает одно: если вы пишете переносимый код, нельзя полагаться на конкретные размеры.
Давайте разберёмся, что же на самом деле гарантирует стандарт C++ (начиная с C++98 и до C++23) и как не попасть в ловушку платформозависимости.
Что гарантирует стандарт C++? Минимальные размеры и иерархия
Вместо того чтобы сказать "int — это 4 байта", стандарт устанавливает минимальные диапазоны значений и строгую иерархию размеров. Вот ключевые правила из черновика стандарта (раздел [basic.fundamental]):
- Минимальный размер
char: 1 байт (8 бит, хотя формально байт в C++ может быть и больше). - Иерархия размеров:
sizeof(char) ≤ sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long). - Минимальные биты:
short: не менее 16 бит.int: не менее 16 бит.long: не менее 32 бит.long long: не менее 64 бит (добавлен в C++11).
- Размер
int: стандарт рекомендует, чтобыintимел "естественный размер" для целевой архитектуры (обычно 4 байта на 32/64-битных системах).
Обратите внимание: long может быть равен int (на 32-битных Windows) или long long (на 64-битных Linux). Это абсолютно легально!
Практический пример: проверка на вашей системе
Запустите этот код и сравните результаты с коллегами из разных ОС:
#include <iostream>
#include <climits> // для INT_MAX, LONG_MAX и т.д.
int main() {
std::cout << "Размеры в байтах:\n";
std::cout << "short: " << sizeof(short) << "\n";
std::cout << "int: " << sizeof(int) << "\n";
std::cout << "long: " << sizeof(long) << "\n";
std::cout << "long long: " << sizeof(long long) << "\n\n";
std::cout << "Максимальные значения:\n";
std::cout << "INT_MAX: " << INT_MAX << "\n";
std::cout << "LONG_MAX: " << LONG_MAX << "\n";
std::cout << "LLONG_MAX: " << LLONG_MAX << "\n";
return 0;
}
Вы увидите, что на Windows (MSVC) long часто равен 4 байтам, а на Linux (GCC/Clang) — 8 байтам. Это классический источник багов при переносе кода между платформами.
Почему стандарт не фиксирует размеры? Исторический контекст
Представьте себе 1970-е годы. Процессоры были 8-битными, 16-битными, а 32-битные только появлялись. Создатели C (а затем и C++) хотели, чтобы язык мог эффективно работать на любом оборудовании. Если бы int был жёстко закреплён как 4 байта, на 8-битных микроконтроллерах каждая операция с int требовала бы нескольких тактов, что убило бы производительность.
Поэтому родилась гениальная идея: int — это "натуральный" размер слова для данной архитектуры. На 16-битном процессоре int — 2 байта, на 32-битном — 4, на 64-битном — обычно 4 (но могло бы быть и 8, если бы не обратная совместимость).
Эта гибкость стала и проклятием: код, работающий на одной платформе, может неожиданно сломаться на другой из-за переполнения или несовпадения размеров.
Как писать переносимый код? Три стратегии
1. Используйте типы фиксированной ширины из <cstdint>
Начиная с C++11, стандартная библиотека предоставляет типы с гарантированным размером:
#include <cstdint>
#include <iostream>
int main() {
int32_t my_int; // всегда 4 байта, если доступно
int64_t my_long; // всегда 8 байт
int16_t my_short; // всегда 2 байта
std::cout << "int32_t: " << sizeof(int32_t) << " байт\n";
std::cout << "int64_t: " << sizeof(int64_t) << " байт\n";
return 0;
}
Важно: эти типы опциональны. Они гарантированно есть на всех современных платформах, но могут отсутствовать на очень старых или экзотических (например, 16-битные DSP). Если нужна максимальная переносимость, используйте int_least32_t (как минимум 32 бита) или int_fast32_t (самый быстрый тип, способный хранить 32 бита).