Меню

Главная
Случайная статья
Настройки
Си (язык программирования)
Материал из https://ru.wikipedia.org

Си (от лат. буквы C англ. языка) — компилируемый статически типизированный язык программирования общего назначения, разработанный в 1969—1973 годах сотрудником Bell Labs Деннисом Ритчи как развитие языка Би. Первоначально был разработан для реализации операционной системы UNIX, но впоследствии был перенесён на множество других платформ. Согласно дизайну языка, его конструкции близко сопоставляются типичным машинным инструкциям, благодаря чему он нашёл применение в проектах, для которых был свойственен язык ассемблера, в том числе как в операционных системах, так и в различном прикладном программном обеспечении для множества устройств — от суперкомпьютеров до встраиваемых систем. Язык программирования Си оказал существенное влияние на развитие индустрии программного обеспечения, а его синтаксис стал основой для таких языков программирования, как C++, C#, Java и Objective-C.

Содержание

История

Язык программирования Си разрабатывался в период с 1969 по 1973 годы в лабораториях Bell Labs, и к 1973 году на этот язык была переписана большая часть ядра UNIX, первоначально написанного на ассемблере PDP-11/20. Название языка стало логическим продолжением старого языка «Би»[a], многие особенности которого были положены в основу.

По мере развития язык сначала стандартизировали как ANSI C, а затем этот стандарт был принят комитетом по международной стандартизации ISO как ISO C, ставший также известным под названием C90. В стандарте С99 язык получил новые возможности, такие как массивы переменной длины и встраиваемые функции. А в стандарте C11 в язык добавили реализацию потоков и поддержку атомарных типов; поддержка массивов переменной длины стала необязательной. 31 октября 2024 года официально вышел стандарт C23.

Общие сведения

Язык Си разрабатывался как язык системного программирования, для которого можно создать однопроходный компилятор. Стандартная библиотека также невелика. Как следствие данных факторов — компиляторы разрабатываются сравнительно легко[2]. Поэтому данный язык доступен на самых различных платформах. К тому же, несмотря на свою низкоуровневую природу, язык ориентирован на переносимость. Программы, соответствующие стандарту языка, могут компилироваться под различные архитектуры компьютеров.

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

Основные особенности Си:
  • простая языковая база, из которой в стандартную библиотеку вынесены многие существенные возможности, вроде математических функций или функций работы с файлами;
  • ориентация на процедурное программирование;
  • система типов, предохраняющая от бессмысленных операций;
  • использование препроцессора для абстрагирования однотипных операций;
  • доступ к памяти через использование указателей;
  • небольшое число ключевых слов;
  • передача параметров в функцию по значению, а не по ссылке (передача по ссылке эмулируется с помощью указателей);
  • наличие указателей на функции и статические переменные;
  • области видимости имён;
  • структуры и объединения — определяемые пользователем собирательные типы данных, которыми можно манипулировать как одним целым.


В то же время в Си отсутствуют:

Часть отсутствующих возможностей может имитироваться встроенными средствами (например, сопрограммы можно имитировать с помощью функций setjmp и longjmp), часть добавляется с помощью сторонних библиотек (например, для поддержки многозадачности и для сетевых функций можно использовать библиотеки pthreads, sockets и тому подобные; существуют библиотеки для поддержки автоматической сборки мусора[3]), часть реализуется в некоторых компиляторах в виде расширений языка (например, вложенные функции в GCC). Существует несколько громоздкая, но вполне работоспособная методика, позволяющая реализовывать на Си механизмы ООП[4], базирующаяся на фактической полиморфности указателей в Си и поддержке в этом языке указателей на функции. Механизмы ООП, основанные на данной модели, реализованы в библиотеке GLib и активно используются во фреймворке GTK+. GLib предоставляет базовый класс GObject, возможности наследования от одного класса[5] и реализации множества интерфейсов[6].

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

Синтаксис и семантика

Синтаксис языка Си достаточно сложный, а семантика неоднозначная[7]. Основными двумя особенностями языка на момент его появления были унифицирование работы с массивами и указателями, а также схожесть того, как что-либо объявляется, с тем, как это в дальнейшем используется в выражениях[8]. Однако в последующем эти две особенности языка были в числе наиболее критикуемых[8], и обе являются сложными для понимания среди начинающих программистов[9]. Стандарт языка, определяя его семантику, не стал слишком сильно ограничивать реализации языка компиляторами, но этим самым сделал семантику недостаточно определённой. В частности, в стандарте есть 3 типа недостаточно определённой семантики: определяемое реализацией поведение, не заданное стандартом поведение и неопределённое поведение[10].

