Зона кода

А давайте немного попрограммируем...

Составные литералы в C99

C99Полезное

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

Для начала, освежим в памяти понятие "простого" литерала.

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

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

В качестве примеров литералов можно привести 123, 's', -1.56, "Это строка". Литералы имеют типы, соответствующие базовым типам языка C99 (правда, из этого правила соответствия имеется одно исключение). Так, первые два литерала из перечисленных выше, имеют тип int, третий литерал — тип double, а четвёртый литерал является строковым.

В языке C99 отсутствует строковый тип, поэтому строковому литералу не соответствует никакой базовой тип (это то самое исключение, о котором шла речь). Однако в программном коде строковый литерал всегда интерпретируется как адрес первого символа этого литерала, т. е. значение типа char *. Таким образом, везде, где допустимо использовать адреса символов, синтаксически допустимо использовать и строковые литералы.

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

Но в C99 не предусмотрены литералы составных типов, таких, как структуры, объединения и массивы. Отчасти заполнить этот пробел и призваны составные литералы. С моей точки зрения, имеет смысл рассматривать 2 группы составных литералов: структурного типа и массивового типа. Я не встречал таких названий, так что данная терминология — моя собственная. А теперь рассмотрим подробно каждую из групп.

Составные литералы структурного типа

Предположим, что нам нужно написать программу, обрабатывающую точки на плоскости, имеющие целочисленные координаты. Каждая точка будет задаваться переменной структурного типа, имеющей поля x и y типа int, предназначенные для хранения абсциссы и ординаты точки соответственно. Типом точек будет являться структура point, описанная следующим образом:

typedef struct
{
    int x;
    int y;
} point;

Переменную типа point можно инициализировать одновременно с её описанием, если использовать список инициализации, например, следующим образом:

point pnt = {1, -2};

Предположим, теперь, что мы хотим присвоить новые значения полям уже созданной ранее переменной типа point. Синтаксис C99 не позволяет сделать это с помощью списка инициализации, например, следующим образом:

pnt = {-3, 4};    //Ошибка! Код не будет скомпилирован!

Но если мы "применим" к списку инициализации операцию приведения к типу point, то такой код будет вполне корректен:

pnt = (point) {-3, 4};

Синтаксическая конструкция, стоящая в правой части оператора присваивания, — это и есть составной литерал структурного типа. А именно — того типа, который указан в скобках; в данном случае — типа point. В этом и следующем разделах я, говоря "составные литералы", всегда буду иметь в виду составные литералы структурного типа.

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

pnt = (point) {rand(), rand()};

Здесь rand() — это стандартная библиотечная функция, объявленная в заголовочном файле <stdlib.h>, генерирующая псевдослучайные числа.

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

И действительно, составной литерал структурного типа — это, фактически, и есть анонимная (т. е. не имеющая имени) переменная данного типа. Если в каком-либо месте корректной синтаксической конструкции находится переменная определённого структурного типа, то вместо неё допустимо использовать составной литерал того же самого типа.

Например, можно читать поля составного литерала:

printf("%d", (point) {2, -3}.y);

В результате выполнения этой строчки кода на консоль будет выведено число -3.

А можно изменять значения полей составного литерала:

printf("%d", (point) {2, -3}.x = 10);

Теперь будет напечатано число 10.

Можно присваивать составному литералу значение переменной того же типа:

point pnt = {1, -2};
(point) {2, -3} = pnt;

А можно присвоить одному составному литералу значение другого составного литерала:

(point) {1, 2} = (point) {3, 4};

Составные литералы и их адреса можно передавать функциям в качестве параметров. Пусть, например, функции create_line1() и create_line2(), выполняющие построения отрезков по координатам их вершин, имеют следующие прототипы:

void create_line1(point pnt1, point pnt2);
void create_line2(point *ppnt1, point *ppnt2);

Тогда следующие вызовы этих функций будут совершенно корректными:

create_line1((point) {4, -3}, (point) {2, -1});
create_line2(&(point) {4, -3}, &(point) {2, -1});

Как мы видим, операция взятия адреса & к составным литералам структурного типа, так же как и к переменным этого же типа, применима.

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

point *ppnt = &(point) {1, -2};
printf ("%d %d", ppnt->x, ppnt->y);

Здесь мы присвоили адрес составного литерала указателю ppnt, после чего вывели на печать его поля, воспользовавшись данным указателем.

Большая часть приведённых примеров (чтение и изменение значений полей составных литералов, присвоение составным литералам значений) не имеет никакого практического значения. Все примеры призваны, в первую очередь, продемонстрировать почти полное отсутствие различий между составными литералами и структурными переменными. А чем могут быть полезны составные литералы на практике? Об этом мы поговорим в следующем разделе.

