Содержание


Малоизвестные возможности и последние нововведения в языке C. Часть 1. Компилятор GCC и новые типы данных

Comments

Язык C по праву считается одним из старых языков программирования, его первые реализации относятся к 1972-му году. Другие языки, появившиеся одновременно или раньше языка С, уже отошли в прошлое, выполнив свою миссию (ALGOL, COBOL, BCPL, ...), или применяются только в специфических областях (FORTRAN, LISP, ...). Такой длительный жизненный цикл языка С во многом связан с тем, что на нём было написано (и до сих пор пишется) подавляющее большинство системного программного обеспечения для UNIX/POSIX операционных систем, например, Linux.

Конечно, для настолько распространённого языка существует множество руководств и учебных пособий. Ещё одной особенностью C является то, что самые первые руководства по языку были написаны непосредственно его авторами (Б. Керниган, Д. Ритчи) и выдержали 34 переиздания в США. Подобная преемственность способствовала краткости и однозначности толкований понятий языка и простоте его освоения.

Имеет ли смысл, при такой истории и таком объёме существующих описаний, снова обращаться к деталям реализации языка C? Да, и для этого можно выделить несколько причин:

  1. За 40 лет язык претерпел несколько модернизаций, и в последних действующих стандартах (C89, C99) был внесён ряд существенных нововведений (например, комплексная арифметика), которые не освещаются в "классической" литературе по C, написанной много лет назад.
  2. Язык C изначально разрабатывался как язык для системного программирования в ОС семейства UNIX. Но с широким распространением платформы Linux, С в основном стал применяться для проектов GNU и другого свободного ПО для ОС Linux. Подобные приложения рассчитаны на использование компилятора GCC, который имеет ряд собственных расширений языка.
  3. Поддержка символьных представлений UNICODE и кодировки UTF-8 (по умолчанию поддерживаемой всеми дистрибутивами Linux). Эта функциональность языка недостаточно описана в англоязычной литературе, а русскоязычные авторы часто обходят её стороной.
  4. Кроме компилятора GCC, в последние годы начинает широко практиковаться использование компилятора Clang (из проекта LLVM — Low Level Virtual Machine), при этом ряд расширений GCC поддерживается Clang, а некоторые возможности — нет.
  5. Существует функциональность, не являющаяся частью самого языка, а скорее относящаяся к традициям его использования для определённых задач. Например, использование циклических двунаправленных списков в ядре Linux вместо любых других связанных структур данных.

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

Расширения для компилятора GCC

Точка отсчёта существования компилятора GCC ведётся с 1985 года, когда Ричард Столман представил его первый вариант. С 1987 года GCC начал позиционироваться как компилятор для открытых проектов GNU. При компиляции исходных кодов на языке С (GCC поддерживает и другие языки) компилятор позволяет использовать расширения языка, некоторые из которые входят в стандарт C99 (ISO/IEC 9899:1999), а другие относятся непосредственно к GCC.

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

Одно из важнейших расширений GCC — это возможность описания вложенных функций (подобная функциональность присутствует в языке Pascal), демонстрируемая в листинге 1. Полный код примера можно найти в файле extent.c в архиве lang_c_details.tgz разделе "Материалы для скачивания".

Листинг 1. Вложенные определения функций (файл extent.c)
void test090( void ) {         // вложенные функции
   int array[] = { 1, -2, 3, -4, 5, -6, 7 },
       size = sizeof( array ) / sizeof( *array ), i;
   void pow2( void ) {        // вложенное описание функции pow2()
      int decr( int arg ) {   // ещё один уровень вложенности функции decr()
         return arg - 1;
      }
      for( i = 0; i < size; i++ )
         array[ i ] = decr( array[ i ] ) * decr( array[ i ] );
   }
   printf( "вложенные функции GCC:\n" );
   printf( "до\t:" );
   for( i = 0; i < size; i++ )
      printf( "%2d%s", array[ i ], ( i == size - 1 ? "\n" : " , " ) );
   pow2();
   printf( "после\t:" );
   for( i = 0; i < size; i++ )
      printf( "%2d%s", array[ i ], ( i == size - 1 ? "\n" : " , " ) );
}

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

$ ./extent 5
05 ---------------------------------------
вложенные функции GCC (файл: extent.c):
до	: 1 , -2 ,  3 , -4 ,  5 , -6 ,  7
после	: 0 ,  9 ,  4 , 25 , 16 , 49 , 36
------------------------------------------

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

Следующее расширение, введенное в стандарте C99 и поддерживаемое компилятором GCC — это возможность использования массивов с динамически определяемыми размерами (VLA — variable-length array, массивы переменной длины). Подобные массивы допускается описывать только локально внутри использующей их функции. Продемонстрировать эту возможность можно на фрагменте транспонирования прямоугольной (не квадратной) матрицы в листинге 2.

Листинг 2. Транспонирование матрицы (файл extent.c)
static void show( int *arr, int row, int col ) {
   int r, c;
   printf( "матрица : %d x %d [ %d ]\n",
           row, col, row * col );
   for( r = 0; r < row; r++ ) {
      for( c = 0; c < col; c++ )
         printf( "%3d", arr[ r * col + c ] );
      printf( "\n" );
   }
}

static void transpa( int *arr, int *row, int *col ) {
   int r, c;
   int wrk[ *row * *col ];    // массив с динамической размерностью
   for( r = 0; r < *row; r++ )
      for( c = 0; c < *col; c++ ) {
         int i1 = r * *col + c,
             i2 = c * *row + r;
         wrk[ i2 ] = arr[ i1 ];
      }
   for( r = 0; r < *row * *col; r++ ) {
      arr[ r ] = wrk[ r ];
   }
   r = *row;
   *row = *col;
   *col = r;
}

#define COL 5
#define ROW 2

void test020( void ) {           // динамические массивы VLA (C99)
   int c[ ROW ][ COL ] = {
          { 1, 2, 3, 4, 5 },
          { 2, 3, 4, 5, 6 }
       },
       col = COL, row = ROW;
   show( (int*)c, row, col );
   printf( "транспонирование не квадратной матрицы:\n" );
   transpa( (int*)c, &row, &col );
   show( (int*)c, row, col );
}

Запустим код и проверим результат: превращение матрицы 2х5 в матрицу 5х2:

$ ./extent 0
00 ---------------------------------------
транспонирование не квадратной матрицы (файл: t020.c):
матрица : 2 x 5 [ 10 ]
  1  2  3  4  5
  2  3  4  5  6
матрица : 5 x 2 [ 10 ]
  1  2
  2  3
  3  4
  4  5
  5  6
------------------------------------------

Массивы переменной длины вводятся в стандарте C99 вместо плохо стандартизованного (между различными компиляторами и платформами) и считающегося небезопасным вызова alloca(). Эти два механизма решают одинаковые задачи, но разными способами. Интересно (хотя это никак не отражается в документации), что VLA массивы могут быть выделены не только в отдельной функции, но и в блоке кода (в скобках [...] показывается смещение нового размещения относительно «дна» стека), как показано в листинге 3.

Листинг 3. Выделение VLA массивов в блоке кода (файл extent.c)
#define N 4

void test025( void ) {           // динамические массивы VLA (C99) в блоке
   int i, j;
   void *p = &p;                 // дно кадра стека
   TITLE( "VLA в блоке" );
   printf( "дно стека = %p\n", p );
   for( i = 0; i < N; i++ ) {
      short V[ i + 1 ];          // это место может показаться странным
      for( j = 0; j <= i; j++ ) V[ j ] = j + i + 1;
      printf( "%p[%03d]\t=> { ", V, p - (void*)V );
      for( j = 0; j <= i; j++ )
         printf( "%d%s", V[ j ], ( j == i ? " }\n" : " , " ) );
   }
}

Ещё одно новое расширение GCC (поддерживаемое и Clang) — это возможность описания массивов нулевой длины для построения структур (в том числе и массивов) произвольно изменяемого размера, как показано в листинге 4.

Листинг 4. Массивы нулевой длины (файл extent.c)
typedef struct vararr {
   int n, data[ 0 ];  // массив нулевой длины
} vararr_t;

static void varfunc( vararr_t *a ) {
   int ni = a->n, j;
   printf( "массив размера %d\t=> { ", ni );
   int *va = (int*)a;
   for( j = 1; j <= ni; j++ )
      printf( "%d%s", va[ j ], j != ni ? " , " : " }\n" );
}

void test040( void ) {
   TITLE( "структуры переменного размера" );
   int var[] = { 3, 5, 7, 10 }, i;
   for( i = 0; i < sizeof( var ) / sizeof( *var ); i++ ) {
      int len = var[ i ], j;
      vararr_t *arr = (vararr_t*)calloc( len + 1, sizeof( int ) );
      arr->n = len;
      for( j = 0; j < len; j++ ) arr->data[ j ] = j + 1;
      varfunc( arr );
      free( arr );
   }
}

Эквивалент расширения массива нулевой длины, но уже определяемый стандартом C99 — это массив с переменными границами. При этом структура vararr выше может быть записана (без каких либо изменений в остальном коде) так:

typedef struct vararr {
   int n, data[];     // массив с переменными границами
} vararr_t;

А теперь сравним, как сработают 3 обсуждаемых альтернативных реализации:

$ ./extent 1 2 4
01 ---------------------------------------
VLA в блоке (файл: t025.c):
дно стека = 0xbfd6bafc
0xbfd6bacc[048]	=> { 1 }
0xbfd6bacc[048]	=> { 2 , 3 }
0xbfd6bacc[048]	=> { 3 , 4 , 5 }
0xbfd6bacc[048]	=> { 4 , 5 , 6 , 7 }
02 ---------------------------------------
alloca() в блоке (файл: t027.c):
дно стека = 0xbfd6bafc
0xbfd6bac0[060]	=> { 1 }
0xbfd6baa0[092]	=> { 2 , 3 }
0xbfd6ba80[124]	=> { 3 , 4 , 5 }
0xbfd6ba60[156]	=> { 4 , 5 , 6 , 7 }
04 ---------------------------------------
структуры переменного размера (файл: t040.c):
массив размера 3	=> { 1 , 2 , 3 }
массив размера 5	=> { 1 , 2 , 3 , 4 , 5 }
массив размера 7	=> { 1 , 2 , 3 , 4 , 5 , 6 , 7 }
массив размера 10	=> { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 }
------------------------------------------

Примечание. Обратим внимание на то, что в блоке цикла VLA-массивы (отличающихся размеров) создаются "на месте" друг друга (единое смещение, замещая друг друга), а при использовании alloca() — "вослед" друг другу (нарастающие смещения). Другими словами, временем жизни VLA-массива является заключающий его блок, а для размещения alloca() — вся функция, до переразметки кадра стека при возврате. Это означает, что при всей сходности двух методов они принципиально отличаются своей семантикой. Подобное поведение будет наблюдаться и в компиляторе Clang, рассматриваемом далее.

Ещё один пример принципиально важного расширения синтаксиса, предусмотренного в GCC — это возможность инлайновых ассемблерных фрагментов в коде. Для ассемблерных вставок используется особый синтаксис (внутри конструкции, определяемой ключевым словом asm или __asm__). Сам ассемблерный фрагмент описывается как символьные строки, в виде макроопределения. Важно то, что в таких ассемблерных вставках доступны (как по чтению, так и по записи) все переменные из обрамляющего вставку C-кода.

GCC компилятор использует AT&T синтаксис ассемблера (как в инлайновых фрагментах, так и при компиляции отдельных ассемблерных файлов). Синтаксис AT&T заметно отличается от синтаксиса Intel (который используется в Windows). Важным свойством синтаксиса AT&T (и GCC) является то, что в нём можно записывать ассемблерный код для всех различных процессорных платформ, поддерживаемых GCC (а не только Intel x86). Описание синтаксиса инлайновых вставок GCC и ассемблера AT&T выходит за рамки этой статьи, однако ссылки на информацию по этому вопросу приведены в разделе "Ресурсы".

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

Листинг 6. Определение тактовой частоты процессора (файл extent.c)
unsigned long long rdtsc( void ) {
   unsigned long long int x;
   inline asm volatile ( "rdtsc" : "=A" (x) ); // команда RDTSC
   return x;
}

void test030( void ) {                       // ассемблерные вставки (GCC)
   time_t t1, t2;
   unsigned long long cf, cs;
   time( &t1 );
   while( t1 == time( &t2 ) ) cf  = rdtsc(); // начало очередной секунды
   while( t2 == time( &t1 ) ) cs  = rdtsc(); // завершение этой секунды
   printf( "тактовая частота процессора %.3f Ghz\n",
           (double)( cs - cf ) / 1.E9 );
}

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

$ ./extent 3
03 ---------------------------------------
ассемблерные вставки (файл: t030.c):
тактовая частота процессора 1.630 Ghz
------------------------------------------

