Гибкие целочисленные типы в C не были ошибкой проектирования. Именно благодаря им язык добился переносимости в мире 12-, 18-, 36- и 60-битных компьютеров.

Многие разработчики, изучающие язык C, приходят из таких языков, как Python, JavaScript, Java, C#, Swift и другие.

И новичкам не требуется много времени, чтобы понять: многие вещи, которые они воспринимали как данность в своем основном языке, в C вовсе не гарантируются. Довольно часто мы ловим себя на таких вопросах:

  • Сколько бит эта переменная занимает в памяти?

  • Какой паддинг (выравнивание) у этой структуры?

  • Нормально ли это поле выровнено в RAM?

❯ Int’ы – это не 32 бита

Еще один обряд инициации для начинающих сишников – это понять, как использовать и как вообще работает оператор sizeof.

Видеть, как программа выводит 4, когда у нее запрашивают sizeof(int), кажется вполне логичным, учитывая, что на большинстве современных машин знаковый int действительно занимает 4 байта в памяти.

sizeof(int): 4 байта
sizeof(int): 4 байта

К сожалению, мне также приходится говорить моим студентам, что когда я только начинал учиться программировать, если я запрашивал sizeof(int), моя 386-я машина выдавала 2 байта!

sizeof(int): 2 байта
sizeof(int): 2 байта

И вот примерно здесь мы начинаем дискуссию о том, что нативные целочисленные типы в C не имеют фиксированного размера. Типы языка вроде char, int, short и long не дают никаких гарантий насчет того, сколько именно байт они занимают в памяти.

Поскольку в коде нам обычно нужны фиксированные размеры, мы обычно предлагаем студентам подключать заголовочный файл STDINT.H, чтобы использовать нормальные типы с фиксированным размером, такие как:

  • int8_t: знаковые целые, ровно 8 бит

  • uint8_t: беззнаковые целые, ровно 8 бит

  • int16_t: знаковые целые, ровно 16 бит

  • uint16_t: беззнаковые целые, ровно 16 бит

  • int32_t: знаковые целые, ровно 32 бита

  • uint32_t: беззнаковые целые, ровно 32 бита

  • и т. д.

Хедер <STDINT.H> завезли как часть стандарта C99 (вышедшего в 1999 году), и он помогает программистам гарантировать, что их переменные будут занимать точное количество байт вне зависимости от компилятора и архитектуры машины, под которую мы всё это собираем.

❯ Нефиксированные размеры интов – это ошибка проектирования?

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

Будем честны, в этом есть доля правды. Большинство разрабов, использующих C на современных системах, именно так и скажут. int равен 16 битам на одной машине и 32 битам на другой, long занимает 64 бита на Linux, но 32 на 64-битном Windows, и все это породило десятилетия багов переносимости. Так что, учитывая, что сегодня почти каждый CPU представляет собой 64-битную железку, умеющую в 8, 16, 32 и 64-битную арифметику… почему бы C просто сразу не зафиксировать размеры с самого начала?

По сути, мы тут пытаемся судить дизайнерские решения 1970-х годов по меркам 2020-х! C создавался как портабельный язык системного программирования, который мог бы эффективно мапиться на самые разные компьютерные архитектуры. Гибкие размеры типов не были недосмотром; именно так C и добился этой самой переносимости.

Имейте в виду, что цель C по «переносимости» означала совсем не то, что джавовое обещание переносимости в духе «write once, run anywhere». И чтобы понять почему, надо вспомнить, чем было понятие «компьютер», когда C только проектировался.

❯ Мир до того, как 8-битные байты победили

Сегодня мы многое принимаем как данность. Почти любая машина, которую вы пощупаете, имеет:

  • 8-битные байты

  • Побайтово адресуемую память

  • 32- или 64-битные регистры

  • Дополнительный код для знаковых чисел

  • Общепринятую модель памяти

В 1960-х и 70-х ничего из этого гарантировано не было. Индустрия еще не пришла к единому стандарту, и размеры машинных слов были какими угодно:

Машина

Размер слова

Примечания

DEC PDP-8

12 бит

Дико популярный мини-компьютер

DEC PDP-7

18 бит

Где на ассемблере родился UNIX

DEC PDP-11

16 бит

Побайтовая адресация; родина C

DEC PDP-10 / DECSYSTEM-20

36 бит

Символы часто упаковывались по 7 или 9 бит

