Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методичка_c+.doc
Скачиваний:
18
Добавлен:
04.11.2018
Размер:
464.38 Кб
Скачать

2. Пример простейшей программы

Приступить к изучению программирования на С++ лучше с простого примера. Вот его задание: необходимо разработать программу, реализующую функциональность стека.

Для начала попытаемся определить те сущности, которые присутствуют в задании. Начнём с анализа определения стека. Стек – это упорядоченный набор элементов, обладающий чётким правилом работы с составляющими, которое кратко можно охарактеризовать следующим образом: последний вошёл, первый вышел. Из данного определения можно получить сразу 2 сущности: непосредственно стек (Stack) и элемент стека (Item).

Теперь следует определить основную функциональность и характеристики объектов. Для создания связного набора элементов необходимо реализовать функциональность списка. В данном случае нам будет достаточно однонаправленного линейного списка, первый элемент которого будет вершиной стека (Item* first). Для реализации данного принципа необходимо чтобы каждый из элементов имел ссылку на следующий (Item* next). А в самом стеке необходимо реализовать механизм связывания.

Ну и, наконец, должно быть реализовано самое главное – механизм стека, а именно запись (write) в стек и чтение (read) из стека.

В итоге, после проведённого анализа, получаем следующую структуру классов, их свойств и методов:

////////////////////////////////////////////////////////////

// Элемент (списка) стека

class Item

{

public:

int val; // Целое число, сохраняемое в стеке

Item* next; // Следующий элемент в списке стека

Item(int v=0) {val=v; next=NULL;}

};

////////////////////////////////////////////////////////////

// Абстрактный стек с неограниченным числом элементов

class Astack // Абстрактный базовый класс

{

protected:

Item* first; // На первый элемент стека

int count; // Текущее число элементов в стеке

virtual ~Astack();// Уничтожение объекта стек

virtual void write(int vl)=0; // Запись числа vl в стек

virtual int read()=0; // Чтение числа из вершины стека

};

////////////////////////////////////////////////////////////

// Стек с ограниченным числом элементов

class Stack: protected Astack // Производный класс.

// Наследует все данные и функции базового Astack.

{

private:

int max; // Максимальное число элементов;

// при max==0 - стек с неограниченным числом элементов

public:

Stack (int _max=0); // Создание объекта стек

Stack (const Stack& st); // Создание объекта стек

void write(int vl); // Запись числа vl в стек

int read(); // Чтение числа из вершины стека

// Определение текущего числа элементов в стеке

friend int Count_item (const Stack& st);

};

////////////////////////////////////////////////////////////

// Обработка ошибок

class Error

{

public:

char* str; // На начало строки

Error(char* s=NULL);

};

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

Главное – появилось деление класса стека на 2: абстрактный базовый класс общего представления стека Astack и конкретный производный класс Stack, моделирующий реальный объект внешнего мира. Количество патронов в магазине пистолета всегда ограничено, что соответствует параметру max. Концепция базового класса фундаментальна. Она, во-первых, представляет общий стандартный интерфейс последующих продуктов, а во-вторых, общие данные и обработчики, поскольку производный класс наследует все свойства базового. Описатель virtual позволяет в производных классах изменить определение соответствующей функции, изменив поведение объекта. Причем следует подчеркнуть, что такое изменение (то есть разработка производного класса) может быть сделано гораздо позднее (и другими людьми в другом месте), чем создание общего интерфейса. Это по истине изумительнейшее изобретение конца ХХ-го столетия! Описатель “=0” представляет чистый виртуальный интерфейс, и соответствующие функции должны быть определены в производном классе. Последнее делает класс абстрактным.

