- •Методические указания к лабораторным работам
- •Лабораторная работа №1 простые программы с циклами и операторами консольного ввода/вывода
- •Задание
- •Описание примера
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №2 работа с текстовыми файлами, структурами данных и меню
- •Задание
- •Структурное программирование и функциональная декомпозиция системы
- •Функции
- •Организация меню в консольном приложении
- •Структуры данных
- •Операции с файлами
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №3 разработка и спецификация функций и модулей программы
- •Задание
- •Модульная структура программ
- •Параметры командной строки
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №4 разработка и спецификация структур данных, использование указателей и динамических массивов структур
- •Задание
- •Указатели
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №5 использование объектно-ориентированного программирования в разработке приложений
- •Задание
- •Конструкторы и деструкторы
- •Конструктор по умолчанию
- •Конструктор копирования
- •Массивы объектов
- •Friend-конструкции
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №6 использование наследования, полиморфизма и абстрактных классов
- •Задание
- •Наследование данных и методов
- •Полиморфизм и виртуальные функции
- •Абстрактный класс
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №7
- •Сложные структуры из объектов классов
- •Цель работы - изучение организации различных структур данных и разработка методов манипулирования данными.
- •Задание
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №8 разработка windows-интерфейса приложения
- •Задание
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №9 разработка и использование com-сервера
- •Задание
- •Шаблоны классов
- •Использование библиотеки atl для создания серверов сом
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Литература
Абстрактный класс
Абстрактный класс содержит по крайней мере одну чисто виртуальную функцию. Чисто виртуальная функция объявляется путем указания =0 в конце ее объявления.
class Shape // Абстрактный класс
{
public:
virtual void draw() = 0; // Чисто виртуальная функция
};
class Rectangle: public Shape
{
public:
void draw();
};
class Circle : public Shape
{
public:
void draw();
};
Объекты абстрактного класса не могут существовать. Однако указатель или ссылка на него могут быть созданы. Объект абстрактного класса не может быть в качестве типа возвращаемого функцией значения и в качестве параметра функции. Если в абстрактном классе объявлена чисто виртуальная функция и она не переопределена (overriden) в производном классе, то этот производный класс тоже становится абстрактным, так как он наследовал чисто виртуальную функцию. Абстрактные классы используются только для того, чтобы от них могли быть образованы производные классы. Обычно они стоят во главе иерархического дерева классов. Конструктор абстрактного класса служит не для создания объектов своего класса, а для инициализации объектов производных (не абстрактных классов). Точнее, именно тех полей производного класса, которые перешли по наследству от абстрактного. Методы абстрактного класса могут быть вызваны в теле конструктора, исключая, конечно, чисто виртуальные функции.
Методика выполнения
Запустите MS Visual Studio и откройте проект консольного приложения из работы №5.
Измените объявление класса так, чтобы он стал абстрактным базовым классом для задачи.
Объявите нужное количество дочерних классов.
Определите методы дочерних классов.
Сделайте необходимые изменения в объявлениях и определениях функций обработки данных. Используйте полиморфизм позднего связывания. Отладьте программу. В процессе отладки используйте заглушки и комментарии для постепенного включения исправлений в программу.
Содержание отчета
Отчет готовится в письменном или печатном виде один на бригаду. В отчет включить листинги модулей, результаты тестовых прогонов программы. Листинги должны содержать спецификации модулей и функций, иметь структурированный вид и комментарии.
Контрольные вопросы
Как в классе указывается родительский класс?
Что такое тип наследования и как он влияет на доступность элементов класса?
Что такое полиморфизм раннего связывания?
Что такое полиморфизм позднего связывания?
Какие возможны варианты переопределения функций в классах?
Что такое абстрактный класс?
Какая функция называется чисто виртуальной?
В чем достоинства и недостатки использования в программах виртуальных функций?
Лабораторная работа №7
Сложные структуры из объектов классов
Цель работы - изучение организации различных структур данных и разработка методов манипулирования данными.
Задание
Разработать на языке С++ 6-ю версию консольного приложение для хранения и отображения сведений об объектах в виде структуры в соответствии с вариантом задания, указанном в таблице.
Обеспечить выполнение следующих операций:
Хранение сведений об объектах в файле, организация ввода/вывода элементов структуры.
Поиск объекта в структуре.
Редактирование сведений об объекте.
Вставка объектов в структуру.
Удаление объектов из структуры.
Сортировка объектов в структуре и вывод отчета на экран.
Все функции реализовать как методы класса.
Операции файлового ввода/вывода реализовать с помощью библиотеки потоковых классов.
Бригада № |
Вид структуры |
1 |
Линейный однонаправленный список |
2 |
Циклический однонаправленный список |
3 |
Коллекция |
4 |
Циклический двунаправленный список |
5 |
Бинарное дерево |
6 |
Линейный двунаправленный список |
7 |
Очередь FIFO |
8 |
Стек |
9 |
Циклический двунаправленный список |
10 |
Бинарное дерево |
11 |
Очередь FIFO |
12 |
Коллекция |
Краткая теория
Вложенность классов
Поле данных класса может иметь любой тип, в том числе быть объектом какого-либо класса или указателем на объект класса. Это обстоятельство используется при разработке сложных структур данных, состоящих их различных классов. В целях экономии памяти более предпочтительным является вариант работы с динамически создаваемыми объектами, что предполагает использование указателей.
Существует специально зарезервированное ключевое слово this, определяемое как указатель на объект, которому было послано обрабатываемое сообщение. В каком-то конкретном методе указатель this содержит адрес объекта, который инициировал этот метод. Это справедливо только для методов класса, не имеющих описатель static. При любом вызове метода класса указатель this передается как скрытый (hidden) параметр, то есть передается неявно. Он является локальной переменной внутри метода и неявно используется при обращении к данным и методам класса. Иногда его используют явно.
Рассмотрим эти возможности на примере инкапсуляции в классе Node такой распространенной структуры данных, как узел линейного односвязного списка. Класс Node имеет в качестве элемента данных указатель next на объект своего собственного класса.
class Node {
char *data; // Информационная часть узла
Node *next; // Указатель на следующий узел
publiс:
Node (char *s) // Конструктор
{
data = strcpy(new char[strlen(s)+l], s);
next = NULL;
}
void GetData()
{
puts (this->data);
}
void SetNextTo (Node& n)
{
n.next = this;
}
Node* GetNext()
{
return next;
}
~Node ()
{
puts("\nDeleting data");
delete data;
}
};
void main ()
{ // Использование класса Node
Node n1(“First"), n2("Second");
n1.GetData();
n2.GetData();
n2.SetNextTo(n1);
n1.GetNext()->GetData();
}
Конструктор класса Node дважды вызывается в функции main при объявлении объектов n1 и n2. Каждый раз он запрашивает память из области heap и размещает там строку символов (содержимое узла списка), которая передастся конструктору в качестве параметра. Метод класса GetData выводит в поток stdout (стандартный выходной поток) поле data того объекта, которому передается сообщение. Здесь мы специально вставили выбор с помощью указателя (this->), чтобы проиллюстрировать то, что обычно делает компилятор. Его можно оставить, а можно и убрать. Любое обращение к data в любом методе класса Node компилятор расширяет до this->data. Оператор программы n1.GetData(); выведет строку "First", а оператор n2.GetData(); — строку "Second". Внутри конструктора мы обращаемся к переменной data, не указывая явно, какому объекту принадлежит это поле данных. Компилятор C++ сам расширяет это обращение до this->data. Метод SetNextTo получает в качестве параметра ссылку на объект класса Node. Поскольку этот метод принадлежит тому же классу, что и параметр, то внутренний компонент next объекта n прямо доступен в данном методе. Ему присваивается адрес объекта, который инициировал метод (n.next=this;). Адрес содержится в переменной this. Так как в нашем случае объекту n2 послано сообщение SetNextTo(n1), то указатель this содержит адрес объекта n2, и он присваивается полю next объекта nl. Получается, что объект n1 имеет в качестве следующего узла списка объект n2, или можно сказать, что n2 зацеплен за n1.
Метод GetNext позволяет получить адрес следующего элемента списка, то есть поле next объекта, которому послано сообщение. В последней строке функции main он используется для того, чтобы добыть объект, стоящий в списке за объектом n1. Выражение n1.GetNext() имеет результатом адрес объекта n2, которому посылается сообщение GetData. Следовательно, результатом всего выражения будет вывод поля data объекта n2, то есть строка "Second".
Сложные структуры данных
Стеком называется упорядоченный набор элементов, в котором размещение новых элементов и удаление существующих производится только с одного его конца, называемого вершиной стека. Его еще называют структурой типа LIFO (Last In First Out, первым вошел — последним вышел). Максимальное число элементов, которые можно разместить в стеке, не должно ограничиваться программным окружением: по мере вталкивания в стек новых элементов и выталкивания старых, память под него должна динамически запрашиваться и освобождаться. Состояние стека рассматривается только по отношению к его вершине, а не ко всему содержимому. Операции, выполняемые над стеком, имеют специальные названия. При добавлении элемента в стек мы говорим, что элемент вталкивается в стек (push). Для стека s и элемента sItem определена операция push(s, sItem), по которой в стек s добавляется элемент sItem. Аналогичным образом определяется операция выталкивания из стека - pop(s), по которой из стека s "верхний" элемент удаляется и возвращается в качестве значения функции. Следовательно, операция присваивания sItem = pop(s); удалит элемент из стека и присвоит его значение переменной sItem. На применение операции выталкивания из стека существует единственное ограничение: она не может применяться к пустому стеку, т. е. к такому, который не содержит ни одного элемента. Помимо этих двух основных операций часто бывает необходимо прочитать элемент в вершине стека, не извлекая его оттуда. Для этих целей используют операцию peek(s).
Очередью называется упорядоченный набор элементов, которые могут удаляться с одного ее конца (называемого началом очереди) и помещаться в другой конец этого набора (называемого концом очереди). Ее также называют структурой данных, организованной по принципу FIFO (First In First Out, первый вошел — первым вышел). Как и для стека, максимальное число элементов в очереди не должно лимитироваться используемым программным обеспечением: память должна запрашиваться и освобождаться динамически по мере того, как в очередь добавляются новые, и из нее удаляются находящиеся там элементы.
Рассмотренные структуры — стек и очередь, являются специальными разновидностями более общей структуры данных — обобщенного связанного списка. Приведенные ниже функции позволяют работать со списком:
insert — добавить новый элемент в список, сохраняя установленный порядок следования.
destroy — разрушить список.
display — вывести все элементы списка.
remove — удалить элемент списка.
Существует множество способов его реализации. В линейном однонаправленном списке поле next содержит адрес следующего элемента списка. Последний элемент в поле next хранит значение NULL. В линейном двунаправленном списке кроме поля next имеется также поле prev, которое содержит адрес предыдущего элемента списка. Поле prev первого элемента списка содержит NULL. В циклическом однонаправленном списке поле next последнего элемента содержит указатель назад на первый элемент. Такой список не имеет первого и последнего элементов. Однако в некоторых случаях удобно использовать внешний указатель на последний элемент, что автоматически делает первым следующий за ним элемент. Альтернативный вариант предполагает использование указателя на первый элемент. В циклическом двунаправленном списке поле prev первого элемента содержит указатель на его последний элемент, а поле next последнего элемента – указатель на первый элемент.
Коллекция - это упорядоченный набор объектов, построенный на базе динамического массива и записи, что позволяет, с одной стороны, перенумеровать все элементы семейства, а с другой — иметь прямой доступ к объектам — элементам семейства — по значению определенного поля, называемого ключом. Ключ — это строковое выражение, которое может быть использовано вместо индекса для доступа к элементу семейства. Класс Collection имеет одно свойство Count и три метода — Add, Item и Remove. Свойство Count возвращает количество элементов семейства.
Метод Add (элемент [, ключ][, до] [, после]) добавляет объект в семейство. Его обязательным аргументом является элемент, он указывает добавляемый в семейство элемент. Параметр ключ задает ключ, по которому возможно будет произвести поиск этого элемента. Параметры до и после указывают на то, перед каким или после какого элемента добавляется новый. По умолчанию элемент добавляется в конец семейства.
Метод Remove(ключ) удаляет элемент из семейства. Параметр ключ - это ключ или индекс, указывающие на удаляемый элемент. Заметьте, что при удалении элемента из семейства не остается "дыр": индексы перенумеровываются, значение свойства Count уменьшается на единицу.
Метод Item (ключ) возвращает значение элемента семейства с ключом ключ. Как в случае с методом Remove, параметр ключ может быть как ключом, так и индексом.
Бинарное дерево — это конечное множество элементов, которое либо пусто, либо содержит один элемент, называемый корнем дерева, а остальные элементы множества делятся на два непересекающихся подмножества, каждое из которых само является бинарным деревом. Эти подмножества называются левым и правым поддеревьями исходного дерева. Каждый элемент бинарного дерева называется узлом дерева. Функции для работы с бинарными деревьями те же, что и для списка.
При работе с деревьями одной из основных операций является прохождение дерева - обход всего дерева, при котором каждый узел посещается один раз. Существуют три способа, отличающиеся порядком посещения корня и прохождения его левого и правого поддеревьев:
Прямой порядок (нисходящий)
попасть в корень;
пройти в прямом порядке левое поддерево;
пройти в прямом порядке правое.
Симметричный порядок (последовательный)
пройти в симметричном порядке левое поддерево;
попасть в корень;
пройти в симметричном порядке правое поддерево.
Обратный порядок (восходящий)
пройти в обратном порядке левое поддерево;
пройти в обратном порядке правое поддерево;
попасть в корень.
Во всех случаях используется рекурсивный вызов соответствующей функции.
Библиотека потоковых классов C++
В целом библиотека потоковых классов C++ состоит из многих классов, объединенных в два иерархических дерева.
Класс streambuf является базовым классом для классов filebuf, stdiobuf и strstreambuf, обеспечивающих буферизованный интерфейс между данными пользователя и такими областями хранения данных, как память или физическое устройство. Более 30 методов класса streambuf позволяют выбирать символы из буфера, помещать их туда, позиционировать указатель, запрашивать количество оставшихся символов в буфере и т. п. Методы производных классов позволяют открывать и закрывать файлы, манипулировать дескриптором файла, управлять вводом-выводом на консоль, создавать буфер для форматирования строки символов в памяти.
Второй базовый класс ios порождает дерево классов istream, ostream, iostream. Каждый из этих классов, в свою очередь, имеет несколько производных классов. В результате дерево состоит более чем из 10 классов, методы которых дают возможность производить ввод-вывод с использованием операций форматирования высокого уровня. Рассмотрим пример использования некоторых из классов библиотеки.
#include iostream.h
void main()
{
int с;
const char *fn = "test.txt";
ofstream fOut; // Объявление выходного потока
streambuf *bufOut, *bufIn = cin.rdbuf();
// Открыть файл, встать в конец, разрешить дозапись
fOut.open (fn, ios::ate | ios::app);
if (!fOut) cerr << "Can't open " << fn; exit(-l);
bufOut = fOut. rdbuf (); // Связывание of stream и streambuf
clog << "Enter some text (^Z - to stop)" << endl;
// Ввод с клавиатуры, вывод на экран и в файл
while ((c=bufIn->sbumpc()) != EOF)
{
cout << char (с); //Эхо на экран
if (bufOut->sputc(c) = = EOF) cerr << "Output error";
}
fOut.close();
// Позиционирование во входном потоке
int Size = 0;
// Объявление входного потока и попытка его открытия
ifstream fIn (fn, ios::in | ios::nocreate);
fIn. seekg (0L,ios:: end); // Переместиться в конец файла
if ((Size = fIn.tellg()) < 0)
{
сеrr << fn << " not found";
exit(-l);
}
Cout<<fn<<" Size = "<<Size<<endl;
}
Сначала в примере создается файл test.txt, в который вводится фрагмент текста. Класс ofstream происходит от класса ostream. Следующая строка программы
streambuf *bufOut, *bufIn = cin.rdbuf();
определяет два указателя на класс streambuf, которые используются для управления буферами потоков вывода и ввода. Метод cin.rdbuf() возвращает текущее значение указателя в потоке ввода, связанном с клавиатурой. Оператор
fOut.open (fn, ios::ate | ios::app);
обращается к методу open класса ofstream, который открывает файл с именем, переданным первым параметром в режиме, заданном вторым параметром. Второй параметр является выражением, операнды (ate и арр) которого определены в классе ios. Они являются элементами перечисления enum openmode и допускают выполнение логических операций над собой с целью задания набора битовых признаков режима открытия файла. В нашем случае объединяются признаки арр (разрешение добавлять в конец файла) и ate (режим перемещения в конец файла при его открытии). Операция | переопределена так, что нулевой код возврата свидетельствует об ошибке при открытии файла. Сообщение об ошибке выводится в поток сеrr, который обычно направлен на экран монитора. Оператор bufOut = fOut.rdbuf(); связывает буфер вывода с выходным потоком, так как метод rdbuf класса ofstream возвращает текущее значение указателя потока (файла), которое присваивается указателю bufOut типа streambuf*.
В цикле while осуществляется последовательный ввод символов из входного потока, вывод их сначала на экран cout<<char(c);, затем в буфер, связанный с файлом. Метод sbumpc() класса streambuf возвращает текущий символ буфера ввода и сдвигает указатель буфера на следующий символ. Аналогичным образом метод sputc() помещает символ в буфер вывода. Символ Ctrl+Z служит признаком конца файла (EOF — End Of File). Чтобы закончить ввод с клавиатуры, необходимо ввести такую последовательность: Enter, ^Z, Enter.
Следующая часть программы иллюстрирует, как можно позиционировать указатель в файле. Указатель переводится в конец созданного файла, и выясняется его позиция относительно начала файла. Таким образом определяется размер файла в байтах. Класс ifstream происходит от класса istream. Наличие двух аргументов при объявлении объекта заставляет выбрать один из конструкторов класса ifstream, который сразу открывает файл с именем, переданным первым параметром, и в режиме, задаваемом вторым параметром. Режим задан в виде выражения ios::in | ios::nocreate. Результатом побитовой операции ИЛИ является установка двух признаков - открыть файл для ввода и не создавать файл, если он не существует. Позиционирование в файле осуществляется с помощью функции seekg(), которая определена в классе istream и наследуется классом ifstream. Результатом вызова будет перемещение указателя потока в конец файла. Метод tellg() класса istream возвращает значение текущей позиции потока, которое численно на единицу меньше размера файла в байтах.