Реклама
Спонсорские ссылки
-
Статья
Версия для печати
Обсудить на форуме
Что такое typedef, и чем он отличается от #define?


Часть 2. Для чего еще нужен typedef...


  Итак, в первой части мы уже определились с тем, что такое typedef и чем он так похож и не похож на #define.

  typedef вводит новое имя (синоним) для существующего типа. Но только этим все не ограничивается Чего такого еще может typedef, чего невозможно сделать с #define?

  С помощью typedef может быть объявлен любой тип, включая типы функции или массива.
 
Код:
  typedef double (* MATH)(); // MATH - новое имя типа, представляющее указатель на
                             //функцию, возвращающую значения типа double
  MATH cos;  // cos - это указатель на функцию, возвращающую значения типа double
 

  Можно привести эквивалентное объявление - double (* cos)();
 
Код:
  typedef char SIMB[40]; //SIMB - массив из сорока символов
  SIMB person; //переменная person - тоже массив из сорока символов
 

  Это эквивалентно объявлению -  char person[40];

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

  typedef способен весьма облегчить нам жизнь. И не только нам...

  typedef можно использовать, чтобы значительно упростить синтаксис сложных объявлений (воззаботимся же, братия, о тех бедных программистах, которые приИдут после нас). Используя typedef, можно сделать простым даже объявление стандартной функции set_new_handler:
 
Код:
  typedef void (*new_handler)();
  new_handler set_new_handler(new_handler);
 

  Итак, new_handler - указатель на функцию, которая не имеет параметров и ничего не возвращает, и set_new_handler - функция, которая берет new_handler как параметр и возвращает в результате new_handler. Все выглядит очень просто. Если сделать это без typedef, тому, кто будет сопровождать потом этот код, будет лучше сразу застрелиться.
 
Код:
  void (*set_new_handler(void (*)()))();
 

  Это, по сути, то же самое. Но зато выглядит ужасно.

  Давайте рассмотрим класс, используемый для осуществления связи по протоколу IP. Это может быть что-то, похожее на такое определение сокета:
 
Код:
  typedef int   AddressFamily;
  typedef int   SocketType;
  typedef int   Protocol;

  class Socket
  {
    public:
      Socket(AddressFamily family, SocketType type, Protocol protocol);
    . . .
 

  Мы видим определение трех typedef - AddressFamily, SocketType, и Protocol. Они идентифицируют три определяющие характеристики, необходимые для создания сокета связи (смотрим книги Стивенса по TCP/IP - пригодится). В этом контексте typedef использовался, чтобы определить три различных логических типа, хотя все они, в этом случае, один и тот же фактический тип - int. Такое использование typedef иногда называют "концептуальное определение типов".

  Таким образом, конструктор Socket явно более понятен, чем если бы эти три параметра были бы определены просто, как int. Одно из преимуществ концептуального определения типов состоит в том, что они обеспечивают логически более понятное и простое определение классов.

  Другое преимущество концептуального определения типов в том, что оно обеспечивает некоторую независимость от платформы. Посмотрим на стандартные типы size_t и ptrdiff_t, которые обычно определяются так:
 
Код:
  typedef unsigned int  size_t;
  typedef int           ptrdiff_t;
 

  Когда тип ptrdiff_t встречается в коде, сразу становится понятно, что кое-кто здесь собирается оценить разницу между указателями. Если же тип был бы просто int, скорее всего сначала подумалось бы об арифметике или операциях индексации, которые более обычны для простого int. Точно так же и size_t склоняет думать о размерности (количестве байтов), а не о некой произвольной целочисленной мере. Однако, это еще не все. int (или unsigned int) могут и не быть типом, соответствующим для представления этих величин на данной платформе (ну, не хватит их размерности). В конкретной реализации эти типы могут быть переопределены, чтобы они соответствовали поддерживаемым (реализацией) платформам. И тогда не будет никакой необходимости менять что-либо в пользовательском коде - независимость.

  Одной из возможных ошибок, связанных с применением typedef, может стать ошибка с перегрузкой функций. Напомним, что сигнатуры перегруженных функций должны отличаться, как минимум, типом параметра или количеством параметров. Но списки параметров могут быть одинаковыми, несмотря на то, что выглядят абсолютно разными.
 
Код:
  struct Phone {...};
  struct Record {...};

  typedef Phone Tel_no;

  Record lookup(const Phone&);
  Record lookup(const Telno&); // Telno и Phone это тот же самый тип
 

  Выглядит, как вполне разные типы. А на самом деле - одно и тоже, ведь Telno - не новый тип, а всего лишь синоним типа Phone.

  Между прочим, и C и C++ поддерживают повторные определения того же самого typedef, пока все определения идентичны. Однако, это не самая лучшая идея - переопределять любой typedef, и лучше, конечно, так не делать. Должна быть единственная точка определения, обычно в пределах общедоступного инклуд-файла, вместо того, чтобы иметь кучу независимых определений, которые могут вести к очень неприятным побочным эффектам. Особенно, если отличающиеся определения происходят в отдельно компилируемых модулях, которые линкуются совместно, особенно когда линковка динамическая.

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

  Если у вас возникает потребность подробного рассмотрения какого-либо вопроса по С++, пишите нам, мы попытаемся вам помочь.

  Сергей Малышев (aka Михалыч).
Версия для печати
Обсудить на форуме