Типы данных

Примитивные типы

Размер целочисленных типов данных варьируется от не менее 8 до не менее 32 бит. Стандарт C99 увеличивает максимальный размер целого числа — не менее 64 бит. Целочисленные типы данных используются для хранения целых чисел (тип char также используется для хранения ASCII-символов). Все размеры диапазонов представленных ниже типов данных минимальны и на отдельно взятой платформе могут быть больше[11].

Как следствие минимальных размеров типов стандарт требует, чтобы для размеров целочисленных типов выполнялось условие:

1 = sizeof(char) sizeof(short) sizeof(int) sizeof(long) sizeof(long long).

Таким образом, размеры некоторых типов по количеству байт могут совпадать, если будет удовлетворяться условие по минимальному количеству бит. Даже char и long могут иметь одинаковый размер, если один байт будет занимать 32 бита или более, но такие платформы будут очень редки или не будут существовать. Стандарт гарантирует, что тип char всегда равен 1 байту. Размер байта в битах определяется константой CHAR_BIT из заголовочного файла limits.h, у POSIX-совместимых систем равен 8 битам[12].

Минимальный диапазон значений целых типов по стандарту определяется с -(2N-1-1) по 2N-1-1 для знаковых типов и с 0 по 2N-1 — для беззнаковых, где N — разрядность типа. Реализация компиляторов может расширять этот диапазон по своему усмотрению. На практике для знаковых типов чаще используется диапазон с -2N-1 по 2N-1-1. Минимальное и максимальное значения каждого типа указывается в файле limits.h в виде макроопределений.

Отдельное внимание стоит уделить типу char. Формально это отдельный тип, но фактически char эквивалентен либо signed char, либо unsigned char, в зависимости от компилятора[13].

Для того, чтобы избежать путаницы между размерами типов стандарт C99 ввел новые типы данных, описанные в файле stdint.h. Среди них такие типы как: intN_t, int_leastN_t, int_fastN_t, где N = 8, 16, 32 или 64. Приставка least- обозначает минимальный тип, способный вместить N бит, приставка fast- обозначает тип размером не менее 16 бит, работа с которым наиболее быстрая на данной платформе. Типы без приставок обозначают типы с фиксированном размером, равным N бит.

Типы с приставками least- и fast- можно считать заменой типам int, short, long, с той лишь разницей, что первые дают программисту выбрать между скоростью и размером.
Основные типы данных для хранения целых чисел
Тип данных Размер Минимальный диапазон значений Стандарт
signed char минимум 8 бит от 127[14] (= -(271)) до 127 C90[b]
int_least8_t C99
int_fast8_t
unsigned char минимум 8 бит от 0 до 255 (=281) C90[b]
uint_least8_t C99
uint_fast8_t
char минимум 8 бит от 127 до 127 или от 0 до 255 в зависимости от компилятора C90[b]
short int минимум 16 бит от 32,767 (= -(2151)) до 32,767 C90[b]
int
int_least16_t C99
int_fast16_t
unsigned short int минимум 16 бит от 0 до 65,535 (= 2161) C90[b]
unsigned int
uint_least16_t C99
uint_fast16_t
long int минимум 32 бита от 2,147,483,647 до 2,147,483,647 C90[b]
int_least32_t C99
int_fast32_t
unsigned long int минимум 32 бита от 0 до 4,294,967,295 (= 2321) C90[b]
uint_least32_t C99
uint_fast32_t
long long int минимум 64 бита от 9,223,372,036,854,775,807 до 9,223,372,036,854,775,807 C99
int_least64_t
int_fast64_t
unsigned long long int минимум 64 бита от 0 до 18,446,744,073,709,551,615 (= 2641)
uint_least64_t
uint_fast64_t
int8_t 8 бит от 127 до 127
uint8_t 8 бит от 0 до 255 (=281)
int16_t 16 бит от 32,767 до 32,767
uint16_t 16 бит от 0 до 65,535 (= 2161)
int32_t 32 бита от 2,147,483,647 до 2,147,483,647
uint32_t 32 бита от 0 до 4,294,967,295 (= 2321)
int64_t 64 бита от 9,223,372,036,854,775,807 до 9,223,372,036,854,775,807
uint64_t 64 бита от 0 до 18,446,744,073,709,551,615 (= 2641)
В таблице приведён минимальный диапазон значений согласно стандарту языка. Компиляторы языка Си могут расширять диапазон значений.


