Типы данных и типизация переменных в C и C#

Типы данных и типизация переменных в C и C# Полезное

Типизация переменных — это набор правил, по которым каждой переменной, значению и выражению сопоставляется тип данных, определяющий, какие значения там допустимы и какие операции над ними разрешены. И C, и C# статически типизированы: тип переменной известен на этапе компиляции. Но системы типов у них разные, поэтому ниже я развожу их отдельными таблицами и примерами. Разберу, что такое статическая и сильная типизация, встроенные типы обоих языков, их размеры и диапазоны, деление на value и reference в C# и приведение типов.

Статическая типизация — это не то же, что сильная

Два свойства часто путают, разведу их сразу.

  • Статическая типизация — тип переменной фиксируется при компиляции, а не в момент выполнения. Компилятор знает тип каждой переменной заранее и ловит несоответствия до запуска. Это про C и C#.
  • Сильная (строгая) типизация — язык почти не выполняет неявных преобразований «за спиной» и запрещает бессмысленные операции над несовместимыми типами.

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

Встроенные типы в C: размеры платформозависимы

Стандарт C задает не точные размеры, а минимальные гарантии: char — не меньше 8 бит, short и int — не меньше 16 бит, long — не меньше 32, long long — не меньше 64. Конкретный размер зависит от платформы и модели данных компилятора (LP64 на Linux/macOS, LLP64 на Windows). Поэтому таблицу ниже читайте как «типичные значения», а точные размеры на своей системе проверяйте через sizeof.

Тип C Типичный размер Диапазон (знаковый вариант) printf
char 1 байт зависит от знаковости реализации %c
signed char 1 байт [-128, 127] (гарантия не уже [-127, 127]) %d
unsigned char 1 байт [0, 255] %hhu
short 2 байта [-32768, 32767] %hd
unsigned short 2 байта [0, 65535] %hu
int 2 или 4 байта [-2147483648, 2147483647] при 4 байтах %d
unsigned int 2 или 4 байта [0, 4294967295] при 4 байтах %u
long 4 или 8 байт зависит от модели данных %ld
long long 8 байт [-9223372036854775808, 9223372036854775807] %lld
float 4 байта одинарная точность IEEE 754 %f
double 8 байт двойная точность IEEE 754 %f, %g
long double 8-16 байт расширенная точность (зависит от платформы) %Lf

Минимальный полный пример, который печатает реальные размеры и диапазоны, — его можно скопировать и запустить.

#include <stdio.h>
#include <limits.h>
#include <stdbool.h>

int main(void) {
    printf("CHAR_BIT = %d\n", CHAR_BIT);
    printf("sizeof: char=%zu short=%zu int=%zu long=%zu long long=%zu\n",
           sizeof(char), sizeof(short), sizeof(int),
           sizeof(long), sizeof(long long));
    printf("sizeof: float=%zu double=%zu long double=%zu\n",
           sizeof(float), sizeof(double), sizeof(long double));
    printf("INT_MAX=%d LONG_MAX=%ld\n", INT_MAX, LONG_MAX);

    bool flag = 42;              /* любое ненулевое значение станет 1 */
    printf("bool from 42 = %d\n", flag);
    return 0;
}

Вывод на macOS (Apple Silicon, clang, модель LP64):

CHAR_BIT = 8
sizeof: char=1 short=2 int=4 long=8 long long=8
sizeof: float=4 double=8 long double=8
INT_MAX=2147483647 LONG_MAX=9223372036854775807
bool from 42 = 1

Именно поэтому размеры нельзя запоминать как абсолют. На Linux x86-64 с gcc та же строка про long double покажет 16 (80-битный расширенный формат хранится в 16 байтах), а на Windows с MSVC long займет 4 байта, а не 8, потому что там модель LLP64.

Логический тип в C появился в C99 как _Bool. Заголовок <stdbool.h> вводит привычные bool, true, false. Особенность видна в примере выше: присваивание bool flag = 42 сохраняет не 42, а 1 — любое ненулевое значение нормализуется к единице.

Встроенные типы в C#: размеры фиксированы спецификацией

В C# ключевые слова вроде int или long — это псевдонимы типов из пространства System (например, int = System.Int32). Их размеры одинаковы на всех платформах, в отличие от C. Ниже целочисленные и вещественные типы.

Тип C# Псевдоним System Размер Диапазон / точность
sbyte SByte 8 бит [-128, 127]
byte Byte 8 бит [0, 255]
short Int16 16 бит [-32768, 32767]
ushort UInt16 16 бит [0, 65535]
int Int32 32 бита [-2147483648, 2147483647]
uint UInt32 32 бита [0, 4294967295]
long Int64 64 бита [-9.2e18, 9.2e18]
ulong UInt64 64 бита [0, 1.8e19]
float Single 32 бита ~7 значащих цифр, до ~3.4e38
double Double 64 бита ~15-17 значащих цифр, до ~1.7e308
decimal Decimal 128 бит 28-29 значащих цифр, до ~7.9e28
char Char 16 бит одна кодовая единица UTF-16, [0, 65535]
bool Boolean реализационно true / false

Несколько практических уточнений к таблице.