Серия Honeywell 6000

36 бит

9-битные символы; ранняя цель для C

UNIVAC 1100 / Unisys 2200

36 бит

Арифметика в обратном коде (ones’-complement), 9-битные char’ы; компилятор C для нее есть даже сегодня

IBM 7090 / 7094

36 бит

6-битные кодировки символов

SDS 940, ICL 1900, Harris

24 бита

ICL использовал 6-битные символы

Семейство Burroughs B5000

48 бит

Тегированная стековая архитектура

CDC 6600

60 бит

6-битные символы, побайтовой адресации нет вообще

Cray-1

64 бита

Пословная адресация; в C short, int и long могли быть по 64 бита

Data General Nova

16 бит

Пословная адресация; байтовые указатели имели другое представление, чем указатели на слова

Intel 8086

16 бит

Сегментированная память; near- и far-указатели

Миникомпьютер PDP-8 от DEC
Миникомпьютер PDP-8 от DEC

С символами тоже не было никакого единообразия. Были 6-битные наборы символов, 7-битный ASCII, 9-битные байты на 36-битных машинах и EBCDIC на мейнфреймах IBM. Отрицательные числа могли храниться в дополнительном коде, обратном коде или в виде прямого кода со знаковым битом. Некоторые машины умели адресовать отдельные байты. Другие адресовали только слова целиком, так что «указатель на символ» должен был представлять собой адрес слова плюс смещение.

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

❯ Откуда взялись типы в C

Философия целых чисел в C становится понятнее, если взглянуть на его предков.

BCPL (Мартин Ричардс, 1967) и B (Кен Томпсон, примерно 1969) были бестиповыми языками. Там существовал только один вид значения: машинное слово. Переменная просто хранила слово. Вы могли трактовать ее как int, адрес или битовую маску в зависимости от примененного оператора. На машинах с пословной адресацией, под которые эти языки писались, это было элегантно и шустро.

Затем Bell Labs получила PDP-11. PDP-11 был с побайтовой адресацией, 16-битными словами и вот-вот должен был получить аппаратный блок плавающей точки. Деннис Ритчи в своей работе «The development of the C language» (1993) описывал, насколько плохо модель языка B «всё есть слово» ложилась на эту машину. Работа с символами была неуклюжей, указатели приходилось масштабировать между адресами слов и байтов, а значения с плавающей точкой в одно слово попросту не влезали.

Деннис Ритчи и Кен Томпсон работают на PDP-11/20 в Bell Labs
Деннис Ритчи и Кен Томпсон работают на PDP-11/20 в Bell Labs

Система типов C и была создана, чтобы устранить это несовпадение. char дал доступ к байту. А int сохранил дух машинного слова из BCPL: натуральное целое число данной машины. Эта идея стала частью языка. Даже сегодня в стандарте C (C11 §6.2.5) сказано:

«Обычный объект типа int имеет естественный размер, предполагаемый архитектурой среды исполнения».

И это осознанное решение. int никогда не задумывался как «строго 32 бита». Он означал: «то, с чем этой конкретной машине работать быстрее и удобнее всего».

❯ C выходит за пределы PDP-11

Настоящая проверка боем случилась в 1977 и 1978 годах, когда Ритчи и Стив Джонсон портировали UNIX и C на Interdata 8/32 – 32-битную машину, которая сильно отличалась от PDP-11. Они написали об этом в статье «Portability of C programs and the UNIX system» (Bell System Technical Journal, 1978). Примерно в то же время Portable C Compiler (pcc) Джонсона сделал перенос C на новые архитектуры практической реальностью, и C разошелся по огромному количеству разного железа.

В первом издании книги «Язык программирования C» (Керниган и Ритчи, 1978) есть таблица, в которую я люблю тыкать людей. В ней приведены размеры типов на четырех машинах, на которых C уже тогда работал:

Тип

DEC PDP-11

Honeywell 6000

IBM 370

Interdata 8/32

char

8 бит

9 бит

8 бит

8 бит

short

16

36

16

16

int

16

36

32

32

long

32

36

32

32

float

32

36

32

32

double

64

72

64

64

В 1978 году C уже вполне крутился на машинах с 16-битными, 32-битными и 36-битными интами, а также с 8- и 9-битными char’ами. Гибкие размеры были заложены с самого старта, и это работало. Один и тот же язык и практически те же самые исходники компилировались нативно и эффективно под всё это железо.