Также со стандарта C99 добавлены типы intmax_t и uintmax_t, соответствующие самым большим знаковому и беззнаковому типам соответственно. Данные типы удобны при использовании в макросах для хранения промежуточных или временных значений при операциях над целочисленными аргументами, так как позволяют уместить значения любого типа. Например, эти типы используются в макросах сравнения целочисленных значений библиотеки модульного тестирования Check для языка Си[15].

В Си существует несколько дополнительных целочисленных типов для безопасной работы с типом данных указателей: intptr_t, uintptr_t и ptrdiff_t. Типы intptr_t и uintptr_t из стандарта C99 предназначены для хранения соответственно знакового и беззнакового значений, которые по размеру могут уместить в себе указатель. Эти типы часто применяются для хранения произвольного целого числа в указателе, например, как способ избавиться от лишнего выделения памяти при регистрации функций обратной связи[16] либо при использовании сторонних связных списков, ассоциативных массивов и прочих структур, в которых данные хранятся по указателю. Тип ptrdiff_t из заголовочного файла stddef.h предназначен для безопасного хранения разности двух указателей.

Для хранения размера предусмотрен беззнаковый тип size_t из заголовочного файла stddef.h. Данный тип способен уместить максимально возможное количество байт, доступное по указателю, и обычно используется для хранения размера в байтах. Значение именно этого типа возвращает оператор sizeof[17].

Преобразования целочисленных типов могут происходить как явно, с помощью оператора приведения типов, так и неявно. Значения типов, меньших по размеру, чем int, при участии в каких-либо операциях или при передаче в вызов функции автоматически приводятся к типу int, а в случае невозможности преобразования — к типу unsigned int. Зачастую подобные неявные приведения необходимы, чтобы результат вычисления оказался правильным, но иногда приводят к интуитивно-непонятным ошибкам в вычислениях. Например, если в операции участвуют числа типа int и unsigned int, а знаковое значение отрицательно, то преобразование отрицательного числа к беззнаковому типу приведёт к переполнению и возникновению очень большого положительного значения, что может привести к неверному результату операций сравнения[18].
Сравнение правильного и ошибочного автоматического приведения типов
Знаковый и беззнаковый типы меньше, чем int Знаковый меньше беззнакового, а беззнаковый не менее int
#include <stdio.h>

signed char x = -1;
unsigned char y = 0;
if (x > y) { // условие ложно
    printf("Сообщение не будет показано.\n");
}
if (x == UCHAR_MAX) {
    // условие ложно
    printf("Сообщение не будет показано.\n");
}
#include <stdio.h>

signed char x = -1;
unsigned int y = 0;
if (x > y) { // условие истинно
    printf("Переполнение в переменной x.\n");
}
if ((x == UINT_MAX) && (x == ULONG_MAX)) {
    // условие всегда будет истинным
    printf("Переполнение в переменной x.\n");
}
В данном примере оба типа, знаковый и беззнаковый, будут приведены к знаковому типу int, поскольку он позволяет уместить диапазоны обоих типов. Поэтому сравнение в условном операторе будет корректным. Знаковый тип будет приведён к беззнаковому, поскольку беззнаковый больше или равен по размеру типу int, но произойдёт переполнение, поскольку в беззнаковом типе невозможно представить отрицательное значение.


Также автоматическое приведение типов сработает, если в выражении используется два или более разных целочисленных типа. Стандарт определяет ряд правил, согласно которым выбирается такое преобразование типов, которое может дать правильный результат вычислений. Разным типам назначены разные ранги в рамках преобразования, а сами ранги основаны на размере типа. При участии в выражении разных типов обычно выбирается приведение этих значений к типу большего ранга[18].

Числа с плавающей запятой в языке Си представлены тремя основными типами: float, double и long double.

