Главная :: FreeFem++ :: Управляющие структуры в FreeFem++
Ваши руки выполнили недопустимую операцию и будут ампутированы

Управляющие структуры в FreeFem++

Условные операторы if и if/else. Эта структура работает аналогично C++. В первом варианте if, если условие истинно, во выполняется оператор. Во втором варианте if/else, если условие истинно, во выполняется первый оператор, а если ложно, то второй.

real a = 1.5, b = 2.;
if ( a < b )
  cout << a << " < " << b << endl;

if ( a < b )
  cout << a << " < " << b << endl;
else
  cout << b << " < " << a << endl;

Если операторов несколько, то они помещаются в фигурные скобки { ... }. В следующем примере, проверяется целочисленное и вещественное деление (сравните с m = 3).

int m = 4;
real d1, d2;
if ( m / 2 == m / 2. ) {
  d1 = m / 2;
  d2 = m / 2.;
  cout << " m / 2 = m / 2., " << d1 << " = " << d2 << endl;
} else {
  d1 = m / 2;
  d2 = m / 2.;
  cout << " m / 2 != m / 2., " << d1 << " != " << d2 << endl;
}

Иногда бывают случаи когда во время отладки нужно отключать или включать выполнение части кода. Можно поместить этот код в конструкцию if( 0 ) { ... }. Для его включения достаточно поменять ее на if( 1 ) { ... }. Более универсально ввести логическую переменную, например, bool doSamething и присваивая ей соответствующее значение включать или отключать выполнение части кода if( doSamething ) { ... }.

Еще одним типом условия является арифметическое условие (тернарный оператор) - условный оператор используемый в арифметических выражениях. Его формат условие?выражение 1:выражение 2. Если условие истинно, то подставляется выражение 1, иначе - выражение 2. Пример определения максимального из двух чисел:

real a1 = 3.5, a2 = 2.5, amax;
amax = a1 > a2 ? a1 : a2;
cout << "max(" << a1 << ", " << a2 << ") = " << amax << endl;

Цикл, управляемый счетчиком (for). Эта структура работает аналогично C++. Структура for имеет три аргумента: инициализация счетчика, условие повторения и правило обновления счетчика. Переменную счетчика можно описать при инициализации (int i = 0), тогда область существования этой переменной находится внутри структуры цикла.

for (int i1 = 0; i1 < 3; i++)
  cout << " i1 = " << i1 << endl;

Если переменная не была описана до начала цикла, то по его окончанию ее не больше не существует, например, в следующем примере компилятор выдаст ошибку на последней строке

for (int i1 = 0; i1 < 3; i1++)
  cout << " i1 = " << i1 << endl;
cout << i1 << endl; // Ошибка. i1 не существует

Если переменная счетчика описана заранее и в области инициализации она не описывается повторно, то по окончанию цикла переменная имеет последнее значение счетчика не удовлетворяющее условию повтора (например, если правило обновления счетчика - увеличение на единицу i++, то по завершению цикла переменная будет иметь значение на единицу больше, чем последнее значение счетчика использованное внутри структуры цикла).

int i2;
for (i2 = 0; i2 < 3; i2++)
  cout << " i2 = " << i2 << endl;          // i2 = 0, 1, 2
cout << " End value i2 = "  << i2 << endl; // i2 = 3

Если переменная счетчика описана заранее и в области инициализации она описывается повторно, то по окончанию цикла переменная имеет то значение, что она имела до начала цикла.

int i3 = 10;
for (int i3 = 0; i3 < 3; i3++)
  cout << " i3 = " << i3 << endl;          // i3 = 0, 1, 2
cout << " End value i3 = "  << i3 << endl; // i3 = 10

Если тело цикла содержит несколько операторов, то они заключаются внутри фигурных скобок.

int s = 0;
for (int i4 = 0; i4 < 5; i4++) {
  s += i4;
  cout << " i4 = " << i4 << ", s = " << s << endl;
}