Использование составных литералов структурного типа

Составные литералы структурного типа могут быть полезными в следующих ситуациях.

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

    pnt = (point) {-3, 4};

    Мы можем присваивать новые значения только некоторым полям:

    pnt = (point) {.y = 4};

    А можем не присваивать значения ни одному из полей вообще:

    pnt = (point) {};

    Но нужно иметь в виду важную вещь. Если мы создаём составной литерал и указываем значения только части его полей, то остальным полям присваиваются нулевые значения. Таким образом, поля составного литерала всегда инициализированы. В этом его отличие от обычной структурной переменной, поля которой могут быть не инициализированы (такие поля получают произвольные значения).

    Из сказанного следует, что после выполнения предпоследнего фрагмента кода значение поля x переменной pnt окажется равным нулю. А в результате выполнения последнего фрагмента все поля pnt будут нулевыми.

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

    create_line1((point) {4, -3}, (point) {2, -1});

    Мы создаём составные литералы прямо в списке фактических аргументов. А, например, при использовании языка C89, не поддерживающего составные литералы, мы были бы вынуждены предварительно создать 2 структурные переменные:

    point pnt1 = {4, -3}, pnt2 =  {2, -1};
    create_line1(pnt1, pnt2);

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

  3. Если функция возвращает значение структурного типа, то, используя составной литерал, результат можно сформировать "на лету", не создавая предварительно структурной переменной для хранения результата. Вот пример такой функции:

    point random_point()
    {
        return (point) {rand(), rand()};
    }
  4. Если в программе встречаются (или могут встречаться) структурные переменные с заранее известными значениями всех полей, неизменными в ходе использования этих переменных, то имеет смысл вместо переменных использовать макросы, определённые как псевдонимы составных литералов. Каждый такой макрос, фактически, выступает в качестве константы (фиксированного значения) структурного типа.

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

    typedef struct
    {
        uchar red;
        uchar green;
        uchar blue;
    } color;

    Здесь uchar — это псевдоним unsigned char, определённый с помощью спецификатора typedef. Каждое из трёх полей переменных типа color отвечает за интенсивность одной из компонент цвета. Далее были созданы макросы, обозначающие 16 наиболее распространённых цветов. Например, пурпурный цвет задавался с помощью следующего макроопределения:

    #define PURPLE (color) {128, 0, 128}

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

  5. Иногда в программе удобнее хранить адреса структурных переменных, а не их имена. Причиной этого может быть, например, частый вызов функций, формальными параметрами которых являются не сами структурные переменные, а указатели на них. В отсутствие составных литералов пришлось бы объявлять промежуточную переменную структурного типа, а потом сохранять в указателе её адрес, например, так:

    point pnt = {-3, 4}, *ppnt = &pnt;

    А используя составные литералы, можно обойтись без промежуточной переменной:

    point *ppnt = &(point) {-3, 4};

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

Составные литералы масссивового типа

Прежде чем говорить о составных литералах массивового типа (в этом и следующем разделах я буду иногда называть их просто "составными литералами") вспомним, какая связь имеется между одномерными массивами и указателями.

Имя одномерного массива без квадратных скобок можно рассматривать как переменную, содержащую адрес первого элемента массива. Данная переменная по своим свойствам очень близка к указателю, однако, всё же, отличается от него. Например, значение этой переменной не подлежит изменению. В силу этих различий я предлагаю называть её массивовой переменной. Более подробно о сходствах и различиях между массивовыми переменными и указателями рассказано в этой статье.

А теперь — к массивовому составному литералу. С точки зрения синтаксиса он представляет собой список инициализации массива, перед которым помещена операция приведения к массиву данного типа. Вот пример:

(double[]) {1.2, -3.4, 5.6};

В результате выполнения приведённой синтаксической конструкции создаётся безымянный массив, базовым типом которого является double, состоящий из трёх элементов, значения которых перечислены в списке инициализации. В выражениях данная конструкция интерпретируется как безымянная массивовая переменная, связанная с данным массивом.

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

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

Проиллюстрируем сказанное следующим примером (не имеющим никакого смысла, кроме демонстрационного):

printf("%g\n", (double[]) {1.2, -3.4, 5.6}[2]);
printf("%g\n", (double[]) {1.2, -3.4, 5.6}[1] = -7.8);
printf("%g\n", *(double[]) {1.2, -3.4, 5.6});
printf("%g\n", *((double[]) {1.2, -3.4, 5.6} + 1));
printf ("%d", sizeof (double[]) {1.2, -3.4, 5.6});

В результате выполнения данного фрагмента на экран будет выведено:

5.6
-7.8
1.2
-3.4
24