Вещественные числа имеют представление, сильно отличающее их от целых. Константы вещественных чисел разных типов, записанные в десятичном представлении, могут быть не равны друг другу. Например, условие 0.1 == 0.1f будет ложным из-за потери точности у типа float, в то время как условие 0.5 == 0.5f будет истинным, поскольку эти числа конечны в двоичном представлении. Однако условие с приведением (float) 0.1 == 0.1f также будет истинным, поскольку при приведении типа к менее точному теряются разряды, из-за которых в этих двух константах есть различия.

Арифметические операции с вещественными числами также являются неточными и зачастую имеют некоторую плавающую погрешность[19]. Наибольшая погрешность будет возникать при операциях над значениями, близкими к минимально возможному для конкретного типа. Также погрешность может оказаться большой при вычислениях над одновременно очень маленькими ( 1) и очень большими по модулю числами ( 1). В ряде случаев погрешность может быть снижена изменением алгоритмов и методик вычислений. Например, при замене многократного сложения умножением погрешность может снизиться во столько раз, сколько изначально было операций сложения.

Также в заголовочном файле math.h присутствуют два дополнительных типа float_t и double_t, которые соответствуют как минимум типам float и double соответственно, но могут быть отличными от них. Типы float_t и double_t добавлены в стандарте C99, а их соответствие основным типам определяется значением макроса FLT_EVAL_METHOD.
Вещественные типы данных
Тип данных Размер Стандарт
float 32 бита IEC 60559 (IEEE 754), расширение F стандарта Си[20][c], число одинарной точности
double 64 бита IEC 60559 (IEEE 754), расширение F стандарта Си[20][c], число двойной точности
long double минимум 64 бита зависит от реализации
float_t (C99) минимум 32 бита зависит от базового типа
double_t (C99) минимум 64 бита зависит от базового типа
Соответствие дополнительных типов базовым[21]
FLT_EVAL_METHOD float_t double_t
1 float double
2 double double
3 long double long double


Строки

Хотя как такового специального типа для строк в Си не предусмотрено, в языке активно используются нуль-терминированные строки. ASCII-строки объявляются как массив типа char, последним элементом которого должен быть символ с кодом 0 ('\0'). В этом же формате принято хранить и строки в формате UTF-8. Однако все функции, работающие с ASCII-строками, рассматривают каждый символ как байт, что ограничивает применение стандартных функций при использовании данной кодировки.

Несмотря на широкое распространение идеи нуль-терминированных строк и удобство их использования в некоторых алгоритмах, у них есть несколько серьёзных недостатков.
  1. Необходимость добавления в конец строки терминального символа не даёт возможность получить подстроку без необходимости её копирования, а функций для работы с указателем на подстроку и её длиной в языке не предусмотрено.
  2. Если требуется заранее выделять память под результат алгоритма на основе входных данных, каждый раз требуется обходить всю строку для подсчёта её длины.
  3. При работе с большими объёмами текста подсчёт длины может оказаться узким местом.
  4. Работа со строкой, которая по ошибке не терминирована нулём, может приводить к неопределённому поведению программы, в том числе к ошибкам сегментирования, ошибкам переполнения буфера и к уязвимостям.


В современных условиях, когда производительность кода приоритетнее расхода памяти, может оказаться эффективнее и проще использовать структуры, содержащие в себе как саму строку, так и её размер[22]
struct string_t {
    char *str; // указатель на строку
    size_t str_size; // размер строки
};
typedef struct string_t string_t; // альтернативное имя для упрощения кода


Альтернативным вариантом хранения размера строки с низким потреблением памяти может оказаться подход добавления в начало строки её размера в формате размера переменной длины[англ.]. Подобный подход применяется в протокольных буферах, однако только на этапе передачи данных, но не их хранения.

Строковые литералы в Си по своей сути являются константами[23]. При объявлении заключаются в двойные кавычки, а терминирующий 0 добавляются компилятором автоматически. Допускается два способа присваивания строкового литерала: по указателю и по значению. При присваивании по указателю в переменную типа char * заносится указатель на неизменяемую строку, то есть формируется константная строка. Если же заносить строковый литерал в массив, то происходит копирование строки в область стека.
#include <stdio.h>
#include <string.h>