❯ Почему гибкие размеры были полезны

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

❯ 1. На 16-битной машине (PDP-11, 8086)

Каждая операция с int требовала бы двух машинных инструкций вместо одной. Сложение двух 32-битных значений на PDP-11 означает вызов ADD для младших слов, за которым следует ADC (сложение с переносом) для старших. Сравнения, сдвиги и умножения становятся еще тяжелее. Каждый индекс массива и каждый счетчик цикла жрал бы в два раза больше регистров и в два раза больше инструкций.

int i;
for (i = 0; i < n; i++) {
  total += table[i];
}

С «нативным» int этот цикл компилируется в операции с одним регистром на PDP-11, IBM 370, Honeywell 6000 или Cray. А с жестко забитым 32-битным int версия для PDP-11 работала бы примерно в два раза медленнее – притом, что PDP-11 как раз и была машиной, под которую C изначально писался!

❯ 2. На 36-битной машине (PDP-10, Honeywell, UNIVAC)

Фиксированный 32-битный int здесь выглядит пожалуй еще хуже, потому что само железо шире этого типа. Чтобы соблюдать точный 32-битный wraparound при переполнении, компилятору пришлось бы навешивать маску почти после каждой арифметической операции:

/* во что превратилось бы "a = a + b" для соблюдения точной 32-битной семантики */
a = (a + b) & 0xFFFFFFFF;
/* ...плюс ручное расширение знака с 31-го бита, если a знаковый */

А это лишние инструкции на каждое сложение, вычитание и умножение, плюс 4 бита впустую в каждом машинном слове. Если же позволить int быть 36-битным – оверхед равен нулю.

❯ 3. На машинах с обратным кодом (UNIVAC 1100 / Unisys 2200, CDC 6600)

Если бы язык требовал дополнительного кода и соответствующего поведения при переполнении, то каждая знаковая операция на машине с обратным кодом требовала бы программной эмуляции. C обошел это, разрешив все три представления знака (дополнительный код, обратный код и знаковый бит) и объявив знаковое переполнение неопределенным поведением (UB).

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

❯ 4. На машинах без 8-битных байтов (CDC, PDP-10, DSP с пословной адресацией)

C требует, чтобы char содержал как минимум 8 бит (CHAR_BIT >= 8), а не строго 8. На 36-битной машине четыре 9-битных char’а идеально ложатся в одно слово без остатка. На машине, которая может адресовать только слова целиком, char может просто быть размером в слово.

Именно поэтому C никогда не обещал, что все указатели будут иметь одинаковый вид в памяти. На Data General Nova типы char * и int *, указывающие на одно и то же место, имели разный набор бит. Правила C относительно приведения указателей (и необходимость явных кастов и void *) появились как раз из-за таких машин.

Миникомпьютер Data General Nova
Миникомпьютер Data General Nova

❯ 5. На мелких 8-битных микроконтроллерах (Z80, 6502, 8080)

C не разрешает делать int настолько маленьким, чтобы он совпадал с 8-битными регистрами процессора. Стандарт задает минимальный диапазон от -32767 до 32767, поэтому int должен быть как минимум 16-битным. Тут виден баланс дизайна: размер адаптируется под платформу, но есть нижняя планка, чтобы программист мог хоть на что-то рассчитывать.

❯ Паттерн дизайна: минимумы вместо жестких размеров

Когда ANSI стандартизировал C в 1989 году, эту философию закрепили официально. Вы получаете не точные размеры, а гарантированные минимальные диапазоны, доступные через <limits.h>:

Тип

Гарантированный минимум

char

8 бит (CHAR_BIT >= 8)

short

16 бит

int

16 бит

long

32 бита

long long (C99)

64 бита

Портабельная программа на C задает вопрос: «какой минимальный тип гарантированно вместит мой диапазон значений?», а не «какой тип имеет размер ровно N бит?». Если вам нужны значения до 100000, вы берете long, потому что long гарантированно вместит их на любой платформе. Если ваши данные влезают в ±32767, int отлично подойдет и будет самым быстрым выбором на любом железе.

В документе ANSI C Rationale «дух C» сведен к нескольким принципам, и два из них имеют прямое отношение к теме:

  • Доверяй программисту

  • Делай быстро, даже если это не гарантирует стопроцентную переносимость

