Указатели дают больше гибкости
Наше решение по выводу на дисплей в предыдущей секции имеет два основных недостатка. Во-первых, оно ограничено выводом шести числовых последовательностей - если пользователь угадает все шесть, программа сразу завершится. Во-вторых, она всегда выводит те же самые шесть пар элементов в той же последовательности. Как же увеличить гибкость программы?
Одно из возможных решений - создать шесть векторов, по одному на каждую последовательность, рассчитанных на одинаковое количество элементов. На каждом проходе цикла мы выбираем пары элементов из разных векторов. При повторном использовании вектора мы возьмем пару элементов из другой части вектора. Это приблизит нас к устранению обоих отмеченных недостатков.
Как и в предыдущем решении, хотелось бы иметь «прозрачный» доступ к разным векторам. В предыдущем разделе мы достигали «прозрачности» за счет доступа по индексам, а не по имени. На каждом проходе цикла мы увеличивали значение индекса на 3. Сам код оставался неизменным.
В этом разделе мы добьемся «прозрачности» тем, что будем обращаться к вектору косвенно, через указатель, вместо обращения по имени. Указатель вносит определенный уровень косвенности в программу. Вместо того чтобы работать с объектом напрямую, мы будем работать с указателем, сохраняющим адрес объекта. Мы определим указатель, который будет адресоваться вектору чисел целого типа. При каждой итерации цикла мы будем модифицировать указатель, адресуясь к разным векторам. Фактический код для манипуляций с указателями не изменится.
Использование указателей дает два преимущества нашей программе: увеличивает гибкость программы и добавляет уровень гибкости, отсутствующий при прямой работе с объектом. Этот раздел убедит вас в правдивости обоих утверждений.
Мы уже знаем, как определять объект. Следующее выражение определяет ival как объект целого типа int, инициализированного значением 102-1:
Int ival = 1024;
Указатель сохраняет адрес объекта данного типа. Для определения указателя мы добавляем к имени типа звездочку:
Int *pi; // pi указатель на объект типа int
Pi - указатель на объект типа int. Как мы должны инициализировать его для указания на ival? Определением имени объекта, примерно так:
Ival; // определяет значение ival
Мы определяем ассоциированное значение, в нашем случае 1024. Для получения адреса объекта, а не его значения, мы добавляем оператор адресации (&):
&ival; // определяет адрес ival
Для инициализации pi как адреса ival запишем следующее:
Int *pi = fcival;
Для доступа к объекту, адресуемому указателем, мы должны разыменовать указатель, что даст содержимое объекта по адресу, содержащемуся в указателе. Для этого добавим звездочку к указателю, следующим образом:
// разыменовываем рі для доступа к объекту им адресуемому
If (*pi! = 1024) // читаем *pi = 1024; // пишем
Сложность инициализации с использованием указателя, как вы видите, исходит от запутывающего синтаксиса. Виною тому двойственная природа указателя. Мы можем манипулировать адресом, содержащимся в указателе, а можем манипулировать объектом, на который он указывает. Когда мы пишем:
Pi; // определяет адрес сохраняемый в pi
То фактически управляем указателем объекта. А когда мы пишем:
*pi; // определяет значение объекта адресуемого pi
То управляем объектом, адресуемым pi.
Вторая сложность в понимании указателей - возможность отсутствия адресуемого объекта. Например, когда мы пишем *pi, это может быть, а может и не быть причиной краха программы при выполнении. Если pi адресуется к объекту, разы - меновывание pi работает совершенно правильно. Если же pi не адресуется к объекту, попытка разыменовать pi влечет непредсказуемое поведение программы во время выполнения. Это означает, что когда мы используем указатель, то должны быть уверены, что он адресуется к объекту, прежде чем сделаем попытку разыменовать его.
Указатель, который не адресуется к объекту, имеет адресуемое значение «0» (иногда его называют нулевым указателем). Любой тип указателя может быть инициализирован, или определен со значением «0»:
// инициализация каждого указателя без адресации к
Объекту
Int *pi = 0;
Double *pd = 0;
String *ps = 0;
Для защиты от разыменовывания нулевого указателя мы проверяем указатель, чтобы убедится, что его адресуемое значение не равно «О». Например,
If (pi ScSc *рі ! = 1024) *рі = 1024;
Выражение:
If (pi && ...)
Становится истинным, только если pi содержит иной адрес, чем «0». Если оно ложно, оператор И не выполняется во втором выражении. Для проверки мы обычно пользуемся оператором логического отрицания НЕ:
If (! pi) // истинно, если pi установлено в О
А вот наши шесть объектов векторов последовательностей:
Vector<int> fibonacci, lucas, pell, triangular, square, pentagonal;
На что похож указатель вектора объектов целого типа? Что ж, в общем, указатель имеет такую форму:
(тип_объекта_указывающего_на * имя_указателя__объекта) type__of_object_pointecLto * name__of_pointer_object
Наш указатель адресует тип vector<int>. Назовем его pv и инициализируем нулем:
Vector<int> *pv = 0;
Pv может адресоваться каждому вектору последовательности по очереди. Конечно, мы можем определить pv адресацией к каждой последовательности:
Pv = fcfibonacci;// ... pv = &lucas;
Но этим приносится в жертву «прозрачность» кода. Альтернативное решение - запомнить адреса каждой последовательности в векторе. Этот прием позволяет нам добраться до них «прозрачно», через индекс:
Const int seq_cnt = 6; // массив seq_cnt указателей на // объекты типа vector<int>
Vector<int> *seq_addrs[seq_cnt] = {fcfibonacci, fclucas, &pell, ^triangular, fcsquare, ^pentagonal};
Seq_addrs - это встроенный массив элементов типа vector<int>*. seq_addrs[0] содержит адрес fibonacci вектора, seq_addrs [ 1 ] - адрес lucas вектора и т. д. Мы используем это для доступа к различным векторам через индекс, а не по имени:
^ector<int> *current_vec = 0; // ...
For (int ix = 0; ix < seq_cnt; ++ix) { current_vec = seq_addrs[ix];
// вывод на дисплей всех элементов осуществляется
// косвенно через current_vec }
Оставшейся проблемой с задуманной реализацией является полная предсказуемость. Последовательности всегда Fibonacci, Lucas, Pell... Мы бы хотели сделать вывод на дисплей последовательностей случайным. Это возможно с использованием стандартной библиотеки языка С функциями rand() или srand():
#include <cstdlib> srand(sequent);
Seq_index = rand() % seq_cnt; current_vec = seq__addrs [seq_index];
RandO и srand () - функции стандартной библиотеки, которые поддерживают псевдослучайную генерацию, srand () активизирует генератор с его параметрами. Каждый вызов rand () возвращает целое значение в диапазоне от 0 до максимального целого значения, представленного int. Мы должны это ограничить числами от 0 до 5, чтобы они были правильными индексами seq_addrs. Оператор остатка (%) гарантирует нам индексацию между 0 и 5. Файл заголовка cstdlib содержит объявление обеих функций.
Мы сохраняем указатель на класс объекта несколько иначе, чем делали это с указателем на объект встроенного типа, потому что класс объекта имеет связанное с ним множество операций, которые мы можем вызвать. Например, для проверки, является ли первый элемент вектора f ibonacci единицей, можно написать:
If (! fibonacci. empty () && (fibonacci [1] == 1))
Как мы могли бы осуществить ту же проверку через pv? Объединение f ibonacci и empty О через точку называется оператором выбора члена. Оно используется для выбора операций класса через объект класса. Для выбора операции класса через указатель используем оператор-стрелку (->) выбора члена:
! pv->empty()
Поскольку указатель может адресоваться к отсутствующему объекту, перед тем как мы используем empty () через pv, необходимо проверить, что адресация не нулевая:
Pv && ! pv->empty()
Окончательно для вызова оператора индексов мы должны разыменовать pv (понадобятся дополнительные скобки вокруг разыменованного pv из-за более высокого приоритета индексного оператора):
If (pv ScSc I pv->empty () ScSc ((*pv) [ 1 ] == 1)) Запись и чтение файлов
Если пользователю случится запустить нашу программу повторно, было бы здорово, если бы счет сохранялся для обеих сессий. Чтобы это стало возможным, мы должны:
• записать имя пользователя и данные сессии в файл в конце сессии;
• прочитать данные предыдущей сессии в программу при ее повторном запуске.
Посмотрим, как мы можем это сделать. Для чтения и записи в файл мы должны включить файл заголовка f stream:
#include <fstream>
Чтобы открыть файл для вывода, определим объект класса of stream (an output file stream - поток вывода файла), передавая его имя в открываемый файл:
// seq_data. txt открыт на вывод ofstream outfile("seq_data. txt");
Что происходит, когда мы объявляем out file? Если его не существует, он создается и открывается на вывод. Если же он существует, то открывается на вывод, а все данные, которые в нем содержатся, игнорируются.
Если мы хотим добавить, а не замещать данные в существующем файле, необходимо открыть файл в режиме добавления.
Мы делаем это, передавая второе значение ios_base:: арр объекту of stream:
// seq_data. txt открывается в append mode (режим добавления)
// новые данные добавляются в конец файла ofstream outfіle("seq_data. txt", ios_base::арр) ;
Файл может не открыться. Прежде чем записывать в него, нужно убедиться, что он открылся успешно. Простейший путь проверки - убедиться в истинности объекта класса:
// если outfile определяется, как false, // файл не может быть открыт if (! outfile)
Если файл не может быть открыт, объект класса ofstream становится ложным. В этом примере мы предупредим пользователя, выводом сообщения cerr. сегг представляет стандартную ошибку, сегг, подобно cout, выводится на тер минал пользователя. Разница в том, что вывод сегг не буферизуется, оно выводится сразу на терминал:
If (! outfile)
// по какой-то причине не открывается...
Сегг « "Бах! Не могу сохранить данные сессии!п";
Else
// ok: outfile открыт, давайте писать данные outfile « usr_name « " " « num_tries « " " « num_right « endl;
Если файл открывается успешно, мы непосредственно выводим в него данные, как делаем это для объектов класса ostream cout и cerr. В этом примере мы пишем три значения в out file, последние два отделены пробелами. Endl - предопределенный манипулятор, поставляемый библиотекой iostream.
Манипулятор выполняет несколько операций с iostream, отличных от записи и чтения данных, endl вставляет символ перевода на новую строку, а затем сбрасывает на диск выходной буфер. Другие предопределенные манипуляторы включают hex, отображающий на дисплее целое число в ше - стнадцатеричном виде, oct, который отображает целое в восьмеричном виде, и setprecision(n), устанавливающий точность отображения чисел с плавающей точкой в п.
Чтобы открыть файл на ввод, мы определяем объект класса if stream (an input file stream - поток ввода в файл), передавая ему имя файла. Если файл не может быть открыт, объект класса if stream определяется как ложный. Иначе, файл позиционируется в начало данных, записанных в него:
// in file открыт в output mode ifstream infіle("seq_data. txt") ; int num_tries = 0; int num_cor = 0;
If (! infile){
// по какой-то причине файл не открывается...
// мы будем предполагать, что это новый пользователь...}
Else {
// ok: читаем каждую линию входного файла
// смотрим, играл ли пользователь раньше —
// формат каждой линиии:
// name num_tries num_correct
// nt: количество попыток
// пс: количество отгадываний
String name;
Int nt;
Int nc;
While (infile » name){ infile » nt » nc; if (name == usr_name) {
11 нашлиI
Cout « "С возвращением, " « usr_name« "пВаш текущий счет " « nc « " out of " « nt « "ХпУдачи! n" ; nurrutries = nt; num_cor = nc;}}}
Каждый проход цикла while прочитывает новую линию файла, пока не будет достигнут конец файла. Когда мы пишем:
Infile » name
Возвращаемое значение входного выражения - объект класса, из которого мы читаем— infile в данном случае. Когда конец файла достигнут, условие true объекта класса сменяется на false. Это причина, по которой условное выражение цикла while прерывается, когда достигается конец файла:
While (infile » name)
Каждая линия файла содержит строку, за которой следуют два целых в форме:
Anna 24 19 danny 16 12 ...
Выражение:
Infile » nt » nc;
Читает по очереди количество попыток пользователя в nt и количество угадываний в пс.
Если мы хотим и читать, и писать в тот же самый файл, мы определяем объект класса fstream. Для открывания его в режиме дополнения мы должны передать второе значение в форме:
Ios_base:: in I ios_base:: app : fstream iofile("seq_data. txt",ios_base::in Iios_base::app) ; if (! iofile)
// файл не открывается по какой-то причине ______ гаді
{ // переходим к началу файла для начала чтения iofile. seekg(O);
// ok: все остальное без изменений...}
Когда мы открываем файл в режиме добавления, текущая позиция - конец файла. Если мы пытаемся читать файл без перепозиционирования, то просто получаем конец файла. Оператор seekgO возвращает iofile к началу файла. Поскольку он открыт в режиме дополнения, любая операция записи добавляет данные в конец файла.
♦в ncunst