int main(void)
{
    const char *s1 = "Константная строка";
    char s2[] = "Строка, которую можно менять";
    memcpy(s2, "с", strlen("с")); // замена первой буквы на маленькую
    puts(s2); // выведется текст строки
    memcpy((char *) s1, "к", strlen("к")); // ошибка сегментирования
    puts(s1); // строка не будет исполнена
}


Поскольку строки являются обычными массивами символов, вместо литералов можно использовать инициализаторы, если каждый символ умещается в 1 байт:
char s[] = {'I', 'n', 'i', 't', 'i', 'a', 'l', 'i', 'z', 'e', 'r', '\0'};


Однако на практике такой подход имеет смысл только в крайне редких случаях, когда к ASCII-строке требуется не добавлять терминирующий ноль.
Кодировка типа wchar_t в зависимости от платформы
Платформа Кодировка
GNU/Linux USC-4[24]
macOS
Windows USC-2[25]
AIX
FreeBSD Зависит от локали,

не документировано[25]

Solaris


Альтернативой обычным строкам могут служить широкие строки, в которых каждый символ хранится в специальном типе wchar_t. Данный тип по стандарту должен быть способен уместить в себе все символы самой большой из существующих локалей. Функции для работы с широкими строками описаны в заголовочном файле wchar.h, а функции для работы с широкими символами описаны в заголовочном файле wctype.h.

При объявлении строковых литералов для широких строк используется модификатор L:
const wchar_t *wide_str = L"Широкая строка";


В форматированном выводе используется спецификатор %ls, однако спецификатор размера, если задан, указывается в байтах, а не в символах[26].

Тип wchar_t задумывался для того, чтобы в него мог поместиться любой символ, а широкие строки — для хранения строк любой локали, но в результате API оказался неудобным, а реализации — платформозависимыми. Так, на платформе Windows в качестве размера типа wchar_t было выбрано 16 бит, а позже появился стандарт UTF-32, таким образом тип wchar_t на платформе Windows уже не способен уместить в себе все символы из кодировки UTF-32, в результате чего теряется смысл данного типа[25]. В то же время на платформах Linux[24] и macOS данный тип занимает 32 бита, поэтому для реализации кроссплатформенных задач тип wchar_t не подходит.

Существует много разных кодировок, в которых отдельный символ может быть запрограммирован разным количеством байт. Такие кодировки называются многобайтовыми. К ним относится также и UTF-8. В Си существует набор функций для преобразования строк из многобайтовых в рамках текущей локали в широкие и наоборот. Функции для работы с многобайтовыми символами имеют префикс либо суффикс mb и описаны в заголовочном файле stdlib.h. Для поддержки многобайтовых строк в программах на языке Си, такие строки должны поддерживаться на уровне текущей локали. Для явного задания кодировки можно менять текущую локаль с помощью функции setlocale() из заголовочного файла locale.h. Однако задание кодировки для локали должно поддерживаться используемой стандартной библиотекой. Так, например, стандартная библиотека Glibc полностью поддерживает кодировку UTF-8 и способна преобразовывать текст во множество других кодировок[27].

Начиная со стандарта C11 язык поддерживает также 16-битные и 32-битные широкие многобайтовые строки с соответствующими типами символа char16_t и char32_t из заголовочного файла uchar.h, а также объявление строковых литералов в формате UTF-8 с помощью модификатора u8. 16-битные и 32-битные строки могут использоваться для хранения кодировок UTF-16 и UTF-32, если в заголовочном файле uchar.h заданы макроопределения __STDC_UTF_16__ и __STDC_UTF_32__, соответственно. Для задания строковых литералов в данных форматах используются модификаторы: u для 16-битных строк и U для 32-битных строк. Примеры объявления строковых литералов для многобайтовых строк:
const char *s8 = u8"Многобайтовая строка в кодировке UTF-8";
const char16_t *s16 = u"16-битная многобайтовая строка";
const char32_t *s32 = U"32-битная многобайтовая строка";


Следует иметь в виду, что функция c16rtomb() для преобразования из 16-битной строки в многобайтовую работает не так, как задумывалось, и в стандарте C11 оказалась неспособной переводить из UTF-16 в UTF-8[28]. Исправление работы данной функции может зависеть от конкретной реализации компилятора.

Пользовательские типы

