Главная страница
Навигация по странице:

  • return ; } }1.5.3. Встраивание

  • 1.5.5. Функция main Функция main принципиально не отличается от любой другой функции. В стандарте имеются две разрешенные сигнатуры:int main()

  • Оборонительное программирование

  • return 1 ; return 0

  • 1.6.2.2. Генерация исключения

  • 1.6.2.3. Перехват исключений Чтобы отреагировать на исключение, его следует перехватить. Это делается с помощью блока try-catch:try { } catch

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


    Скачать 1.68 Mb.
    НазваниеОсновы C Моим детям. Никогда не смейтесь, помогая мне осваивать
    Дата12.10.2022
    Размер1.68 Mb.
    Формат файлаpdf
    Имя файлаsovremennyy-cpp-piter.pdf
    ТипГлава
    #729589
    страница5 из 9
    1   2   3   4   5   6   7   8   9
    1.5.2.2. Отсутствие возвращаемого значения
    Синтаксически каждая функция должна возвращать что-то, даже если воз- вращать нечего. Эта дилемма решается с помощью имени пустого типа — void.
    Например, функция, которая просто выводит значение x, не должна возвращать что-либо:
    void print_x (int x)
    { std::cout << "Значение x = " << x << '\n';
    }
    void не является реальным типом и используется как заполнитель, позволяющий нам обойтись без возвращаемого значения. Мы не можем определить объект типа void:
    void nothing; // Ошибка: объектов void не бывает
    Функция void может быть завершена раньше с помощью инструкции return без аргумента:
    void heavy_compute ( const vector & x, double eps, vector & y)
    { for (...) { if (two_norm(y) < eps)
    return;
    }
    }
    1.5.3. Встраивание
    Вызов функции является относительно дорогой операцией: нужно сохранить регистры, скопировать аргументы в стек и т.д. Чтобы избежать этого лишнего тру- да, компилятор может встраивать вызовы функций. В этом случае вызов функции заменяется операциями, содержащимися в функции. Программист может попро- сить компилятор сделать это с помощью соответствующего ключевого слова:
    inline double square(double x) { return x*x; }
    Однако компилятор не обязан выполнять встраивание. Наоборот, он мо- жет встраивать функции без ключевого слова inline, если это представляется
    02_ch01.indd 59 14.07.2016 10:46:37

    Основы C++
    60
    перспективным c точки зрения производительности. Тем не менее объявление inline имеет свое применение для включения функции в несколько единиц ком- пиляции, которые мы будем обсуждать в разделе 7.2.3.2.
    1.5.4. Перегрузка
    Функции в C++ могут совместно использовать одно и то же имя, если объяв- ления их параметров достаточно различны. Это называется перегрузкой функций.
    Давайте сначала рассмотрим пример:
    #include
    #include int divide(int a, int b) { return a / b;
    } float divide(float a, float b) { return std::floor(a / b);
    } int main () { int x= 5, y= 2; float n= 5.0, m= 2.0; std::cout << divide(x,y) << std::endl; std::cout << divide(n,m) << std::endl; std::cout << divide(x,m) << std::endl; // Ошибка: неоднозначность
    }
    Здесь мы определили функцию divide дважды: с параметрами int и с пара- метрами double. Когда мы вызываем divide, компилятор выполняет разрешение
    перегрузки
    Имеется ли перегрузка, в которой типы аргументов в точности соответству-
    1.
    ют переданным значениям? Если да, использовать ее, в противном случае:
    Имеются ли перегрузки с соответствием типов после выполнения преобра-
    2.
    зований типов? Сколько?
    0. Ошибка — подходящая функция не найдена.

    1. Использовать эту функцию.

    > 1. Ошибка — неоднозначный вызов.

    Как это относится к нашему примеру? Вызовы divide(x,y) и divide(n,m) имеют точные соответствия. Для divide(x,m) нет точно соответствующей пере- грузки и есть две перегрузки, соответствующие после неявного преобразования, так что мы сталкиваемся неоднозначностью.
    02_ch01.indd 60 14.07.2016 10:46:37

    1.5. Функции
    61
    Термин “неявное преобразование” требует пояснений. Мы уже видели, что числовые типы могут преобразовываться один в другой. Это неявные преобразо- вания, как демонстрируется в примере. Когда позже мы определим собственные типы, мы сможем реализовать преобразования из других типов в наши, и обрат- но, из наших новых типов в существующие. Такие преобразования могут быть объявлены как явные (
    explicit), и тогда они применимы только когда преобра- зование запрошено явно, но не для соответствия аргументов функций.

    c++11/overload_testing.cpp
    Формулируя более официально, перегрузки функций должны различаться сиг-
    натурами
    . Сигнатура в C++ состоит из имени функции;

    количества аргументов, именуемого

    арностью
    ;
    типов аргументов (в указанном в объявлении порядке).

    Перегрузки, различающиеся только возвращаемым типом или именами аргу- ментов, имеют одинаковые сигнатуры и рассматриваются как (запрещенные) пе- реопределения:
    void f(int x) {} void f(int y) {} // Переопределение: отличие только в имени аргумента
    long f(int x) {} // Переопределение: отличие только в типе возврата
    Функции с разными именами или арностью безоговорочно являются различ- ными. Наличие символа ссылки превращает тип аргумента в другой тип аргумен- та (таким образом, f(int) и f(int&) могут сосуществовать). Следующие три перегрузки имеют разные сигнатуры:
    void f(int x) {} void f(int & x) {} void f(const int & x) {}
    Этот фрагмент кода компилируется без ошибок. Однако при вызове f начина- ются проблемы:
    int i= 3; const int ci= 4; f(3); f(i); f(ci);
    Все три вызова функции неоднозначны, потому что в каждом случае лучши- ми соответствиями являются первая перегрузка с аргументом, передаваемым по значению, и одна из перегрузок с передачей аргумента по ссылке. Смешивание перегрузок с передачей по значению и по ссылке почти всегда ведет к проблемам.
    Таким образом, когда одна перегрузка имеет ссылочный аргумент, то и другие
    02_ch01.indd 61 14.07.2016 10:46:37

    Основы C++
    62
    перегрузки должны иметь ссылочные аргументы. В нашем примере мы можем навести порядок, удалив первую перегрузку с передачей аргумента по значению.
    Тогда вызовы f(3) и f(ci) будут разрешаться в перегрузки с константной ссыл- кой, а f(i) — с изменяемой.
    1.5.5. Функция
    main
    Функция main принципиально не отличается от любой другой функции. В стандарте имеются две разрешенные сигнатуры:
    int main()
    и int main(int argc, char * argv[])
    Последняя запись эквивалентна следующей:
    int main( int argc, char ** argv)
    Параметр argv содержит список аргументов, а argc — его длину. Первый ар- гумент (
    argv[0]) в большинстве систем содержит имя выполняемого файла (ко- торое может отличаться от имени исходного файла). Чтобы поработать с аргумен- тами, напишем короткую программу под названием argc_argv_test:
    int main (int argc, char * argv [])
    { for(int i= 0; i < argc; ++i) cout << argv[i] << '\n'; return 0;
    }
    Вызов этой программы со следующими параметрами командной строки argc_argv_test first second third fourth дает вывод на экран argc_argv_test first second third fourth
    Как видите, каждый пробел в командной строке разделяет аргументы. Функ- ция main возвращает целое число в качестве кода завершения, который указы- вает, закончилось ли выполнение программы успешно. Значение 0 (или макрос
    EXIT_SUCCESS из заголовочного файла ) указывает на успешное за- вершение, а любое другое значение — на сбой. Стандарт разрешает опустить оператор return в функции main. В этом случае компилятором автоматически вставляется return 0;. Некоторые дополнительные сведения можно найти в раз- деле А.2.5.
    02_ch01.indd 62 14.07.2016 10:46:37

    1.6. Обработка ошибок
    63
    1.6. Обработка ошибок
    Оплошность не становится ошибкой,
    пока вы не отказываетесь ее исправить.
    — Джон Ф. Кеннеди
    Два главных способа обработки неожиданного поведения программы в C++ — утверждения и исключения. Первые предназначены для обнаружения ошибок в программировании, а вторые — для исключительных ситуаций, которые мешают продолжению правильной работы программы. Честно говоря, это различие дале- ко не всегда очевидно.
    1.6.1. Утверждения
    Макрос assert из заголовочного файла унаследован от языка программирования C, но он полезен и в C++. Он вычисляет переданное ему вы- ражение, и если результат равен false, то выполнение программы немедленно завершается. Этот макрос должен использоваться для обнаружения ошибок про- граммирования. Допустим, мы реализуем крутой алгоритм вычисления квадрат- ного корня из неотрицательных действительных чисел. Из математики мы знаем, что результат является неотрицательным числом. В противном случае в нашем расчете что-то сделано неверно:
    #include double square_root ( double x)
    { check_somehow(x >= 0);
    assert(result >= 0.0); return result;
    }
    Как реализовать первоначальную проверку — оставим этот вопрос пока что открытым. Если полученный нами результат отрицательный, выполнение про- граммы прекратится с выводом на экран сообщения наподобие следующего:
    assert_test: assert_test.cpp:10: double square_root(double):
    Утверждение 'result >= 0.0' не выполнено.
    Тот факт, что полученный результат оказался меньше нуля, говорит о том, что наша реализация содержит ошибку, и мы должны исправить ее, прежде чем ис- пользовать разработанную функцию в серьезных приложениях.
    После того как мы исправили ошибку, может возникнуть соблазн удалить assert. Не стоит этого делать. Может быть, в один прекрасный день мы изменим реализацию; после этого желательно провести все тесты заново. Утверждения для постусловий, по сути, представляют собой модульные мини-тесты.
    02_ch01.indd 63 14.07.2016 10:46:38

    Основы C++
    64
    Большое преимущество assert заключается в том, что все такие провер- ки можно отключить единственным объявлением макроса. Перед включением
    в исходный текст можно просто определить NDEBUG:
    #define NDEBUG
    #include
    Все утверждения при этом будут отключены, т.е. они не будут вносить в вы- полнимый файл никакого кода. Вместо изменения исходного текста программы каждый раз, когда мы переключаемся между отладочной и производственной вер- сиями, лучше и проще объявить макрос
    NDEBUG с помощью флагов компилятора
    (обычно
    -D в Linux и /D в Windows):
    g++ my_app .cpp -o my_app -O3 -DNDEBUG
    Программное обеспечение с утверждениями в критических местах кода может оказаться замедленным в два или более раз, если не отключить их в режиме вы- пуска. Хорошие системы построения, такие как
    CMake, автоматически включают флаг компиляции
    -DNDEBUG в режиме выпуска.
    Поскольку утверждения можно так легко отключить, прислушайтесь к следу- ющей рекомендации.
    Оборонительное программирование
    Тестируйте столько свойств, сколько вы в состоянии протестировать.
    Даже если вы уверены, что некоторое свойство явно выполняется в вашей ре- ализации, напишите утверждение. Иногда система не ведет себя в точности так, как предполагалось, компилятор может содержать ошибку (это очень редкое, но возможное событие) или мы сделали что-то немного отличающееся от того, что намеревались сделать первоначально. Словом, независимо от того, насколько мы тщательно и обдуманно пишем код, рано или поздно какое-то из утверждений может сработать. В случае, когда таких свойств так много, что функциональность начинает запутываться и затормаживаться тестами, их можно перенести в другую функцию.
    Ответственные программисты всегда реализуют большие наборы тестов. Тем не менее они не гарантируют, что программа будет работать при любых обстоя- тельствах. Приложение может работать в течение многих лет, как часы, но в один далеко не прекрасный день выйти из строя. В этой ситуации мы можем запустить приложение в режиме отладки, когда будут включены все утверждения, и в боль- шинстве случаев они окажутся большим подспорьем в поиске причины аварий- ной ситуации. Однако это требует, чтобы аварийная ситуация была воспроизво- димой и чтобы программа в более медленном отладочном режиме могла достичь критического раздела за разумное время.
    02_ch01.indd 64 14.07.2016 10:46:38

    1.6. Обработка ошибок
    65
    1.6.2. Исключения
    В предыдущем разделе мы рассмотрели, как утверждения помогают обнару- жить ошибки программирования. Однако есть много критических ситуаций, кото- рые мы не можем предотвратить каким бы то ни было разумным программирова- нием, например файлы, которые должна читать наша программа, могут оказаться удаленными. Или, например, наша программа потребует больше памяти, чем доступно на данном компьютере. Некоторые проблемы теоретически могут быть предотвращены, но практические усилия оказываются непропорционально высо- ки. Например, проверить, является ли матрица регулярной, можно, но для этого придется выполнить работу, которая может оказаться большей, чем работа над фактической задачей. В таких случаях обычно эффективнее попытаться решить задачу и убедиться в отсутствии исключений во время вычислений.
    1.6.2.1. Мотивация
    Прежде чем проиллюстрировать обработку ошибок в старом стиле, мы пред- ставим вам нашего антигероя Герберта
    9
    , который является гениальным математи- ком и рассматривает программирование как необходимое зло для демонстрации того, как великолепно работают его алгоритмы. Он научился программировать, как настоящий мужчина, и невосприимчив к новомодным бессмыслицам совре- менного программирования.
    Его любимый подход к решению вычислительных задач — возвращать код ошибки (как это делает функция main). Скажем, мы хотим прочитать матрицу из файла и проверяем, на месте ли интересующий нас файл. Если его нет, то мы возвращаем код ошибки 1:
    int read_matrix_file ( const char * fname, ...)
    { fstream f( fname ); if (!f.is_open())
    return 1;
    return 0;
    }
    Таким образом мы проверяем все, что может пойти не так, и сообщаем об этом вызывающей функции с помощью соответствующего кода ошибки. Это нормаль- ное решение, когда вызывающая функция проверяет возвращенное значение и реагирует соответствующим образом. Но что происходит, если вызывающая фун- кция просто игнорирует код возврата? Да ничего! Программа продолжает рабо- тать и позже может столкнуться с аварийной ситуацией из-за абсурдных данных или, что еще хуже, получить бессмысленные результаты, которые небрежные люди могут использовать для построения автомобилей или самолетов. Конечно, создатели автомобилей и самолетов не столь небрежны, но в более реалистичном
    9
    Автор приносит извинения всем Гербертам, читающим книгу, за использование их имени.
    02_ch01.indd 65 14.07.2016 10:46:38

    Основы C++
    66
    программном обеспечении даже аккуратные программисты не в состоянии отсле- дить каждую крошечную деталь.
    Тем не менее эти соображения не в состоянии убедить динозавров от програм- мирования, таких как Герберт. “Вы не только глупы настолько, что передаете моей прекрасно реализованной функции несуществующий файл, но еще и не проверя- ете код возврата! Все неправильно делаете именно вы, а не я”.
    Другой недостаток кодов ошибок заключается в том, что мы не можем просто вернуть результаты наших вычислений и должны передавать их через ссылочные аргументы. Это не позволяет нам просто создавать выражения с участием резуль- татов наших вычислений. Другой способ — вернуть из функции результат вычис- лений, а код ошибки передать через ссылочный аргумент — оказывается ничуть не менее громоздким.
    1.6.2.2. Генерация исключения
    Наилучший подход — сгенерировать исключение с помощью оператора throw:
    matrix read_matrix_file ( const char * fname, ...)
    { fstream f( fname ); if (!f.is_open())
    throw "Невозможно открыть файл.";
    }
    В этой версии мы генерируем исключение. Вызывающее приложение теперь обязано отреагировать на него — в противном случае программа аварийно завер- шит работу.
    Преимущество исключений над кодами ошибок заключается в том, что мы имеем дело с проблемой только там, где можем ее обработать. Например, функция read_matrix_file может не иметь возможности обработать отсутствие файла.
    В этой ситуации код просто генерирует исключение. Таким образом, нам не нуж- но запутывать нашу программу, возвращая коды ошибок. В случае исключения оно просто будет передано соответствующему обработчику исключений. В нашем случае такая обработка может выполняться в пользовательском интерфейсе, где у пользователя запросят новый файл. Таким образом, исключения позволяют сде- лать исходный текст более удобочитаемым и при этом обеспечить более надеж- ную обработку ошибок.
    C++ позволяет сгенерировать исключение любого вида: строки, числа, пользо- вательские типы и т.д. Однако лучше всего определить специальные типы исклю- чений или использовать таковые из стандартной библиотеки:
    struct cannot_open_file {}; void read_matrix_file( const char * fname, ...)
    { fstream f( fname ); if (!f.is_open())
    02_ch01.indd 66 14.07.2016 10:46:39

    1.6. Обработка ошибок
    67
    throw cannot_open_file {};
    }
    Здесь мы ввели наш собственный тип исключения. В главе 2, “Классы”, мы подробно рассмотрим, как могут быть определены классы. В приведенном выше примере мы определили пустой класс, для которого необходимы только открыва- ющие и закрывающие скобки и точка с запятой. Крупные проекты обычно созда- ют целую иерархию типов исключений, которые часто являются производными
    (глава 6, “Объектно-ориентированное программирование”) от std::exception.
    1.6.2.3. Перехват исключений
    Чтобы отреагировать на исключение, его следует перехватить. Это делается с помощью блока try-catch:
    try {
    }
    catch (e1_type & e1) { ... }
    catch (e2_type & e2) { ... }
    Там, где мы ожидаем проблему, которую мы в состоянии решить (или по край- ней мере что-то сделать), мы открываем блок try. После закрывающей скобки мы можем перехватить исключение и начать “спасательную операцию” в зависимос- ти от типа перехваченного исключения и, возможно, его значения. Рекомендуется перехватывать исключения по ссылке [45, совет 73], в особенности когда речь идет о полиморфных типах (определение 6.1 из раздела 6.1.3). Когда в блоке сгенери- ровано исключение, выполняется первый блок catch с типом, соответствующим типу исключения. Прочие блоки catch того же типа (или подтипов, раздел 6.1.1) игнорируются. Блок catch с троеточием перехватывает все исключения:
    try {
    } catch ( e1_type & e1) { ... } catch ( e2_type & e2) { ... } catch (... ) { // Перехват всех остальных исключений
    }
    Очевидно, что такой обработчик любых исключений должен быть последним.
    Если мы не в состоянии сделать ничего иного, можно перехватить исключе- ние хотя бы для того, чтобы вывести информативное сообщение об ошибке перед тем, как завершить выполнение программы:
    try {
    A = read_matrix_file("does_not_exist.dat");
    } catch(cannot_open_file & e) {
    1   2   3   4   5   6   7   8   9


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