C пытался быть переносимым и быстрым, позволяя каждой реализации мапить язык на железо наиболее дешевым путем. Это инженерный компромисс, а не факап.

❯ Пример переносимого C: кодовая база Lua

Если вы хотите увидеть, как эта философия применяется в реальном современном проекте, гляньте исходники Lua. Lua заводится вообще на всем: от 64-битных серверов до микроконтроллеров и старых экзотических тулчейнов. Lua написана на том, что сами авторы называют «Clean C», – общем подмножестве стандарта ANSI C (C89). Долгое время Lua вообще не зависела от <stdint.h>, и там до сих пор используется классический подход:

«Спроси у компилятора через <limits.h>, что у него есть, и выбирай типы на основе гарантированных диапазонов».

Вот пара примеров из Lua 5.4:

❯ Проверка, достаточно ли велик int, без использования int32_t

В файле luaconf.h:

/*
@@ LUAI_IS32INT истинно тогда и только тогда, когда int имеет (как минимум) 32 бита.
*/
#define LUAI_IS32INT	((UINT_MAX >> 30) >= 3)

Это чистый C89. Код не спрашивает: «является ли int строго 32-битным?», потому что на 36-битной машине этот вопрос не имел бы смысла. Он проверяет: «вмещает ли unsigned int диапазон хотя бы в 32 бита?». И эта штука одинаково корректно отрабатывает на 16-, 32-, 36- и 64-битных интах.

❯ Инструкции виртуальной машины: «хотя бы 4 байта», а не «строго 4 байта»

В файле llimits.h:

/*
** тип для инструкций виртуальной машины;
** должен быть unsigned-размером (как минимум) 4 байта (подробности в lopcodes.h)
*/
#if LUAI_IS32INT
typedef unsigned int l_uint32;
#else
typedef unsigned long l_uint32;
#endif

typedef l_uint32 Instruction;

Тип называется l_uint32, но обратите внимание на коммент: он должен быть как минимум 32-битным. На машине с 16-битным int код фоллбэчится на long, который по стандарту гарантированно не меньше 32 бит. Декодер инструкций в lopcodes.h выдергивает поля через сдвиги и маски, поэтому любые лишние старшие биты на более широких машинах просто игнорируются.

❯ Небольшие числа и подсчет памяти

/* char'ы используются как малые натуральные числа (чтобы char оставался под символы) */
typedef unsigned char lu_byte;
typedef signed char ls_byte;

Lua использует unsigned char там, где в современной кодовой базе влепили бы uint8_t. И это валидно везде, включая платформы, где байт равен 9 или 16 битам. Типа uint8_t на таких железках просто не существовало бы.

Типы для учета памяти следуют тому же паттерну: используются size_t/ptrdiff_t, когда int равен 32 битам и больше, и unsigned long/long на 16-битных системах, где size_t может оказаться слишком узким для подсчета общего объема памяти.

❯ Выбор lua_Integer из того, что предоставляет платформа

Целочисленный тип в Lua настраивается через luaconf.h (LUA_INT_INT, LUA_INT_LONG, LUA_INT_LONGLONG), с опцией LUA_32BITS для слабых платформ:

#if defined(LUA_32BITS)		/* { */
/*
** 32-битные целые и float
*/
#if LUAI_IS32INT  /* юзаем int, если он достаточно жирный */
#define LUA_INT_TYPE	LUA_INT_INT
#else  /* иначе берем long */
#define LUA_INT_TYPE	LUA_INT_LONG
#endif
#define LUA_FLOAT_TYPE	LUA_FLOAT_FLOAT

По дефолту используется long long, если компилятор его поддерживает (это чекается через LLONG_MAX), с откатом на long для строгих компиляторов C89, где этого типа нет.

❯ Бинарная сериализация с помощью CHAR_BIT

Пожалуй, самый наглядный пример – это функции string.pack и string.unpack в lstrlib.c. Они читают и пишут бинарные инты побайтово, при этом не закладываясь на то, что байт обязан быть строго 8-битным:

/* количество бит в символе */
#define NB	CHAR_BIT

/* маска для одного символа (NB единиц) */
#define MC	((1 << NB) - 1)

Упаковка целых чисел происходит через постоянное взятие n & MC со сдвигом вправо на NB. Порядок байтов (endianness) разруливается в коде явно, а не тупым кастом указателя на авось. Вот так и пишется бинарный ввод-вывод, который работает на любой валидной реализации C.

