Основы C Моим детям. Никогда не смейтесь, помогая мне осваивать
Скачать 1.68 Mb.
|
if (x < 0.0) x= -x; // Теперь мы знаем, что x >= 0.0 (постусловие) Ветви инструкции if являются областями видимости, что делает следующие инструкции неверными: 02_ch01.indd 49 14.07.2016 10:46:11 Основы C++ 50 if (x < 0.0) int absx = -x; else int absx = x; cout << "|x| = " << absx << "\n"; // absx уже за областью видимости Выше мы ввели две новые переменные, обе имеющие имя absx. Они не кон- фликтуют, поскольку находятся в разных областях видимости. Ни одна из них не существует после инструкции if, и обращение к absx в последней строке являет- ся ошибкой. На самом деле переменные, объявленные в ветвях, могут использо- ваться только внутри этих ветвей. Каждая ветвь if состоит из одной инструкции. Для выполнения нескольких операций мы можем использовать фигурные скобки, как в приведенной реализа- ции метода Кардано: double D= q*q /4.0 + p*p*p /27.0; if (D > 0.0) { double z1= ...; complex } else if (D == 0.0) { double z1 = ..., z2 = ..., z3 = ...; } else { // D < 0.0 complex } Всегда полезно вначале написать фигурные скобки. Многие руководства по стилю программирования требуют применять фигурные скобки даже для одной инструкции, тогда как автор предпочитает в этом случае обходиться без них. В любом случае настоятельно рекомендуется использовать отступ ветви для луч- шей удобочитаемости. Инструкции if могут быть вложенными; при этом каждое else связано с по- следним открытым if. Если вас интересуют конкретные примеры, обратитесь к разделу A.2.3. И вот еще один совет. Совет Хотя пробелы никак не влияют на компиляцию программ C++, отступы должны отражать структуру программы. Редакторы, понимающие C++ (наподобие интег- рированной среды разработки Visual Studio или редактора emacs в режиме C++) и автоматически добавляющие отступы, очень облегчают структурное программи- рование. Всякий раз, когда строка имеет не тот отступ, который ожидается, скорее всего, имеет место не та вложенность, которая предполагалась. 02_ch01.indd 50 14.07.2016 10:46:11 1.4. Выражения и инструкции 51 1.4.3.2. Условное выражение Хотя в этом разделе описываются инструкции, мы бы хотели поговорить здесь об условном выражении из-за его близости к инструкции if. Результатом выпол- нения condition ? result_for_true : result_for_false является второе подвыражение (т.е. result_for_true), если вычисление condition дает true, и result_for_false — в противном случае. Например, min = x <= y ? x : y; соответствует следующей инструкции if: if ( x <= y ) min = x ; else min = y ; Для начинающих вторая версия может быть более удобочитаемой, но опытные программисты часто предпочитают первую версию из-за ее краткости. ?: является выражением и, следовательно, может использоваться для инициа- лизации переменных: int x = f(a), y = x < 0 ? -x : 2*x ; С помощью этого оператора легко вызвать функцию с выбором нескольких аргументов: f(a, (x < 0 ? b : c), (y < 0 ? d : e)); Это будет выглядеть очень неуклюже при использовании инструкции if. Если вы этому не верите, попробуйте сами. В большинстве случаев не важно, что используется: if или условное выраже- ние. Так что используйте то, что удобнее для вас. Забавно. Примером, в котором существенен выбор между if и ?:, является опе- рация replace_copy из стандартной библиотеки шаблонов (STL). Она обыч- но реализуется с помощью условного оператора, в то время как if было бы бо- лее обобщенным решением. Эта “ошибка” оставалась ненайденной около 10 лет и была обнаружена только с помощью автоматического анализа в кандидатской диссертации Джереми Сика (Jeremy Siek) [38]. 1.4.3.3. Инструкция switch Инструкция switch представляет собой особую разновидность инструкции if. Она обеспечивает краткую запись ситуации, когда для различных целочисленных значений должны выполняться разные действия: 02_ch01.indd 51 14.07.2016 10:46:12 Основы C++ 52 switch ( op_code ) { case 0: z = x + y; break; case 1: z = x - y; cout << " разность\n"; break; case 2: case 3: z = x * y; break; default: z = x / y; } Несколько неожиданным поведением является продолжение выполнения кода следующих вариантов, если только мы не прекратим выполнение с помощью ин- струкции break. Таким образом, для случаев 2 и 3 в нашем примере выполняются одни и те же операции. Расширенное использование switch можно найти в при- ложении A.2.4. 1.4.4. Циклы 1.4.4.1. Циклы while и do-while Как предполагается в названии, цикл while повторяется до тех пор, пока вы- полняется некоторое условие. Давайте реализуем пример с рядом Коллатца, опре- деляемым следующим образом. Алгоритм 1.1. Ряд Коллатца Вход. x 0 1 while x i ≠ 1 do 2 x i = 3x i – 1 + 1, если x i – 1 нечетно; x i – 1 / 2, если x i – 1 четно. Если не беспокоиться о переполнении, реализовать этот алгоритм очень прос- то с помощью цикла while: int x= 19; while (x != 1) { cout << x << '\n'; if (x % 2 == 1) // Нечетно x= 3 * x + 1; else // Четно x= x / 2; } Как и инструкцию if, цикл можно записывать без фигурных скобок, если в нем только одна инструкция. C++ предлагает также цикл do-while. В этом случае условие продолжения вы- полнения цикла проверяется в конце итерации: double eps= 0.001; do { cout << "eps= " << eps << '\n'; 02_ch01.indd 52 14.07.2016 10:46:12 1.4. Выражения и инструкции 53 eps /= 2.0; } while ( eps > 0.0001 ); Такой цикл выполняется как минимум один раз — даже для очень малого значе- ния eps в нашем примере. 1.4.4.2. Цикл for Наиболее распространенным циклом в C++ является цикл for. В качестве про- стого примера сложим два вектора 6 и выведем получившийся результат: double v[3], w[] = {2., 4., 6.}, x[] = {6., 5., 4}; for(int i = 0; i < 3; ++i) v[i]= w[i] + x[i]; for(int i = 0; i < 3; ++i) cout << "v[" << i << "]= " << v[i] << '\n'; Заголовок этого цикла состоит из трех компонентов: инициализация; • условие • продолжения ; операция продвижения. • В приведенном выше примере мы видим типичный цикл for. В инициализа- ции обычно объявляется и инициализируется (как правило, нулем — это началь- ный индекс большинства индексируемых структур данных) новая переменная. В условии обычно проверяется, меньше ли индекс цикла определенного значения, а последняя операция обычно увеличивает индексную переменную цикла. В этом примере мы выполняем преинкремент переменной цикла i. Для встроенных ти- пов, таких как int, не имеет значения, пишем ли мы ++i или i++. Однако это имеет значение для пользовательских типов, для которых постфиксный инкре- мент выполняет излишнее копирование (ср. с разделом 3.3.2.5). Чтобы быть пос- ледовательными, в этой книге мы всегда используем префиксный инкремент для индексов цикла. Очень популярной ошибкой начинающих является запись условия как i <= size(..). Поскольку индексы в C++ начинаются с нуля, индекс i == size(..) выходит за границы диапазона. Людям с опытом работы в Fortran или MATLAB необходимо некоторое время, чтобы привыкнуть к индексации “с нуля”. Для многих индексация, начинающаяся с единицы, кажется более ес- тественной, а кроме того, она используется в математической литературе. Одна- ко расчеты индексов и адресов почти всегда проще вести при нулевом начальном индексе. 6 Позже мы рассмотрим истинные классы векторов, а пока что возьмем простые массивы. 02_ch01.indd 53 14.07.2016 10:46:12 Основы C++ 54 В качестве еще одного примера вычислим ряд Тейлора для экспоненциальной функции: 0 ! n x i x e n ∞ = = ∑ до десятого члена: double x= 2.0, xn= 1.0, exp_x = 1.0; unsigned long fac= 1; for(unsigned long i = 1; i <= 10; ++i) { xn *= x; fac *= i; exp_x += xn / fac; cout << "eˆx = " << exp_x << '\n'; } Здесь оказывается гораздо проще вычислить нулевой член отдельно и начать цикл с первого члена. Мы также использовали в условии сравнение “меньше или равно” для того, чтобы гарантировать вычисление члена x 10 /10!. Цикл for в C++ очень гибкий. Инициализирующая часть может быть любым выражением, объявлением переменной или пустой. Можно вводить в этой части несколько новых переменных одного и того же типа. Это может использоваться для того, чтобы избежать повторения вычислений в условии одной и той же опе- рации, например for(int i = xyz.begin(), end = xyz.end(); i < end; ++i) ... Переменные, объявленные в части инициализации, видимы только в цикле и скрывают переменные с теми же именами вне цикла. Условие может быть любым выражением, преобразуемым в bool. Пустое ус- ловие всегда истинно и цикл в этом случае повторяется бесконечно. Он может быть прекращен внутри тела цикла — этот способ мы рассмотрим в следующем разделе. Мы уже упоминали, что индекс цикла обычно увеличивается в третье подвыражении for. В принципе, мы можем изменять его и в теле цикла. Однако код станет гораздо понятнее, если делать это только в заголовке цикла. С другой стороны, нет никаких ограничений, требующих использования только одной пе- ременной, и увеличения ее значения только на 1. Мы можем изменять столько переменных, сколько захотим, используя оператор запятой (раздел 1.3.5), причем изменять любым способом, например for(int i = 0, j = 0, p = 1; ...; ++i, j+= 4, p*= 2) ... Это, конечно, сложнее, чем простое увеличение индекса цикла, но все равно более удобочитаемо, чем объявление/изменение индексных переменных перед циклом или внутри его тела. 02_ch01.indd 54 14.07.2016 10:46:13 1.4. Выражения и инструкции 55 1.4.4.3. Цикл for для диапазона Очень компактная запись получается при использовании новой возможности C++, которая именуется циклом for для диапазона . Мы поговорим о нем более подробно при рассмотрении концепции итераторов (раздел 4.1.2). Пока что мы будем рассматривать его как сжатую форму записи для выполне- ния итерации над всеми записями массива или другого контейнера: int primes []= {2, 3, 5, 7, 11, 13, 17, 19}; for(int i : primes ) std::cout << i << " "; Этот код выводит все числа массива, разделенные пробелами. 1.4.4.4. Управление циклом Имеются две инструкции, которые изменяют обычную работу цикла: break • ; continue • Инструкция break полностью завершает цикл, а continue завершает только текущую итерацию и заставляет цикл перейти к следующей итерации, например for (...; ...; ...) { if (dx == 0.0) continue; x+= dx; if (r < eps) break; } В приведенном выше примере мы считаем, что оставшаяся часть итерации не нужна, если dx == 0.0. В некоторых итеративных вычислениях в средине итера- ции может стать понятно, что работа уже выполнена (здесь при r < eps). 1.4.5. goto Все ветвления и циклы внутренне реализуются с помощью переходов. C++ предоставляет инструкцию безусловного перехода goto. Однако учтите следую- щий совет. Совет Не используйте goto! Нигде и никогда! C++11 02_ch01.indd 55 14.07.2016 10:46:36 Основы C++ 56 Применение goto в C++ более ограничено, чем в C (например, мы не можем выполнять переход через инициализации), но по-прежнему может разрушить структуру нашей программы. Написание программ без использования goto называется структурным про- граммированием . Однако в настоящее время этот термин используется редко, так как применение этого стиля в высококачественном программном обеспечении подразумевается само собой. 1.5. Функции Функции являются важными строительными блоками программ на C++. Пер- вый пример, который мы видели, — это функция main в первой же рассмотрен- ной программе. Об этой функции мы поговорим подробнее в разделе 1.5.5. Общий вид функции в C++ выглядит как [ inline ] возвращаемый_тип имя_функции( список_аргументов ) { Тело функции } В этом разделе мы рассмотрим эти компоненты более подробно. 1.5.1. Аргументы C++ различает два способа передачи аргументов в функции — по значению и по ссылке. 1.5.1.1. Передача аргументов по значению Когда мы передаем аргумент в функцию, по умолчанию создается его копия. Например, следующая инструкция увеличивает x, но внешний по отношению к функции код этого не видит: void increment(int x) { x++; } int main () { int i = 4; increment(i); // Не приводит к увеличению i cout << "i = " << i << '\n'; } Эта программа выводит на экран значение 4. Операция x++ в функции increment увеличивает только локальную копию i, но не саму переменную i. Такая передача аргументов в функции называется передачей по значению. 02_ch01.indd 56 14.07.2016 10:46:36 1.5. Функции 57 1.5.1.2. Передача аргументов по ссылке Чтобы иметь возможность модифицировать параметры функций, аргументы в функцию должны передаваться по ссылке: void increment(int & x) { x++; } Теперь увеличивается сама переменная, так что будет выведено значение 5, как и ожидалось. Мы будем обсуждать ссылки более подробно в разделе 1.8.4. Временные переменные — такие, как результаты операций — не могут быть переданы по ссылке: increment(i + 9); // Ошибка: временное значение поскольку мы в любом случае не в состоянии вычислить (i + 9)++. Для того что- бы вызвать такую функцию с некоторым временным значением, его следует сна- чала сохранить в переменной, и уже ее передать в функцию. Большие структуры данных, такие как векторы или матрицы, почти всегда пе- редаются по ссылке, чтобы избежать дорогостоящей операции копирования: double two_norm(vector & v) { ... } Такая операция, как вычисление нормы, не должна изменять свой аргумент. Но передача вектора по ссылке несет риск случайной его перезаписи. Чтобы га- рантировать, что наш вектор не меняется (и не копируется), мы передаем его как константную ссылку: double two_norm(const vector & v) { ... } Если мы попытаемся изменить v в этой функции, компилятор сообщит об ошибке. И передача аргумента по значению, и передача как константной ссылки обеспе- чивают неизменность аргумента, но различными средствами. Аргументы, переданные по значению, могут изменяться в функции, пос- • кольку функция работает с копией 7 При передаче константных ссылок мы работаем непосредственно с пере- • даваемым аргументом, но все операции, которые могут его изменить, при этом запрещены. В частности, такие аргументы не могут находиться в левой части присваивания или быть переданы другим функциям через неконстан- тные ссылки (фактически левая часть присваивания также является некон- стантной ссылкой). 7 В предположении корректного копирования. Пользовательские типы с некорректными реализа- циями копирования могут подрывать целостность переданных данных. 02_ch01.indd 57 14.07.2016 10:46:36 Основы C++ 58 В отличие от изменяемых 8 ссылок константные ссылки позволяют передавать временные значения: alpha = two_norm(v + w); Это, правда, не совсем согласуется с дизайном языка, но зато намного облегча- ет жизнь программистам. 1.5.1.3. Аргументы по умолчанию Если аргумент обычно имеет одно и то же значение, его можно объявить со значением по умолчанию. Скажем, если мы реализуем функцию, которая вычис- ляет корень n-й степени, но в основном применяется для вычисления квадратного корня, то мы можем написать double root(double x, int degree = 2) { ... } Эта функция может быть вызвана как с двумя, так и с одним аргументом: x = root(3.5, 3); y = root(7.0); // То же, что и root(7.0,2) Можно объявить несколько значений по умолчанию, но только в конце списка аргументов. Другими словами, после аргумента со значением по умолчанию мы не можем указывать аргумент без такового. Значения по умолчанию полезны при добавлении дополнительных парамет- ров. Давайте предположим, что у нас есть функция, которая рисует круги: draw_circle(int x, int y, float radius); Все эти круги черные. Позже мы добавляем возможность указывать цвет кругов: draw_circle(int x, int y, float radius, color c= black); Благодаря аргументу по умолчанию нам не нужно переделывать наше прило- жение, поскольку вызовы draw_circle с тремя аргументами по-прежнему будут корректно работать. 1.5.2. Возврат результатов В приведенных ранее примерах мы возвращали только double или int. Это хорошо ведущие себя типы возвращаемых значений. Теперь мы рассмотрим край- ности — очень большие возвращаемые данные или их отсутствие. 1.5.2.1. Возврат большого количества данных Функции, вычисляющие новые значения больших структур данных, оказы- ваются более трудными. Детали мы рассмотрим позже, а пока только вскользь рассмотрим эту тему. Хорошая новость заключается в том, что компиляторы достаточно умны, чтобы во многих случаях не создавать копию возвращаемого 8 Слово изменяемый (mutable) в этой книге используется как синоним слова “неконстантный”. В C++ имеется также ключевое слово mutable (раздел 2.6.3), которое мы практически не используем. 02_ch01.indd 58 14.07.2016 10:46:36 1.5. Функции 59 значения (см. раздел 2.3.5.3). Кроме того, копирование позволяет избежать семан- тика перемещения (раздел 2.3.5), когда происходит непосредственный захват вре- менных данных. Современные библиотеки вообще избегают возвращения боль- ших структур данных, используя методы, именуемые шаблонами выражений, и откладывают вычисления до тех пор, пока не станет известно, где будет храниться результат (раздел 5.3.2). В любом случае мы не должны возвращать ссылки на локальные переменные функции (раздел 1.8.6). |