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

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

  strcpy( pc2, pc );
  if ( strcmp( pc2, pc ))
  ++errors;
 
  delete [] pc2;
  }
  cout << "C-строки: "
  << errors << " ошибок.\n";
 }
 
 // ***** Реализация с использованием класса string *****
 
 #include
 #include
 
 int main()
 {
  int errors = 0;
  string str( "a very long literal string" );
 
  for ( int ix = 0; ix < 1000000; ++ix )
  {
  int len = str.size();
  string str2 = str;
  if ( str != str2 )
  }
  cout << "класс string: "
  << errors << " ошибок.\n;
 }
 Что эти программы делают?
 Оказывается, вторая реализация выполняется в два раза быстрее первой. Ожидали ли вы такого результата? Как вы его объясните?
 Упражнение 3.15
 Могли бы вы что-нибудь улучшить или дополнить в наборе операций класса string, приведенных в последнем разделе? Поясните свои предложения.
 3.5. Спецификатор const
 Возьмем следующий пример кода:
 for ( int index = 0; index < 512; ++index )
  ... ;
 С использованием литерала 512 связаны две проблемы. Первая состоит в легкости восприятия текста программы. Почему верхняя граница переменной цикла должна быть равна именно 512? Что скрывается за этой величиной? Она кажется случайной...
 Вторая проблема касается простоты модификации и сопровождения кода. Предположим, программа состоит из 10 000 строк, и литерал 512 встречается в 4% из них. Допустим, в 80% случаев число 512 должно быть изменено на 1024. Способны ли вы представить трудоемкость такой работы и количество ошибок, которые можно сделать, исправив не то значение?
 Обе эти проблемы решаются одновременно: нужно создать объект со значением 512. Присвоив ему осмысленное имя, например bufSize, мы сделаем программу гораздо более понятной: ясно, с чем именно сравнивается переменная цикла.
 index < bufSize
 В этом случае изменение размера bufSize не требует просмотра 400 строк кода для модификации 320 из них. Насколько уменьшается вероятность ошибок ценой добавления всего одного объекта! Теперь значение 512 локализовано.
 int bufSize = 512; // размер буфера ввода
 // ...
 
 for ( int index = 0; index < bufSize; ++index )
  // ...
 Остается одна маленькая проблема: переменная bufSize здесь является l-значением, которое можно случайно изменить в программе, что приведет к трудно отлавливаемой ошибке. Вот одна из распространенных ошибок – использование операции присваивания (=) вместо сравнения (==):
 // случайное изменение значения bufSize
 if ( bufSize = 1 )
  // ...
 В результате выполнения этого кода значение bufSize станет равным 1, что может привести к совершенно непредсказуемому поведению программы. Ошибки такого рода обычно очень тяжело обнаружить, поскольку они попросту не видны.
 Использование спецификатора const решает данную проблему. Объявив объект как
 const int bufSize = 512; // размер буфера ввода
 мы превращаем переменную в константу со значением 512, значение которой не может быть изменено: такие попытки пресекаются компилятором: неверное использование оператора присваивания вместо сравнения, как в приведенном примере, вызовет ошибку компиляции.
 // ошибка: попытка присваивания значения константе
 if ( bufSize = 0 ) ...
 Раз константе нельзя присвоить значение, она должна быть инициализирована в месте своего определения. Определение константы без ее инициализации также вызывает ошибку компиляции:
 const double pi; // ошибка: неинициализированная константа
 Давайте рассуждать дальше. Явная трансформация значения константы пресекается компилятором. Но как быть с косвенной адресацией? Можно ли присвоить адрес константы некоторому указателю?
 const double minWage = 9.60;
 
 // правильно? ошибка?
 double *ptr = &minWage;
 Должен ли компилятор разрешить подобное присваивание? Поскольку minWage – константа, ей нельзя присвоить значение. С другой стороны, ничто не запрещает нам написать:
 *ptr += 1.40; // изменение объекта minWage!
 Как правило, компилятор не в состоянии уберечь от использования указателей и не сможет сигнализировать об ошибке в случае подобного их употребления. Для этого требуется слишком глубокий анализ логики программы. Поэтому компилятор просто запрещает присваивание адресов констант обычным указателям.
 Что же, мы лишены возможности использовать указатели на константы? Нет. Для этого существуют указатели, объявленные со спецификатором const:
 const double *cptr;
 где cptr – указатель на объект типа const double. Тонкость заключается в том, что сам указатель – не константа, а значит, мы можем изменять его значение. Например:
 const double *pc = 0;
 const double minWage = 9.60;
 
 // правильно: не можем изменять minWage с помощью pc
 pc = &minWage;
 
 double dval = 3.14;
 
 // правильно: не можем изменять minWage с помощью pc
 // хотя dval и не константа
 pc = &dval; // правильно
 
 dval = 3.14159; //правильно
 *pc = 3.14159; // ошибка
 Адрес константного объекта присваивается только указателю на константу. Вместе с тем, такому указателю может быть присвоен и адрес обычной переменной:
 pc = &dval;
 Константный указатель не позволяет изменять адресуемый им объект с помощью косвенной адресации. Хотя dval в примере выше и не является константой, компилятор не допустит изменения переменной dval через pc. (Опять-таки потому, что он не в состоянии определить, адрес какого объекта может содержать указатель в произвольный момент выполнения программы.)
 В реальных программах указатели на константы чаще всего употребляются как формальные параметры функций. Их использование дает гарантию, что объект, переданный в функцию в качестве фактического аргумента, не будет изменен этой функцией. Например:
 // В реальных программах указатели на константы чаще всего
 // употребляются как формальные параметры функций
 int strcmp( const char *str1, const char *str2 );
 (Мы еще поговорим об указателях на константы в главе 7, когда речь пойдет о функциях.)
 Существуют и константные указатели. (Обратите внимание на разницу между константным указателем и указателем на константу!). Константный указатель может адресовать как константу, так и переменную. Например:
 int errNumb = 0;
 int *const currErr = &errNumb;
 Здесь curErr – константный указатель на неконстантный объект. Это значит, что мы не можем присвоить ему адрес другого объекта, хотя сам объект допускает модификацию. Вот как мог бы быть использован указатель curErr:
 do_something();
 
 if ( *curErr ) {
  errorHandler();
  *curErr = 0; // правильно: обнулим значение errNumb
 }
 Попытка присвоить значение константному указателю вызовет ошибку компиляции:
 curErr = &myErNumb; // ошибка
 Константный указатель на константу является объединением двух рассмотренных случаев.
 const double pi = 3.14159;
 const double *const pi_ptr = π
 Ни значение объекта, на который указывает pi_ptr, ни значение самого указателя не может быть изменено в программе.
 Упражнение 3.16
 Объясните значение следующих пяти определений. Есть ли среди них ошибочные?
 (a) int i; (d) int *const cpi;
 (b) const int ic; (e) const int *const cpic;
 (c) const int *pic;
 Упражнение 3.17
 Какие из приведенных определений правильны? Почему?
 (a) int i = -1;
 (b) const int ic = i;
 (c) const int *pic = ⁣
 (d) int *const cpi = ⁣
 (e) const int *const cpic = ⁣
 Упражнение 3.18
 Используя определения из предыдущего упражнения, укажите правильные операторы присваивания. Объясните.
 (a) i = ic; (d) pic = cpic;
 (b) pic = ⁣ (i) cpic = ⁣
 (c) cpi = pic; (f) ic = *cpic;
 3.6. Ссылочный тип
 Ссылочный тип, иногда называемый псевдонимом, служит для задания объекту дополнительного имени. Ссылка позволяет косвенно манипулировать объектом, точно так же, как это делается с помощью указателя. Однако эта косвенная манипуляция не требует специального синтаксиса, необходимого для указателей. Обычно ссылки употребляются как формальные параметры функций. В этом разделе мы рассмотрим самостоятельное использование объектов ссылочного типа.
 Ссылочный тип обозначается указанием оператора взятия адреса (&) перед именем переменной. Ссылка должна быть инициализирована. Например:
 int ival = 1024;
 
 // правильно: refVal - ссылка на ival
 int &refVal = ival;
 
 // ошибка: ссылка должна быть инициализирована
 int &refVal2;
 Хотя, как мы говорили, ссылка очень похожа на указатель, она должна быть инициализирована не адресом объекта, а его значением. Таким объектом может быть и указатель:
 int ival = 1024;
 
 // ошибка: refVal имеет тип int, а не int*
 int &refVal = &ival;
 
 int *pi = &ival;
 
 // правильно: ptrVal - ссылка на указатель
 int *&ptrVal2 = pi;
 Определив ссылку, вы уже не сможете изменить ее так, чтобы работать с другим объектом (именно поэтому ссылка должна быть инициализирована в месте своего определения). В следующем примере оператор присваивания не меняет значения refVal, новое значение присваивается переменной ival – ту, которую адресует refVal.
 int min_val = 0;
 
 // ival получает значение min_val,
 // а не refVal меняет значение на min_val
 refVal = min_val;
 Все операции со ссылками реально воздействуют на адресуемые ими объекты. В том числе и операция взятия адреса. Например:
 refVal += 2;
 прибавляет 2 к ival – переменной, на которую ссылается refVal. Аналогично
 int ii = refVal;
 присваивает ii текущее значение ival,
 int *pi = &refVal;
 инициализирует pi адресом ival.
 Если мы определяем ссылки в одной инструкции через запятую, перед каждым объектом типа ссылки должен стоять амперсанд (&) – оператор взятия адреса (точно так же, как и для указателей). Например:
 // определено два объекта типа int
 int ival = 1024, ival2 = 2048;
 
 // определена одна ссылка и один объект
 int &rval = ival, rval2 = ival2;
 
 // определен один объект, один указатель и одна ссылка
 int inal3 = 1024, *pi = ival3, &ri = ival3;
 
 // определены две ссылки
 int &rval3 = ival3, &rval4 = ival2;
 Константная ссылка может быть инициализирована объектом другого типа (если, конечно, существует возможность преобразования одного типа в другой), а также безадресной величиной – такой, как литеральная константа. Например:
 double dval = 3.14159;
 
 // верно только для константных ссылок
 const int &ir = 1024;
 const int &ir2 = dval;
 const double &dr = dval + 1.0;
 Если бы мы не указали спецификатор const, все три определения ссылок вызвали бы ошибку компиляции. Однако, причина, по которой компилятор не пропускает таких определений, неясна. Попробуем разобраться.
 Для литералов это более или менее понятно: у нас не должно быть возможности косвенно поменять значение литерала, используя указатели или ссылки. Что касается объектов другого типа, то компилятор преобразует исходный объект в некоторый вспомогательный. Например, если мы пишем:
 double dval = 1024;
 const int &ri = dval;
 то компилятор преобразует это примерно так:
 int temp = dval;
 const int &ri = temp;
 Если бы мы могли присвоить новое значение ссылке ri, мы бы реально изменили не dval, а temp. Значение dval осталось бы тем же, что совершенно неочевидно для программиста. Поэтому компилятор запрещает такие действия, и единственная возможность проинициализировать ссылку объектом другого типа – объявить ее как const.
 Вот еще один пример ссылки, который трудно понять с первого раза. Мы хотим определить ссылку на адрес константного объекта, но наш первый вариант вызывает ошибку компиляции:
 const int ival = 1024;
 
 // ошибка: нужна константная ссылка
 int *&pi_ref = &ival;
 Попытка исправить дело добавлением спецификатора const тоже не проходит:
 const int ival = 1024;
 
 // все равно ошибка
 const int *&pi_ref = &ival;
 В чем причина? Внимательно прочитав определение, мы увидим, что pi_ref является ссылкой на константный указатель на объект типа int. А нам нужен неконстантный указатель на константный объект, поэтому правильной будет следующая запись:
 const int ival = 1024;
 
 // правильно
 int *const &pi­ref = &ival;
 Между ссылкой и указателем существуют два основных отличия. Во-первых, ссылка обязательно должна быть инициализирована в месте своего определения. Во-вторых, всякое изменение ссылки преобразует не ее, а тот объект, на который она ссылается. Рассмотрим на примерах. Если мы пишем:
 int *pi = 0;
 мы инициализируем указатель pi нулевым значением, а это значит, что pi не указывает ни на какой объект. В то же время запись
 const int &ri = 0;
 означает примерно следующее:
 int temp = 0;
 const int &ri = temp;
 Что касается операции присваивания, то в следующем примере:
 int ival = 1024, ival2 = 2048;
 int *pi = &ival, *pi2 = &ival2;
 
 pi = pi2;
 переменная ival, на которую указывает pi, остается неизменной, а pi получает значение адреса переменной ival2. И pi, и pi2 и теперь указывают на один и тот же объект ival2.
 Если же мы работаем со ссылками:
 int &ri = ival, &ri2 = ival2;
 
 ri = ri2;
 то само значение ival меняется, но ссылка ri по-прежнему адресует ival.
 В реальных С++ программах ссылки редко используются как самостоятельные объекты, обычно они употребляются в качестве формальных параметров функций. Например:
 // пример использования ссылок
 
 // Значение возвращается в параметре next_value
 bool get_next_value( int &next_value );
 
 // перегруженный оператор
 Matrix operator+( const Matrix&, const Matrix& );
 Как соотносятся самостоятельные объекты-ссылки и ссылки-параметры? Если мы пишем:
 int ival;
 while (get_next_value( ival )) ...
 это равносильно следующему определению ссылки внутри функции:
 int &next_value = ival;
 (Подробнее использование ссылок в качестве формальных параметров функций рассматривается в главе 7.)
 Упражнение 3.19
 Есть ли ошибки в данных определениях? Поясните. Как бы вы их исправили?
 (a) int ival = 1.01; (b) int &rval1 = 1.01;
 (c) int &rval2 = ival; (d) int &rval3 = &ival;
 (e) int *pi = &ival; (f) int &rval4 = pi;
 (g) int &rval5 = pi*; (h) int &*prval1 = pi;
 (i) const int &ival2 = 1; (j) const int &*prval2 = &ival;
 Упражнение 3.20
 Если ли среди нижеследующих операций присваивания ошибочные (используются определения из предыдущего упражнения)?
 (a) rval1 = 3.14159;
 (b) prval1 = prval2;
 (c) prval2 = rval1;
 (d) *prval2 = ival2;
 Упражнение 3.21
 Найдите ошибки в приведенных инструкциях:
 (a) int ival = 0;
  const int *pi = 0;
  const int &ri = 0;
 
 (b) pi = &ival;
  ri = &ival;
  pi = &rval;
 3.7. Тип bool
 Объект типа bool может принимать одно из двух значений: true и false. Например:
 // инициализация строки
 string search_word = get_word();
 
 // инициализация переменной found
 bool found = false;
 
 string next_word;
 while ( cin >> next_word )
  if ( next_word == search_word )
  found = true;
 // ...
 
 // сокращенная запись: if ( found == true )
 if ( found )
  cout << "ok, мы нашли слово\n";
  else cout << "нет, наше слово не встретилось.\n";
 Хотя bool относится к одному из целых типов, он не может быть объявлен как signed, unsigned, short или long, поэтому приведенное определение ошибочно:
 // ошибка
 short bool found = false;
 Объекты типа bool неявно преобразуются в тип int. Значение true превращается в 1, а false – в 0. Например:
 bool found = false;
 int occurrence_count = 0;
 
 while ( /* mumble */ )
 {
  found = look_for( /* something */ );
 
  // значение found преобразуется в 0 или 1
  occurrence_count += found;
 }
 Таким же образом значения целых типов и указателей могут быть преобразованы в значения типа bool. При этом 0 интерпретируется как false, а все остальное как true:
 // возвращает количество вхождений
 extern int find( const string& );
 bool found = false;
 if ( found = find( "rosebud" ))
  // правильно: found == true
 
 // возвращает указатель на элемент
 extern int* find( int value );
 
 if ( found = find( 1024 ))
  // правильно: found == true
 3.8. Перечисления
 Нередко приходится определять переменную, которая принимает значения из некоего набора. Скажем, файл открывают в любом из трех режимов: для чтения, для записи, для добавления.

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

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