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

  • 1.6.2.4. Кто может генерировать исключения

  • 1.7.1. Стандартный вывод

  • 11 * 19

  • (age > 65 "Он мудр\n" : "Да он просто мальчишка!\n")

  • 1.7.3. Ввод-вывод в файлы

  • include

  • 1.7.4. Обобщенная концепция потоков

  • 1.7.6. Обработка ошибок ввода-вывода

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


    Скачать 1.68 Mb.
    НазваниеОсновы C Моим детям. Никогда не смейтесь, помогая мне осваивать
    Дата12.10.2022
    Размер1.68 Mb.
    Формат файлаpdf
    Имя файлаsovremennyy-cpp-piter.pdf
    ТипГлава
    #729589
    страница6 из 9
    1   2   3   4   5   6   7   8   9
    cerr << "Ваш файл не существует! Закрываем лавочку.\n";
    exit( EXIT_FAILURE );
    }
    02_ch01.indd 67 14.07.2016 10:46:39

    Основы C++
    68
    После того как исключение перехватывается, считается, что проблема решена и продолжается выполнение инструкций, находящихся после блока catch. Выше для прекращения выполнения мы использовали функцию exit из заголовочного файла
    . Эта функция завершает выполнение программы, даже если мы находимся не в функции main. Она должна использоваться только тогда, когда дальнейшее выполнение программы слишком опасно и нет никакой надежды, что вызывающая функция сможет справиться с этим исключением.
    В качестве альтернативы можно продолжить — после вывода сообщения или каких-то частичных действий — обработку исключения в охватывающих блоках, генерируя его заново:
    try {
    A= read_matrix_file("does_not_exist.dat");
    } catch(cannot_open_file & e) { cerr << "Этого файла здесь нет! Спасайте.\n";
    throw e;
    }
    В нашем случае мы уже находимся в функции main, так что в стеке вызовов нет ничего, что могло бы перехватить такое регенерированное исключение. Для повторной генерации того же исключения, которое было перехвачено, можно вос- пользоваться сокращенной записью:
    } catch(cannot_open_file & e) {
    throw;
    }
    Предпочтительнее использовать эту сокращенную запись, так как она меньше подвержена ошибкам и более ясно показывает, что мы хотим заново сгенериро- вать исходное исключение. Проигнорировать исключение легко с помощью пус- того блока:
    } catch(cannot_open_file &) {} // Файл не нужен, продолжаем
    В действительности пока что наша обработка исключений не решает проблему отсутствующего файла. Решать ее можно по-разному. Например, если имя файла указывает пользователь, мы можем докучать ему до тех пор, пока не получим тре- буемый файл:
    bool keep_trying = true; do { char fname [80]; // Лучше использовать std::string cout << "Введите имя файла: "; cin >> fname; try {
    A= read_matrix_file(fname);
    keep_trying = false;
    02_ch01.indd 68 14.07.2016 10:46:39

    1.6. Обработка ошибок
    69
    } catch(cannot_open_file & e) { cout << "Невозможно открыть файл. Попробуйте еще раз!\n";
    } catch (...) cout << "Что-то пошло не так. Попробуйте еще раз!\n";
    }
    } while(keep_trying);
    Когда мы добрались до конца try-блока, мы знаем, что исключение не было сгенерировано, так что работа успешно выполнена. В противном случае окажемся в одном из catch-блоков, и значение переменной keep_trying останется равным true.
    Большим преимуществом исключений является то, что проблемы, которые не могут быть решены в контексте, в котором они обнаружены, можно отложить на более поздний срок. Пример из практики автора касается LU-разложения. Оно не может быть выполнено для сингулярной матрицы. С этим ничего нельзя поделать.
    Однако в случае, когда факторизация является частью итеративных вычислений, мы могли бы продолжать итерации и без факторизации. Хотя такая обработка ошибок могла быть реализована и традиционными методами, исключения поз- воляют нам осуществить ее гораздо более удобочитаемо и элегантно. Мы можем запрограммировать факторизацию для регулярной матрицы, а при обнаружении сингулярности генерировать исключение. Вызывающая функция, перехватив ис- ключение, может принять соответствующие данному контексту меры — насколь- ко это возможно.
    1.6.2.4. Кто может генерировать исключения
    В C++03 разрешено указывать, какие типы исключений может генерировать некоторая функция. Не вдаваясь в подробности, скажем, что эти спецификации оказались не очень полезными, и в настоящее время считаются устаревшими.
    В C++11 в язык добавлен новый квалификатор для указания того факта, что данная функция не должна генерировать исключения, например:
    double square_root( double x) noexcept { ... }
    Преимущество такого квалификатора в том, что вызывающий код не обязан проверять после вызова square_root, не было ли сгенерировано исключение.
    Если исключение, несмотря на наличие квалификатора, все же сгенерировано, программа завершается.
    Генерируется ли исключение в шаблонных функциях, может зависеть от того, генерируют ли исключения аргументы типов. Для корректной обработки этой ситуации noexcept может зависеть от условий времени компиляции (см. раз- дел 5.2.2).
    Что более предпочтительно — использование утверждений или исключений — вопрос непростой, и у нас нет короткого и однозначного ответа на него. Пока что этот вопрос, скорее всего, не будет вас беспокоить. Поэтому мы отложим обсужде- ние до раздела A.2.6 и оставим окончательное решение за вами, когда вы прочтете этот раздел.
    C++11 02_ch01.indd 69 14.07.2016 10:46:39

    Основы C++
    70
    1.6.3. Статические утверждения
    Программные ошибки, которые могут быть обнаружены уже во время компи- ляции, можно проверять с помощью статического утверждения static_assert.
    В этом случае компилятор выдает сообщение об ошибке и прекращает компиля- цию. Детальное описание и рассмотрение конкретного примера пока что не имеет смысла; мы отложим его до раздела 5.2.5.
    1.7. Ввод-вывод
    Для выполнения операций ввода-вывода в последовательные устройства на- подобие терминала или клавиатуры C++ использует удобную абстракцию, име- нуемую потоками. Поток — это объект, в который программа может вставлять символы или извлекать их оттуда. Стандартная библиотека C++ содержит заго- ловочный файл
    , в котором объявляются стандартные потоки ввода и вывода.
    1.7.1. Стандартный вывод
    По умолчанию стандартный вывод из программы записывается на экран, и в программе мы можем получить доступ к потоку с именем cout. Он используется с оператором вставки
    << (такой же, как и сдвиг влево). Мы уже видели, что этот оператор может использоваться в одной инструкции более одного раза. Это осо- бенно полезно, когда мы хотим выводить комбинацию из текста, переменных и констант, например:
    cout << "Квадратный корень из " << x << " = " << sqrt (x) << endl;
    Вывод при этом имеет вид наподобие
    Квадратный корень из 5 = 2.23607
    endl генерирует символ новой строки. Альтернативой применения endl является использование символа
    \n. Для эффективности вывод может быть буферизован, и в этом отношении endl и \n различаются. Первый из них приводит к сбросу буфера, а последний — нет. Сброс буфера может помочь нам при отладке (без отладчика), чтобы определить, между какими двумя выводами происходит ава- рийное завершение программы. Но при записи большого количества исходного текста в файл очистка буфера после каждой строки может существенно замедлить работу.
    К счастью, оператор вставки имеет относительно низкий приоритет, так что арифметические операции могут быть записаны непосредственно, без использо- вания скобок:
    std::cout << "11 * 19 = " << 11 * 19 << std::endl;
    C++11 02_ch01.indd 70 14.07.2016 10:46:40

    1.7. Ввод-вывод
    71
    Все сравнения, логические и побитовые операции должны быть сгруппирова- ны с помощью скобок. То же самое относится и к условному оператору:
    std::cout << (age > 65 ? "Он мудр\n" : "Да он просто мальчишка!\n");
    Если вы забудете скобки, компилятор напомнит вам о них (предлагая расшиф- ровать загадочные сообщения об ошибках).
    1.7.2. Стандартный ввод
    Стандартным устройством ввода обычно является клавиатура. Обработка стан- дартного ввода в C++ осуществляется с помощью перегруженного оператора из- влечения
    >> из потока cin:
    int age;
    std::cin >> age;
    std::cin считывает символы из устройства ввода и интерпретирует их как значе- ние типа переменной (здесь — int), в которую его сохраняет (здесь — age). Ввод с клавиатуры обрабатывается после нажатия клавиши .
    Мы также можем запросить у cin несколько данных от пользователя:
    std::cin >> width >> length;
    Это эквивалентно записи std::cin >> width; std::cin >> length;
    В обоих случаях пользователь должен предоставить два значения: одно — для ширины и другое — для длины. Они могут быть разделены любым корректным пробельным разделителем — пробелом, знаком табуляции или символом новой строки.
    1.7.3. Ввод-вывод в файлы
    C++ предоставляет следующие классы для выполнения файлового ввода и вы- вода.
    ofstream
    Запись в файлы ifstream
    Чтение из файлов fstream
    Чтение из файлов и запись в них
    Мы можем использовать файловые потоки таким же образом, как cin и cout, с той лишь разницей, что мы должны связать эти потоки с физическими файла- ми. Вот пример такого использования:
    #include int main()
    {
    02_ch01.indd 71 14.07.2016 10:46:40

    Основы C++
    72
    std::ofstream square_file; square_file.open("squares.txt");
    for (int i= 0; i < 10; ++i) square_file << i << "ˆ2 = " << i*i << std::endl; square_file.close();
    }
    Этот код создает файл с именем squares.txt (или перезаписывает его, если таковой уже существует) и выполняет запись в него — так же, как выполняется за- пись в cout. C++ устанавливает общую концепцию потока, которой удовлетворя- ют как файл вывода, так и поток std::cout. Это означает, что мы можем писать в файл все, что можно писать в std::cout, и наоборот. Определяя operator << для нового типа, мы делаем это один раз для типа ostream (раздел 2.7.3), и он может работать с консолью, с файлами и с любым другим потоком вывода.
    В качестве альтернативы для неявного открытия файла можно передать его имя как аргумент конструктора потока. Этот файл так же неявно закрывается, когда square_file выходит из области видимости
    10
    , в данном случае — в конце функции main. Краткая версия предыдущей программы имеет следующий вид:
    #include int main()
    { std::ofstream square_file("squares.txt"); for (int i= 0; i < 10; ++i) square_file << i << "ˆ2 = " << i*i << std::endl;
    }
    Как обычно, мы предпочитаем короткую запись. Явное открытие и закрытие необходимо только в случае, когда файл сначала объявлен, а открывается по ка- кой-либо причине позже. Аналогично явный вызов close требуется только тогда, когда файл должен быть закрыт, прежде чем он выйдет из области видимости.
    1.7.4. Обобщенная концепция потоков
    Потоки не ограничиваются экранами, клавиатурами и файлами; любой класс может использоваться как поток, если он является производным
    11
    от istream, ostream или iostream и предоставляет реализации функций этих классов.
    Например, Boost.Asio предоставляет потоки TCP/IP, а Boost.IOStream — аль- тернативу описанному выше вводу-выводу. Стандартная библиотека содержит stringstream, который может использоваться для создания строки из любых ти- пов, которые могут быть выведены в поток. Метод str() класса stringstream возвращает внутреннюю строку потока типа string.
    10
    Благодаря мощной технологии под названием “RAII”, о которой мы поговорим в разделе 2.4.2.1.
    11
    О том, что это означает, вы узнаете из главы 6, “Объектно-ориентированное программирование”.
    Здесь мы просто отметим, что выходные потоки технически порождены из std::ostream.
    02_ch01.indd 72 14.07.2016 10:46:40

    1.7. Ввод-вывод
    73
    Мы можем написать функции вывода, которые принимают поток любого вида, используя изменяемую ссылку на ostream в качестве аргумента:
    #include
    #include
    #include void write_something(std::ostream & os)
    {
    os << "Знаете ли вы, что 3 * 3 = " << 3 * 3 << std::endl;
    } int main (int argc, char * argv [])
    {
    std::ofstream myfile("example.txt");
    std::stringstream mysstream; write_something(std::cout); write_something(myfile); write_something(mysstream); std::cout << "mysstream = " // В этой строке есть
    << mysstream.str(); // символ новой строки
    }
    Аналогично универсальный ввод может быть реализован с помощью istream.
    1.7.5. Форматирование

    c++03/formatting.cpp
    Потоки ввода-вывода форматируются с помощью так называемых манипулято- ров ввода-вывода, которые описаны в заголовочном файле
    . По умол- чанию C++ выводит только несколько цифр чисел с плавающей точкой. Мы мо- жем повысить точность:
    double pi= M_PI; cout << "pi = " << pi << '\n'; cout << "pi = " << setprecision(16) << pi << '\n';
    и получить более точное значение числа:
    pi = 3.14159 pi = 3.141592653589793
    В разделе 4.3.1 мы покажем, как можно корректировать точность до количества цифр, представимых типом.
    Когда мы выводим таблицы, векторы или матрицы, желательно выравнивать значения для удобочитаемости. Для этого мы можем задать ширину выходных данных:
    cout << "pi = " << setw(30) << pi << '\n';
    02_ch01.indd 73 14.07.2016 10:46:40

    Основы C++
    74
    Результат имеет следующий вид:
    pi = 3.141592653589793
    setw влияет только на следующий вывод данных, в то время как setprecision влияет на все последующие выводы (числовых данных), подобно прочим манипу- ляторам. Предоставленная ширина рассматривается как минимальная, и, если вы- водимому значению требуется больше знакомест, таблицы превращаются в нечто уродливое.
    Мы можем запросить выравнивание по левому краю и заполнение пустого пространства выбранным нами символом, например
    -:
    cout << "pi = " << setfill('-') << left
    << setw(30) << pi << '\n';
    Результат имеет следующий вид:
    pi = 3.141592653589793-------------
    Еще один способ форматирования — непосредственная установка флагов. Не- которые реже используемые настройки форматирования можно применять толь- ко таким образом, как, например, нужно ли показывать знак для положительных значений. Кроме того, можно заставить выводить значения в “научной” записи — нормализованном представлении с показателем степени:
    cout.setf(ios_base::showpos); cout << "pi = " << scientific << pi << '\n';
    Результат имеет следующий вид:
    pi = +3.1415926535897931e+00
    Целые числа могут быть выведены в восьмеричной и шестнадцатеричной сис- темах счисления:
    cout << "63 восьмеричное = " << oct << 63 << ".\n"; cout << "63 щестнадцатеричное = " << hex << 63 << ".\n"; cout << "63 десятичное = " << dec << 63 << ".\n";
    Результат имеет следующий вид:
    63 восьмеричное = 77.
    63 шестнадцатеричное = 3f.
    63 десятичное = 63.
    Логические значения по умолчанию выводятся как целые числа 0 и 1. При не- обходимости можно потребовать выводить их как true и false:
    cout << "pi < 3 = " << (pi < 3) << '\n'; cout << "pi < 3 = " << boolalpha << (pi < 3) << '\n';
    Наконец можно сбросить все измененные флаги форматирования:
    02_ch01.indd 74 14.07.2016 10:46:41

    1.7. Ввод-вывод
    75
    int old_precision = cout.precision(); cout << setprecision(16) cout.unsetf(ios_base::adjustfield | ios_base::basefield
    | ios_base::floatfield | ios_base::showpos
    | ios_base::boolalpha); cout.precision(old_precision);
    Каждый флаг представляет бит в переменной состояния. Чтобы включить не- сколько флагов, их можно объединить с помощью операции побитового ИЛИ.
    1.7.6. Обработка ошибок ввода-вывода
    Определимся сразу: ввод-вывод в C++ не защищен от ошибок (не говоря уж о дураках). Об ошибках может быть сообщено различными способами, и наша обработка ошибок должна соответствовать этим способам. Давайте рассмотрим следующий программный пример:
    int main ()
    { std::ifstream infile("some_missing_file.xyz"); int i; double d; infile >> i >> d; std::cout << "i = " << i << ", d = " << d << '\n'; infile.close();
    }
    Хотя файл не существует, операция открытия не приводит к аварийному за- вершению, и выполнение продолжается. Мы даже можем читать из несуществу- ющего файла. Излишне говорить, что “прочитанные” значения i и d не имеют смысла:
    i = 1, d = 2.3452e-310
    По умолчанию потоки не генерируют исключений. Причины такого поведе- ния исторические: потоки старше исключений, поэтому такое поведение оставле- но для обратной совместимости, чтобы не нарушать работоспособность старых программ.
    Чтобы быть уверенным, что все операции успешны, мы должны проверять флаги ошибок, в принципе, после каждой операции ввода-вывода. Следующая программа запрашивает у пользователя новое имя файла до тех пор, пока файл не будет открыт. После прочтения его содержимого мы вновь проверяем успешность выполненной операции:
    int main()
    { std::ifstream infile; std::string filename{"some_missing_file.xyz"}; bool opened = false; while(!opened) {
    02_ch01.indd 75 14.07.2016 10:46:41

    Основы C++
    76
    infile.open(filename); if (infile.good()) { opened = true;
    } else { std::cout << "Файл '" << filename
    << "' не существует, введите новое имя: "; std::cin >> filename;
    }
    } int i; double d; infile >> i >> d; if (infile.good()) std::cout << "i = " << i << ", d = " << d << '\n'; else std::cout << "Ошибка чтения содержимого файла.\n"; infile.close();
    }
    Из этого простого примера видно, что написание надежных приложений с ис- пользованием файлового ввода-вывода может потребовать определенных трудов.
    Если мы хотим использовать исключения, можно разрешить их во время вы- полнения каждого потока:
    cin.exceptions(ios_base::badbit|ios_base::failbit); cout.exceptions(ios_base::badbit|ios_base::failbit); std::ifstream infile("f.txt"); infile.exceptions(ios_base::badbit|ios_base::failbit);
    Потоки генерируют исключения каждый раз, когда происходит сбой операции или когда они находятся в “плохом” (bad) состоянии. Исключения могут также ге- нерироваться по достижении (непредвиденного) конца файла. Однако в конец фай- ла более удобно определять с помощью проверки (например, while(!f.eof())).
    В приведенном выше примере исключения для infile включаются только после открытия файла (или попытки открытия). Для проверки результата опера- ции открытия с помощью исключений необходимо сначала создать поток, затем включить исключения и только после этого явно открыть файл. Включение ис- ключений дает нам как минимум гарантию, что если программа корректно заве- шает работу, то все операции ввода-вывода выполняются успешно. Мы можем сделать нашу программу более надежной, перехватывая исключения, которые мо- гут быть сгенерированы.
    Исключения файлового ввода-вывода только частично защищают нас от оши- бок. Например, приведенная далее программа, очевидно, неверна (не совпадают типы и не разделены числа):
    void with_io_exceptions(ios & io)
    { io.exceptions(ios_base::badbit|ios_base::failbit);
    }
    02_ch01.indd 76 14.07.2016 10:46:41

    1.7. Ввод-вывод
    1   2   3   4   5   6   7   8   9


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