Система типов Си

Эта статья находится на начальном уровне проработки, в одной из её версий выборочно используется текст из источника, распространяемого под свободной лицензией
Материал из энциклопедии Руниверсалис

Система типов Си — реализация понятия типа данных в языке программирования Си. Сам язык предоставляет базовые арифметические типы, а также синтаксис для создания массивов и составных типов. Некоторые заголовочные файлы из стандартной библиотеки Си содержат определения типов с дополнительными свойствами[1][2].

Базовые типы

Язык Си предоставляет множество базовых типов. Большинство из них формируется с помощью одного из четырёх арифметических спецификаторов типа, (char, int, float и double), и опциональных спецификаторов (signed, unsigned, short и long). Хотя стандартом установлен диапазон, вычисляемый по формуле от −(2n−1−1) до 2n−1−1, все известные компиляторы (gcc, clang и Microsoft compiler) допускают диапазон от −(2n−1) до 2n−1−1, где n — разрядность типа.

В таблице ниже предполагается, что 1 байт = 8 битам.

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

Тип Пояснение Спецификатор формата
char Целочисленный, самый маленький из возможных адресуемых типов. Может содержать базовый набор символов. Может быть как знаковым, так и беззнаковым, зависит от реализации. Содержит CHAR_BIT (как правило, 8) бит.[3] %c
signed char Того же размера что и char, но гарантированно будет со знаком. Может принимать значения как минимум из диапазона [−127, +127][3], обычно в реализациях [−128, +127][4] %c (также %d или %hhi (%hhx, %hho) для вывода в числовой форме)
unsigned char Того же размера что и char, но гарантированно без знака. Диапазон: [0, 2CHAR_BIT − 1][3]. Как правило, [0, 255] %c (или %hhu для вывода в числовой форме)
short
short int
signed short
signed short int
Тип короткого целого числа со знаком. Может содержать числа как минимум из диапазона [−32767, +32767][3], обычно в реализациях [−32768, +32767][4]. Таким образом, это по крайней мере 16 бит (2 байта). %hi
unsigned short
unsigned short int
Такой же, как short, но беззнаковый. Диапазон: [0, +65535] %hu
int
signed
signed int
Основной тип целого числа со знаком. Может содержать числа как минимум из диапазона [−32767, +32767][3]. Таким образом, это по крайней мере 16 бит (2 байта). Как правило, в современных компиляторах для 32- и более -разрядных платформ имеет размер 4 байта и диапазон [−2 147 483 648, +2 147 483 647], однако на 16- и 8-битных платформах имеет размер, как правило, 2 байта в диапазоне значений [−32768, +32767], что часто вызывает путаницу и приводит к несовместимости неаккуратно написанного кода %i или %d
unsigned
unsigned int
Такой же как int, но беззнаковый. Диапазон: [0, +4 294 967 295] %u
long
long int
signed long
signed long int
Тип длинного целого числа со знаком. Может содержать числа, как минимум, в диапазоне [−2 147 483 647, +2 147 483 647].[3][4][5]Таким образом, это по крайней мере 32 бита (4 байта). %li или %ld
unsigned long
unsigned long int
Такой же как long, но беззнаковый. Диапазон: [0, +4 294 967 295] %lu
long long
long long int
signed long long
signed long long int
Тип длинного длинного (двойного длинного) целого числа со знаком. Может содержать числа как минимум в диапазоне
[−9 223 372 036 854 775 808, +9 223 372 036 854 775 807].[3][4] Таким образом, это по крайней мере 64 бита. Утверждён в стандарте C99.
%lli или %lld
unsigned long long
unsigned long long int
Похож на long long, но беззнаковый. Диапазон : [0, 18 446 744 073 709 551 615]. %llu
float Тип вещественного числа с плавающей запятой, обычно называемый типом числа одинарной точности с плавающей запятой. Подробные свойства в стандарте не указаны (за исключением минимальных пределов), однако на большинстве систем это IEEE 754 бинарный формат с плавающей запятой одинарной точности. Этот формат требуется для опциональной арифметики с плавающей запятой Annex F «IEC 60559 floating-point arithmetic». %f (автоматически преобразуется в double для printf())
double Тип вещественного числа с плавающей запятой, обычно называемый типом числа двойной точности с плавающей запятой. На большинстве систем соответствует IEEE 754 бинарный формат с плавающей запятой двойной точности. %f (%F)