Представленные классы демонстрируют еще и управление доступом к данным и функциям-членам класса. Для этого существует три типа меток: public, protected и private. Метка public означает, что все данные и функции-члены (далее – компоненты) класса, которые находятся между ней и следующей меткой управления доступом является доступными вне класса. Метка protected уже ограничивает доступ, разрешая его только внутри функций-членов этого класса и классов, производных от него. И, наконец, метка private полностью запрещает доступ к ниже следующим компонентам (до следующей метки управления доступом) вне данного класса – именно это свойство позволяет сохранять целостность объекта от внешних ошибочных действий пользователя, не разбирающегося в тонкостях реализации класса. Одновременно это облегчает отладку путем ограничения области поиска: если объект нарушился, то это может произойти только из-за ошибок в функциях-членах класса. Следует производить тщательный отбор типа доступа при проектировании класса, не давая лишней информации пользователю. Например, обратите внимание, что в классе Stack все операции осуществляются только через вершину стека посредством открытых функций записи write() и чтения read(), и нет никакой возможности испортить список элементов стека и изменить максимальное количество его элементов. Это и правильно: попробуйте изменить максимальное количество патронов в магазине конкретного пистолета – потребуется создать другой пистолет. По умолчанию, то есть если после ключевого слова class отсутствует метка управления доступом, то считается, что она определена как private.

Обратите внимания, что спецификатор управления доступом присутствует и перед именем базового класса:

class Stack: protected Astack // Производный класс.

Это означает соответствующее определение прав доступа к компонентам базового класса. В данном случае возможен доступ к компонентам типа public и protected. Заметим, что если в базовом классе компонент определён как private, то это делает невозможным доступ к нему даже в производном классе.

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

// Создание стека с максимальным числом элементов _max

Stack::Stack(int _max)

{

count=0;

max=_max;

first=NULL;

};

// Создание стека с тем же содержимым, что и st

Stack::Stack(const Stack& st)

{

Item *point1, *point2, **point3;

count=st.count;

max=st.max;

point3=&first; // Первый адрес указателя point3

// определит внутри цикла first

for(point1=st.first; point1; point1=point1->next)

{

point2=new Item;

point2->val=point1->val;

*point3=point2;

// На первом цикле определяется first; на последующих-

// указатель next в предыдущей структуре Item

point3=&(point2->next);

// Определение адреса указателя next

}

};

Обратите внимание, что конструктор не определен в абстрактном классе Astack, поскольку в нем не существует реальное физическое ограничение размера стека max, причем размер max определяется только при создании объекта в конструкторе. Следует подчеркнуть, что нельзя создать объект абстрактного класса Astack, поскольку в нем существует чистый интерфейс “=0”.

Созданный объект необходимо уничтожить, когда он становится не нуж­ным. Деструктор – это функция уничтожения объекта, которая автоматически вызывается, когда выполнение программы выходит из области, где этот объект определен. Деструктор имеет то же имя, что и класс с префиксом “~”.

// Уничтожение стека с вычеркиванием его списка

// из динамической области

Astack::~Astack()

{

Item* point;

while(first)

{

point=first;

first=first->next;

delete(point);

}

count=0;

};

Деструктор необходим тогда, когда объект порождает в памяти нечто большее, чем содержится в его непосредственном определении. В данном случае в определении класса Astack есть только данные first и count; а вот список элементов типа Item возникает в динамической области памяти уже в процессе функционирования конкретного объекта. Методы не занимают места в памяти конкретного объекта, а являются сущностью, общей для всех объектов класса. Обратите внимание, что деструктор не определен в производном классе Stack, поскольку этот класс не порождает новых динамических данных сверх базового класса Astack. Однако, определение virtual ~Astack() дает возможность в будущем определить другой производный класс, в котором эти данные могут появиться. Теперь приведём функции класса Stack.

// Запись числа vl в стек

void Stack::write(int vl)

{

Item *point;

if(!max || max>count) // при !max – стек с

// неограниченным числом элементов

{

point=new Item(vl);

count++;

point->next=first;

first=point;

}

else

throw Error("Stack is full\n");

};

// Чтение числа из вершины стека

int Stack::read()

