Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
235
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

1062

[ 0 ]

Программа не вывела ни одного символа, видимо, полагая, что файл пуст. Проблема в том, что файл открыт для дозаписи и потому позиционирован на конец. При выполнении

инструкции

inOut.get( ch );

мы читаем конец файла, цикл while завершается и выводится значение 0.

Хотя мы допустили серьезную ошибку, исправить ее совсем несложно, поскольку причина понятна. Надо лишь перед чтением переустановить файл на начало. Это делается с помощью обращения:

inOut.seekg( 0 );

Запустим программу заново. На этот раз она печатает:

Call me Ishmael. Some years ago, never mind [ 45 ]

Как видим, выводится лишь первая строка текста и счетчик для нее, а оставшиеся шесть строк проигнорированы. Ну что ж, исправление ошибок неотъемлемая часть профессии программиста. А проблема опять в том, что файл открыт в режиме дозаписи. Как только мы в первый раз вывели cnt, файл оказался позиционирован на конец. При следующем обращении к get() читается конец файла, и цикл while снова завершается преждевременно.

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

// запомнить текущую позицию ios_base::pos_type mark = inOut.tellg(); inOut << cnt << sp;

этого понадобятся еще две инструкции:

inOut.seekg( mark ); // восстановить позицию

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

Дело в том, что inOut находится в состоянии конец файла”, в котором операции ввода и вывода не выполняются. Для решения проблемы необходимо сбросить это состояние с помощью функции-члена clear():

inOut.clear(); // обнулить флаги состояния

Окончательный вариант программы выглядит так:

С++ для начинающих

1063

#include <iostream> #include <fstream>

int main()

{

fstream inOut( "copy.out", ios_base::in|ios_base::app ); int cnt=0;

char ch;

inOut.seekg(0);

while ( inOut.get( ch ))

{

cout.put( ch ); cnt++;

if ( ch == '\n' )

{

// запомнить текущую позицию ios_base::pos_type mark = inOut.tellg(); inOut << cnt << ' ';

inOut.seekg( mark ); // восстановить позицию

}

}

inOut.clear();

inOut << cnt << endl;

cout << "[ " << cnt << " ]\n";

return 0;

}

Вот теперь наконец-то!все правильно. При реализации этой программы было необходимо явно сформулировать поведение, которое мы собирались поддержать. А

каждое наше исправление было реакцией на выявившуюся ошибку вместо анализа проблемы в целом.

Упражнение 20.12

Пользуясь операторами вывода для класса Date, которые вы определили в упражнении 20.7, или для класса CheckoutRecord из упражнения 20.8 (см. раздел 20.4), напишите программу, позволяющую создать файл и писать в него.

Упражнение 20.13

Напишите программу для открытия и чтения файла, созданного в упражнении 20.12. Выведите содержимое файла на стандартный вывод.

Упражнение 20.14

Напишите программу для открытия файла, созданного в упражнении 20.12, для чтения и дозаписи. Выведите экземпляр класса Date или CheckoutRecord:

(a)в начало файла

(b)после второго из существующих объектов

(c)в конец файла

С++ для начинающих

1064

20.7. Состояния потока

Пользователей библиотеки iostream, разумеется, интересует, находится ли поток в

int ival;

ошибочном состоянии. Например, если мы пишем cin >> ival;

и вводим слово "Borges", то cin переводится в состояние ошибки после неудачной попытки присвоить строковый литерал целому числу. Если бы мы ввели число 1024, то чтение прошло бы успешно и поток остался бы в нормальном состоянии.

Чтобы выяснить, в каком состоянии находится поток, достаточно проверить его значение

if ( !cin )

на истину:

// операция чтения не прошла или встретился конец файла

while ( cin >> word )

Для чтения заранее неизвестного количества элементов мы обычно пишем цикл while:

// операция чтения завершилась успешно ...

Условие в цикле while будет равно false, если достигнут конец файла или произошла ошибка при чтении. В большинстве случаев такой проверки потокового объекта достаточно. Однако при реализации оператора ввода для класса WordCount из раздела 20.5 нам понадобился более точный анализ состояния.

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

if ( inOut.eof() )

eof() возвращает true, если достигнут конец файла:

//отлично: все прочитано ...

bad() возвращает true при попытке выполнения некорректной операции, например при установке позиции за концом файла. Обычно это свидетельствует о том, что поток находится в состоянии ошибки;

fail() возвращает true, если операция завершилась неудачно, например не удалось открыть файл или передан некорректный формат ввода:

С++ для начинающих

 

1065

 