Цикл с предусловием (while). Эта структура работает аналогично C++. Структура while имеет один аргумент логического типа, цикл повторяется до тех пор, пока условие остается верным.

int j = -2;
while ( j < 2 )
  cout << " j = " << ++j << endl; // j = -1, 0, 1, 2

Структура цикла while часто используется в итерационных методах - поиск решения с заданной точностью. Рассмотрим пример численного решения нелинейного уравнения x²+x-2=0 методом Ньютона с начальным приближением x=0.5 (точное решение x=1). При инициализации переменной точности eps присваивается заведомо большое значение для старта цикла, внутри цикла точность пересчитывается для нового значения x пока не будет найден корень уравнения с заданной в условии цикла точностью.

real eps = 1.e+10;
real xx = 0.5, dx, f, jac;
while ( eps > 1.e-10 ) {
  f = xx * xx + xx - 2.;
  jac = 2.*xx + 1.;
  dx = - f / jac;
  xx += dx;
  eps = abs(xx * xx + xx - 2.);
  cout << " x = " << xx << " dx = " << dx << ", eps = " << eps << endl;
}

Результат расчета

 x = 1.125, dx = 0.625, eps = 0.390625
 x = 1.00481, dx = -0.120192, eps = 0.0144462
 x = 1.00001, dx = -0.00480001, eps = 2.30401e-05
 x = 1, dx = -7.68e-06, eps = 5.89822e-11

Для тренировки можно запустить тот же пример с начальным приближением x=-1.5, для того, чтобы найти второй корень уравнения (точное значение x=-2), а так же запустить для различных значений точности eps.

Еще один пример использования цикла while - это создание бесконечного цикла while (1). Зачем это может понадобится? Например, цикл должен стартовать даже при несоблюдении условия выполнения. Или же необходимо реализовать структуру цикла с постусловием (в FreeFem++ отсутствует структура do/while). Для выхода из цикла используется команда break.

int k = 3;
while ( 1 ) {
  cout << " k = " << --k << endl; // k = 2, 1, 0
  if ( !k ) break;
}
cout << "End with k = " << k << endl;

Прерывание цикла (break). Прерывает текущий цикл (for или while), выполнение программы продолжается со следующего за циклом оператором.

for (int i = 0; i < 5; i++)
  if (i > 3)
    break;
  else
    cout << " i = " << i << endl;  // i = 0, 1, 2, 3

Продолжение цикла (continue). Игнорирует операторы после команды continue и переходит на начало цикла.

for (int i = 0; i < 5; i++)
  if (i < 3)
    continue;
  else
    cout << " i = " << i << endl;  // i = 3, 4

Неявный цикл (for). Данная структура появилась в FreeFem++ начиная с версии 3.43 и не имеет аналоги в C++. Цикл имеет три аргумента - переменная цикла, ссылка на элемент вектора или матрицы и сам вектор или матрица:

for [i, vi : v] { ... }

Для одномерного вектора v размерности n переменная цикла i пробегает значения от 0 до n-1, а ссылка указывает на элемент вектора v(i). В следующем примере массиву присваивается значение половины квадрата его индекса:

real [int] v(5);
for [i, vi : v] vi = 0.5 * i^2;
cout << " v = " << v << endl;

Для двумерного массива u размерности n×m переменные цикла пробегают значения i=0..n-1 и j=0..m-1, а ссылка указывает на элемент массива u(i,j).

real [int, int] u(3, 5);
for [i, j, uij : u] {
  uij = 1.5 + i - j;
  if( uij < 0. ) uij = 0.;
}
cout << " u = " << u << endl;

Обработка исключительных ситуаций (try/catch). В случае фатальной ошибки выполнение программы останавливается. Если в коде есть опасные участки, где может произойти ошибка, то они могут быть помещены в блок try. При возникновении ошибки выполнение переключается на блок catch, где можно ее исправить без аварийного прерывания программы.

real c;
try{
    c = 1./0.;
}
catch (...) // обработка всех исключительных ситуаций
{
    cout << "Catch an ExecError" << endl;
    c = 0.;
}
cout << " c = " << c << endl;

Приведем еще один пример обработки исключительной ситуации при появлении ошибки в численной схеме. Метод Ньютона для решения нелинейных уравнений обеспечивает хорошую (квадратичную сходимость), но если выбрано начальное приближение в точке экстремума, то метод не сходится, т.к. якобиан равен нулю. Эту ситуацию можно обработать внутри команды исключив ее аварийное завершение.

real eps = 1.e+10;
real xx = -0.5, dx, f, jac;
while ( eps > 1.e-10 ) {
  f = xx * xx + xx - 2.;
  jac = 2.*xx + 1.;
  try {
    dx = - f / jac;
    xx += dx;
    eps = abs(xx * xx + xx - 2.);
  }
  catch (...) {
    cout << " Error: extremum" << endl;
    dx = 0.;
    eps = 1.e10;
    xx += 0.1;
  }
  cout << " x = " << xx << ", dx = " << dx << ", eps = " << eps << endl;
}

Результат выполнения программы:

2.25/0 : d d d
  current line = 107
Exec error :  Div by 0
   -- number :1
 Error: extremum
 x = -0.4, dx = 0, eps = 1e+10
 x = 10.8, dx = 11.2, eps = 125.44
 x = 5.24956, dx = -5.55044, eps = 30.8074
 x = 2.57045, dx = -2.67911, eps = 7.17764
 x = 1.40162, dx = -1.16883, eps = 1.36616
 x = 1.04241, dx = -0.359209, eps = 0.129031
 x = 1.00058, dx = -0.0418276, eps = 0.00174955
 x = 1, dx = -0.000582957, eps = 3.39839e-07
 x = 1, dx = -1.1328e-07, eps = 1.33227e-14

Данный код указан в качестве примера, конечно, конкретно к этой задаче можно использовать более эффективных подход без обработки исключительной ситуации. Приведем пример, в котором учитывается не только деление на ноль, но и область с низкой сходимостью (область вблизи экстремума).

real eps = 1.e+10;
real xx = -0.4999999, dx, f, jac;
int ic = 0;
while ( eps > 1.e-10 ) {
  ic++;
  f = xx * xx + xx - 2.;
  jac = 2.*xx + 1.;
  if ( abs(jac) < 0.25 ) {
    cout << " Updating x value from " << xx << " to ";
    xx += 0.1;
    cout << xx << endl;
    continue;
  }
  dx = - f / jac;
  xx += dx;
  eps = abs(xx * xx + xx - 2.);
  cout << " x = " << xx << ", dx = " << dx << ", eps = " << eps << ", jac = " << jac << endl;
}
cout << " Total steps: " << ic << endl;

Результат выполнения программы:

 Updating x value from -0.5 to -0.4
 Updating x value from -0.4 to -0.3
 x = 5.225, dx = 5.525, eps = 30.5256, jac = 0.4
 x = 2.55901, dx = -2.66599, eps = 7.10751, jac = 11.45
 x = 1.39727, dx = -1.16174, eps = 1.34963, jac = 6.11801
 x = 1.04159, dx = -0.355677, eps = 0.126506, jac = 3.79454
 x = 1.00056, dx = -0.041031, eps = 0.00168355, jac = 3.08318
 x = 1, dx = -0.000560972, eps = 3.1469e-07, jac = 3.00112
 x = 1, dx = -1.04897e-07, eps = 1.06581e-14, jac = 3
 Total steps: 9

Без проверки на близость к экстремуму на решение ушло бы 28 итераций вместо 9. В более сложных расчетах (требующие минуты или часы на одну итерацию) уменьшение количества итераций значительно экономит время.