{

int val;

Item *point;

if(first)

{

count--;

val=first->val;

point=first;

first=first->next;

delete point;

return val;

}

else

throw Error("Stack is empty\n");

};

// Определение текущего числа элементов в стеке

int Count_item (const Stack& st)

{ return st.count;

}

Обратите внимание, что все функции класса определяются с указанием имени данного класса, Это естественно, поскольку вне класса они не существуют, и, кроме того, аналогичные по назначению функции (“Нарисовать”) имеют одинаковые имена (draw), тем более если это функции производного класса.

Обработка ошибок осуществляется путем копирования сообщения об ошибке в динамическую область памяти:

// Обработка ошибок

Error::Error(char* s)

{ str=new char[strlen(s)+1]; strcpy(str, s);

}

И, наконец, приведём программу тестирования корректной работы стека.

// Программа тестирования стека

void main()

{

try

{

Stack st1(2);

st1.write(5);

st1.write(6);

// st1.write(7);

Stack st2(st1);

cout << st2.read() << '\n';

cout << Count_item(st2) << '\n';

cout << st2.read() << '\n';

cout << Count_item(st2) << '\n';

/*

cout << st2.read() << '\n';

cout << Count_item(st2) << '\n';

*/

}

catch(Error& s)

{ cout << s.str;

}

}

Закомментированные строки в программе порождают исключительные ситуации переполнения стека или чтения из пустого стека. Они порождают выполнение оператора прерывания throw нормального хода выполнения программы с переходом к оператору захвата catch исключительной ситуации; причем оператору catch автоматически передается ссылка на объект класса Error&, образованный оператором throw. По этой причине конструктор класса Error::Error(char* s) копирует входную строку s именно в динамическую область памяти, чтобы она не потерялась при выходе из области видимости функции Stack::read().

3. Проект программы и функции оболочки Microsoft Visual C++

В этом разделе мы попытаемся обеспечить Вам быстрое вхождение в среду проектирования программ Microsoft Visual C++. Для этого возьмем наш простой пример программы тестирования стека из раздела 2 и опишем последовательность действий для её разработки в данной среде. Этот раздел имеет смысл прорабатывать, только выполняя соответствующие действия на компьютере. Поэтому подготовьтесь. Параллельно среду следует изучать, используя практикум [2]. Следует подчеркнуть, что среда проектирования программ Microsoft Visual C++ настолько сложна концептуально, что попытка систематического изложения её функций и режимов приведет к справочнику очень большого объема, в котором Вам будут непонятны даже большинство основных терминов! Такой справочник, конечно, существует и называется Microsoft Developer Network (MSDN) – на инсталляционных дисках на английском языке объемом, измеряемым гигабайтами. Именно метод интуитивного познания через действия с параллельным заглядыванием в соответствующие места сначала книг, а затем и в оригинал MSDN обеспечит Вам оптимальный процесс обучения.

Итак, после инсталляции системы Microsoft Visual C++ запустите головную программу через кнопку “Пуск” по пути Программы/ Microsoft Visual Studio 6.0/ Microsoft Visual C++ 6.0. Вы получили окно со строкой меню наверху, следующей строкой инструментальных кнопок, повторяющих функции, которые можно указать через меню, полем рабочего пространства (Workspace) слева, областью вывода (Output) сообщений системы снизу и основным рабочим полем, в котором будут размещаться открытые файлы – по центу (см. рис. 1). Внимательно рассмотрите это окно, некоторые вещи Вам будут интуитивно понятны, однако, основные функции системы мы будем объяснять по ходу изложения в тот момент, когда Вы к этому будете подготовлены. И не ранее.