«Отсутствие фиксированных типов» не означает «отсутствие контроля над размерами». Вы описываете то, что вам нужно (диапазон, минимальную ширину, байт), чекаете это через <limits.h>, а реализация сама выбирает наиболее производительный тип.

Именно такой дисциплины дизайн C и ожидал от разработчиков.

❯ Будем объективны: в чем были проблемы

Я не собираюсь делать вид, что гибкие размеры дались бесплатно. Грабли тут вполне реальные:

  • Люди не писали код так, как это сделано в Lua. Куча сишного кода писалась в расчете на то, что int – это 16 бит (во времена DOS) или 32 бита (позже), народ закладывался на равенство sizeof(int) == sizeof(void *) или кастовал указатели в int. Переход на 32-битные, а затем на 64-битные машины предсказуемо всё это ломал. Язык давал возможность писать переносимо, но не заставлял это делать.

  • Раскол 64-битных моделей данных. Unix-подобные ОС выбрали LP64 (где long и указатели 64-битные), а 64-битная Windows пошла по пути LLP64 (где long остался 32-битным, а 64 бита получили только long long и указатели). Из-за этого код, использующий long как «большое число» или «число размером с указатель», ведет себя на этих системах по-разному.

  • Стандартные типы фиксированной ширины завезли слишком поздно. Хедер <stdint.h> появился только в C99, так что на протяжении двух десятилетий каждый проект городил свои typedef’ы вроде u32/INT32, обмазываясь лесами из #ifdef. Это как раз тот воркэраунд, который советуют и сегодня, просто стандарт в итоге сделал его официальным.

Кстати, даже <stdint.h> сохранил оригинальную философию C. Типы точной ширины (int8_t, uint32_t и т.д.) на самом деле опциональны: компилятор предоставляет их только в том случае, если на целевой машине реально есть тип ровно такой ширины, без бит-паддинга и строго в дополнительном коде. При этом обязательными являются типы вроде int_least32_t («наименьший тип размером не менее 32 бит») и int_fast32_t («самый быстрый тип размером не менее 32 бит»). То есть это всё та же идея гарантированных минимумов из ANSI, просто с более понятными именами. Комитет по стандартизации снова отказался делать вид, будто везде в мире байт равен 8 битам.

Примечание. Это не просто дела давно минувших дней. У многих DSP-процессоров, которые производятся прямо сейчас, CHAR_BIT равен 16 или 32 (например, у некоторых чипов от TI и Analog Devices), плюс существуют сишные компиляторы под 36-битную линейку Unisys 2200 с обратным кодом. На таких системах uint8_t банально отсутствует, а код, написанный по канонам Lua, спокойно собирается и шуршит.

❯ Как с этим у современных языков (и почему они могут себе это позволить)

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

  • Java (1995) фиксирует вообще всё: byte – 8 бит, short – 16, int – 32, long – 64, всё строго в дополнительном коде и с жестко заданным поведением при переполнении. Java могла пойти на это отчасти потому, что она крутится поверх виртуальной машины. JVM прячет под капот все нюансы конкретного железа, а проблемы эмуляции (если они вообще возникают) – это уже забота самой JVM.

  • C# следует точно такой же модели внутри среды исполнения .NET.

  • В Rust вообще нет дефолтного голого int. Там пишут i8, u16, i32, u64, i128 и так далее, явно указывая разрядность. Поведение при переполнении четко определено: в debug-сборках это паника, а в релизных – wrapping по умолчанию, плюс есть явные методы (wrapping_add, checked_add, saturating_add), когда нужно строго конкретное поведение.

  • Zig идет еще дальше и позволяет задавать инты произвольной ширины, такие как u7, i3 или u48, что дико удобно для битовых полей и работы с регистрами железа. Кроме того, там есть типы c_int, c_long и прочие для интеропа с C, поскольку язык понимает, что сишные размеры плавают.

  • Go жестко фиксирует типы от int8 до int64, но его базовые int и uint – это 32 или 64 бита, в зависимости от платформы.

  • Swift делает то же самое: Int там равен размеру машинного слова, наряду с явными Int8–Int64.

И вот последние два пункта весьма показательны. Даже современные языки сохранили концепцию натурального, платформозависимого целого числа: isize/usize в Rust, int в Go, Int в Swift, usize в Zig. Эта идея никуда не делась. Она просто сузилась от «родного слова процессора» до «размера указателя», потому что индексация памяти – это то место, где размер слова все еще критичен.

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

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