Однако нельзя массивовой переменной присвоить значение составного литерала, равно как и последнему нельзя присваивать какие-либо значения:

double arr[3];
arr = (double[]) {1.2, -3.4, 5.6};                         //Ошибка!
(double[]) {1.2, -3.4, 5.6} = arr;                         //И это ошибка!
(double[]) {1.2, -3.4, 5.6} = (double[]) {1.2, -3.4, 5.6}; //Это тоже ошибка!

Но, поскольку значением составного литерала массивового типа, равно как и массивовой переменной, является адрес, то это значение можно присвоить указателю соответствующего типа:

double *p = (double[]) {1.2, -3.4, 5.6};

Заметим, что если мы хотим многократно использовать массивовый составной литерал, то мы должны сразу после его создания сохранить его значение (т. е. адрес первого элемента массива, который он представляет) в переменной, например так, как это сделано в приведённой выше строке кода.

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

int *p = (int[]) {1, rand(), [5] = 55};
for (int i = 0; i < 6; i++)
    printf("%d\n", p[i]);

Как мы видим, 1-й элемент массивового составного литерала инициализирован единицей, значение 2-го становится известным только во время выполнения программы, а 6-й инициализирован числом 55. Всем остальным элементам (с 3-го по 5-й) автоматически присваиваются значения по умолчанию — нули. В итоге, p содержит адрес первого элемента 6-элементного масссива.

В результате выполнения фрагмента на консоль может быть выведен следующий результат:

1
41
0
0
0
55

Вместо числа 41 может быть напечатано какое-либо другое целое число. Конкретное значение зависит от способа реализации в компиляторе датчика псевдослучайных чисел.

Давайте теперь выясним, какую пользу могут принести массивовые составные литералы.

Использование составных литералов массивового типа

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

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

    double *p = (double[]) {1.2, -3.4, 5.6};  //p содержит адрес первого массива
    ... ... ... ...                           //Поработали с первым массивом
    p = (double[]) {2.1, -4.3, 6.5, -8.7};    //"Переключили" p на другой массив

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

    Заметим, что если бы составные литералы не поддерживались бы, то нам пришлось бы каждый элемент массива обновлять по-отдельности, но в этом случае мы не смогли бы изменить количество элементов массива. Ещё мы могли бы использовать другой вариант: изначально выделить динамически память под массив, затем освободить её, после чего снова выделить память, но уже под другой массив. Очевидно, вариант, в котором задействованы составные литералы, наиболее удобен.

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

    Пусть, например, функция sum() имеет следующий прототип:

    double sum(double[] arr, int size);

    Эта функция вычисляет и возвращает сумму первых size элементов массива arr. Данную функцию можно вызвать, например, следующим образом:

    double result = sum((double[]) {1.2, 3.4, 5.6, 7.8}, 4);

    В результате в переменную result будет помещено число 18. Кстати, приведённый выше вызов был бы корректен и в случае, если бы функция sum() имела бы такой прототип:

    double sum(double *p, int size);

    В обоих случаях, в саму функцию при вызове передаётся, разумеется, адрес первого элемента массива.

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

    #define MY_BIRTHDAY (int[]) {1999, 11, 22}

    Теперь можно использовать, например, такую синтаксическую конструкцию:

    int *bd = MY_BIRTHDAY;

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

Время жизни составных литералов

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

Это нужно учитывать и не допускать обращения к уже "мёртвым" составным литералам. Рассмотрим, например, следующую функцию:

int *random_array()
{
    return (int[]) {rand(), rand(), rand()};
}

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

Я использовал слово "может", поскольку такого может и не произойти в случае, если память не освобождается, а лишь помечается как свободная, и если на это место ещё не была записана новая информация (конкретные действия зависят от компилятора). Но на это рассчитывать не следует. Может случиться так, что программа 1000 раз выполнялась без сбоев, а на 1001-й раз по причине описанной ошибки завершилась аварийно. А ведь причины таких редко возникающих аварийных ситуаций находить, как правило, непросто.

Недостатки составных литералов

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

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

Однако я понимаю, что требую от компилятора слишком многого.

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

point pnt = {-3, 4};
//... ... ... ...
pnt = (point) {5, -6};

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

Аналогичное замечание можно сформулировать и для составных литералов массивового типа.

Заключение

Итак, в этой статье мы познакомились с составными литералами. Главный вывод, к которому мы пришли, заключается в том, что структурные и массивовые составные литералы — это ни что иное, как анонимные структурные переменные и анонимные массивы соответственно. Если думать о составных литералах именно в таком ключе, то правила обращения с ними сразу же станут понятны.

Составные литералы, пожалуй, можно отнести к "синтаксическому сахару" языка C99. Без них вполне можно обойтись, но их применение нередко упрощает написание программ.