Итак, Ваша первая задача – открыть уже упомянутое рабочее пространство (Workspace), которое является местом организации одного или нескольких проектов, объединенных общим смыслом; этот смысл Вы без труда можете уловить из предметной области Вашего задания. Делается это через меню File/New, в появившемся окне (рис. 1) следует нажать вкладку Workspace, в поле Location установить каталог (H:\ – здесь и далее используйте букву диска, соответствующую Вашему рабочему месту) и в поле Workspace name – имя рабочего пространства (STACK); ввод последнего сопровождается повторением этой информации в поле Location. В результате после нажатия кнопки ОК будет создан каталог H:\STACK и рабочее пространство (файл STACK.dsw) в нём.

Рис. 1. Создание рабочего пространства STACK

Вторым шагом является создание проекта (Project) программы в этом рабочем пространстве через меню File/New. В появившемся окне (рис. 2) следует нажать вкладку Projects, в поле Location установить только что созданный каталог (H:\STACK) и в поле Project name – имя программы (Test_stack) тестирования класса STACK. На рис. 2 Вы видите большое число типов проектов, содержание которых Вы постигните ещё не скоро. Мы будем создавать проект Win 32 Console Application (обычное для Вас скролинговое приложение, какое Вы делали в системе Borland C), для чего надо указать мышкой на соответствующую позицию в списке. Кроме того, проект мы создаем в текущем пространстве STACK и, поэтому, надо отметить пункт Add to current workspace.

Рис. 2. Создание проекта программы Test_stack

После нажатия кнопки ОК рекомендуется выбрать строку A “Hello, World” application и завершить процесс нажатием кнопки Finish. В результате после нажатия кнопки ОК будет создан каталог H:\STACK\Test_stack и проект (файл Test_stack.dsp) в нём. Отмеченный вариант проекта хорош тем, что Вы сразу получаете простейшую работоспособную программу, которую можно скомпилировать (Build/Rebuild All) и выполнить (Build/Execute Test_stack.exe). Сделайте это и Вы получите результат – черное окно вывода программы с приветствием “Hello, World!”. Эту программу можно запускать и через Far в обычном скролинговом режиме. Поздравляем, Ваши глаза на мир в системе Microsoft Visual C++ уже открылись.

Изучите полученный проект. Нажмите на все кнопки “+” в поле рабочего пространства. Открыть указанные там файлы в рабочее поле можно путем двойного щелчка на имени файла. Откройте файл Test_stack.cpp. Обратите внимание на строку #include "stdafx.h". Этот файл создан автоматически на этапе генерации проекта и представляет собой стандартный заголовочный файл, который компилируется предварительно один раз (путем компиляции файла stdafx.cpp) и далее должен подключаться ко всем cpp-файлам проекта, но это происходит уже из оперативной памяти компилятора без повторного анализа текстов. Обратите внимание, что в файле stdafx.h уже есть подключение библиотеки ввода/вывода #include <stdio.h>.