❯ Переносимость нужна не всегда

При этом стоит держать в голове, что далеко не всем проектам требуется запредельная портабельность. Иногда мы досконально знаем целевое железо и нам не нужно гадать о размерах типов: процессор – например, MIPS R3000, выравнивание памяти строго такое, какого требует RISC-архитектура MIPS, компилятор берем из оригинального Sony SDK, и мы со стопроцентной уверенностью знаем, сколько байт занимают int, short и long.

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

Ну, если только вы внезапно не решите использовать тот же самый код при портировании игры на другие платформы. Именно на эти грабли, собственно, и наступила Psygnosis с исходниками Wipeout для PS1, когда портировала игру под DOS и Windows ?

❯ Заключение

Любое инженерное решение нужно оценивать через призму проблемы, которую оно решало, и мира, для которого оно создавалось. Перед C в 1970-х стояла задача быть языком системного уровня, способным заменить ассемблер на машинах с 12-, 16-, 18-, 24-, 32-, 36-, 48-, 60- и 64-битными словами, с 6-, 7-, 8- и 9-битными символами, да еще и с тремя разными способами представления отрицательных чисел – и при этом выдавать бинарник, работающий почти так же быстро, как написанный вручную ассемблерный код.

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

Если вы железно уверены, что ваш код будет крутиться только на современных 64-битных машинах с 8-битным байтом и дополнительным кодом – берите язык, созданный для этого мира, либо подключайте <stdint.h> в C. Но если вам, как и мне, доводится ковырять старые или нестандартные архитектуры, гибкие целые числа в C до сих пор отлично делают ту работу, ради которой их когда-то придумали.

Никакой ошибкой это не было. В переносимости как раз и заключалась вся суть.

❯ Что почитать по теме

  • Dennis M. Ritchie, «The development of the C language» (HOPL-II, 1993)

  • S. C. Johnson and D. M. Ritchie, «Portability of C programs and the UNIX system» (Bell System Technical Journal, 1978)

  • Kernighan and Ritchie, «The C programming language», 1-е изд. (1978), §2.2

  • «Rationale for American national standard for information systems, programming language C» (ANSI X3.159-1989)

  • Исходники Lua 5.4: luaconf.h, llimits.h, lopcodes.h, lstrlib.c

Может быть интересно:
Перейти ↩

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram‑канале ↩

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


  1. sanchas
    13.10.2026 08:39

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


  1. SIISII
    13.10.2026 08:39

    Проблема не в том, что типы в Сях (и унаследованные в Си++) имеют зависящий от реализации размер -- проблема в том, что типы, имеющие строго определённый размер, были включены в язык очень поздно, т.е. разработчики типа переносимого языка абсолютно не озаботились переносимостью программ, хотя о проблемах, связанных с шириной машинных слов, просто не могли не знать (сами с этим сталкивались -- в частности, при переезде с PDP-7 на PDP-11).

    Правильней было бы, скажем, объявить, что int всегда имеет ширину, соответствующую естественной ширине машинного слова (16 бит для PDP-11, 32 бита для Системы 360 и её наследников до ESA/390 включительно, но 64 бита на z/Architecture и на современных процессорах AMD64/Intel64, ну и т.д.), тип char -- ширине символа на данной платформе, а также предусмотреть те самые типы, которые были включены лишь в С99 -- со строго фиксированной шириной (тем более, Си появился уже достаточно поздно, это отнюдь не Фортран с Коболом, и что байты побеждают, уже было очевидно -- а соответственно, можно было бы уже тогда догадаться включить типы вроде int8, int16 и int32). Альтернатива -- ширина указывается явным образом, а транслятор уже должен уложить её на конкретную архитектуру, как ему будет удобно (или выдать ошибку, если не может это сделать); подобный подход был использован в Аде.


    1. CrashLogger
      13.10.2026 08:39

      Ничего там не было очевидно еще. Даже в машинах одного производителя (DEC например) был лютый зоопарк. И откуда компилятор возьмет int32 на машине, у которой регистр 36 бит ? Обмазывать все операции масками и проверками ? Тут еще надо учитывать условия, в которых все это писалось. Типичная PDP11 имела 64-256 Кбайт оперативки, в которые надо было уместить и компилятор, и то, что он компилирует.