ifstream iFile( filename,

ios_base::in );

 

 

if ( iFile.fail() )

//

не удалось открыть

error_message( ... );

good() возвращает true, если все вышеперечисленные условия ложны:

if ( inOut.good() )

Существует два способа явно изменить состояние потока iostream. С помощью функции-члена clear() ему явно присваивается указанное значение. Функция setstate() не сбрасывает состояние, а устанавливает один из флагов, не меняя значения остальных. Например, в коде оператора ввода для класса WordCount при обнаружении

неверного формата мы используем setstate() для установки флага fail в состоянии

if ((ch = is.get()) != '<' )

{

is.setstate( ios_base::failbit ); return is;

объекта istream:

}

ios_base::badbit ios_base::eofbit ios_base::failbit

Имеются следующие значения флагов состояния: ios_base::goodbit

Для установки сразу нескольких флагов используется побитовый оператор ИЛИ:

is.setstate( ios_base::badbit | ios_base::failbit );

if ( !cin ) {

cerr << "Ошибка ввода WordCount" << endl; return -1;

При тестировании оператора ввода в классе WordCount (см. раздел 20.5) мы писали:

}

Возможно, вместо этого мы предпочли бы продолжить выполнение программы, предупредив пользователя об ошибке и попросив повторить ввод. Но перед чтением нового значения из потока cin необходимо перевести его в нормальное состояние. Это можно сделать с помощью функции-члена clear():

cin.clear(); // сброс ошибок

С++ для начинающих

1066

В более общем случае clear() используется для сброса текущего состояния и установки одного или нескольких флагов нового. Например:

cin.clear( ios_base::goodbit );

восстанавливает нормальное состояние потока. (Оба вызова эквивалентны, поскольку goodbit является для clear() аргументом по умолчанию.)

ios_base::iostate old_state = cin.rdstate();

cin.clear(); process_input();

// перевести поток cin в прежнее состояние

Функция-член rdstate() позволяет получить текущее состояние объекта: cin.clear( old_state );

Упражнение 20.15

Измените один (или оба) оператор ввода для класса Date из упражнения 20.7 и/или класса CheckoutRecord из упражнения 20.8 (см. раздел 20.4) так, чтобы они устанавливали состояние объекта istream. Модифицируйте программы, которыми вы пользовались для тестирования этих операторов, для проверки явно установленного состояния, вывода его на печать и сброса в нормальное. Протестируйте программы, подав на вход правильные и неправильные данные.

20.8. Строковые потоки

Библиотека iostream поддерживает операции над строковыми объектами в памяти. Класс ostringstream вставляет символы в строку, istringstream читает символы из строкового объекта, а stringstream может использоваться как для чтения, так и для записи. Чтобы работать со строковым потоком, в программу необходимо включить

заголовочный файл

#include <sstream>

Например, следующая функция читает весь файл alice_emma в объект buf класса ostringstream. Размер buf увеличивается по мере необходимости, чтобы вместить все символы:

С++ для начинающих

1067

#include <string> #include <fstream> #include <sstream>

string read_file_into_string()

{

ifstream ifile( "alice_emma" ); ostringstream buf;

char ch;

while ( buf && ifile.get( ch )) buf.put( ch );

return buf.str();

}

Функция-член str() возвращает строку объект класса string, ассоциированный со строковым потоком ostringstream. Этой строкой можно манипулировать так же, как и обычнымобъектом класса string. Например, в следующей программе text почленно

int main()

{

string text = read_file_into_string();

// запомнить позиции каждого символа новой строки vector< string::size_type > lines_of_text; string::size_type pos = 0;

while ( pos != string::npos )

{

pos = text.find( '\n' pos ); lines_of_text.push_back( pos );

}

// ...

инициализируется строкой, ассоциированной с buf:

}

Объект класса ostringstream можно использовать для автоматического форматирования составной строки, т.е. строки, составленной из данных разных типов. Так, следующий оператор вывода автоматически преобразует любой арифметический тип в соответствующее строковое представление, поэтому заботиться о выделении нужного количества памяти нет необходимости:

С++ для начинающих

1068

#include <iostream> #include <sstream>

int main()

{

int

ival

= 1024;

int *pival = &ival;

double

dval

= 3.14159;

double *pdval = &dval;

ostringstream format_message;

// преобразование значений в строковое представление format_message << "ival: " << ival

<<" адрес ival: " << pival << 'n'

<<"dval: " << dval

<<" адрес dval: " << pdval << endl;

string msg = format_message.str();

cout << " размер строки сообщения: " << msg.size() << " сообщение: " << msg << endl;

}

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