decimal хранит десятичную мантиссу и масштаб, поэтому не накапливает двоичную ошибку округления в пределах своей точности (28-29 значащих цифр) — его берут для денег и финансовых расчетов. «Точный» здесь значит «без двоичного дрейфа в пределах масштаба», а не «с бесконечной точностью»: результат все равно округляется до доступных разрядов.

char в C# — это одна 16-битная кодовая единица UTF-16, а не полный символ Unicode. Символы вне базовой плоскости (например, многие эмодзи) кодируются суррогатной парой из двух char, так что один видимый символ может занимать два элемента. ASCII (0-127) — подмножество Unicode, поэтому латиница и цифры укладываются в один char.

bool в C# не смешивается с целыми: 1 не превращается в true, а 0 — в false, в отличие от C. Это как раз проявление большей строгости.

Актуально на 2026: в современных .NET есть и дополнительные числовые типы — Half (16-битное вещественное, с .NET 5), Int128/UInt128 (с .NET 7) и платформозависимые nint/nuint. В таблицу выше я их не включил как неклассические для базового курса.

Value и reference: где хранится значение

В C# все типы делятся на две категории (это ключевое отличие от C, где такого деления в системе типов нет).

  • Тип значения (value type) хранит данные непосредственно. К ним относятся все числовые типы из таблицы, bool, char, а также struct и enum.
  • Ссылочный тип (reference type) хранит ссылку на объект. Это class, string, массивы, делегаты.

Упрощенная модель «типы значений в стеке, ссылочные в куче» удобна для старта, но это именно упрощение. Точнее так: сам объект ссылочного типа всегда живет в управляемой куче, а переменная-ссылка хранит адрес. Значение value-типа лежит там, где объявлено: локальная переменная — обычно в стеке, но поле value-типа внутри объекта класса лежит в куче вместе с этим объектом. Так что «стек против кучи» зависит от того, где объявлена переменная, а не только от категории типа.

Приведение типов

В C преобразования во многом неявные: int спокойно повышается до double в выражении. Опасны сужающие преобразования — вот тройка «неверно — результат — как правильно».

int over = 300;
char c = over;          /* сужение int -> char: данные не влезают */
printf("%d\n", c);

Результат на платформе, где char знаковый (macOS/clang):

44

Число 300 не помещается в диапазон char, значение усекается по модулю — получается не 300. Если значение действительно должно быть символом-байтом, используйте подходящий тип и контролируйте диапазон явно:

if (over >= 0 && over <= 255) {
    unsigned char c = (unsigned char) over;   /* явно и осознанно */
}

В C# сужающие преобразования, где возможна потеря данных, требуют явного приведения — компилятор не даст присвоить double в int молча. Расширяющие (например, int в long) выполняются неявно. Переполнение при явном приведении по умолчанию не проверяется; чтобы получить исключение вместо тихого усечения, оборачивают код в блок checked.

int i = 1_000_000;
long l = i;                 // неявно, безопасно
double d = 3.99;
int back = (int) d;         // явное приведение, дробная часть отбрасывается: 3

int a = int.MaxValue;
checked {
    // int sum = a + 1;     // здесь бросит OverflowException, а не даст мусор
}

Литералы задают тип значения: суффикс L — для long, U — для беззнаковых (UL для ulong), f/F — для float, d/D — для double, m/M — для decimal. Без суффикса дробный литерал считается double, поэтому float x = 3.14; не скомпилируется — нужно 3.14f.

Выводы

  • И C, и C# статически типизированы, но C# строже: меньше неявных преобразований, bool и целые не смешиваются.
  • В C размеры типов платформозависимы (стандарт задает лишь минимумы) — проверяйте через sizeof; в C# размеры фиксированы спецификацией.
  • В C# типы делятся на value и reference; правило «стек/куча» — упрощение, важнее, где объявлена переменная.
  • char в C# — это кодовая единица UTF-16, а не всегда целый символ; decimal точен в пределах своих 28-29 разрядов.
  • Сужающие преобразования опасны: в C они часто неявные и молча усекают, в C# требуют явного приведения, а checked ловит переполнение.

Где применяется / связь с практикой

Понимание системы типов — фундамент любого кода на C и C#: от выбора int или long под счетчик до decimal под деньги вместо double. Ошибка на этом уровне тихо портит данные (переполнение, потеря точности, неверное сужение) и находится уже в проде. Поэтому типы разбирают в самом начале обучения.

Освойте тему на практике

Если хотите пройти это на практике — с задачами и ревью, — посмотрите курс C# Basic. Перед стартом полезно сходить на открытые уроки Otus: там разбирают темы вживую и можно оценить формат.

FAQ

Почему float x = 3.14; в C# не компилируется?
Дробный литерал без суффикса считается double, а его нельзя неявно сузить до float. Нужно написать 3.14f.

Чем decimal лучше double для денег?
decimal хранит десятичную мантиссу и не накапливает двоичную ошибку округления в пределах 28-29 значащих цифр, тогда как double не может точно представить даже 0.1 в двоичном виде.

Почему один эмодзи в C# может занимать два char?
char — это 16-битная кодовая единица UTF-16. Символы вне базовой плоскости кодируются суррогатной парой из двух кодовых единиц, поэтому в строке они занимают два char.

OTUS Журнал