(%lf (%lF) для scanf())
%g %G
%e %E (для научной нотации)[6]

long double Тип вещественного числа с плавающей запятой, обычно ставящийся в соответствие к формату числа повышенной точности[en] с плавающей запятой. В отличие от float и double, может быть 80-битным форматом с плавающей запятой, не-IEEE «double-double» или «IEEE 754 бинарный формат с плавающей запятой четырёхкратной точности». Если более точного формата не предоставлено, эквивалентен double. Смотрите the article on long double для подробностей. %Lf %LF
%Lg %LG
%Le %LE[6]

Также не были упомянуты следующие спецификаторы типов: (%s для строк, %p для указателей, %x (%X) для шестнадцатеричного представления, %o для восьмеричного.

Реальный размер целочисленных типов зависит от реализации. Стандарт лишь оговаривает отношения в размерах между типами и минимальные рамки для каждого типа:

Так long long не должен быть меньше long, который в свою очередь не должен быть меньше int, который в свою очередь не должен быть меньше short. Так как char — наименьший из возможных адресуемых типов, другие типы не могут иметь размер меньше него.

Минимальный размер для char — 8 бит, для short и int — 16 бит, для long — 32 бита, для long long — 64 бита.

Желательно, чтобы тип int был таким целочисленным типом, с которым наиболее эффективно работает процессор. Это позволяет достигать высокой гибкости, например, все типы могут занимать 64 бита. Однако, есть популярные схемы, описывающие размеры целочисленных типов.[7]

На практике это означает, что char занимает 8 бит, а short 16 бит (также, как и их беззнаковые аналоги). int на большинстве современных платформ занимает 32 бита, а long long 64 бита. Длина long варьируется: для Windows это 32 бита, для UNIX-подобных систем — 64 бита.

Стандарт C99 включает новые вещественные типы: float_t и double_t, определённые в <math.h>. Также он включает комплексные типы: float _Complex, double _Complex, long double _Complex.

Логический тип

В C99 был добавлен логический тип _Bool. Также, дополнительный заголовочный файл <stdbool.h> определяет для него псевдоним bool, а также макросы true (истина) и false (ложь). _Bool ведёт себя так же, как обычный встроенный тип, за одним исключением: любое ненулевое (не ложное) присваивание _Bool хранится как единица. Такое поведение защищает от переполнения. Например:

unsigned char b = 256;

if (b) {
   /* do something */
}

b считается ложным, если unsigned char занимает 8 бит. Однако, смена типа делает переменную истинной:

_Bool b = 256;

if (b) {
   /* do something */
}

Типы размера и отступа указателя

Спецификация языка C включает обозначения типов (typedef) size_t и ptrdiff_t. Их размер определяется относительно арифметических возможностей процессора. Оба этих типа определены в <stddef.h> (cstddef для C++).

size_t — беззнаковый целый тип, предназначенный для представления размера любого объекта в памяти (включая массивы) в конкретной реализации. Оператор sizeof возвращает значение типа size_t. Максимальный размер size_t записан в макроконстанте SIZE_MAX, определённой в <stdint.h> (cstdint для C++). size_t должен быть, как минимум, 16 бит. К тому же POSIX включает ssize_t, который является встроенным знаковым типом, по размеру равным size_t.

ptrdiff_t — это встроенный знаковый тип, который определяет разность между указателями. Гарантируется, что он будет действовать с указателями одного и того же типа. Арифметика между указателями разных типов зависит от реализации.

Интерфейс к свойствам базовых типов

Информация о фактических свойствах, таких как размер, основных встроенных типов предоставлена через макро-константы в двух заголовках: заголовок <limits.h> (climits в C++) определяет макросы для целочисленных типов, заголовок <float.h> (cfloat в C++) определяет макросы для вещественных типов. Конкретные значения зависят от реализации.

Свойства целочисленных типов
  • CHAR_BIT — размер char в битах (минимум 8 бит)
  • SCHAR_MIN, SHRT_MIN, INT_MIN, LONG_MIN, LLONG_MIN(C99) — минимальные возможные значения знаковых целых типов: signed char, signed short, signed int, signed long, signed long long
  • SCHAR_MAX, SHRT_MAX, INT_MAX, LONG_MAX, LLONG_MAX(C99) — максимальные возможные значения знаковых целых типов: signed char, signed short, signed int, signed long, signed long long
  • UCHAR_MAX, USHRT_MAX, UINT_MAX, ULONG_MAX, ULLONG_MAX(C99) — максимальные возможные значения беззнаковых целых типов: unsigned char, unsigned short, unsigned int, unsigned long, unsigned long long
  • CHAR_MIN — минимальное возможное значение char
  • CHAR_MAX — максимальное возможное значение char
  • MB_LEN_MAX — максимальное число байт в многобайтовых символьных типах.
Свойства вещественных типов
  • FLT_MIN, DBL_MIN, LDBL_MIN — минимальное нормализованное положительное значение для float, double, long double соответственно
  • FLT_TRUE_MIN, DBL_TRUE_MIN, LDBL_TRUE_MIN (C11) — минимальное положительное значение дляfloat, double, long double соответственно
  • FLT_MAX, DBL_MAX, LDBL_MAX — максимальное конечное значение для float, double, long double соответственно
  • FLT_ROUNDS — способ округления для целочисленных операций
  • FLT_EVAL_METHOD (C99) — метод оценки выражений, включающих различные типы с плавающей запятой
  • FLT_RADIX — основание экспоненты вещественных типов.
  • FLT_DIG, DBL_DIG, LDBL_DIG — число десятичных цифр, которые могут быть представлены, не теряя точность для float, double, long double соответственно
  • FLT_EPSILON, DBL_EPSILON, LDBL_EPSILON — разница между 1.0 и следующим числом для float, double, long double соответственно
  • FLT_MANT_DIG, DBL_MANT_DIG, LDBL_MANT_DIG — количество цифр в мантиссе для float, double, long double соответственно
  • FLT_MIN_EXP, DBL_MIN_EXP, LDBL_MIN_EXP — минимальное целое отрицательное число, такое что FLT_RADIX, возведённое в степень на единицу меньше нормализованного float, double, long double соответственно
  • FLT_MIN_10_EXP, DBL_MIN_10_EXP, LDBL_MIN_10_EXP — минимальное отрицательное целое число такое, что 10, возведенное что в эту степень — это нормализованное float, double, long double соответственно
  • FLT_MAX_EXP, DBL_MAX_EXP, LDBL_MAX_EXP — максимальное положительное целое число, такое, что FLT_RADIX возведенное в степень на единицу меньше нормализованного числа float, double, long double соответственно
  • FLT_MAX_10_EXP, DBL_MAX_10_EXP, LDBL_MAX_10_EXP — максимальное отрицательное целое число такое, что 10, возведенное что в эту степень — это нормализованное float, double, long double соответственно
  • DECIMAL_DIG (C99) — минимальное количество десятичных цифр такое, что любое число самого большого вещественного типа может быть представлено в десятичном виде с точностью DECIMAL_DIG цифр и переведено обратно в изначальный вещественный тип без изменения значения. DECIMAL_DIG равен хотя бы 10.

Целые типы фиксированной длины

Стандарт C99 включает определения нескольких новых целочисленных типов для повышения переносимости программ.[2] Уже доступные целочисленные базовые типы были сочтены неудовлетворительными, так как их размер зависел от реализации. Новые типы находят широкое применение в встраиваемых системах. Все новые типы определены в заголовочном файле <inttypes.h> (cinttypes в C++) и также доступны в <stdint.h> (cstdint в C++). Типы можно разделить на следующие категории:

  • Целые с точно заданным размером N бит в любой реализации. Включаются, только если доступны в реализации/платформе.
  • Наименьшие целые, размер которых является минимальным в реализации, состоят минимум из N бит. Гарантируется что определены типы для N=8,16,32,64.
  • Наибыстрейшие целые типы, которые являются гарантировано наиболее быстрыми в конкретной реализации, состоят минимум из N бит. Гарантируется что определены типы для N=8,16,32,64.
  • Целые типы для указателей, которые гарантировано смогут хранить адрес в памяти. Включены, только если доступны на конкретной платформе.
  • Наибольшие целые, размер которых является максимальным в реализации.

Следующая таблица показывает эти типы (N означает число бит):

Категория типа Знаковые типы Беззнаковые типы
Тип Минимальное значение Максимальное значение Тип Минимальное значение Максимальное значение
Точный размер intN_t INTN_MIN INTN_MAX uintN_t 0 UINTN_MAX
Минимальный размер int_leastN_t INT_LEASTN_MIN INT_LEASTN_MAX uint_leastN_t 0 UINT_LEASTN_MAX
Наибыстрый int_fastN_t INT_FASTN_MIN INT_FASTN_MAX uint_fastN_t 0 UINT_FASTN_MAX
Указатель intptr_t INTPTR_MIN INTPTR_MAX uintptr_t 0 UINTPTR_MAX
Максимальный размер intmax_t INTMAX_MIN INTMAX_MAX uintmax_t 0 UINTMAX_MAX

Спецификаторы формата для printf и scanf

Заголовочный файл <inttypes.h> (cinttypes в C++) расширяет возможности типов, определённых в <stdint.h>. В них входят макросы, которые определяют спецификаторы типов для строки формата printf и scanf и несколько функций, которые работают с типами intmax_t и uintmax_t. Этот заголовочный файл был добавлен в C99.

Строка формата printf

Макросы определены в формате PRI{fmt}{type}. Здесь {fmt} означает формат вывода и принадлежит d (десятичный), x (шестнадцатиричный), o (восьмеричный), u (беззнаковый) или i (целый). {type} определяет тип аргумента и принадлежит к N, FASTN, LEASTN, PTR либо MAX, где N означает число бит.

Строка формата scanf

Макросы определены в формате SCN{fmt}{type}. Здесь {fmt} означает формат вывода и принадлежит d (десятичный), x (шестнадцатиричный), o (восьмиричный), u (беззнаковый) или i (целый). {type} определяет тип аргумента и принадлежит к N, FASTN, LEASTN, PTR либо MAX, где N означает число бит.

Функции

Структуры

Структуры в Си позволяют хранить несколько полей и в одной переменной. В других языках могут называться записями или кортежами. Например, данная структура хранит в себе имя человека и дату рождения:

struct birthday
{
    char name[20];
    int day;
    int month;
    int year;
};

Объявление структур в теле программы всегда должно начинаться с ключевого struct (необязательно в C++). Доступ к элементам структуры осуществляется с помощью оператора . или ->, если мы работаем с указателем на структуру. Структуры могут содержать указатели на самих себя, что позволяет реализовывать многие структуры данных, основанных на связных списках. Такая возможность может показаться противоречивой, однако все указатели занимают одинаковое число байт, поэтому размер этого поля не изменится от числа полей структуры.

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

Некоторые особенности структур:

  1. Адрес памяти первого поля структуры равен адресу самой структуры
  2. Структуры могут быть инициализированы или приведены к какому-либо значению, с помощью составных литералов
  3. Пользовательские функции могут возвращать структуру, хотя часто не очень эффективны во время выполнения. С C99, структура может оканчиваться массивом переменного размера.

Массивы

Для каждого типа T, кроме void и типов функций, существует тип «массив из N элементов типа T». Массив — это коллекция значений одного типа, хранящихся последовательно в памяти. Массив размера N индексируется целым числом от 0 до N-1. Также возможны массивы, с неизвестным для компилятора размером. В роли размера массива должна выступать константа. Примеры

int cat[10] = {5,7,2};  // массив из 10 элементов, каждый типа int
int bob[];    // массив с неизвестным количеством элементов типа 'int'.

Массивы могут быть инициализированы с помощью списка инициализации, но не могут быть присвоены друг к другу. Массивы передаются в функции, с помощью указателя на первый элемент (имя массива и есть адрес первого элемента). Многомерные массивы являются массивами массивов. Примеры:

int a[10][8];  // массив из 10 элементов, каждый типа 'массив из 8 int элементов'
float f[][32] = {{0},{4,5,6}};

Типы указателей

Для любого типа T существует тип «указатель на T».

Переменные могут быть объявлены как указатели на значения различных типов с помощью символа *. Для того чтобы определить тип переменной как указатель, нужно предварить её имя звёздочкой.

char letterC = 'C';
char *letter = &letterC; //взятие адреса переменной letterC и присваивание в переменную letter
printf("This code is written in %c.", *letter); //"This code is written in C."

Помимо стандартных типов, можно объявлять указатели на структуры и объединения:

struct Point { int x,y; } A;
A.x = 12;
A.y = 34;
struct Point *p = &A;
printf("X: %d, Y: %d", (*p).x, (*p).y); //"X: 12, Y: 34"

Для обращения к полям структуры по указателю существует оператор «стрелочка» ->, синонимичный предыдущей записи: (*p).x — то же самое, что и p->x.

Поскольку указатель — тоже тип переменной, правило «для любого типа T» выполняется и для них: можно объявлять указатели на указатели. К примеру, можно пользоваться int***:

int w = 100;
int *x = &w;
int **y = &x;
int ***z = &y;
printf("w contains %d.", ***z); //"w contains 100."

Существуют также указатели на массивы и на функции. Указатели на массивы имеют следующий синтаксис:

char *pc[10]; // массив из 10 указателей на char
char (*pa)[10]; // указатель на массив из 10 переменных типа char

pc — массив указателей, занимающий 10 * sizeof(char*) байт (на распространённых платформах — обычно 40 или 80 байт), а pa — это один указатель; занимает он обычно 4 или 8 байт, однако позволяет обращаться к массиву, занимающему 10 байт: sizeof(pa) == sizeof(int*), но sizeof(*pa) == 10 * sizeof(char). Указатели на массивы отличаются от указателей на первый элемент арифметикой. Например, если указатели pa указывает на адрес 2000, то указатель pa+1 будет указывать на адрес 2010.

char (*pa)[10];
char array[10] = "Wikipedia";
pa = &array;
printf("An example for %s.\n", *pa); //"An example for Wikipedia."
printf("%c %c %c", (*pa)[1], (*pa)[3], (*pa)[7]); //"i i i"

Объединения

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

union
{
    int i;
    float f;
    struct
    {
        unsigned int u;
        double d;
    } s;
} u;

В примере выше u по размеру равна u.s (размер которой является суммой u.s.u и u.s.d), так как s больше i и f. Чтение из объединения не включает преобразования типов.

Перечисления

Перечисления позволяют определять в коде пользовательские типы. Пример:

enum
{
    red,
    green=3,
    blue
} color;

Перечисления улучшают читабельность кода, однако они не типобезопасны (например, для системы 3 и green одно и то же. В C++ для исправления этого недостатка были введены enum class), так как являются целочисленными. В данном примере значение red равно нулю, а значение blue четырём.

Указатели на функции

Указатели на функции позволяют передавать одни функции в другие и реализуют механизм обратного вызова. Указатели на функции позволяют ссылаться на функции с определённой сигнатурой. Пример создания указателя на функцию abs, принимающую int и возвращающую int с именем my_int_f:

int (*my_int_f)(int) = &abs;
// оператор & необязателен, но вносит ясность, явно показывая что мы передаём адрес

Указатели на функции вызываются по имени, как обычные вызовы функций. Указатели на функции отделены от обычных указателей и указателей на void.

Более сложный пример:

char ret_a(int x)
{
    return 'a'+x;
}

typedef char (*fptr)(int);

fptr another_func(float a)
{
    return &ret_a;
}

Здесь для удобства мы создали псевдоним с именем fptr для указателя на функцию, возвращающую char и принимающую int. Без typedef синтаксис был бы сложнее для восприятия:

char ret_a(int x)
{
    return 'a'+x;
}

char (*func(float a, int b))(int)
{
    char (*fp)(int) = &ret_a;
    return fp;
}

char (*(*superfunc(double a))(float, int))(int)
{
    char (*(*fpp)(float, int))(int)=&func;
    return fpp;
}

Функция func возвращает не char, как может показаться, а указатель на ф-цию, возвращающую char и принимающую int. И принимает float и int.

Квалификаторы типов

Вышеупомянутые типы могут иметь различные квалификаторы типов. По стандарту C11, существует четыре квалификатора типа:

  • const (C89) — означает что данный тип неизменяем после инициализации. (константа)
  • volatile (C89) — означает что значение данной переменной часто подвержено изменениям.
  • restrict (C99) — означает, что данный указатель адресует область памяти, на которую не ссылается никакой другой указатель.
  • _AtomicC11) — означает, что данный тип является атомарным.[8] Также может именоваться atomic, если подключить stdatomic.h.

Также с 99го стандарта был добавлен квалификатор для функций inline, который является подсказкой компилятору, говорящей включить код из тела функции, вместо вызова самой функции.

Одной переменной могут принадлежать несколько квалификаторов. Пример:

const volatile int a = 5;
volatile int const * b = &a; //указатель на const volatile int
int * const c = NULL; // const указатель на int

Классы хранения

Также в Си существует четыре класса хранения:

  • auto — по умолчанию для всех переменных.
  • register — подсказка компилятору хранить переменные в регистрах процессора. Для таких переменных отсутствует операция взятия адреса.
  • static — статические переменные. Имеют область видимости файла.
  • extern — переменные объявленные вне файла.

См. также

Примечания

  1. Barr, Michael Portable Fixed-Width Integers in C (2 декабря 2007). Дата обращения: 8 ноября 2011. Архивировано 9 октября 2010 года.
  2. 2,0 2,1 ISO/IEC 9899:1999 specification (неопр.). — С. 264, § 7.18 Integer types. Архивная копия от 15 августа 2011 на Wayback Machine
  3. 3,0 3,1 3,2 3,3 3,4 3,5 3,6 ISO/IEC 9899:1999 specification, TC3 (неопр.). — С. 22, § 5.2.4.2.1 Sizes of integer types <limits.h>. Архивная копия от 11 января 2018 на Wayback Machine
  4. 4,0 4,1 4,2 4,3 Хотя стандартом установлен диапазон, вычисляемый по формуле от −(2n−1−1) до 2n−1−1, все известные компиляторы (gcc, clang и Microsoft compiler Архивная копия от 12 января 2016 на Wayback Machine) допускают диапазон от −(2n−1) до 2n−1−1, где n — разрядность типа.
  5. c - Зачем нужен тип long когда есть int?. Stack Overflow на русском. Дата обращения: 11 марта 2020. Архивировано 27 февраля 2021 года.
  6. 6,0 6,1 Регистр формата влияет на регистр выводимых данных Например заглавные %E, %F, %G будут выводить нечисловые данные в верхнем регистре: INF, NAN и E (экспонента). То же самое касается %x и %X
  7. 64-Bit Programming Models: Why LP64?. The Open Group. Дата обращения: 9 ноября 2011. Архивировано 25 декабря 2008 года.
  8. C11:The New C Standard Архивная копия от 12 июля 2018 на Wayback Machine, Thomas Plum