string

format( string msg, int expected, int received )

{

ostringstream message;

message << msg << " ожидалось: " << expected << " принято: " << received << "\n";

return message.str();

}

string format( string msg, vector<int> *values );

форматирования:

// ... и так далее

Приложение может сохранить такие строки для последующего отображения и даже рассортировать их по серьезности. Обобщить эту идею помогают классы Notify (извещение), Log (протокол) и Error (ошибка).

Поток istringstream читает из объекта класса string, с помощью которого был сконструирован. В частности, он применяется для преобразования строкового представления числа в его арифметическое значение:

С++ для начинающих

1069

#include <iostream> #include <sstream> #include <string>

int main()

{

int

ival

=

1024;

int *pival = &ival;

double

dval

=

3.14159;

double *pdval = &dval;

// создает строку, в которой значения разделены пробелами ostringstream format_string;

format_string << ival << " " << pival << " "

<<dval << " " << pdval << endl;

//извлекает сохраненные значения в коде ASCII

//и помещает их в четыре разных объекта istringstream input_istring( format_string.str() );

input_istring >> ival >> pival >> dval >> pdval;

}

Упражнение 20.16

В языке Си форматирование выходного сообщения производится с помощью функций

int

ival

= 1024;

double dval

= 3.14159;

char

cval

= 'a';

char

*sval

= "the end";

printf( "ival: %d\tdval% %g\tcval: %c\tsval: %s",

семейства printf(). Например, следующий фрагмент ival, dval, cval, sval );

печатает:

ival: 1024 dval: 3.14159 cval: a sval: the end

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

%d целое число

%g число с плавающей точкой

%c char

(полное описание см. в [KERNIGHAN88]):

%s C-строка

С++ для начинающих

1070

Дополнительные аргументы printf() на позиционной основе сопоставляются со спецификаторами формата, начинающимися со знака %. Все остальные символы в форматной строке рассматриваются как литералы и выводятся буквально.

Основные недостатки семейства функций printf() таковы: во-первых, форматная строка не обобщается на определенные пользователем типы, и, во-вторых, если типы или число аргументов не соответствуют форматной строке, компилятор не заметит ошибки, а вывод будет отформатирован неверно. Однако у функций printf() есть и достоинство компактность записи.

1.Получите так же отформатированный результат с помощью объекта класса ostringstream.

2.Сформулируйте достоинства и недостатки обоих подходов.

20.9. Состояние формата

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

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

#include <iostream.h>

int main()

{

bool illustrate = true;

cout << "объект illustrate типа bool установлен в true: " << illustrate << '\n';

имеющий значение true (а также литеральная константа true), выводится как целая ‘1’:

}

Чтобы поток cout выводил переменную illustrate в виде слова true, мы применяем манипулятор boolalpha:

Примечание [O.A.6]: Нумера ция сносок сбита.

1 Кроме того, программист может устанавливать и сбрасывать флаги состояния формата с помощью функций-членов setf() и unsetf(). Мы их рассматривать не будем; исчерпывающие ответы на вопросы, относящиеся к этой теме, можно получить в [STROUSTRUP97].

С++ для начинающих

1071

#include <iostream.h>

int main()

{

bool illustrate = true;

cout << "объект illustrate типа bool установлен в true: ";

//изменяет состояние cout так, что булевские значения

//печатаются в виде строк true и false

cout << boolalpha;

cout << illustrate << '\n';

}

Поскольку манипулятор возвращает потоковый объект, к которому он применялся, то допустимо прицеплять его к выводимым данным и другим манипуляторам. Вот как

#include <iostream.h>

int main()

{

bool illustrate = true;

cout << "объект illustrate типа bool: "

<<illustrate

<<"\nс использованием boolalpha: "

<<boolalpha << illustrate << '\n';

//...

можно перемежать данные и манипуляторы в нашей программе:

}

Вывод данных и манипуляторов вперемежку может сбить пользователя с толку.

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

Чтобы отменить сделанную модификацию потока cout, необходимо использовать

cout << boolalpha // устанавливает внутреннее состояние cout << illustrate

манипулятор noboolalpha:

<< noboolalpha // сбрасывает внутреннее состояние cout

Как мы покажем, для многих манипуляторов имеются парные.

По умолчанию значения арифметических типов читаются и записываются в десятичной системе счисления. Программист может изменить ее на восьмеричную или шестнадцатеричную, а затем вернуться к десятичной (это распространяется только на целые типы, но не на типы с плавающей точкой), пользуясь манипуляторами hex, oct и dec: