Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf240 |
Часть I * Введение в г1рогро^г«^ирование на'С-^Ф |
для обозначения ESC-символа в именах маршрутов). Если файла с указанным именем на диске нет, то он создается. Если же файл существует, то он без уведом ления удаляется, и заводится новый файл с тем же именем. (Операционная систе ма, поддерживающая версии файлов, создает следующую версию файла.)
Листинг 6.14. Использование динамического массива для считывания набора строк и их записи в файл на диске
#inclucle |
<iostream> |
|
|
/ / для объектов ifstream, |
ofstream |
|||||
#inclucle |
<fsteam> |
|
|
|||||||
using |
namespace std; |
|
|
|
|
|
||||
int main (void) |
|
|
|
|
|
|||||
{ |
|
|
|
|
|
|
|
// короткий буфер для ввода |
||
const int LEN = 8; char buf[LEN]; |
|
|||||||||
int cnt = 0; |
|
|
|
// счетчик строк |
|
|||||
ofStream f("data.out"); |
|
// новый файловый объект для вывода |
||||||||
cout « |
" Введите данные (или нажмите Enter для завершения): п"; |
|
||||||||
do { |
|
|
|
|
|
|
|
/ / |
начало цикла для ввода строк |
|
int len = 0; |
|
|
АО' |
//начальная длина данных |
||||||
char *data = new char[1]; data[0] |
|
|
|
|||||||
do { |
|
|
|
|
|
/ / |
начало внутреннего цикла для |
|||
|
cin.get(buf,LEN); |
|
/ / |
сегментов строк |
|
|||||
|
|
/ / |
получить следующий сегмент строки |
|||||||
|
len+=strlen(buf); |
|
/ / |
обновить общую длину |
строки |
|||||
|
char *temp = newchar[len+1]; |
|
|
|
|
|||||
|
strcpy(temp,data); strcat(temp,buf); |
|
|
|
||||||
|
delete [] data; data = temp; |
|
/ / |
расширение для длинной строки |
||||||
|
int ch = cin.peekO; |
|
/ / |
что находится слева в буфере? |
||||||
|
if (ch == '\n' M |
ch == EOF) |
|
/ / |
выход, если новая строка или EOF |
|||||
|
|
{ ch = cin.getO; break; } |
|
/ / |
сначала удалить из ввода |
|||||
|
} while |
(true); |
|
|
/ / |
продолжать до новой строки |
||||
i f |
(len |
== 0) |
break; |
|
|
/ / |
выход, если вводимая строка пуста |
|||
cout |
« |
" |
строка " « |
++cnt « ": " |
« data « |
endl; |
|
|
||
f |
« |
data |
« |
endl; |
|
|
/ / |
сохранить данные в файле |
||
delete |
[ ] |
data; |
|
|
/ / |
во избежании "утечек |
памяти" |
|||
} while |
(true); |
|
|
/ / |
продолжать до пустой |
строки |
||||
cout |
« |
" Данные сохранены в файле data.out" |
« endl; |
|
|
|||||
return |
0; |
|
|
|
|
|
|
|
Что если диск переполнен или защищен от записи? Операция создания файла просто не выполняется. Никакой ошибки этапа выполнения не генерируется.
Один из способов решения этой проблемы состоит в вызове компонентной функции failO, возвращающей true, если предыдущая операция ввода-вывода была завершена неудачно (по любой причине), и false в случае успешного завер шения.
ofstream |
f("data.out"); |
/ / открывает |
выходной файл data.out |
|
i f ( f . f a i l O ) |
/ / проверка на успешное выполнение, |
отказ, если неудача |
||
{ cout |
« "Невозможно открыть файл" « endl; |
return 0; } |
|
Многие программисты полагают, что переполнение диска или диск, защищен ный от записи,— случаи редкие и данной вероятностью можно пренебречь. С этим трудно согласиться, поскольку такая программа не будет переносимой. В листин ге 6.14 мы этим пренебрегли, а напрасно.
Глава 6 • Управление памятью |
243 |
тем же (в большинстве систем). Поскольку программа не предупреждена, что больше данных нет, остальная часть цикла обрабатывает последнее значение (как если бы оно повторилось в строке ввода). На следуюш,ей итерации цикл завершится по концу файла.
О с т о р о ж н о ! В C++, когда программа считывает из файла последний элемент, условие конца файла не возникает. Оно возникает при следующем чтении, когда программа пытается считать данные за последним элементом в файле.
#Избегайте подобной ситуации.
В листинге 6.15 приведена версия программы из листинга 6.13, считывающей данные из файла, а не с клавиатуры. Чтобы легче было сравнивать, здесь за комментированы операторы чтения данных с клавиатуры. Как видно, перейти от чтения данных с клавиатуры к чтению из файла совсем нетрудно. Результаты программы показаны на рис. 6.18.
Листинг 6.15. Использование связанного списка узлов в динамически распределяемой памяти для чтения данных из файла на диске
#include |
<iostream> |
|
#include |
<iomanip> |
|
#include |
<fstream> |
/ / для класса ifstream |
using namespace std;
typedef |
double Item; |
|
struct |
Node { |
|
Item item; |
|
|
Node* next; } ; |
|
|
int main () |
|
|
{ |
|
//счетчик amount |
int count =0; |
||
Node *data=0, *last; |
// указатели наначало и конец списка |
|
ifstream f("amounts.dat"); |
// файл для чтения данных |
if (f.failO)
{ cout « "Невозможно открыть файл" « endl; return 0; }
do { |
|
//пока не встретится EOF |
||
double amount; |
|
// локальная переменная для ввода |
||
// cout « |
" Введите сумму (или Одля завершения): "; |
|
||
// cin » |
amount; |
//получить от пользователя след. значение double |
||
// if (amount ==0) break; |
//получить следующее значение double из файла |
|||
f » amount; |
||||
if (f.eofO) break; |
// создать вдинамической области новый узел |
|||
Node* q = new Node; |
||||
if (q ==0) |
//проверка науспешное выполнение запроса |
|||
{ cout « "Нет памяти вдинамической области" « |
endl; break; } |
|||
q->item =amount; q->next =NULL; |
|
|
|
|
(data == 0 ? data : last->next) = q; |
last=last->next; также годится |
|||
last =q; |
II |
|||
count++; |
|
|
|
|
} while (true); |
|
" знач.\п"; |
|
|
cout « |
"\n5cero загружено " « count « |
|
||
if (count ==0) return 0; |
// нет вывода, если нет ввода из файла |
|||
cout « |
"\Номер Сумма Промежуточный |
итог\п\п"; |
//печать заголовка |
|
cout.setf(ios: ifixed); |
|
// фиксированный формат для double |
||
cout.precision(2); |
//цифры после десятичной точки |
I |
244 |
I |
Часть ! ^ Введение в програттирошаимв на C+-f |
|
||||||
|
|
double total = 0; |
|
|
|
// сумма для ввода значений |
|
|||
|
|
int i = 0; |
|
|
|
|
//OK |
|
||
|
|
for (Node *q = data; q != NULL; q = q ->next) |
|
|||||||
|
|
|
{ total +=q->item; |
|
|
// накопление итоговой суммы |
|
|||
|
|
|
G0ut.setw(3); « i+1; |
|
// номер транзакции |
|
||||
|
|
|
cout.setw |
(10); « |
q->item; |
endl; |
// значение транзакции |
|
||
|
|
|
cout.setw |
(11); « |
total « |
// текущий итог |
|
|||
|
|
|
} |
|
|
= data; |
|
|
|
|
|
|
Node *p = data, *r |
|
|
|
|||||
|
|
while (p != 0) |
|
|
|
// предотвращение "зависания" последнего узла |
||||
|
|
{ p = p-> next; |
|
|
|
|||||
|
|
|
delete |
r; г = p; } |
|
|
|
|
||
|
|
return 0; |
|
|
|
|
|
|
||
|
Всего загружено |
4 значений |
|
|
Файл amount.dat, который использовался для получения |
|||||
|
|
|
результата для рис. 6.18, содержит следуюш,ие строки: |
|
||||||
|
Номер |
Сумма |
Промежуточный итог |
330.16 |
|
|
||||
|
1 |
|
330.16 |
|
330.16 |
|
76.33 |
|
|
|
|
2 |
|
76.33 |
|
406.49 |
|
50.00 |
|
|
|
|
3 |
|
50.00 |
|
456.49 |
|
120.00 |
|
|
|
|
4 |
|
120.00 |
|
576.49 |
|
|
|
||
1 |
i |
««..Ш |
...«..•-....UM.U. |
|
|
|
|
Многих программистов функция eof() вполне устраива |
||
Рис. |
6.18. Результат |
|
|
ет, однако она делает программу уязвимой в случае ошибок |
||||||
|
|
в форматировании файла ввода. |
|
|||||||
|
|
|
выполнения |
программы |
|
|||||
|
|
|
из листинга |
6.15 |
Предположим, что при наборе числа 50 в третьей строке |
|||||
|
|
|
|
|
|
|
|
место О была нажата буква ' о'. Когда оператор f >> amount; |
||
|
|
|
|
|
считает эту строку, он найдет там 5, а затем ' о'. Программа сделает вывод, что |
|||||
|
|
|
|
|
вводится значение 5, оставляет символ ' о' в строке ввода и выполняет следующий |
|||||
|
|
|
|
|
оператор. На следующей итерации оператор f >> amount находит ' о' в строке вво |
|||||
|
|
|
|
да, делает вывод, что ввод закончен, и завершается. Выполняется следующий опе |
||||||
|
|
|
|
|
ратор, и программа зацикливается. |
|
||||
|
|
|
|
|
Конечно, опечатки такого рода более вероятны при вводе с клавиатуры, чем из |
|||||
|
|
|
|
|
файла, ведь файл можно проверить перед выполнением программы. Тем не менее |
|||||
|
|
|
|
|
они случаются. Некоторые программисты избегают использования операции |
» , |
||||
|
|
|
|
так как она слишком уязвима к ошибкам формата ввода. Зацикливание — непри |
||||||
|
|
|
|
ятное явление, как при вводе с клавиатуры, так и из файла. Вместо операции |
» |
|||||
|
|
|
|
они применяют для чтения символьных данных описанные ранее функции get() |
||||||
|
|
|
|
и getlineO. Когда введенная строка находится в памяти, программа может проа |
||||||
|
|
|
|
нализировать данные и сгенерировать интеллектуальное сообщение об ошибке, |
||||||
|
|
|
|
если они некорректны. |
|
|
||||
|
|
|
|
|
Еще один источник уязвимости — способ завершения файла. В приведенном |
|||||
|
|
|
|
выше примере символ новой строки вводился после каждого значения, включая |
||||||
|
|
|
|
последнее — 120. Если за последней записью в файле следует символ новой стро |
||||||
|
|
|
|
ки в конце файла, функция извлечения » при чтении ввода останавливается перед |
||||||
|
|
|
|
этим символом новой строки. В таком случае условие "конец файла" достигается, |
||||||
|
|
|
|
только когда программа читает запись, следующую за последней записью. |
|
|||||
|
|
|
|
|
Что происходит, если последний символ новой строки добавлен не был? Или |
|||||
|
|
|
|
все значение набраны на одной строке без завершающего символа новой строки? |
||||||
|
|
|
|
В этом случае функция извлечения данных считывает маркер конца файла и воз |
||||||
|
|
|
|
никает условие "конец файла". Функция eof (), которая вызывается после опера |
||||||
|
|
|
|
тора f » |
amount;, возвращает true, и цикл завершается без обработки последнего |
|||||
|
|
|
|
значения. |
|
|
|
|
Глава 6 * Управление памятью |
245 |
Это нехорошо. Программа должна быть написана так, чтобы ее поведение не изменялось в зависимости от того, поместит пользователь (или телекоммуника ционное ПО) вслед за последней записью в файле символ новой строки или нет. Чтобы устранить проблему, некоторые программисты избегают применять функ цию eof (). Вместо нее они используют старую добрую функцию fail():
do { |
|
/ / |
структура цикла C++ или Java |
||
double amount; |
/ / |
локальная |
переменная для ввода |
||
f » |
amount; |
/ / |
получить из файла следующее значение |
||
i f |
( f . f a i l O ) break; |
/ / |
остановить |
ввод, |
если больше нет данных |
|
} |
/ / |
остальная |
часть |
цикла |
Функция failO возвращает значение true, когда операция по какой-то причине (включая достижение конца файла) завершается неудачно. Если вместо 50 на брать 5о, то 5 счйтывается, а ' о' обнаруживается в потоке ввода при следующей итерации цикла. Оператор f>> amount; ничего не считывает, и функция fail() возвращает true. Цикл ввода завершается. Во-первых, преждевременное завер шение лучше, чем зацикливание. Во-вторых, после завершения цикла программа может проанализировать ситуацию и сгенерировать сообщение об ошибке в слу чае, если это произошло преждевременно.
Во втором примере, когда за значением 120 не следует символ новой строки, возникает условие "конец файла", но функция fail() возвращает false, так как значение 120 было считано оператором f » amount; корректно. Лишь на следую щем проходе цикла, когда программа пытается прочитать следующее значение, эта функция возвращает true. Следовательно, последнее значение в файле обра ботано корректно.
Файловые объекты ввода и вывода
Кроме if St ream и ofstream библиотека iostream в C++ определяет большое число других классов. В 99% случаев программисту не потребуется ничего знать о них. Здесь упоминается только один потоковый класс fstream, так как он объе диняет в себя характеристики классов if stream и ofstream.
При создании объектов типа if stream и ofstream режим открытия не указыва ется: if St ream создается по умолчанию для чтения, а ofstream — для записи. Для объектов класса fst ream задается режим открытия. Для этого при создании объек та используется второй аргумент:
fstream |
of("data.out,ios::out); |
/ / |
выходной файл |
fstream |
infC'amounts.dat,ios::in); |
/ / |
входной файл |
Режим ввода задается по умолчанию, его можно опустить. В число других доступных режимов входят ios: :арр (файл открывается для добавления данных в конец), ios: :binary (файл открывается в двоичном, а не в текстовом формате) и другие. Эти режимы реализованы как двоичные флаги. Если нужно, их можно комбинировать с помощью двоичной операции "включающее ИЛИ" — ('|').
fstream mystream("archive.dat", i o s : : i n | i o s : : o u t ) ; |
/ / ввод-вывод |
В C++ существует несколько способов проверки успешного или неуспешного выполнения операции с файлом. Кроме описанной выше функции fail(), можно применять функцию good():
fstream infC'amounts.dat",ios::in); |
/ / входной файл |
i f (! inf .goodO) |
//еще один способ |
{cout « "Невозможно открыть файл" « endl; return 0; }
246 |
Часть I • Введен1^е в програ^1^ировани[е но С-ь+ |
Можно даже интерпретировать файловый объект как числовое значение. Когда операция завершается неудачно, оно равно О, а в случае успешного выполнения содержит нечто, отличное от нуля. Вот еш.е один пример проверки успешного открытия файла:
fstream inf("amounts.dat,ios::in); |
/ / |
входной файл |
|
i f |
(!inf ) |
/ / |
еще один способ |
{ |
count « "Невозможно открыть файл" « |
endl; return 0; } |
|
Тот же синтаксис можно применять для проверки успешного выполнения опера ций чтения и записи. Например, нетрудно подсчитать число символов в файле с помощ.ью функции get О с односимвольным параметром. Если операция чтения завершается неудачно (из-за достижения конца файла или по любой другой при чине), функция возвраш.ает О и это значение можно использовать для завершения цикла while.
int Count = 0; char oh; while (inf.get(ch))
count++;
cout « "Всего символов: " « count « endl;
Обычно закрывать файлы не требуется. Они закрываются, когда в конце об ласти действия уничтожается ассоциированный с файлом объект. Однако иногда возникает необходимость закрыть файл явно. Это делается с помош,ью функции close():
inf .closeO; |
/ / закрыть файл |
Такая необходимость может возникать, если требуется закрыть файл до того, как завершится область действия его файлового объекта, например когда откры вается несколько файлов и следуюш,ий файл открыть не удается. В такой ситуации до попытки восстановления или завершения программы лучше явно закрыть все открытые файлы. Может понадобиться закрывать файл и в том случае, если нежелательно хранить несколько файлов открытыми, например когда данные считываются из одного файла, обрабатываются в памяти, а затем результаты записываются в другой файл, чтобы их можно было использовать позднее.
В листинге 6.16 показана модифицированная программа из листинга 6.15. Кроме вывода результатов на экран, она сохраняет отчет в файле amounts, rep. Здесь путем сравнения файловых объектов с нулем проверяется успешность выполнения операций ввода-вывода. В С+-1- это общепринятый подход. Входной файл закрывается в конце ввода.
Листинг 6.16. Чтение из файла, вывод на экран и в выходной файл
#inclucle <iostream> #inclucle <iomanip> #inclucle <fstream> using namespace std;
typedef double Item; struct Node {
Item item; Node* next; } ;
int main () |
|
|
|
|
{ |
|
|
/ / |
счетчик amount |
int |
count |
= 0; |
||
Node *data=0, *last; |
/ / |
указатели на начало и конец списка |
||
fstream inf("amounts.dat",ios: :in); |
/ / |
файл для чтения данных |
||
i f |
(!inf ) |
{ cout « "Невозможно открыть файл" « |
endl; return 0; } |
|
|
|
|
Глава 6 • Управление памятью |
247 |
|||
do { |
|
|
|
|
// пока не встретится EOF |
|
||
double amount; |
|
|
// локальная переменная для ввода |
|
||||
inf » amount; |
|
|
// получить следующее значение double из файла |
|||||
if (!inf)break; |
|
|
// создать в динамической области новый узел, |
|||||
Node* q = new Node; |
"Her памяти вдинам |
|||||||
if (q == 0) {cout « |
области" « endl; break; } |
|
||||||
q->item = amount; q->next = NULL; |
// заполнить данными |
|
||||||
(data == 0 ? data |
: last->next) = q; |
|
|
|
|
|||
last = q; count++; |
|
|
|
|
|
|
||
} while (true); |
|
|
|
// файл больше ненужен |
|
|||
inf .closeO; |
|
|
|
|
||||
fstream of("amounts, rep. ios::out); |
|
// файл для записи данных |
|
|||||
if (!of) |
{cout << "Невозможно открыть выходной файл |
' « endl; } |
|
|||||
cout « "\пВсего загружено " « count << "знач.\п"; |
|
|
|
|||||
of « "\пВсего загружено " « count « " значДп"; |
// нет вывода, если нет ввода изфайла |
|||||||
if (count == 0) return 0; |
|
|||||||
cout « "\Номер Сумма Промежуточный итог\п\п"; |
// печать заголовка |
|
||||||
of « "\Номер Сумма Промежуточный итог\п\п"; |
|
// печать заголавка |
|
|||||
cout. setf(ios:ifixed); cout precision (2); |
|
// точность для экрана |
|
|||||
of.setf(ios::fixed); of.precision(2); |
|
// точность для файла |
|
|||||
double total = 0; int i = 0; |
|
// промежуточный итог, счетчик строк |
||||||
for (Node *q = data; q !=NULL; q = q ->next) |
|
//ОК |
|
|||||
{ total += q->item; |
|
|
/ / |
накопление итоговой суммы |
|
|||
cout « |
setw(3) « i; |
|
/ / |
номер транзакции |
|
|||
cout « |
setw(10) « |
q->item; |
|
/ / |
значение транзакции |
|
||
cout « |
setw(11) « |
total « endl; |
|
/ / |
текущий итог |
|
||
of « |
setw(3) « |
++i « setw(10) « q->item |
/ / |
транзакция |
|
|||
of « |
setw(11) « |
total « endl; } |
|
/ / |
текущий итог |
|
||
Node *p = data, *r = data; |
|
|
|
|
||||
while (p != 0) |
|
|
|
|
|
|
||
{ p = p-> next; |
|
|
/ / предотвращение "зависания" последнего узла |
|||||
delete r; r = p; } |
|
|
|
|
|
|
||
return 0; |
|
|
|
|
|
|
|
Вы видите, операторы форматирования данных при выводе в файл те же, что и при выводе данных на экран. При вводе данных из рис. 6.18 программа из лис тинга 6.16 создает выходной файл со следующими данными:
Всего |
загружено 4 значения |
|
Номер |
Сумма |
Промежуточный итог |
1 |
330.16 |
330.16 |
2 |
76.33 |
406.49 |
3 |
50.00 |
456.49 |
4 |
120.00 |
576.49 |
Это все, что нужно знать о работе с файлами при помощи библиотеки lost ream. Данная библиотека содержит значительно больше средств, чем здесь описано, но пока что не стоит вдаваться в детали. Сначала нужно познакомиться с классами и наследованием. На самом деле даже после изучения классов и наследования может не потребоваться знать больше того, о чем рассказывалось выше. Библио тека lost ream предлагает несколько способов выполнения одних и тех же дейст вий, и вовсе не обязательно изучать все их сразу. Вместо этого лучше обратить
248 |
Часть I * Введение в программирование на С^* |
внимание на базовые средства языка и убедиться в понимании основных концеп ций. Тем самым можно подготовиться к использованию других библиотечных средств или их пониманию, если они встречаются в программах других разработ чиков.
Итоги
в данной главе изложен достаточно сложный материал. Мы рассмотрели несколько примеров использования указателей в С+4-. Первый предусматривал применение указателей для ссылки на обычные переменные, распределяемые в стеке, и реализации альтернативного метода доступа к данным переменным через псевдонимы. За исключением передачи параметров через указатель (о чем подробнее рассказано в следуюш,их главах) этот метод не особенно полезен. Но, некоторые программисты считают, что подобная техника делает их программы более понятными. Поэтому нужно быть готовым к тому, чтобы иметь дело с такого рода операциями с указателями в унаследованном программном коде.
Второй тип использования указателей состоит в выделении памяти для отдель ных переменных не в стеке, а в динамически распределяемой области. Переменные в динамически распределяемой области памяти не имеют имен, и указатели — единственный способ доступа к их содержимому. В то же время применение пере менных в динамически распределяемой области вместо обычных переменных в стеке не дает практических преимуществ, его следует избегать. Некоторые по лагают, что такая техника дает возможность снизить вероятность переполнения стека, а потому нужно быть готовым к применению подобных операций с указате лями в унаследованных программах.
Два других вида использования указателей вполне законны, полезны и весьма распространены в программах на C+ + . Неименованные динамические массивы представляют превосходную альтернативу именованным массивам C+ + , размер которых определяется на этапе компиляции. Динамические массивы исключают опасность переполнения массива и непроизводительной траты памяти. Операции с ними выполняются быстро и достаточно просто. При определении динамических массивов убедитесь в том, что массив не распределяется и не освобождается повторно. Место для определения такого массива нужно выбирать так, чтобы это не сказалось негативно на производительности программы.
Еще одним ценным методом применения указателей являются связанныеструктуры. Это самый гибкий способ распределения памяти, поскольку память не резервируется заранее, а выделяется по мере необходимости. Но операции с ука зателями при включении и удалении узлов достаточно сложны, их нельзя назвать интуитивно понятными. Это же относится к операциям перебора списка. Ошибки
воперациях с указателями очень трудно обнаружить. Они не всегда проявляются
внекорректном поведении программы. Распределение и освобождение памяти выполняется фрагментарно, индивидуально для каждого узла, что может сказаться на производительности программы.
Если возникает необходимость использовать связанные списки, нужно рас смотреть альтернативные варианты. Один из них — динамические массивы, вто рой — применение стандартной библиотеки шаблонов с готовыми вариантами реализации таких структур, как связанные списки, очереди, деревья и пр. Подоб ные библиотеки позволяют программисту комбинировать гибкость динамического распределения памяти с простотой применения, так что нужно этим пользоваться.
В последних разделах главы рассмотрены последовательности данных, длина которых не определена заранее. Это физические файлы. Здесь демонстрировались способы определения библиотечных объектов, позволяющих использовать при работе с файлами такие же операции, что и при вводе с клавиатуры и выводе на экран. Применение файлов позволяет программе сохранять данные на диске.
Глава 6 • Управление памятью |
249 |
где они могут находиться неопределенно яолго. После сохранения на данные не повлияет аварийное или нормальное завершение программы или сбой питания. Кроме того, что еще более важно, данные могут использоваться другой програм мой в другое время и/или в другом месте. Это существенно расширяет гибкость информационных систем.
Данная глава завершает обсуждение необъектно-ориентированных средств C++. В следующих главах мы начнем детальное обсуждение функций и классов C+ + , изучим создание объектно-ориентированных программ. Весьма впечатляющая тема! Как уже упоминалось, объектно-ориентированный подход является, вероятно, единственным, помогающим разработчикам создавать программы из относительно независимых компонентов и выражать свои идеи, понятные сопрово>вдающему приложение программисту, непосредственно в исходном коде. Такие навыки не вырабатываются автоматически, просто в процессе изучения языка. Нужно на деяться, что дальнейшее чтение книги поможет вам освоить это важное искусство.