Отправляет email-рассылки с помощью сервиса Sendsay

C и C++ для начинающих

  Все выпуски  

C и C++ для начинающих Выпуск 15. Синонимы типов и препроцессор


Служба Рассылок Subscribe.Ru проекта Citycat.Ru
Архив рассылки Подписаться Статистика рассылки Форум

C и C++ для начинающих. Выпуск 15. Синонимы типов и препроцессор

Доброго времени суток! Приношу извинения за невыход рассылки в течение такого длительного периода времени. Теперь рассылка снова выходит, и я вместе с вами надеюсь, что она будет выходить и дальше. Правда, это всецело зависит от количества имеющегося у меня свободного времени, которого остается все меньше и меньше... Так что теперь рассылка будет, скорее всего, выходить раз в неделю. Когда именно - пока непонятно.

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

Как водится, в начале года (учебного) о моих планах на будущее. Вообще-то у меня осталось всего три темы по C, которые я хотел бы обсудить. Это сегодняшняя тема, объединения и битовые поля и, наконец, разделение программы на несколько файлов. После этого будет разбор полетов (мой вариант решения домашних заданий), а потом мы начнем C++.

Синонимы типов

Одна из возможностей языка C, делающая его столь мощным, - это возможность создавать собственные типы. Эти типы базируются на нескольких фундаментальных, таких как int, char, float и другие. Но, используя указатели, массивы и структуры, из этих простых типов можно получать достаточно сложные. Правда, в этом есть одно неудобство. Рассмотрим пример:
 #include <windows.h>
 #include <stdio.h>

 void thread (void *pInt)
 {
  // Печать параметра
  printf ("%d\n", *((int*) pInt));
 }

 int main (void)
 {
  HANDLE hLib;
  // О том, что такое HANDLE, я расскажу позже, пока что
  // считайте его void*

  int i = 5;
  int (*BeginThread)(void (*)(void*), void*);

  hLib = LoadLibrary ("MYLIB.DLL");
  BeginThread = (int (*)(void (*)(void*), void))
   GetProcAddress (hLib, "MyBeginThread");
  (*BeginThread) (thread, &i);

  FreeLibrary (hLib);

  return 0;
 }
В данном случае мы загружаем библиотеку в память, выделяем из нее функцию и вызвываем ее. Но функция этого примера в данном случае не важна. Важно другое: даже в таком коротком участке кода дважды встретилось достаточно громоздкое определение типа int (*)(void (*)(void*), void). А в большой программе определения типов таких размеров встретятся десятки раз. Чтобы избавить программиста от необходимости повторять такие громоздкие описания каждый раз, в С введены синонимы типов.

Что такое синоним типа? Это просто лишь его альтернативное имя. Тип и его синоним полностью идентичны, можно пользоваться как одним, так и другим. У типа может быть сколько угодно синонимов. Описываются синонимы как переменные, с той разницей, что в начало описания добавляется ключевое слово typedef :
 typedef unsigned long ULONG, DWORD;
В данном примере объявляются два новых типа: ULONG и DWORD, которые являются синонимами unsigned long. Теперь мы можем писать так:
 unsigned long a;
 ULONG b;
 DWORD c;
 // Все три переменные одного и того же типа
А теперь начинается самое интересное. Как я говорил, описание синонимов сходно с описанием переменных. То есть мы имеем полное право написать так:
 typedef unsigned long DWORD, *LPDWORD, DWORD_ARRAY [10];
Теперь мы объявили тип DWORD, являющийся синонимом unsigned long, тип LPDWORD, являющийся синонимом указателя на unsigned long, и тип DWORD_ARRAY, являющийся синонимом массива из 10 значений типа unsigned long.

Переменные, объявленные с помощью синонима типа, ничем не отлючаются от обычных. С ними можно так же оперировать:
 #include <stdio.h>
 #include <stdlib.h>
 typedef int *PINT;

 int main (void)
 {
  PINT p;

  p = (PINT) malloc (sizeof (int));
  *p = 5;
  printf ("%d\n", *p);
  free (p);

  return 0;
 }
Теперь мы можем переписать пример из начала следующим образом:
 #include <windows.h>
 #include <stdio.h>

 void thread (void *pInt)
 {
  // Печать параметра
  printf ("%d\n", *((int*) pInt));
 }

 // Теперь я могу рассказать, что такое HANDLE
 typedef struct HANDLE__ { void *pUnused; } HANDLE;
 // Вот так HANDLE описан в windows.h. Почему именно так - не знаю.

 // А вот теперь мы определим наш тип
 typedef int (*BEGINTHREAD) (void (*)(void*), void*);

 int main (void)
 {
  HANDLE hLib;
  int i = 5;
  BEGINTHREAD BeginThread;

  hLib = LoadLibrary ("MYLIB.DLL");
  BeginThread = (BEGINTHREAD) GetProcAddress (hLib, "MyBeginThread");
  (*BeginThread) (thread, &i);

  FreeLibrary (hLib);

  return 0;
 }
Обратите внимание на то, что код функции main существенно упростился. Что конкретно представляет из себя тип BEGINTHREAD, вы, я думаю, в состоянии разобраться сами.

Синонимы применяются еще в одном случае. Как вы помните, объявив структуру MY_STRUCT, мы должны описывать все экземпляры этой структуры как struct MY_STRUCT MyStruct;. Если же мы напишем так:
 typedef struct __MY_STRUCT {
  int a;
  char *b;
  // Еще элементы
 } MY_STRUCT;