Перечисления представляют собой набор именованных целочисленных констант и обозначаются с помощью ключевого слова enum. Если константе не сопоставлено число, то ей автоматически задаётся либо 0 для первой константы в списке, либо число на единицу большее, чем задано в предыдущей константе. При этом сам тип данных перечисления по факту может соответствовать любому знаковому или беззнаковому примитивному типу, в диапазон которого умещаются все значения перечислений; решение о выборе того или иного типа принимает компилятор. Однако явно заданные значения для констант должны быть выражениями типа int[29].

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

На практике перечисления часто используются для обозначения состояний конечных автоматов, для задания вариантов режимов работы или значений параметров[30], для создания целочисленных констант, а также для перечисления каких-либо уникальных объектов или свойств[31].

Структуры представляют собой объединение переменных разных типов данных в рамках одной области памяти; обозначаются ключевым словом struct. Переменные внутри структуры называются полями структуры. С точки зрения адресного пространства поля всегда идут друг за другом в том же порядке, в котором указаны, но компиляторы могут выравнивать адреса полей для оптимизации под ту или иную архитектуру. Таким образом, фактически поле может занимать больший размер, чем указано в программе.

Каждое поле имеет определённое смещение относительно адреса структуры и размер. Смещение можно получить с помощью макроса offsetof() из заголовочного файла stddef.h. При этом смещение будет зависеть от выравнивания и размера предыдущих полей. Размер поля обычно определяется выравниванием структуры: если размер выравнивания типа данных поля меньше значения выравнивания структуры, то размер поля определяется выравниванием структуры. Выравнивание типов данных можно получить с помощью макроса alignof()[d] из заголовочного файла stdalign.h. Размер самой структуры является совокупным размером всех её полей с учётом выравнивания. При этом некоторые компиляторы предоставляют специальные атрибуты, позволяющие упаковывать структуры, убирая из них выравнивания[32].

Полям структур можно явно задавать размер в битах через двоеточие после определения поля и указание количества бит, что ограничивает диапазон их возможных значений, несмотря на тип поля. Подобный подход может использоваться как альтернатива флагам и битовым маскам для обращения к ним. Однако указание количества бит не отменяет возможного выравнивания полей структур в памяти. Работа с битовыми полями имеет ряд ограничений: к ним невозможно применить оператор sizeof или макрос alignof(), на них невозможно получить указатель.

Объединения необходимы в тех случаях, когда требуется обращаться к одной и той же переменной как к разным типам данных; обозначаются ключевым словом union. Внутри объединения может быть объявлено произвольное количество пересекающихся полей, которые по факту предоставляют доступ к одной и той же области памяти как к разным типам данных. Размер объединения выбирается компилятором исходя из размера самого большого поля в объединении. Следует иметь в виду, что изменение одного поля объединения приводит к изменению и всех других полей, но гарантированно правильным будет только значение того поля, которое менялось.

Объединения могут служить более удобной альтернативной приведению указателя к произвольному типу. К примеру, с помощью объединения, помещённого в структуру, можно создавать объекты с динамически меняющимся типом данных:
#include <stddef.h>

enum value_type_t {
    VALUE_TYPE_LONG, // целое число
    VALUE_TYPE_DOUBLE, // вещественное число
    VALUE_TYPE_STRING, // строка
    VALUE_TYPE_BINARY, // произвольные данные
};

struct binary_t {
    void *data; // указатель на данные
    size_t data_size; // размер данных
};

struct string_t {
    char *str; // указатель на строку
    size_t str_size; // размер строки
};

union value_contents_t {
    long as_long; // значение как целое число
    double as_double; // значение как вещественное число
    struct string_t as_string; // значение как строка
    struct binary_t as_binary; // значение как произвольные данные
};

struct value_t {
    enum value_type_t type; // тип значения
    union value_contents_t contents; // содержимое значения
};


Массивы в языке Си примитивны и являются лишь синтаксическим абстрагированием над арифметикой указателей. Сам по себе массив является указателем на область памяти, поэтому вся информация о размерности массива и его границах может быть доступна только на этапе компиляции согласно описанию типа. Массивы могут быть как одномерными, так и многомерными, но обращение к элементу массива сводится к простому вычислению смещения относительно адреса начала массива. Поскольку массивы основаны на адресной арифметике, с ними возможно работать без использования индексов[33]. Так, например, следующие два примера считывания с потока ввода 10 чисел идентичны друг другу:
Downgrade Counter