Скачиваний:
190
Добавлен:
05.07.2021
Размер:
16.53 Mб
Скачать

15. Предостережения относительно использования исключений. Использование и управление исключениями в современных языках программирования.

Обработка исключений должна быть встроена в программу, а не присоединена извне. Однако у такого подхода есть свои недостатки. Например, исключения увеличивают размер программы и снижают скорость ее выполнения. Спецификации исключений плохо сочетаются с шаблонами, т.к. шаблонные функции могут генерировать различные виды исключений, в зависимости от конкретной специализации. Динамическое распределение памяти также не всегда гладко взаимодействует с исключениями.

Рассмотрим немного подробнее совместную работу динамического распределения памяти и исключений. Пусть имеется следующая функция:

void testl(int n){ string mesg("I'm trapped in an endless loop"); if(oh_no) throw exception();

//...

return;

}

Класс string использует динамическое распределение памяти. Обычно деструктор string вызывается, когда функция доходит до оператора return и завершается. Благодаря раскручиванию стека оператор throw позволяет вызвать деструктор, хотя он и завершает функцию преждевременно. То есть в этом случае управление памятью выполняется без проблем.

Теперь рассмотрим следующую функцию:

void test2 (int n){ double* ar = new double [n]; if (oh_no) throw exception();

delete [] ar; return;

}

А вот здесь проблема присутствует. Раскручивание стека удаляет переменную ar из стека. Однако из-за преждевременного завершения функции оператор delete [] в конце тела функции будет пропущен. То есть указатель уничтожен, а память, на которую он указывал, остается выделенной, хотя обратиться к ней невозможно. В данном случае имеется утечка памяти.

Такую утечку можно устранить. Например, можно перехватить исключение в той же функции, которая его сгенерировала, добавить в блок catch код очистки и сгенерировать исключение заново:

void test3(int n){ double * ar = new double [n]; try { if (oh_no) throw exception();

}

catch(exception & ex){ delete [] ar; throw;

//... delete [] ar; return;

}

Однако такой подход при недостаточной внимательности может породить новые ошибки. Другой способ основан на использовании шаблонов интеллектуальных указателей.

В общем, несмотря на исключительную важность управления исключениями в некоторых проектах, оно требует дополнительных усилий по программированию, увеличивает размер программы и замедляет ее работу. Правда, ущерб от отсутствия таких проверок может быть гораздо большим.

В современных библиотеках управление исключениями может достичь новых уровней сложности — в основном из-за недокументированных или плохо документированных подпрограмм обработки исключений. Каждый, кто знаком с современными операционными системами, наверняка сталкивался с ошибками и проблемами, которые связаны с необработанными исключениями.

При этом программистам часто приходится до изнеможения копаться в содержимом библиотек, разбираясь, какие исключения сгенерированы, когда и где они возникли и как их обработать.

Освоение обработки ошибок и исключений на основе документации и кода библиотеки принесет программам большую пользу.

Тема 3: Потоки ввода-вывода в языка С++

1. Особенности реализации ввода и вывода в C++. Стандартный пакет ввода-вывода. Заголовочные файлы iostream, fstream. Особенности их подключения и использования. Этапы управления вводом/выводом данных. Заголовок ios: типы и функции для работы с потоками ввода/вывода.

В большинстве языков программирования операции ввода и вывода встроены в сам язык.

Но ни С, ни C++ не имеют операций ввода и вывода, встроенных в сам язык.

Для организации ввода-вывода C++ полагается на решения C++ вместо решений, предлагаемых С, и это решение представляет собой набор классов, определенных в заголовочных файлах iostream (бывший iostream.h) и fstream (бывший fstream.h).

Операция извлечения из потока >> является методом класса istream, операция вставки в поток << — методом класса ostream. Оба этих класса являются наследниками класса ios.

Классы, используемые для вывода данных на экран и ввода с клавиатуры, описаны в заголовочном файле iostream. При подключении этого файла с помощью директивы #include <iostream> в программе автоматически создаются виртуальные каналы связи cin для ввода с клавиатуры и cout для вывода на экран.

Классы, используемые для ввода/вывода файлов, объявлены в файле fstream.

Некоторые заголовки и классы, используемые для реализации ввода/вывода:

  • Заголовок ios определяет несколько основных типов и функций для работы с потоками ввода/вывода. В заголовке ios определено три класса для реализации основных задач ввода/вывода: ios_base, basic_ios, fpos.

  • Класс ios_base описывает особенности ввода/вывода общие как для входных, так и для выходных потоков, которые не зависят от параметров шаблона.

  • Класс basic_ios является шаблонным и описывает свойства общие как для входных потоков (шаблона класса basic_istream), так и для выходных потоков (шаблона класса basic_ostream), которые зависят от параметров шаблона.

  • Класс basic_istream находится в заголовочном файле <istream> и описывает объекты, управляющие извлечением элементов из буфера потока.

  • Класс basic_ostream находится в заголовочном файле <ostream> описывает объекты, управляющие вставкой элементов в буфер потока.

  • Класс ostream является производным для класса basic_ios и предоставляет методы для реализации вывода. Класс ostream представляет собой реализацию шаблонного класса basic_ostream для типа char и char_traits, специализирующийся на char. Класс ostream представляет собой псевдоним шаблона класса basic_ostream, специализированного для элементов типа char с чертами символов по умолчанию.

  • Класс istream является производным от basic_ios и предоставляет методы ввода из потока. Класс istream представляет собой реализацию шаблонного класса basic_istream для типа char и char_traits, специализирующийся на char. Класс istream представляет собой псевдоним шаблона класса basic_istream, специализированного для элементов типа char с чертами символов по умолчанию.

  • Класс iostream является производным от классов basic_istream и basic_ostream одновременно и предоставляет методы ввода из потока и вывода в поток. Класс iostream представляет собой реализацию шаблонного класса basic_iostream, который управляет вставками через свой базовый класс basic_ostream и извлечениями из потока через свой базовый класс basic_istream. Оба класса basic_iostream и basic_ostream имеют общий базовый класс basic_ios, который наследуется виртуально.

  • Заголовок streambuf используется для определения шаблона класса basic_streambuf, предназначенного для получения буфера потока, который управляет передачей элементов в поток и из него.

Класс streambuf является производным от классов basic_ streambuf представляет собой специализацию класса basic_streambuf, которая использует char в качестве параметров шаблона. Класс streambuf является синонимом шаблона класса basic_streambuf, специализированного для элементов типа char с чертами символов по умолчанию.

2. Особенности использования потоков и буферов при вводе/выводе данных. Буфер: понятие, необходимость использования. Буферизация: понятие, преимущества, недостатки. Буферизированный и небуферизированный ввод/вывод: понятие, преимущества, недостатки.

Программа на языке C++ воспринимает ввод и вывод как потоки байтов. При вводе программа извлекает байты из входного потока, а при выводе помещает байты в выходной поток.

Для текстовых программ каждый байт может представлять символ. В общем случае байты могут образовывать двоичное представление символов и числовых данных. Байты входного потока могут поступать с клавиатуры, но также из устройств хранения: жесткого диска, другой программы, сокета. Аналогично, байты выходного потока могут передаваться на дисплей, на принтер, на устройство хранения или же отправляться другой программе.

Потоки служат посредниками между программой и источником или местом назначения потока. Такой подход позволяет программам на C++ трактовать ввод с клавиатуры так же, как и ввод из файла; программа на C++ просто просматривает поток байтов, не нуждаясь в информации о том, откуда эти байты поступают. Точно так же, используя потоки, программа на C++ может обрабатывать вывод независимо от того, куда направляются байты. Таким образом, управление вводом включает две стадии:

  • ассоциирование потока с вводом программы;

  • подключение потока к файлу.

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

Обычно ввод и вывод может более эффективно обрабатываться с использованием буфера. Буфер— это блок памяти, используемый в качестве промежуточного временного хранилища при передаче информации от устройства в программу или из программы устройству.

Обычно устройства передают информацию блоками размером по 512 байт или более, в то время как программы часто обрабатывают данные по одному байту за раз. Буфер облегчает согласование этих двух различных скоростей передачи информации. Например, предположим, что программа должна подсчитать количество символов доллара в файле на жестком диске. Программа может прочитать один символ из файла, обработать его, прочитать следующий символ из файла и т.д. Чтение из дискового файла по одному символу требует множества операций обращения к оборудованию и выполняется медленно.

Буферизованный подход заключается в чтении большой порции данных с диска, сохранении этой порции в буфере и последующем считывании из буфера по одному символу. Поскольку чтение отдельных байтов из оперативной памяти выполняется намного быстрее, чем с жесткого диска, такой подход значительно быстрее и оптимальнее. Конечно, после того, как программа достигает конца буфера, ей приходится затем считывать следующую порцию данных с диска. Принцип подобен использованию резервуара с водой, накапливающего большой объем дождевой воды во время непогоды, из которого затем вода поступает в дом небольшими порциями по мере необходимости.

Клавиатурный ввод поставляет символы по одному, поэтому в его случае программа не нуждается в буфере для согласования разных скоростей передачи данных. Однако буферизованный клавиатурный ввод обеспечивает пользователю возможность вернуться назад и исправить ввод до того, как он будет передан в программу. Обычно программа C++ очищает буфер ввода при нажатии клавиши <Enter>.

Для вывода данных на дисплей программа C++ обычно очищает выходной буфер при передаче символа новой строки. В зависимости от реализации, программа может очищать ввод и в других случаях — например, при предстоящем вводе. То есть, когда программа достигает оператора ввода, она очищает любой вывод, находящийся в выходном буфере.

3. Очистка выходного буфера (автоматическая и принудительная). Манипуляторы fflush и endl. Настройка ширины полей (метод width() и его перегрузки), установка точности отображения чисел с плавающей точкой (метод precision() и его перегрузки). Примеры использования манипуляторов формата. Символы-заполнители. Метод fill(). Пример изменения символазаполнителя полей.

Поскольку буфер класса ostream управляется объектом cout, вывод не отправляется по назначению немедленно. Вместо этого он накапливается в буфере до тех пор, пока не заполнит его. Затем программа очищает буфер, отправляя его содержимое по назначению и освобождая буфер для приема новых данных. Как правило, размер буфера составляет 512 байт или же кратное этому значению число.

Буферизация — способ экономии времени, когда стандартный вывод подключен к файлу на жестком диске. Намного эффективнее, чтобы программа не обращалась к жесткому диску 512 раз для отправки 512 байт, а накапливала эти 512 байт в буфере и затем записывала их на диск в ходе одной операции.

Однако для экранного вывода первоначальное заполнение буфера менее важно. В самом деле, было бы неудобно, если бы пришлось изменять сообщение типа "Нажмите любую клавишу для продолжения" так, чтобы оно заняло все 512 байт, предусмотренные для заполнения буфера. К счастью, в случае экранного вывода программе не обязательно дожидаться заполнения буфера. Например, отправка символа перевода строки обычно ведет к очистке буфера. Кроме того, как упоминалось ранее, большинство реализаций C++ очищают буфер, когда ожидается ввод. Предположим, что имеется следующий код:

cout<< "Enter a number: "; //вывод приглашения на ввод числа float num;

cin >> num;

Тот факт, что программа ожидает ввода, заставляет ее отобразить сообщение cout (т.е. очистить буфер от сообщения "Enter a number: ") немедленно, даже несмотря на то, что строка вывода не содержит символа новой строки. Не будь этой функциональной особенности, программе пришлось бы ожидать ввода, не выдав пользователю приглашения на ввод посредством сообщения cout.

Если используемая реализация не очищает буфер вывода, когда требуется, это можно сделать принудительно с помощью одного из двух манипуляторов. Манипулятор fflush просто очищает буфер, а манипулятор endl очищает буфер и вставляет символ перевода строки. Эти манипуляторы применяются так же, как имена переменных:

cout << "Hello, good-looking! " << fflush;

cout << "Wait just a moment, please." << endl;

Фактически манипуляторы являются функциями. Например, буфер cout можно очистить, вызвав функцию flush() непосредственно:

flush(cout);

Однако класс ostream перегружает операцию << так, что следующее выражение замещается вызовом функции flush (cout):

cout<< flush;

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

Для размещения чисел различной длины в полях постоянной ширины можно воспользоваться методом-элементом width.

Этот метод имеет следующие прототипы:

Первая форма возвращает текущую установку ширины поля. Вторая устанавливает ширину поля равной _Wide пробелам и возвращает предыдущее значение ширины. Это позволяет сохранить предыдущее значение на случай, если позднее понадобится восстановить старое значение ширины поля.

Метод width() оказывает влияние только на следующий отображаемый элемент, после чего восстанавливается значение поля по умолчанию.

По умолчанию объект cout заполняет неиспользуемые части поля пробелами. Для изменения этого можно воспользоваться функциейэлементом fill(). Например, следующий вызов изменяет символ-заполнитель на звездочку:

cout.fill('*');

Это может быть удобно, например, для распечатки чеков, чтобы получатели не могли добавить к сумме один или два разряда. Применение этой метода-элемента демонстрируется в примере ниже.

//Пример №5. Изменение символа-заполнителя полей

#include <iostream>

int main() {

system("chcp 1251"); system("cls");

using std::cout;

cout.fill('*');

const char* staff[2] = { "Джен Синсеро", "Марк Мэнсон" };

long bonus[2] = { 900, 1350 };

for (int i = 0; i < 2; i++) {

cout << staff[i] << " : $";

cout.width(7);

cout << bonus[i] << "\n";

}

return 0;

}