Главная страница

Основы C Моим детям. Никогда не смейтесь, помогая мне осваивать


Скачать 1.68 Mb.
НазваниеОсновы C Моим детям. Никогда не смейтесь, помогая мне осваивать
Дата12.10.2022
Размер1.68 Mb.
Формат файлаpdf
Имя файлаsovremennyy-cpp-piter.pdf
ТипГлава
#729589
страница4 из 9
1   2   3   4   5   6   7   8   9
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 z2 = ..., z3 = ...;
} else if (D == 0.0) {
double z1 = ..., z2 = ..., z3 = ...;
} else { // D < 0.0 complex z1 = ..., z2 = ..., z3 = ...;
}
Всегда полезно вначале написать фигурные скобки. Многие руководства по стилю программирования требуют применять фигурные скобки даже для одной инструкции, тогда как автор предпочитает в этом случае обходиться без них.
В любом случае настоятельно рекомендуется использовать отступ ветви для луч- шей удобочитаемости.
Инструкции 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).
1   2   3   4   5   6   7   8   9


написать администратору сайта