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

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

 5.2. Инструкции объявления
 В С++ определение объекта, например
 int ival;
 рассматривается как инструкция объявления (хотя в данном случае более правильно было бы сказать определения). Ее можно использовать в любом месте программы, где разрешено употреблять инструкции. В следующем примере объявления помечены комментарием //#n, где n – порядковый номер.
 #include
 #include
 #include
 int main()
 {
  string fileName; // #1
 
  cout << "Введите имя файла: ";
  cin >> fileName;
 
  if ( fileName.empty() ) {
  // странный случай
  cerr << "Пустое имя файла. Завершение работы.\n";
  return -1;
  }
 
  ifstream inFile( fileName.c_str() ); // #2
  if ( ! inFile ) {
  cerr << "Невозможно открыть файл.\n";
  return -2;
  }
 
  string inBuf; // #3
  vector< string > text; // #4
  while ( inFile >> inBuf ) {
  for ( int ix = 0; ix < inBuf .size(); ++ix ) // #5
  // можно обойтись без ch,
  // но мы использовали его для иллюстрации
  if (( char ch = inBuf[ix] )=='.'){ // #6
  ch = '_';
  inBuf[ix] = ch;
  }
  text.push_back( inBuf );
  }
 
  if ( text.empty() )
  return 0;
  // одна инструкция объявления,
  // определяющая сразу два объекта
  vector::iterator iter = text.begin(), // #7
  iend = text.end();
 
  while ( iter != -iend ) {
  cout << *iter << '\n';
  ++iter;
  }
  return 0;
 }
 Программа содержит семь инструкций объявления и восемь определений объектов. Объявления действуют локально; переменная объявляется непосредственно перед первым использованием объекта.
 В 70-е годы философия программирования уделяла особое внимание тому, чтобы определения всех объектов находились в начале программы или блока, перед исполняемыми инструкциями. (В С, например, определение переменной не является инструкцией и обязано располагаться в начале блока.) В некотором смысле это была реакция на идиому использования переменных без предварительного объявления, чреватую ошибками. Такую идиому поддерживал, например, FORTRAN.
 Поскольку в С++ объявление является обычной инструкцией, ему разрешено появляться в любом месте программы, где допустимо употребление инструкции, что дает возможность использовать локальные объявления.
 Необходимо ли это? Для встроенных типов данных применение локальных объявлений является скорее вопросом вкуса. Язык их поощряет , разрешая объявлять переменные внутри условных частей инструкций if, if-else, switch, while, for. Те программисты, которые любят этот стиль, верят, что таким образом делают свои программы более понятными.
 Локальные объявления становятся необходимостью, когда мы используем объекты классов, имеющие конструкторы и деструкторы. Если мы помещаем все объявления в начало блока или функции, происходят две неприятные вещи:
 конструкторы всех объектов вызываются перед исполнением первой инструкции блока. Применение локальных объявлений позволяет “размазать” расходы на инициализацию по всему блоку;
 что более важно, блок или функция могут завершиться до того, как будут действительно использованы все объявленные в начале объекты. Скажем, наша программа из предыдущего примера имеет два аварийных выхода: при вводе пользователем пустого имени файла и при невозможности открыть файл с заданным именем. При этом последующие инструкции функции уже не выполняются. Если бы объекты inBuf и next были объявлены в начале блока, конструкторы и деструкторы этих объектов в случае ненормального завершения функции вызывались бы совершенно напрасно.
 Инструкция объявления может состоять из одного или более определений. Например, в нашей программе мы определяем два итератора вектора в одной инструкции:
 // одна инструкция объявления,
 // определяющая сразу два объекта
 vector::iterator iter = text.begin(),
  lend = text.end();
 Эквивалентная пара, определяющая по одному объекту, выглядит так:
 vector::iterator iter = text.begin();
 vector::iterator lend = text.end();
 Хотя определение одного или нескольких объектов в одном предложении является скорее вопросом вкуса, в некоторых случаях – например, при одновременном определении объектов, указателей и ссылок – это может спровоцировать появление ошибок. Скажем, в следующей инструкции не совсем ясно, действительно ли программист хотел определить указатель и объект или просто забыл поставить звездочку перед вторым идентификатором (используемые имена переменных наводят на второе предположение):
 // то ли хотел определить программист?
 string *ptrl, ptr2;
 Эквивалентная пара инструкций не позволит допустить такую ошибку:
 string *ptr1;
 string *ptr2;
 В наших примерах мы обычно группируем определения объектов в инструкции по сходству употребления. Например, в следующей паре
 int aCnt=0, eCnt=0, iCnt=0, oCnt=0, uCnt=0;
 int charCnt=0, wordCnt=0;
 первая инструкция объявляет пять очень похожих по назначению объектов – счетчиков пяти гласных латинского алфавита. Счетчики для подсчета символов и слов определяются во второй инструкции. Хотя такой подход нам кажется естественным и удобным, нет никаких причин считать его хоть чем-то лучше других.
 Упражнение 5.1
 Представьте себе, что вы являетесь руководителем программного проекта и хотите, чтобы применение инструкций объявления было унифицировано. Сформулируйте правила использования объявлений объектов для вашего проекта.
 Упражнение 5.2
 Представьте себе, что вы только что присоединились к проекту из предыдущего упражнения. Вы совершенно не согласны не только с конкретными правилами использования инструкций объявления, но и вообще с навязыванием каких-либо правил для этого. Объясните свою позицию.
 5.3. Инструкция if
 Инструкция if обеспечивает выполнение или пропуск инструкции или блока в зависимости от условия. Ее синтаксис таков:
 if ( условие )
  инструкция
 условие заключается в круглые скобки. Оно может быть выражением, как в этом примере:
 if(a+b>c) { ... }
 или инструкцией объявления с инициализацией:
 if ( int ival = compute_value() ){...}
 Область видимости объекта, объявленного в условной части, ограничивается ассоциированной с if инструкцией или блоком. Например, такой код вызывает ошибку компиляции:
 if ( int ival = compute_value() ) {
  // область видимости ival
  // ограничена этим блоком
 }
 
 // ошибка: ival невидим
 if ( ! ival ) ...
 Попробуем для иллюстрации применения инструкции if реализовать функцию min(), возвращающую наименьший элемент вектора. Заодно наша функция будет подсчитывать число элементов, равных минимуму. Для каждого элемента вектора мы должны проделать следующее:
 Сравнить элемент с текущим значением минимума.
 Если элемент меньше, присвоить текущему минимуму значение элемента и сбросить счетчик в 1.
 Если элемент равен текущему минимуму, увеличить счетчик на 1.
 В противном случае ничего не делать.
 После проверки последнего элемента вернуть значение минимума и счетчика.
 Необходимо использовать две инструкции if:
 if ( minVal > ivec[ i ] )...// новое значение minVal
 if ( minVal == ivec[ i ] )...// одинаковые значения
 Довольно часто программист забывает использовать фигурные скобки, если нужно выполнить несколько инструкций в зависимости от условия:
 if ( minVal > ivec[ i ] )
  minVal = ivec[ i ];
  occurs = 1; // не относится к if!
 Такую ошибку трудно увидеть, поскольку отступы в записи подразумевают, что и minVal=ivec[i], и occurs=1 входят в одну инструкцию if. На самом же деле инструкция
 occurs = 1;
 не является частью if и выполняется безусловно, всегда сбрасывая occurs в 1. Вот как должна быть составлена правильная if-инструкция (точное положение открывающей фигурной скобки является поводом для бесконечных споров):
 if ( minVal > ivec[ i ] )
 {
  minVal = ivec[ i ];
  occurs = 1;
 }
 Вторая инструкция if выглядит так:
 if ( minVal == ivec [ i ] )
  ++occurs;
 Заметим, что порядок следования инструкций в этом примере крайне важен. Если мы будем сравнивать minVal именно в такой последовательности, наша функция всегда будет ошибаться на 1:
 if ( minVal > ivec[ i ] ) {
  minVal = ivec[ i ];
  occurs = 1;
 }
 // если minVal только что получила новое значение,
 // то occurs будет на единицу больше, чем нужно
 if ( minVal == ivec[ i ] )
  ++occurs;
 Выполнение второго сравнения не обязательно: один и тот же элемент не может одновременно быть и меньше и равен minVal. Поэтому появляется необходимость выбора одного из двух блоков в зависимости от условия, что реализуется инструкцией if-else, второй формой if-инструкции. Ее синтаксис выглядит таким образом:
 if ( условие )
  инструкция1
 else
  инструкция2
 инструкция1 выполняется, если условие истинно, иначе переходим к инструкция2. Например:
 if ( minVal == ivec[ i ] )
  ++occurs;
 else
 if ( minVal > ivec[ i ] ) {
  minVal = ivec[ i ];
  occurs = 1;
 }
 Здесь инструкция2 сама является if-инструкцией. Если minVal меньше ivec[i], никаких действий не производится.
 В следующем примере выполняется одна из трех инструкций:
 if ( minVal < ivec[ i ] )
  {} // пустая инструкция
 else
 if ( minVal > ivec[ i ] ) {
  minVal = ivec[ i ];
  occurs = 1;
 }
 else // minVal == ivec[ i ]
  ++occurs;
 Составные инструкции if-else могут служить источником неоднозначного толкования, если частей else больше, чем частей if. К какому из if отнести данную часть else? (Эту проблему иногда называют проблемой висячего else). Например:
 if ( minVal <= ivec[ i ] )
  if ( minVal == ivec[ i ] )
  ++occurs;
 else {
  minVal = ivec[ i ];
  occurs = 1;
 }
 Судя по отступам, программист предполагает, что else относится к самому первому, внешнему if. Однако в С++ неоднозначность висячих else разрешается соотнесением их с последним встретившимся if. Таким образом, в действительности предыдущий фрагмент означает следующее:
 if ( minVal <= ivec[ i ] ) {
  if ( minVal == ivec[ i ] )
  ++occurs;
  else {
  minVal = ivec[ i ];
  occurs = 1;
  }
 }
 Одним из способов разрешения данной проблемы является заключение внутреннего if в фигурные скобки:
 if ( minVal <= ivec[ i ] ) {
  if ( minVal == ivec[ i ] )
  ++occurs;
 }
 else {
  minVal = ivec[ i ];
  occurs = 1;
 }
 В некоторых стилях программирования рекомендуется всегда употреблять фигурные скобки при использовании инструкций if-else, чтобы не допустить возможности неправильной интерпретации кода.
 Вот первый вариант функции min(). Второй аргумент функции будет возвращать количество вхождений минимального значения в вектор. Для перебора элементов массива используется цикл for. Но мы допустили ошибку в логике программы. Сможете ли вы заметить ее?
 #include
 
 int min( const vector &ivec, int &occurs )
 {
  int minVal = 0;
  occurs = 0;
 
  int size = ivec.size();
 
  for ( int ix = 0; ix < size; ++ix ) {
  if ( minVal == ivec[ ix ] )
  ++occurs;
  else
  if ( minVal > ivec[ ix ] ) {
  minVal = ivec[ ix ];
  occurs = 1;
  }
  }
  return minVal;
 }
 Обычно функция возвращает только одно значение. Однако согласно нашей спецификации в точке вызова должно быть известно не только само минимальное значение, но и количество его вхождений в вектор. Для возврата второго значения мы использовали параметр типа ссылка. (Параметры-ссылки рассматриваются в разделе 7.3.) Любое присваивание значения ссылке occurs изменяет значение переменной, на которую она ссылается:
 int main()
 {
  int occur_cnt = 0;
  vector< int > ivec;
 
  // occur_cnt получает значение occurs
  // из функции min()
  int minval = min( ivec, occur_cnt );
 
  // ...
 }
 Альтернативой использованию параметра-ссылки является применение объекта класса pair, представленного в разделе 3.14. Функция min() могла бы возвращать два значения в одной паре:
 // альтернативная реализация
 // с помощью пары
 
 #include
 #include
 
 typedef pair min_va1_pair;
 
 min_va1_pair
 min( const vector &ivec )
 {
  int minVal = 0;
  int occurs = 0;
 
  // то же самое ...
 
  return make_pair( minVal, occurs );
 }
 К сожалению, и эта реализация содержит ошибку. Где же она? Правильно: мы инициализировали minVal нулем, поэтому, если минимальный элемент вектора больше нуля, наша реализация вернет нулевое значение минимума и нулевое значение количества вхождений.
 Программу можно изменить, инициализировав minVal первым элементом вектора:
 int minVal = ivec[0];
 Теперь функция работает правильно. Однако в ней выполняются некоторые лишние действия, снижающие ее эффективность.
 // исправленная версия min()
 // оставляющая возможность для оптимизации ...
 
 int minVal = ivec[0];
 occurs = 0;
 
 int size = ivec.size();
 
 for ( int ix = 0; ix < size; ++ix )
 {
  if ( minVal == ivec[ ix ] )
  ++occurs;
 
  // ...
 Поскольку ix инициализируется нулем, на первой итерации цикла значение первого элемента сравнивается с самим собой. Можно инициализировать ix единицей и избежать ненужного выполнения первой итерации. Однако при оптимизации кода мы допустили другую ошибку (наверное, стоило все оставить как было!). Сможете ли вы ее обнаружить?
 // оптимизированная версия min(),
 // к сожалению, содержащая ошибку...
 
 int minVal = ivec[0];
 occurs = 0;
 
 int size = ivec.size();
 
 for ( int ix = 1; ix < size; ++ix )
 {
  if ( minVal == ivec[ ix ] )
  ++occurs;
 
  // ...
 Если ivec[0] окажется минимальным элементом, переменная occurs не получит значения 1. Конечно, исправить это очень просто, но сначала надо найти ошибку:
 int minVal = ivec[0];
 occurs = 1;
 К сожалению, подобного рода недосмотры встречаются не так уж редко: программисты тоже люди и могут ошибаться. Важно понимать, что это неизбежно, и быть готовым тщательно тестировать и анализировать свои программы.
 Вот окончательная версия функции min() и программа main(), проверяющая ее работу:
 #include
 #include
 
 int min( const vector< int > &ivec, int &occurs )
 {
  int minVal = ivec[ 0 ];
  occurs = 1;
 
  int size = ivec.size();
  for ( int ix = 1; ix < size; ++ix )
  {
  if ( minVal == ivec[ ix ] )
  ++occurs;
  else
  if ( minVal > ivec[ ix ] ){
  minVal = ivec[ ix ];
  occurs = 1;
  }
  }
  return minVal;
 }
 
 int main()
 {
  int ia[] = { 9,1,7,1,4,8,1,3,7,2,6,1,5,1 };
  vector ivec( ia, ia+14 );
 
  int occurs = 0;
  int minVal = min( ivec, occurs );

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

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