Инлайновые ассемблерные вставки GCC активно используются в коде ядра Linux, и подавляющее большинство архитектурно зависимых вещей написано именно так, а собственно ассемблерных файлов, транслируемых и связываемых отдельно, присутствует крайне мало.

В GCC предусмотрен ещё ряд расширений, но они не так существенны для практического применения.

Новые типы данных

Важным расширением языка С стало дополнение множества скалярных предопределённых типов комплексными числами (появились в стандарте ISO/IEC 9899:1999, определены в файле <complex.h>). Определение комплексных переменных может быть описано с различной точностью, например, как показано в листинге 7.

Листинг 7. Определения комплексных переменных (файл complex.c)
#include <complex.h>

char *cput( complex c ) {
   static char scomp[ 40 ];
   sprintf( scomp, "%+.1f %+.1fi", creal( c ), cimag( c ) );
   return scomp;
}
               
#define print2(x) \
        printf( "комплексное: %s \tразмер = %d \tмодуль = %.2Lf\n", \
                cput(x), sizeof(x), cabsl(x) );
   
void test01( void ) {    // представление комплексных
   double complex z1 = 1. + 1. * I;
   complex z2 = 3 - 4 * I;
   float complex  z3 = 4 - 3 * I;
   long double _Complex z4 = -3 + 3 * I;
   printf( "различные представления: \n" );
   print2( z1 );
   print2( z2 );
   print2( z3 );
   print2( z4 );
}

Выполним эту программу и изучим полученный результат:

$ gcc complex.c -o complex -lm -Wall
$ ./complex 0
00 ---------------------------------------
различные представления:
комплексное: +1.0 +1.0i 	размер = 16 	модуль = 1.41
комплексное: +3.0 -4.0i 	размер = 16 	модуль = 5.00
комплексное: +4.0 -3.0i 	размер = 8 	модуль = 5.00
комплексное: -3.0 +3.0i 	размер = 24 	модуль = 4.24

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

Листинг 8. Преобразования формата (файл complex.c)
inline char* pput( complex p, char* buf ) {
   sprintf( buf, "(%+.1f,%+.1f)", creal( p ), cimag( p ) );
   return buf;
}

void test02( void ) {           // разные представление комплексных
   double PI = 4 * atan( 1.0 ); // можно просто M_PI
   void print( complex p ) {
      char buf[ 40 ];
      printf( "%s => abs = %.3f | arg = %.3f = %.2f*π = %.0f°\n",
           pput( p, buf ), cabs( p ),
           carg( p ), carg( p ) / PI, carg( p ) / PI * 180 );
   }
   printf( "abs * ( sin( arg ) + i * cos( arg ) ) || abs * exp( -i * arg ) :\n" );
   print( z0 );
   print( z1 );
   print( z5 );
   print( z8 );
   print( z9 );
   print( z10 );
}

Запустив программу, мы получим следующий результат:

$ ./complex 1
01 ---------------------------------------
abs * ( sin( arg ) + i * cos( arg ) ) || abs * exp( -i * arg ) :
(+1.0,+1.0) => abs = 1.414 | arg = 0.785 = 0.25*π = 45°
(+2.0,+3.0) => abs = 3.606 | arg = 0.983 = 0.31*π = 56°
(-0.0,-1.7) => abs = 1.732 | arg = -1.571 = -0.50*π = -90°
(-4.0,+1.0) => abs = 4.123 | arg = 2.897 = 0.92*π = 166°
(-3.0,-3.0) => abs = 4.243 | arg = -2.356 = -0.75*π = -135°
(+1.4,-1.0) => abs = 1.732 | arg = -0.615 = -0.20*π = -35°
------------------------------------------

Посмотреть краткую сводку по комплексной арифметике, и далеко не полный список доступных комплексных функций можно командой:

$ man 7 complex
...
SEE ALSO
       cabs(3),  cacos(3),  cacosh(3), carg(3), casin(3), casinh(3), catan(3),
…

Ещё одним новым типом данных, появившимся в С99, является _Bool. В новом заголовочном файле <stdbool.h> определены имена макросов bool, true и false. Это не привносит в язык ничего нового, хотя позволяет создавать код, совместимый с C++.

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

Заключение

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source
ArticleID=967890
ArticleTitle=Малоизвестные возможности и последние нововведения в языке C. Часть 1. Компилятор GCC и новые типы данных
publish-date=04082014