<< Пред.           стр. 16 (из 121)           След. >>

Список литературы по разделу

 Может быть, данное объяснение преобразований типов несколько смутило вас. Запомните основную идею: арифметическое преобразование типов ставит своей целью сохранить точность при вычислении. Это достигается приведением типов всех операндов к типу, способному вместить любое значение любого из присутствующих в выражении операндов.
 4.14.3. Явное преобразование типов
 Явное преобразование типов производится при помощи следующих операторов: static_cast, dynamic_cast, const_cast и reinterpret_cast. Заметим, что, хотя иногда явное преобразование необходимо, оно служит потенциальным источником ошибок, поскольку подавляет проверку типов, выполняемую компилятором. Давайте сначала посмотрим, зачем нужно такое преобразование.
 Указатель на объект любого неконстантного типа может быть присвоен указателю типа void*, который используется в тех случаях, когда действительный тип объекта либо неизвестен, либо может меняться в ходе выполнения программы. Поэтому указатель void* иногда называют универсальным указателем. Например:
 int iva1;
 int *pi = 0;
 char *pc = 0;
 void *pv;
 
 pv = pi; // правильно: неявное преобразование
 pv = pc; // правильно: неявное преобразование
 
 const int *pci = &iva1;
 pv = pci; // ошибка: pv имеет тип, отличный от const void*;
 const void *pcv = pci; // правильно
 Однако указатель void* не может быть разыменован непосредственно. Компилятор не знает типа объекта, адресуемого этим указателем. Но это известно программисту, который хочет преобразовать указатель void* в указатель определенного типа. С++ не обеспечивает подобного автоматического преобразования:
 #inc1ude
 int ival = 1024;
 void *pv;
 int *pi = &iva1;
 const char *pc = "a casting call";
 
 void mumble() {
  pv = pi; // правильно: pv получает адрес ival
  pc = pv; // ошибка: нет стандартного преобразования
 
  char *pstr = new char[ str1en( pc )+1 ];
  strcpy( pstr, pc );
 }
 Компилятор выдает сообщение об ошибке, так как в данном случае указатель pv содержит адрес целого числа ival, и именно этот адрес пытаются присвоить указателю на строку. Если бы такая программа была допущена до выполнения, то вызов функции strcpy(), которая ожидает на входе строку символов с нулем в конце, скорее всего привел бы к краху, потому что вместо этого strcpy() получает указатель на целое число. Подобные ошибки довольно просто не заметить, именно поэтому С++ запрещает неявное преобразование указателя на void в указатель на другой тип. Однако такой тип можно изменить явно:
 void mumble 0 {
  // правильно: программа по-прежнему содержит ошибку,
  // но теперь она компилируется!
  // Прежде всего нужно проверить
  // явные преобразования типов...
 
  pc = static_cast< char* >( pv );
 
  char *pstr = new char[ str1en( pc )+1 ];
 
  // скорее всего приведет к краху
  strcpy( pstr, pc );
 }
 Другой причиной использования явного преобразования типов может служить необходимость избежать стандартного преобразования или выполнить вместо него собственное. Например, в следующем выражении ival сначала преобразуется в double, потом к нему прибавляется dval, и затем результат снова трансформируется в int.
 double dval;
 int iva1;
 ival += dval;
 Можно уйти от ненужного преобразования, явно заменив dval на int:
 ival += static_cast< int >( dval );
 Третьей причиной является желание избежать неоднозначных ситуаций, в которых возможно несколько вариантов применения правил преобразования по умолчанию. (Мы рассмотрим этот случай в главе 9, когда будем говорить о перегруженных функциях.)
 Синтаксис операции явного преобразования типов таков:
 cast-name< type >( expression );
 Здесь cast-name – одно из ключевых слов static_cast, const_cast, dynamic_cast или reinterpret_cast, а type – тип, к которому приводится выражение expression.
 Четыре вида явного преобразования введены для того, чтобы учесть все возможные формы приведения типов. Так const_cast служит для трансформации константного типа в неконстантный и подвижного (volatile) – в неподвижный. Например:
 extern char *string_copy( char* );
 const char *pc_str;
 
 char *pc = string_copy( const_cast< char* >( pc_str ));
 Любое иное использование const_cast вызывает ошибку компиляции, как и попытка подобного приведения с помощью любого из трех других операторов.
 С применением static_cast осуществляются те преобразования, которые могут быть сделаны неявно, на основе правил по умолчанию:
 double d = 97.0;
 char ch = static_cast< char >( d );
 Зачем использовать static_cast? Дело в том, что без него компилятор выдаст предупреждение о возможной потере точности. Применение оператора static_cast говорит и компилятору, и человеку, читающему программу, что программист знает об этом.
 Кроме того, с помощью static_cast указатель void* можно преобразовать в указатель определенного типа, арифметическое значение – в значение перечисления (enum), а базовый класс – в производный. (О преобразованиях типов базовых и производных классов говорится в главе 19.)
 Эти изменения потенциально опасны, поскольку их правильность зависит от того, какое конкретное значение имеет преобразуемое выражение в данный момент выполнения программы:
 enum mumble { first = 1, second, third };
 
 extern int ival;
 mumble mums_the_word = static_cast< mumble >( ival );
 Трансформация ival в mumble будет правильной только в том случае, если ival равен 1, 2 или 3.
 reinterpret_cast работает с внутренними представлениями объектов (re-interpret – другая интерпретация того же внутреннего представления), причем правильность этой операции целиком зависит от программиста. Например:
 complex *pcom;
 char *pc = reinterpret_cast< char* >( pcom );
 Программист не должен забыть или упустить из виду, какой объект реально адресуется указателем char* pc. Формально это указатель на строку встроенного типа, и компилятор не будет препятствовать использованию pc для инициализации строки:
 string str( pc );
 хотя скорее всего такая команда вызовет крах программы.
 Это хороший пример, показывающий, насколько опасны бывают явные преобразования типов. Мы можем присваивать указателям одного типа значения указателей совсем другого типа, и это будет работать до тех пор, пока мы держим ситуацию под контролем. Однако, забыв о подразумеваемых деталях, легко допустить ошибку, о которой компилятор не сможет нас предупредить.
 Особенно трудно найти подобную ошибку, если явное преобразование типа делается в одном файле, а используется измененное значение в другом.
 В некотором смысле это отражает фундаментальный парадокс языка С++: строгая проверка типов призвана не допустить подобных ошибок, в то же время наличие операторов явного преобразования позволяет “обмануть” компилятор и использовать объекты разных типов на свой страх и риск. В нашем примере мы “отключили” проверку типов при инициализации указателя pc и присвоили ему адрес комплексного числа. При инициализации строки str такая проверка производится снова, но компилятор считает, что pc указывает на строку, хотя, на самом-то деле, это не так!
 Четыре оператора явного преобразования типов были введены в стандарт С++ как наименьшее зло при невозможности полностью запретить такое приведение. Устаревшая, но до сих пор поддерживаемая стандартом С++ форма явного преобразования выглядит так:
 char *pc = (char*) pcom;
 Эта запись эквивалентна применению оператора reinterpret_cast, однако выглядит не так заметно. Использование операторов xxx_cast позволяет четко указать те места в программе, где содержатся потенциально опасные трансформации типов.
 Если поведение программы становится ошибочным и непонятным, возможно, в этом виноваты явные видоизменения типов указателей. Использование операторов явного преобразования помогает легко обнаружить места в программе, где такие операции выполняются. (Другой причиной непредсказуемого поведения программы может стать нечаянное уничтожение объекта (delete), в то время как он еще должен использоваться в работе. Мы поговорим об этом в разделе 8.4, когда будем обсуждать динамическое выделение памяти.)
 Оператор dynamic_cast применяется при идентификации типа во время выполнения (run-time type identification). Мы вернемся к этой проблеме лишь в разделе 19.1.
 4.14.4. Устаревшая форма явного преобразования
 Операторы явного преобразования типов, представленные в предыдущем разделе, появились только в стандарте С++; раньше использовалась форма, теперь считающаяся устаревшей. Хотя стандарт допускает и эту форму, мы настоятельно не рекомендуем ею пользоваться. (Только если ваш компилятор не поддерживает новый вариант.)
 Устаревшая форма явного преобразования имеет два вида:
 // появившийся в C++ вид
 type (expr);
 
 // вид, существовавший в C
 (type) expr;
 и может применяться вместо операторов static_cast, const_cast и reinterpret_cast.
 Вот несколько примеров такого использования:
 const char *pc = (const char*) pcom;
 int ival = (int) 3.14159;
 extern char *rewrite_str( char* );
 char *pc2 = rewrite_str( (char*) pc );
 int addr_va1ue = int( &iva1 );
 Эта форма сохранена в стандарте С++ только для обеспечения обратной совместимости с программами, написанными для С и предыдущих версий С++.
 Упражнение 4.21
 Даны определения переменных:
 char cval; int ival;
 float fval; double dva1;
 unsigned int ui;
 Какие неявные преобразования типов будут выполнены?
 (a) cva1 = 'a' + 3;
 (b) fval = ui - ival * 1.0;
 (c) dva1 = ui * fval;
 (d) cva1 = ival + fvat + dva1;
 Упражнение 4.22
 Даны определения переменных:
 void *pv; int ival;
 char *pc; double dval;
 const string *ps;
 Перепишите следующие выражения, используя операторы явного преобразования типов:
 (a) pv = (void*)ps;
 (b) ival = int( *pc );
 (c) pv = &dva1;
 (d) pc = (char*) pv;
 4.15. Пример: реализация класса Stack
 Описывая операции инкремента и декремента, для иллюстрации применения их префиксной и постфиксной формы мы ввели понятие стека. Данная глава завершается примером реализации класса iStack – стека, позволяющего хранить элементы типа int.
 Как уже было сказано, с этой структурой возможны две основные операции – поместить элемент (push) и извлечь (pop) его. Другие операции позволяют получить информацию о текущем состоянии стека – пуст он (empty()) или полон (full()), сколько элементов в нем содержится (size()). Для начала наш стек будет предназначен лишь для элементов типа int. Вот объявление нашего класса:
 #include
 
 class iStack {
 public:
  iStack( int capacity )
  : _stack( capacity ), _top( 0 ) {}
 
  bool pop( int &va1ue );
  boot push( int value );
 
  bool full();
  bool empty();
  void display();
 
  int size();
 
 private:
  int _top;
  vector< int > _stack;
 };
 В данном случае мы используем вектор фиксированного размера: для иллюстрации использования префиксных и постфиксных операций инкремента и декремента этого достаточно. (В главе 6 мы модифицируем наш стек, придав ему возможность динамически меняться.)
 Элементы стека хранятся в векторе _stack. Переменная _top содержит индекс первой свободной ячейки стека. Этот индекс одновременно представляет количество заполненных ячеек. Отсюда реализация функции size(): она должна просто возвращать текущее значение _top.
 inline int iStack::size() { return _top; };
 empty() возвращает true, если _top равняется 0; full() возвращает true, если _top равен _stack.size()-1 (напомним, что индексация вектора начинается с 0, поэтому мы должны вычесть 1).
 inline bool iStack::empty() { return _top ? false : true; }
 inline bool iStack::full() {
  return _top < _stack.size()-l ? false : true;
 }
 Вот реализация функций pop() и push(). Мы добавили операторы вывода в каждую из них, чтобы следить за ходом выполнения:
 bool iStack::pop( int &top_va1ue ) {
  if ( empty() )
  return false;
 
  top_value = _stack[ --_top ];
  cout << "iStack::pop(): " << top_value << endl;
 
  return true;
 }
 
 bool iStack::push( int value ) {
  cout << "iStack::push( " << value << " )\n";
 
  if ( full() )
  return false;
 
  _stack[ _top++ ] = value;
  return true;
 }
 Прежде чем протестировать наш стек на примере, добавим функцию display(), которая позволит напечатать его содержимое. Для пустого стека она выведет:
 
  ( 0 )
 
 Для стека из четырех элементов – 0, 1, 2 и 3 – результатом функции display() будет:
 
 ( 4 )( bot: 0 1 2 3 :top )
 
 Вот реализация функции display():
 void iStack::display() {
  cout << "( " << size() << " )( bot: ";
 
  for ( int ix = 0; ix < _top; ++ix )
  cout << _stack[ ix ] << " ";
 
  cout << " :top )\n";
 }
 А вот небольшая программа для проверки нашего стека. Цикл for выполняется 50 раз. Четное значение (2, 4, 6, 8 и т.д.) помещается в стек. На каждой итерации, кратной 5 (5, 10, 15...), распечатывается текущее содержимое стека. На итерациях, кратных 10 (10, 20, 30...), из стека извлекаются два элемента и его содержимое распечатывается еще раз.
 #inc1ude
 #inc1ude "iStack.h"
 
 int main() {
  iStack stack( 32 ) ;
 
  stack.display();
  for ( int ix = 1; ix < 51; ++ix )
  {
  if ( ix%2 == 0 )
  stack.push( ix );
 
  if ( ix%5 == 0 )
  stack.display();
 
  if ( ix%10 == 0 ) {
  int dummy;
  stack.pop( dummy ); stack.pop( dummy );
  stack.display();
  }
 }
 Вот результат работы программы:
 
 ( 0 )( bot: :top )
 iStack push( 2 )
 iStack push( 4 )
 ( 2 )( bot: 2 4 :top )
 iStack push( 6 )
 iStack push( 8 )
 iStack push ( 10 )
 ( 5 )( bot: 2 4 6 8 10 :top )
 iStack pop(): 10
 iStack pop(): 8
 ( 3 )( bot: 2 4 6 :top )
 iStack push( 12 )
 iStack push( 14 )
 ( 5 )( bot: 2 4 6 12 14 :top )
 iStack::push( 16 )
 iStack::push( 18 )
 iStack::push( 20 )
 ( 8 )( bot: 2 4 6 12 14 16 18 20 :top )
 iStack::pop(): 20
 iStack::pop(): 18
 ( 6 )( bot: 2 4 6 12 14 16 :top )
 iStack::push( 22 )
 iStack::push( 24 )
 ( 8 )( bot: 2 4 6 12 14 16 22 24 :top )
 iStack::push( 26 )
 iStack::push( 28 )
 iStack::push( 30 )
 ( 11 )( bot: 2 4 6 12 14 16 22 24 26 28 30 :top )
 iStack::pop(): 30
 iStack::pop(): 28
 ( 9 )( bot: 2 4 6 12 14 16 22 24 26 :top )
 iStack::push( 32 )
 iStack::push( 34 )
 ( 11 )( bot: 2 4 6 12 14 16 22 24 26 32 34 :top )
 iStack::push( 36 )
 iStack::push( 38 )
 iStack::push( 40 )
 ( 14 )( bot: 2 4 6 12 14 16 22 24 26 32 34 36 38 40 :top )
 iStack::рор(): 40
 iStack::popQ: 38
 ( 12 )( bot: 2 4 6 12 14 16 22 24 26 32 34 36 :top )
 iStack::push( 42 )
 iStack::push( 44 )
 ( 14 )( bot: 2 4 6 12 14 16 22 24 26 32 34 36 42 44 :top )
 iStack::push( 46 )
 iStack::push( 48 )
 iStack::push( 50 )
 ( 17 )( bot: 2 4 6 12 14 16 22 24 26 32 34 36 42 44 46 48 50 :top )
 iStack::pop(): 50
 iStack::pop(): 48
 ( 15 )( bot: 2 4 6 12 14 16 22 24 26 32 34 36 42 44 46 :top )
 
 Упражнение 4.23
 Иногда требуется операция peek(), которая возвращает значение элемента на вершине стека без извлечения самого элемента. Реализуйте функцию peek() и добавьте к программе main() проверку работоспособности этой функции.
 Упражнение 4.24
 В чем вы видите два основных недостатка реализации класса iStack? Как их можно исправить?
 
 5. Инструкции
 Мельчайшей независимой частью С++ программы является инструкция. Она соответствует предложению естественного языка, но завершается точкой с запятой (;), а не точкой. Выражение С++ (например, ival + 5) становится простой инструкцией, если после него поставить точку с запятой. Составная инструкция – это последовательность простых, заключенная в фигурные скобки. По умолчанию инструкции выполняются в порядке записи. Как правило, последовательного выполнения недостаточно для решения реальных задач. Специальные управляющие конструкции позволяют менять порядок действий в зависимости от некоторых условий и повторять составную инструкцию определенное количество раз. Инструкции if, if-else и switch обеспечивают условное выполнение. Повторение обеспечивается инструкциями цикла while, do-while и for.
 5.1. Простые и составные инструкции
 Простейшей формой является пустая инструкция. Вот как она выглядит:
 ; // пустая инструкция
 Пустая инструкция используется там, где синтаксис С++ требует употребления инструкции, а логика программы – нет. Например, в следующем цикле while, копирующем одну строку в другую, все необходимые действия производятся внутри круглых скобок (условной части инструкции). Однако согласно правилам синтаксиса С++ после while должна идти инструкция. Поскольку нам нечего поместить сюда (вся работа уже выполнена), приходится оставить это место пустым:
 while ( *string++ = inBuf++ )
  ; // пустая инструкция
 Случайное появление лишней пустой инструкции не вызывает ошибки компиляции. Например, такая строка
 ival = dval + sval;; // правильно: лишняя пустая инструкция
 состоит из двух инструкций – сложения двух величин с присваиванием результата переменной ival и пустой.
 Простая инструкция состоит из выражения, за которым следует точка с запятой. Например:
 // простые инструкции
 int ival = 1024; // инструкция определения переменной
 ival; // выражение
 ival + 5; // еще одно выражение
 ival = ival +5; // присваивание
 Условные инструкции и инструкции цикла синтаксически требуют употребления единственной инструкции, связанной с ними. Однако, как правило, этого недостаточно. В таких случаях употребляются составные инструкции – последовательность простых, заключенная в фигурные скобки:
 if ( ival0 > ival1 ) {
  // составная инструкция, состоящая
  // из объявления и двух присваиваний
 
  int temp = ivalO;
  ivalO = ival1;
  ival1 = temp;
 }
 Составная инструкция может употребляться там же, где простая, и не нуждается в завершающей точке с запятой.
 Пустая составная инструкция эквивалентна пустой простой. Приведенный выше пример с пустой инструкцией можно переписать так:
 while ( *string++ = *inBuf++ )
  {} // пустая инструкция
 Составную инструкцию, содержащую определения переменных, часто называют блоком. Блок задает локальную область видимости в программе – идентификаторы, объявленные внутри блока (как temp в предыдущем примере), видны только в нем. (Блоки, области видимости и время жизни объектов рассматриваются в главе 8.)

<< Пред.           стр. 16 (из 121)           След. >>

Список литературы по разделу