- то теперь объявлять экземпляры этой структуры можно так:
 MY_STRUCT MyStruct; // синоним struct __MY_STRUCT MyStruct;
- что тоже проще. Ненамного, но все же приятнее. На этом с typedef закончим.

Препроцессор

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

Начнем с самой простой - с макроопределений констант. Мы можем задать какую-либо константу как const int a = 5;. В таком случае a - это полноценная переменная, с собственным адресом и областью видимости. Более того, написав *((int*) &a) = 6;, мы даже сможем изменять содержимое этой константы. По сути дела, модификатор const не задает константу, а лишь указывает компилятору, что значение переменной по идее не должно меняться.

Но есть и другой способ. Он хорош, если требуется задать не переменную, а именно некоторую константу (как правило - параметр). Делается это таким образом:
 #define VAR 5
(обратите внимание на отсутствие точки с запятой). Теперь везде, где в тексте программы встретится VAR, компилятор подставит значение 5 до компиляции. То есть VAR не является переменной. Про такую конструкцию говорят, что это макроопределение.

Где действует макроопределение? Ввиду того, что замена производится до компиляции, областей видимости на этот этап времени еще нет. Поэтому макроопределение действительно с момента его объявления на протяжении всего текста программы до конца файла или явной отмены определения директивой #undef:
 #undef VAR
Теперь, как водится, об ограничениях. Во-первых, в качестве имени определения нельзя использовать ключевые слова. Во-вторых, данные ранее определения переопределять нельзя. Если это надо все же сделать, требуется сначала отменить определение:
 #define VAR 2
 // ...
 #undef VAR 2
 #define VAR 3
 // ....
 #undef VAR 3
 #define VAR 4
И, наконец, определение не должно превышать одну строку.

Чтобы обойти последнее ограничение, используется так называемая склейка строк. Это тоже функция препроцессора. Суть ее заключается в том, что препроцессор удаляет из текста программы идущие подряд обратную косую черту (\) и символ новой строки. Таким образом несколько строк могут быть скреплены в одну:
 #define LONG_STR "Это длинная строка...\
 ...очень длинная строка...\
 ну просто очень длинная строка."
К склейке применимы два правила: 1) Между косой чертой и символом новой строки не должно быть никаких других символов (пробелов, табуляций, ...); 2) Склейка удаляет только символ новой строки и никак не затрагивает символы, идущие после нее (например, табуляции, помещенные с целью повышения читаемости программы). Это критично только для символьных строк, которые рекомендуется склеивать несколько иначе:
 #define LONG_STR "Это длинная строка..."
  "...очень длинная строка..."
  "ну просто очень длинная строка."
Для всего остального лишняя табуляция препятствием не является.

Еще одна функция препроцессора - это условная компиляция. В зависимости от сделанных ранее макроопределений можно исключить из компиляции некоторые строки. Делается это так:
 #if (условие)
  Эти строки будут компилироваться, если "условие" выполняется
 [#else
  А эти - в противном случае
 ]
 #endif
Здесь условие задается при помощи логических операторов (&& ||; !), слова defined (имя), которое считается истинным, если имя определено, и операций отношения (==; !=; <; <=; >; >=) над числами и макроопределениями. #if (defined (имя)) разрешается сокращать до #ifdef имя, #if (!defined (имя)) - соответственно до #ifndef имя.

Если нас интересует лишь факт существования макроопределения, то мы можем задать его так:
 #define VAR
- в этом случае defined (VAR) истинно, но VAR никакого значения не имеет. Применяется такая форма макроопределения преимущественно в условной компиляции.

Теперь примеры.
 // Первый пример
  printf (
 #ifdef V
   "%d "
 #endif
   "%s\n",
 #ifdef V
   i,
 #endif
   s
  );

 // Второй пример
 #ifdef VAR_A_WILL_BE_NEEDED_IN_FUTURE
 int a = 2;
 #endif

 // Третий пример (взято из Visual C Startup Code)
 #ifdef WPRFLAG
  lpszCommandLine = _wwincmdln();
  mainret = wWinMain(
 #else
  lpszCommandLine = _wincmdln();
  mainret = WinMain(
 #endif
   GetModuleHandleA(NULL), NULL, lpszCommandLine,
   StartupInfo.dwFlags & STARTF_USESHOWWINDOW
    ? StartupInfo.wShowWindow
    : SW_SHOWDEFAULT
  );

 // Четвертый пример (из MSDN)
 typedef struct tagENHMETAHEADER {
  DWORD iType;
  DWORD nSize;
  // Вырезано за ненадобностью
  SIZEL szlDevice;
  SIZEL szlMillimeters;
 #if (WINVER >= 0x0400)
  DWORD cbPixelFormat;
  DWORD offPixelFormat;
  DWORD bOpenGL;
 #endif
 #if (WINVER >= 0x0500)
  SIZEL szlMicrometers
 #endif
 } ENHMETAHEADER, *PENHMETAHEADER;
Итак, вы увидели возможные вырианты применения условной компиляции. Можете попрактиковаться. В следующий раз мы разберем макрофункции (макросы). До встречи!

Ведущий рассылки, av

http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу
Рейтингуется SpyLog

В избранное