Третьим шагом является создание и включение в проект реальных файлов, относящихся к программе. Существует несколько вариантов этого процесса. Во-первых, редко когда файл необходимо создавать с нуля, обычно имеются какие-нибудь текстовые заготовки – даже из файлов Microsoft Word подойдут. В данном случае Вы должны были получить у преподавателя текстовый файл stack.txt рассматриваемого примера. Давайте загрузим его с помощью обычной кнопки открытия файла. Обратите внимание, что один файл, включенный в проект, у Вас уже есть – Test_stack.cpp. Просто замените содержимое программы между открывающей скобкой “{“ и оператором return текстом из реальной программы.

Как обычно многие вещи делаются из контекстного меню, которое относится к объекту, над которым нажата правая кнопка мыши. Например, если есть уже готовые h- и cpp-файлы, то их можно подключить, щелкнув правой кнопкой над именем проекта – над строкой Test_stack files. В появившемся меню надо выбрать Add Files to Project и далее выбирать файлы из открытого каталога обычным образом, используя для выделения файлов в разбивку нажатую клавишу Ctrl.

Если готового файла нет, то его следует создать в проекте через меню File/New, нажав кнопку Files, определив тип файла, допустим заголовочный C/C++ Header File, введя его имя (stack) без расширения; местоположение его лучше выбрать в каталоге рабочего пространства H:\STACK (сотрите конец в имени каталога), а не в каталоге проекта: ведь Вы в дальнейшем будете иметь несколько проектов в одном пространстве с общими файлами. Вы получили пустой файл, но уже включенный в проект (смотрите поле рабочего пространства). Далее обычным методом копирования в буфер системы выделенного в файле stack.txt части текста (Ctrl+Insert), относящегося к определению классов, перенесите его в файл stack.h (Shift+Insert). Аналогичную процедуру необходимо сделать для функций-обработчиков объектов классов, записав их в файл stack.cpp (выбрать тип файла C/C++ Source File). Не забудьте подключить необходимые заголовочные файлы; и вот тогда мы получим полностью готовый проект тестирования стека (рис. 3).

Рис. 3. Проект тестирования стека Test_stack

Обратите внимание, что h- и cpp-файлы в отличие txt-файлов подвергаются синтаксическому разбору и, соответственно, раскрашиваются. Некоторые поля окна можно, если они мешают восприятию текста, убирать с экрана нажатием на “Крестик” и возвращать обратно посредством меню View. На рис. 3 убрана область вывода (Output) сообщений.

Для компиляции проекта необходимо поработать с режимами системы Tools/Options/Directories, установив каталог H:\STACK для поиска h-файлов (иначе в модуле Test_stack.cpp не будет найден файл stack.h, поскольку этот модуль находится в другом каталоге). Компиляция производится по Build/Rebuild All, но лучше с помощью нажатия соответствующей кнопки на панели Build (см. рис 4). Запущенная программа выводит на экран числа 6 1 5 0. Обратите внимание, что некоторые строки текстов модулей закомментированы, что предполагает варианты исследования для студента. В частности, раскомментировав две нижние строки cout в программе Test_stack.cpp, Вы получите на экране 6 1 5 0 Stack is empty, то есть исключительную ситуацию чтения из пустого стека.

Обратите также внимание, что сообщение об исключительной ситуации выводится на английском языке, хотя есть закомментированный русский вариант. Прямое использование русских строк приводит к выводу арабских символов, поскольку скролинговый вывод программы предполагает Oem-кодировку (DOS), а Visual C++ использует Ansi-кодировку (Windows) русских символов. Можно вывести и по-русски, но тогда, во-первых, надо раскомментировать #include <afx.h> и функцию AnsiToOem(str, str), а, во-вторых, поработать с режимами проекта Project/Settings/General, установив режим Use MFC in a Shared DLL использования библиотеки Microsoft Foundation Classes, поскольку её использует библиотека afx.h.

Альтернативой является использование прилагаемой функции _printf вместо printf и cout.

// Форматированный вывод в поток строк с русскими символами

#include <afx.h> // Требует режима Use MFC in a Shared DLL

#include <stdarg.h>

void _printf(const char* frm, ...)

{

va_list args;

va_start(args,frm);

char buf[1024];

vsprintf(buf, frm, args);

// форматированный вывод входной информации в строку buf

va_end(args);

AnsiToOem(buf, buf); // Перевод buf в Oem-кодировку (DOS)

// требует #include <afx.h>

printf("%s", buf);

}

Эта функция делает все то же самое, что и printf , предварительно выполнив форматированный вывод в строку buf (функция vsprintf) всего того, что было определено в аргументах функции _printf и переведя все символы buf в Oem-кодировку (функция AnsiToOem). Тогда можно удобно вывести на экран и русские комментарии, а не просто цифры 6 1 5 0, что является само по себе не корректным. Реализация этой альтернативы предлагается читателю в качестве практического упражнения.

И, наконец, если Вы раскомментируете строку st1.write(7), то вместо цифр 6 1 5 0 вы получите сообщение о другой исключительной ситуации: Stack is full – переполнение стека. Хорошо, что мы о ней позаботились, в противном случае было бы просто прерывание, после которого программисту бывает трудно разобраться в чет же дело. И тогда на помощь приходит мощное средство отладчика.

Для выполнения отладки программы необходимо выполнить следующие условия:

  • Обеспечить наличие на экране панели Build (рис. 4), для чего щелкнуть правой кнопкой мыши где-то в области панелей инструментов и выбрать Build. С помощью этой панели удобно устанавливать текущий проект (если их несколько), компилировать и запускать собранную программу на выполнение. Для отладки надо выбрать режим Win32 Debug.

Рис. 4. Инструментальная панель Build компиляции и отладки проекта

  • Установить режимы Project/Settings для возможности работы поисковой системы – браузера. Для этого во вкладке C/C++ установить флажок в поле Generate browse info, а во вкладке Browse info – Build browse info file. Возможности и удобство браузера трудно переоценить. Он становится совершенно необходимым, когда программа большая и состоит из многих модулей: щелкнув правой кнопкой мыши на любом идентификаторе id программы и выбрав Go to definition of id, Вы тут же получите на экране файл, который откроется на том месте, где этот идентификатор определен. Но даже в небольших программах найти определение идентификатора чаще проще, используя описанную процедуру, чем вручную листая файл.

  • Установить рабочий каталог в поле Working directory во вкладке Project/Settings/Debug; это следует сделать для всех конфигураций (All Configurations), установив соответствующую строку в поле Settings For. Именно из этого каталога Ваша программа будет читать исходные файлы и туда записывать результирующие, если они имеются. Следует, также иметь ввиду, что при переносе рабочего пространства на другой компьютер на последнем может не оказаться установленных Вами каталогов из-за чего программа не будет запускаться – их соответственно необходимо изменить.

  • И, наконец, после любого изменения режимов необходима полная перекомпиляция всего проекта Build/Rebuild All.

Время от времени не забывайте записывать на жесткий диск результаты Вашей деятельности: все файлы можно записать, нажав на иконку с несколькими дискетами, а рабочее пространство со всеми режимами – по File/Save Workspace.

Теперь Вы готовы для работы с отладчиком. Для начала установите точку останова, щелкнув мышкой в любом месте строки, перед выполнением которой следует остановиться, и нажав соответствующую кнопку на панели Build (рис. 4). В результате слева отобразится жирная коричневая точка (см. рис. 3, где их две). Запуск отладчика производится путем нажатия кнопки Отладка на панели Build. Программа запустится, остановится перед первой строкой с жирной точкой, и откроется окно отладчика. Далее следует использовать появившуюся в меню кнопку Debug, а лучше пользоваться соответствующими горячими клавишами. Так выполнение одной строки производится по клавише F10 (Step Over), а заход внутрь функции– по клавише F11 (Step Into). Попробуйте самостоятельно, предварительно раскомментировав st1.write(7), отследить по шагам процесс захвата исключительной ситуации, а также тонкое место в алгоритме копирования стека, указанное второй жирной точкой в модуле stack.cpp. Обратите внимание, что в нижнем поле отладчика переменные, имеющиеся в текущей строке, появляются автоматически, а в правом поле Вы можете их сами указать и отслеживать их изменение – укажите point3. Кроме того, просто встав курсором на простую переменную, Вы через секунду получите её значение. На рис. 5 показано окно отладчика в этой точке работы программы, причем курсор стоит на переменной max. Рассмотрите внимательно меню Debug. Часто бывает нереально и не нужно пройти всю программу по шагам из-за длинных циклов. В этом случае можно установить курсор на строку, перед которой следует остановиться и нажать Cntl+F10 (Run to Cursor) или выставить желательную точку останова и нажать F5 (запустить программу в режиме отладки дальше до следующей точки останова или до конца). Есть возможность выйти из функции в любом месте, нажав Shift+F11 (Step Out). Отладчик несравненно удобнее, чем в Borland C 3.1.

Рис.5. Окно отладчика в точке останова перед point3